tex2typst 0.3.23 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/convert.ts CHANGED
@@ -1,11 +1,30 @@
1
- import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TypstLrData, TexArrayData, TypstNamedParams } from "./types";
2
- import { TypstWriterError } from "./typst-writer";
1
+ import { TexNode, Tex2TypstOptions,
2
+ TexToken, TexTokenType, TexFuncCall, TexGroup, TexSupSub,
3
+ TexText, TexBeginEnd, TexLeftRight,
4
+ TexTerminal} from "./tex-types";
5
+ import { TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstMarkupFunc, TypstMatrixLike, TypstNode, TypstSupsub, TypstTerminal } from "./typst-types";
6
+ import { TypstNamedParams } from "./typst-types";
7
+ import { TypstSupsubData } from "./typst-types";
8
+ import { TypstToken } from "./typst-types";
9
+ import { TypstTokenType } from "./typst-types";
3
10
  import { symbolMap, reverseSymbolMap } from "./map";
4
- import { array_intersperse } from "./generic";
11
+ import { array_includes, array_intersperse, array_split } from "./generic";
5
12
  import { assert } from "./util";
6
13
  import { TEX_BINARY_COMMANDS, TEX_UNARY_COMMANDS } from "./tex-tokenizer";
7
14
 
8
15
 
16
+ export class ConverterError extends Error {
17
+ node: TexNode | TypstNode | TexToken | TypstToken | null;
18
+
19
+ constructor(message: string, node: TexNode | TypstNode | TexToken | TypstToken | null = null) {
20
+ super(message);
21
+ this.name = "ConverterError";
22
+ this.node = node;
23
+ }
24
+ }
25
+
26
+ const TYPST_NONE = TypstToken.NONE.toNode();
27
+
9
28
  // native textual operators in Typst
10
29
  const TYPST_INTRINSIC_OP = [
11
30
  'dim',
@@ -18,13 +37,13 @@ const TYPST_INTRINSIC_OP = [
18
37
  // 'sgn
19
38
  ];
20
39
 
21
- function tex_token_to_typst(token: string): string {
40
+ function _tex_token_str_to_typst(token: string): string | null {
22
41
  if (/^[a-zA-Z0-9]$/.test(token)) {
23
42
  return token;
24
43
  } else if (token === '/') {
25
44
  return '\\/';
26
- } else if (token === '\\\\') {
27
- return '\\';
45
+ } else if (['\\\\', '\\{', '\\}', '\\%'].includes(token)) {
46
+ return token.substring(1);
28
47
  } else if (['\\$', '\\#', '\\&', '\\_'].includes(token)) {
29
48
  return token;
30
49
  } else if (token.startsWith('\\')) {
@@ -35,63 +54,100 @@ function tex_token_to_typst(token: string): string {
35
54
  // Fall back to the original macro.
36
55
  // This works for \alpha, \beta, \gamma, etc.
37
56
  // If this.nonStrict is true, this also works for all unknown macros.
38
- return symbol;
57
+ return null;
39
58
  }
40
59
  }
41
60
  return token;
42
61
  }
43
62
 
63
+ function tex_token_to_typst(token: TexToken, options: Tex2TypstOptions): TypstToken {
64
+ let token_type: TypstTokenType;
65
+ switch (token.type) {
66
+ case TexTokenType.EMPTY:
67
+ return TypstToken.NONE;
68
+ case TexTokenType.COMMAND:
69
+ token_type = TypstTokenType.SYMBOL;
70
+ break;
71
+ case TexTokenType.ELEMENT:
72
+ token_type = TypstTokenType.ELEMENT;
73
+ break;
74
+ case TexTokenType.LITERAL:
75
+ // This happens, for example, node={type: 'literal', content: 'myop'} as in `\operatorname{myop}`
76
+ token_type = TypstTokenType.LITERAL;
77
+ break;
78
+ case TexTokenType.COMMENT:
79
+ token_type = TypstTokenType.COMMENT;
80
+ break;
81
+ case TexTokenType.SPACE:
82
+ token_type = TypstTokenType.SPACE;
83
+ break;
84
+ case TexTokenType.NEWLINE:
85
+ token_type = TypstTokenType.NEWLINE;
86
+ break;
87
+ case TexTokenType.CONTROL: {
88
+ if (token.value === '\\\\') {
89
+ // \\ -> \
90
+ return new TypstToken(TypstTokenType.CONTROL, '\\');
91
+ } else if (token.value === '\\!') {
92
+ // \! -> #h(-math.thin.amount)
93
+ return new TypstToken(TypstTokenType.SYMBOL, '#h(-math.thin.amount)');
94
+ } else if (symbolMap.has(token.value.substring(1))) {
95
+ // node.content is one of \, \: \;
96
+ const typst_symbol = symbolMap.get(token.value.substring(1))!;
97
+ return new TypstToken(TypstTokenType.SYMBOL, typst_symbol);
98
+ } else {
99
+ throw new Error(`Unknown control sequence: ${token.value}`);
100
+ }
101
+ }
102
+ default:
103
+ throw Error(`Unknown token type: ${token.type}`);
104
+ }
105
+
106
+ const typst_str = _tex_token_str_to_typst(token.value);
107
+ if (typst_str === null) {
108
+ if (options.nonStrict) {
109
+ return new TypstToken(token_type, token.value.substring(1));
110
+ } else {
111
+ throw new ConverterError(`Unknown token: ${token.value}`, token);
112
+ }
113
+ }
114
+ return new TypstToken(token_type, typst_str);
115
+ }
44
116
 
45
117
  // \overset{X}{Y} -> limits(Y)^X
46
118
  // and with special case \overset{\text{def}}{=} -> eq.def
47
- function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
48
- const [sup, base] = node.args!;
119
+ function convert_overset(node: TexFuncCall, options: Tex2TypstOptions): TypstNode {
120
+ const [sup, base] = node.args;
49
121
 
50
122
  if (options.optimize) {
51
- const is_def = (n: TexNode): boolean => {
52
- if (n.eq(new TexNode('text', 'def'))) {
53
- return true;
54
- }
55
- // \overset{def}{=} is also considered as eq.def
56
- if (n.type === 'ordgroup' && n.args!.length === 3) {
57
- const [a1, a2, a3] = n.args!;
58
- const d = new TexNode('element', 'd');
59
- const e = new TexNode('element', 'e');
60
- const f = new TexNode('element', 'f');
61
- if (a1.eq(d) && a2.eq(e) && a3.eq(f)) {
62
- return true;
63
- }
64
- }
65
- return false;
66
- };
67
- const is_eq = (n: TexNode): boolean => n.eq(new TexNode('element', '='));
68
- if (is_def(sup) && is_eq(base)) {
69
- return new TypstNode('symbol', 'eq.def');
123
+ // \overset{\text{def}}{=} or \overset{def}{=} are considered as eq.def
124
+ if (["\\overset{\\text{def}}{=}", "\\overset{d e f}{=}"].includes(node.toString())) {
125
+ return new TypstToken(TypstTokenType.SYMBOL, 'eq.def').toNode();
70
126
  }
71
127
  }
72
- const limits_call = new TypstNode(
73
- 'funcCall',
74
- 'limits',
128
+ const limits_call = new TypstFuncCall(
129
+ new TypstToken(TypstTokenType.SYMBOL, 'limits'),
75
130
  [convert_tex_node_to_typst(base, options)]
76
131
  );
77
- return new TypstNode('supsub', '', [], {
132
+ return new TypstSupsub({
78
133
  base: limits_call,
79
134
  sup: convert_tex_node_to_typst(sup, options),
135
+ sub: null,
80
136
  });
81
137
  }
82
138
 
83
139
  // \underset{X}{Y} -> limits(Y)_X
84
- function convert_underset(node: TexNode, options: Tex2TypstOptions): TypstNode {
85
- const [sub, base] = node.args!;
140
+ function convert_underset(node: TexFuncCall, options: Tex2TypstOptions): TypstNode {
141
+ const [sub, base] = node.args;
86
142
 
87
- const limits_call = new TypstNode(
88
- 'funcCall',
89
- 'limits',
143
+ const limits_call = new TypstFuncCall(
144
+ new TypstToken(TypstTokenType.SYMBOL, 'limits'),
90
145
  [convert_tex_node_to_typst(base, options)]
91
146
  );
92
- return new TypstNode('supsub', '', [], {
147
+ return new TypstSupsub({
93
148
  base: limits_call,
94
149
  sub: convert_tex_node_to_typst(sub, options),
150
+ sup: null,
95
151
  });
96
152
  }
97
153
 
@@ -118,323 +174,290 @@ function convert_tex_array_align_literal(alignLiteral: string): TypstNamedParams
118
174
  augment_str = `#(vline: (${vlinePositions.join(', ')}))`;
119
175
  }
120
176
 
121
- np['augment'] = new TypstNode('literal', augment_str);
177
+ np['augment'] = new TypstToken(TypstTokenType.LITERAL, augment_str).toNode();
122
178
  }
123
179
 
124
180
  const alignments = chars
125
181
  .map(c => alignMap[c])
126
182
  .filter((x) => x !== undefined)
127
- .map(s => new TypstNode('literal', s!));
183
+ .map(s => new TypstToken(TypstTokenType.LITERAL, s!).toNode());
128
184
 
129
185
  if (alignments.length > 0) {
130
186
  const all_same = alignments.every(item => item.eq(alignments[0]));
131
- np['align'] = all_same ? alignments[0] : new TypstNode('literal', '#center');
187
+ np['align'] = all_same ? alignments[0] : new TypstToken(TypstTokenType.LITERAL, '#center').toNode();
132
188
  }
133
189
  return np;
134
190
  }
135
191
 
136
192
 
137
- export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptions = {}): TypstNode {
138
- switch (node.type) {
139
- case 'empty':
140
- return TYPST_NONE;
141
- case 'whitespace':
142
- return new TypstNode('whitespace', node.content);
143
- case 'ordgroup':
144
- return new TypstNode(
145
- 'group',
146
- '',
147
- node.args!.map((n) => convert_tex_node_to_typst(n, options))
148
- );
149
- case 'element':
150
- return new TypstNode('atom', tex_token_to_typst(node.content));
151
- case 'symbol':
152
- return new TypstNode('symbol', tex_token_to_typst(node.content));
193
+ export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2TypstOptions = {}): TypstNode {
194
+ switch (abstractNode.type) {
195
+ case 'terminal': {
196
+ const node = abstractNode as TexTerminal;
197
+ return tex_token_to_typst(node.head, options).toNode();
198
+ }
153
199
  case 'text': {
154
- if ((/[^\x00-\x7F]+/).test(node.content) && options.nonAsciiWrapper !== "") {
155
- return new TypstNode(
156
- 'funcCall',
157
- options.nonAsciiWrapper!,
158
- [new TypstNode('text', node.content)]
200
+ const node = abstractNode as TexText;
201
+ if ((/[^\x00-\x7F]+/).test(node.head.value) && options.nonAsciiWrapper !== "") {
202
+ return new TypstFuncCall(
203
+ new TypstToken(TypstTokenType.SYMBOL, options.nonAsciiWrapper!),
204
+ [new TypstToken(TypstTokenType.TEXT, node.head.value).toNode()]
159
205
  );
160
206
  }
161
- return new TypstNode('text', node.content);
207
+ return new TypstToken(TypstTokenType.TEXT, node.head.value).toNode();
162
208
  }
163
- case 'literal':
164
- // This happens, for example, node={type: 'literal', content: 'myop'} as in `\operatorname{myop}`
165
- return new TypstNode('literal', node.content);
166
- case 'comment':
167
- return new TypstNode('comment', node.content);
209
+ case 'ordgroup':
210
+ const node = abstractNode as TexGroup;
211
+ return new TypstGroup(
212
+ node.items.map((n) => convert_tex_node_to_typst(n, options))
213
+ );
168
214
  case 'supsub': {
169
- let { base, sup, sub } = node.data as TexSupsubData;
215
+ const node = abstractNode as TexSupSub;
216
+ let { base, sup, sub } = node;
170
217
 
171
218
  // special hook for overbrace
172
- if (base && base.type === 'unaryFunc' && base.content === '\\overbrace' && sup) {
173
- return new TypstNode(
174
- 'funcCall',
175
- 'overbrace',
176
- [convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sup, options)]
219
+ // \overbrace{X}^{Y} -> overbrace(X, Y)
220
+ if (base && base.type === 'funcCall' && base.head.value === '\\overbrace' && sup) {
221
+ return new TypstFuncCall(
222
+ new TypstToken(TypstTokenType.SYMBOL, 'overbrace'),
223
+ [convert_tex_node_to_typst((base as TexFuncCall).args[0], options), convert_tex_node_to_typst(sup, options)]
177
224
  );
178
- } else if (base && base.type === 'unaryFunc' && base.content === '\\underbrace' && sub) {
179
- return new TypstNode(
180
- 'funcCall',
181
- 'underbrace',
182
- [convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sub, options)]
225
+ } else if (base && base.type === 'funcCall' && base.head.value === '\\underbrace' && sub) {
226
+ return new TypstFuncCall(
227
+ new TypstToken(TypstTokenType.SYMBOL, 'underbrace'),
228
+ [convert_tex_node_to_typst((base as TexFuncCall).args[0], options), convert_tex_node_to_typst(sub, options)]
183
229
  );
184
230
  }
185
231
 
186
232
  const data: TypstSupsubData = {
187
233
  base: convert_tex_node_to_typst(base, options),
234
+ sup: sup? convert_tex_node_to_typst(sup, options) : null,
235
+ sub: sub? convert_tex_node_to_typst(sub, options) : null,
188
236
  };
189
- if (data.base.type === 'none') {
190
- data.base = new TypstNode('none', '');
191
- }
192
-
193
- if (sup) {
194
- data.sup = convert_tex_node_to_typst(sup, options);
195
- }
196
-
197
- if (sub) {
198
- data.sub = convert_tex_node_to_typst(sub, options);
199
- }
200
237
 
201
- return new TypstNode('supsub', '', [], data);
238
+ return new TypstSupsub(data);
202
239
  }
203
240
  case 'leftright': {
204
- const [left, _body, right] = node.args!;
205
- const [typ_left, typ_body, typ_right] = node.args!.map((n) => convert_tex_node_to_typst(n, options));
241
+ const node = abstractNode as TexLeftRight;
242
+ const { left, right } = node;
243
+
244
+ const typ_body = convert_tex_node_to_typst(node.body, options);
206
245
 
207
246
  if (options.optimize) {
208
247
  // optimization off: "lr(bar.v.double a + 1/2 bar.v.double)"
209
248
  // optimization on : "norm(a + 1/2)"
210
- if (left.content === '\\|' && right.content === '\\|') {
211
- return new TypstNode('funcCall', 'norm', [typ_body]);
212
- }
249
+ if (left !== null && right !== null) {
250
+ const typ_left = tex_token_to_typst(left, options);
251
+ const typ_right = tex_token_to_typst(right, options);
252
+ if (left.value === '\\|' && right.value === '\\|') {
253
+ return new TypstFuncCall(new TypstToken(TypstTokenType.SYMBOL, 'norm'), [typ_body]);
254
+ }
213
255
 
214
- // These pairs will be handled by Typst compiler by default. No need to add lr()
215
- if ([
216
- "[]", "()", "\\{\\}",
217
- "\\lfloor\\rfloor",
218
- "\\lceil\\rceil",
219
- "\\lfloor\\rceil",
220
- ].includes(left.content + right.content)) {
221
- return new TypstNode('group', '', [typ_left, typ_body, typ_right]);
256
+ // These pairs will be handled by Typst compiler by default. No need to add lr()
257
+ if ([
258
+ "[]", "()", "\\{\\}",
259
+ "\\lfloor\\rfloor",
260
+ "\\lceil\\rceil",
261
+ "\\lfloor\\rceil",
262
+ ].includes(left.value + right.value)) {
263
+ return new TypstGroup([typ_left.toNode(), typ_body, typ_right.toNode()]);
264
+ }
222
265
  }
223
266
  }
224
267
 
225
- const group = new TypstNode(
226
- 'group',
227
- '',
228
- [typ_left, typ_body, typ_right]
229
- );
230
-
231
268
  // "\left\{ a + \frac{1}{3} \right." -> "lr(\{ a + 1/3)"
232
- // "\left. a + \frac{1}{3} \right\}" -> "lr( a + \frac{1}{3} \})"
269
+ // "\left. a + \frac{1}{3} \right\}" -> "lr( a + 1/3 \})"
233
270
  // Note that: In lr(), if one side of delimiter doesn't present (i.e. derived from "\\left." or "\\right."),
234
271
  // "(", ")", "{", "[", should be escaped with "\" to be the other side of delimiter.
235
272
  // Simple "lr({ a+1/3)" doesn't compile in Typst.
236
- const escape_curly_or_paren = function(s: string): string {
237
- if (["(", ")", "{", "["].includes(s)) {
238
- return "\\" + s;
273
+ const escape_curly_or_paren = function(s: TypstToken): TypstToken {
274
+ if (["(", ")", "{", "["].includes(s.value)) {
275
+ return new TypstToken(TypstTokenType.ELEMENT, "\\" + s.value);
239
276
  } else {
240
277
  return s;
241
278
  }
242
279
  };
243
- if (right.content === '.') {
244
- typ_left.content = escape_curly_or_paren(typ_left.content);
245
- group.args = [typ_left, typ_body];
246
- } else if (left.content === '.') {
247
- typ_right.content = escape_curly_or_paren(typ_right.content);
248
- group.args = [typ_body, typ_right];
249
- }
250
- return new TypstNode('funcCall', 'lr', [group]);
251
- }
252
- case 'binaryFunc': {
253
- if (node.content === '\\overset') {
254
- return convert_overset(node, options);
255
- }
256
- if (node.content === '\\underset') {
257
- return convert_underset(node, options);
280
+
281
+ let typ_left = left? tex_token_to_typst(left, options) : null;
282
+ let typ_right = right? tex_token_to_typst(right, options) : null;
283
+ if (typ_left === null && typ_right !== null) { // left.
284
+ typ_right = escape_curly_or_paren(typ_right);
258
285
  }
259
- // \frac{a}{b} -> a / b
260
- if (node.content === '\\frac') {
261
- if (options.fracToSlash) {
262
- return new TypstNode(
263
- 'fraction',
264
- '',
265
- node.args!.map((n) => convert_tex_node_to_typst(n, options))
266
- );
267
- }
286
+ if (typ_right === null && typ_left !== null) { // right.
287
+ typ_left = escape_curly_or_paren(typ_left);
268
288
  }
269
- return new TypstNode(
270
- 'funcCall',
271
- tex_token_to_typst(node.content),
272
- node.args!.map((n) => convert_tex_node_to_typst(n, options))
289
+
290
+ return new TypstLeftright(
291
+ new TypstToken(TypstTokenType.SYMBOL, 'lr'),
292
+ { body: typ_body, left: typ_left, right: typ_right }
273
293
  );
274
294
  }
275
- case 'unaryFunc': {
276
- const arg0 = convert_tex_node_to_typst(node.args![0], options);
277
- // \sqrt{3}{x} -> root(3, x)
278
- if (node.content === '\\sqrt' && node.data) {
279
- const data = convert_tex_node_to_typst(node.data as TexSqrtData, options); // the number of times to take the root
280
- return new TypstNode(
281
- 'funcCall',
282
- 'root',
295
+ case 'funcCall': {
296
+ const node = abstractNode as TexFuncCall;
297
+ const arg0 = convert_tex_node_to_typst(node.args[0], options);
298
+ // \sqrt[3]{x} -> root(3, x)
299
+ if (node.head.value === '\\sqrt' && node.data) {
300
+ const data = convert_tex_node_to_typst(node.data, options); // the number of times to take the root
301
+ return new TypstFuncCall(
302
+ new TypstToken(TypstTokenType.SYMBOL, 'root'),
283
303
  [data, arg0]
284
304
  );
285
305
  }
286
306
  // \mathbf{a} -> upright(bold(a))
287
- if (node.content === '\\mathbf') {
288
- const inner: TypstNode = new TypstNode(
289
- 'funcCall',
290
- 'bold',
307
+ if (node.head.value === '\\mathbf') {
308
+ const inner: TypstNode = new TypstFuncCall(
309
+ new TypstToken(TypstTokenType.SYMBOL, 'bold'),
291
310
  [arg0]
292
311
  );
293
- return new TypstNode(
294
- 'funcCall',
295
- 'upright',
312
+ return new TypstFuncCall(
313
+ new TypstToken(TypstTokenType.SYMBOL, 'upright'),
296
314
  [inner]
297
315
  );
298
316
  }
299
317
  // \overrightarrow{AB} -> arrow(A B)
300
- if (node.content === '\\overrightarrow') {
301
- return new TypstNode(
302
- 'funcCall',
303
- 'arrow',
318
+ if (node.head.value === '\\overrightarrow') {
319
+ return new TypstFuncCall(
320
+ new TypstToken(TypstTokenType.SYMBOL, 'arrow'),
304
321
  [arg0]
305
322
  );
306
323
  }
307
324
  // \overleftarrow{AB} -> accent(A B, arrow.l)
308
- if (node.content === '\\overleftarrow') {
309
- return new TypstNode(
310
- 'funcCall',
311
- 'accent',
312
- [arg0, new TypstNode('symbol', 'arrow.l')]
325
+ if (node.head.value === '\\overleftarrow') {
326
+ return new TypstFuncCall(
327
+ new TypstToken(TypstTokenType.SYMBOL, 'accent'),
328
+ [arg0, new TypstToken(TypstTokenType.SYMBOL, 'arrow.l').toNode()]
313
329
  );
314
330
  }
315
331
  // \operatorname{opname} -> op("opname")
316
- if (node.content === '\\operatorname') {
332
+ if (node.head.value === '\\operatorname') {
317
333
  // arg0 must be of type 'literal' in this situation
318
334
  if (options.optimize) {
319
- if (TYPST_INTRINSIC_OP.includes(arg0.content)) {
320
- return new TypstNode('symbol', arg0.content);
335
+ if (TYPST_INTRINSIC_OP.includes(arg0.head.value)) {
336
+ return new TypstToken(TypstTokenType.SYMBOL, arg0.head.value).toNode();
321
337
  }
322
338
  }
323
- return new TypstNode('funcCall', 'op', [new TypstNode('text', arg0.content)]);
339
+ return new TypstFuncCall(new TypstToken(TypstTokenType.SYMBOL, 'op'), [new TypstToken(TypstTokenType.TEXT, arg0.head.value).toNode()]);
340
+ }
341
+
342
+ // \textcolor{red}{2y} -> #text(fill: red)[$2y$]
343
+ if (node.head.value === '\\textcolor') {
344
+ const res = new TypstMarkupFunc(
345
+ new TypstToken(TypstTokenType.SYMBOL, `#text`),
346
+ [convert_tex_node_to_typst(node.args[1], options)]
347
+ );
348
+ res.setOptions({ fill: arg0 });
349
+ return res;
324
350
  }
325
351
 
326
352
  // \substack{a \\ b} -> `a \ b`
327
353
  // as in translation from \sum_{\substack{a \\ b}} to sum_(a \ b)
328
- if (node.content === '\\substack') {
354
+ if (node.head.value === '\\substack') {
329
355
  return arg0;
330
356
  }
331
357
 
358
+ if (node.head.value === '\\overset') {
359
+ return convert_overset(node, options);
360
+ }
361
+ if (node.head.value === '\\underset') {
362
+ return convert_underset(node, options);
363
+ }
364
+
365
+ // \frac{a}{b} -> a / b
366
+ if (node.head.value === '\\frac') {
367
+ if (options.fracToSlash) {
368
+ return new TypstFraction(node.args.map((n) => convert_tex_node_to_typst(n, options)));
369
+ }
370
+ }
371
+
332
372
  if(options.optimize) {
333
373
  // \mathbb{R} -> RR
334
- if (node.content === '\\mathbb' && arg0.type === 'atom' && /^[A-Z]$/.test(arg0.content)) {
335
- return new TypstNode('symbol', arg0.content + arg0.content);
374
+ if (node.head.value === '\\mathbb' && /^\\mathbb{[A-Z]}$/.test(node.toString())) {
375
+ return new TypstToken(TypstTokenType.SYMBOL, arg0.head.value.repeat(2)).toNode();
336
376
  }
337
377
  // \mathrm{d} -> dif
338
- if (node.content === '\\mathrm' && arg0.eq(new TypstNode('atom', 'd'))) {
339
- return new TypstNode('symbol', 'dif');
378
+ if (node.head.value === '\\mathrm' && node.toString() === '\\mathrm{d}') {
379
+ return new TypstToken(TypstTokenType.SYMBOL, 'dif').toNode();
340
380
  }
341
381
  }
342
382
 
343
383
  // generic case
344
- return new TypstNode(
345
- 'funcCall',
346
- tex_token_to_typst(node.content),
347
- node.args!.map((n) => convert_tex_node_to_typst(n, options))
384
+ return new TypstFuncCall(
385
+ tex_token_to_typst(node.head, options),
386
+ node.args.map((n) => convert_tex_node_to_typst(n, options))
348
387
  );
349
388
  }
350
389
  case 'beginend': {
351
- const matrix = node.data as TexNode[][];
352
- const data = matrix.map((row) => row.map((n) => convert_tex_node_to_typst(n, options)));
390
+ const node = abstractNode as TexBeginEnd;
391
+ const matrix = node.matrix.map((row) => row.map((n) => convert_tex_node_to_typst(n, options)));
353
392
 
354
- if (node.content.startsWith('align')) {
393
+ if (node.head.value.startsWith('align')) {
355
394
  // align, align*, alignat, alignat*, aligned, etc.
356
- return new TypstNode('align', '', [], data);
395
+ return new TypstMatrixLike(null, matrix);
357
396
  }
358
- if (node.content === 'cases') {
359
- return new TypstNode('cases', '', [], data);
397
+ if (node.head.value === 'cases') {
398
+ return new TypstMatrixLike(TypstMatrixLike.CASES, matrix);
360
399
  }
361
- if (node.content === 'subarray') {
362
- const align_node = node.args![0];
363
- switch (align_node.content) {
364
- case 'r':
365
- data.forEach(row => row[0].args!.push(new TypstNode('symbol', '&')));
366
- break;
367
- case 'l':
368
- data.forEach(row => row[0].args!.unshift(new TypstNode('symbol', '&')));
369
- break;
370
- default:
371
- break;
400
+ if (node.head.value === 'subarray') {
401
+ if (node.data) {
402
+ const align_node = node.data;
403
+ switch (align_node.head.value) {
404
+ case 'r':
405
+ matrix.forEach(row => (row[0] as TypstGroup).items.push(new TypstToken(TypstTokenType.CONTROL, '&').toNode()));
406
+ break;
407
+ case 'l':
408
+ matrix.forEach(row => (row[0] as TypstGroup).items.unshift(new TypstToken(TypstTokenType.CONTROL, '&').toNode()));
409
+ break;
410
+ default:
411
+ break;
412
+ }
372
413
  }
373
- return new TypstNode('align', '', [], data);
414
+ return new TypstMatrixLike(null, matrix);
374
415
  }
375
- if (node.content === 'array') {
416
+ if (node.head.value === 'array') {
376
417
  const np: TypstNamedParams = { 'delim': TYPST_NONE };
377
418
 
378
- assert(node.args!.length > 0 && node.args![0].type === 'literal');
379
- const np_new = convert_tex_array_align_literal(node.args![0].content);
419
+ assert(node.data !== null && node.head.type === TexTokenType.LITERAL);
420
+ const np_new = convert_tex_array_align_literal(node.data!.head.value);
380
421
  Object.assign(np, np_new);
381
422
 
382
- const res = new TypstNode('matrix', '', [], data);
423
+ const res = new TypstMatrixLike(TypstMatrixLike.MAT, matrix);
383
424
  res.setOptions(np);
384
425
  return res;
385
426
  }
386
- if (node.content.endsWith('matrix')) {
387
- const res = new TypstNode('matrix', '', [], data);
388
- let delim: TypstNode;
389
- switch (node.content) {
427
+ if (node.head.value.endsWith('matrix')) {
428
+ const res = new TypstMatrixLike(TypstMatrixLike.MAT, matrix);
429
+ let delim: TypstToken;
430
+ switch (node.head.value) {
390
431
  case 'matrix':
391
- delim = TYPST_NONE;
432
+ delim = TypstToken.NONE;
392
433
  break;
393
434
  case 'pmatrix':
394
- // delim = new TypstNode('text', '(');
435
+ // delim = new TypstToken(TypstTokenType.TEXT, '(');
395
436
  // break;
396
437
  return res; // typst mat use delim:"(" by default
397
438
  case 'bmatrix':
398
- delim = new TypstNode('text', '[');
439
+ delim = new TypstToken(TypstTokenType.TEXT, '[');
399
440
  break;
400
441
  case 'Bmatrix':
401
- delim = new TypstNode('text', '{');
442
+ delim = new TypstToken(TypstTokenType.TEXT, '{');
402
443
  break;
403
444
  case 'vmatrix':
404
- delim = new TypstNode('text', '|');
445
+ delim = new TypstToken(TypstTokenType.TEXT, '|');
405
446
  break;
406
447
  case 'Vmatrix': {
407
- delim = new TypstNode('symbol', 'bar.v.double');
448
+ delim = new TypstToken(TypstTokenType.SYMBOL, 'bar.v.double');
408
449
  break;
409
450
  }
410
451
  default:
411
- throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
452
+ throw new ConverterError(`Unimplemented beginend: ${node.head}`, node);
412
453
  }
413
- res.setOptions({ 'delim': delim });
454
+ res.setOptions({ 'delim': delim.toNode()});
414
455
  return res;
415
456
  }
416
- throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
457
+ throw new ConverterError(`Unimplemented beginend: ${node.head}`, node);
417
458
  }
418
- case 'unknownMacro':
419
- return new TypstNode('unknown', tex_token_to_typst(node.content));
420
- case 'control':
421
- if (node.content === '\\\\') {
422
- // \\ -> \
423
- return new TypstNode('symbol', '\\');
424
- } else if (node.content === '\\!') {
425
- // \! -> #h(-math.thin.amount)
426
- return new TypstNode('funcCall', '#h', [
427
- new TypstNode('literal', '-math.thin.amount')
428
- ]);
429
- } else if (symbolMap.has(node.content.substring(1))) {
430
- // node.content is one of \, \: \;
431
- const typst_symbol = symbolMap.get(node.content.substring(1))!;
432
- return new TypstNode('symbol', typst_symbol);
433
- } else {
434
- throw new TypstWriterError(`Unknown control sequence: ${node.content}`, node);
435
- }
436
459
  default:
437
- throw new TypstWriterError(`Unimplemented node type: ${node.type}`, node);
460
+ throw new ConverterError(`Unimplemented node type: ${abstractNode.type}`, abstractNode);
438
461
  }
439
462
  }
440
463
 
@@ -472,263 +495,332 @@ const TYPST_BINARY_FUNCTIONS: string[] = [
472
495
  ];
473
496
  */
474
497
 
475
- function apply_escape_if_needed(c: string) {
476
- if (['{', '}', '%'].includes(c)) {
477
- return '\\' + c;
498
+ function apply_escape_if_needed(c: TexToken): TexToken {
499
+ if (['{', '}', '%'].includes(c.value)) {
500
+ return new TexToken(TexTokenType.ELEMENT, '\\' + c.value);
478
501
  }
479
502
  return c;
480
503
  }
481
504
 
482
- function typst_token_to_tex(token: string): string {
483
- if (/^[a-zA-Z0-9]$/.test(token)) {
484
- return token;
485
- } else if (reverseSymbolMap.has(token)) {
486
- return '\\' + reverseSymbolMap.get(token)!;
505
+
506
+ function typst_token_to_tex(token: TypstToken): TexToken {
507
+ switch (token.type) {
508
+ case TypstTokenType.NONE:
509
+ // e.g. Typst `#none^2` is converted to TeX `^2`
510
+ return TexToken.EMPTY;
511
+ case TypstTokenType.SYMBOL: {
512
+ const _typst_symbol_to_tex = function(symbol: string): string {
513
+ if (reverseSymbolMap.has(symbol)) {
514
+ return '\\' + reverseSymbolMap.get(symbol)!;
515
+ } else {
516
+ return '\\' + symbol;
517
+ }
518
+ }
519
+ return new TexToken(TexTokenType.COMMAND, _typst_symbol_to_tex(token.value));
520
+ }
521
+ case TypstTokenType.ELEMENT: {
522
+ let value: string;
523
+ if (['{', '}', '%'].includes(token.value)) {
524
+ value = '\\' + token.value;
525
+ } else {
526
+ value = token.value;
527
+ }
528
+ return new TexToken(TexTokenType.ELEMENT, value);
529
+ }
530
+ case TypstTokenType.LITERAL:
531
+ return new TexToken(TexTokenType.LITERAL, token.value);
532
+ case TypstTokenType.TEXT:
533
+ return new TexToken(TexTokenType.LITERAL, token.value);
534
+ case TypstTokenType.COMMENT:
535
+ return new TexToken(TexTokenType.COMMENT, token.value);
536
+ case TypstTokenType.SPACE:
537
+ return new TexToken(TexTokenType.SPACE, token.value);
538
+ case TypstTokenType.CONTROL: {
539
+ let value: string;
540
+ switch(token.value) {
541
+ case '\\':
542
+ value = '\\\\';
543
+ break;
544
+ case '&':
545
+ value = '&';
546
+ break;
547
+ default:
548
+ throw new Error(`[typst_token_to_tex]Unimplemented control sequence: ${token.value}`);
549
+ }
550
+ return new TexToken(TexTokenType.CONTROL, value);
551
+ }
552
+ case TypstTokenType.NEWLINE:
553
+ return new TexToken(TexTokenType.NEWLINE, token.value);
554
+ default:
555
+ throw new Error(`Unimplemented token type: ${token.type}`);
487
556
  }
488
- return '\\' + token;
489
557
  }
490
558
 
491
559
 
492
- const TEX_NODE_COMMA = new TexNode('element', ',');
560
+ const TEX_NODE_COMMA = new TexToken(TexTokenType.ELEMENT, ',').toNode();
493
561
 
494
- export function convert_typst_node_to_tex(node: TypstNode): TexNode {
495
- // special hook for eq.def
496
- if (node.eq(new TypstNode('symbol', 'eq.def'))) {
497
- return new TexNode('binaryFunc', '\\overset', [
498
- new TexNode('text', 'def'),
499
- new TexNode('element', '=')
500
- ]);
501
- }
502
- switch (node.type) {
503
- case 'none':
504
- // e.g. Typst `#none^2` is converted to TeX `^2`
505
- return new TexNode('empty', '');
506
- case 'whitespace':
507
- return new TexNode('whitespace', node.content);
508
- case 'atom':
509
- return new TexNode('element', node.content);
510
- case 'symbol': {
511
- // special hook for comma
512
- if(node.content === 'comma') {
513
- return new TexNode('element', ',');
514
- }
515
- // special hook for dif
516
- if(node.content === 'dif') {
517
- return new TexNode('unaryFunc', '\\mathrm', [new TexNode('element', 'd')]);
518
- }
519
- // special hook for hyph and hyph.minus
520
- if(node.content === 'hyph' || node.content === 'hyph.minus') {
521
- return new TexNode('text', '-');
522
- }
523
- // special hook for mathbb{R} <-- RR
524
- if(/^([A-Z])\1$/.test(node.content)) {
525
- return new TexNode('unaryFunc', '\\mathbb', [
526
- new TexNode('element', node.content[0])
527
- ]);
528
- }
529
- return new TexNode('symbol', typst_token_to_tex(node.content));
562
+ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
563
+ switch (abstractNode.type) {
564
+ case 'terminal': {
565
+ const node = abstractNode as TypstTerminal;
566
+ if (node.head.type === TypstTokenType.SYMBOL) {
567
+ // special hook for eq.def
568
+ if (node.head.value === 'eq.def') {
569
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [
570
+ new TexText(new TexToken(TexTokenType.LITERAL, 'def')),
571
+ new TexToken(TexTokenType.ELEMENT, '=').toNode()
572
+ ]);
573
+ }
574
+ // special hook for comma
575
+ if(node.head.value === 'comma') {
576
+ return new TexToken(TexTokenType.ELEMENT, ',').toNode();
577
+ }
578
+ // special hook for dif
579
+ if(node.head.value === 'dif') {
580
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\mathrm'), [new TexToken(TexTokenType.ELEMENT, 'd').toNode()]);
581
+ }
582
+ // special hook for hyph and hyph.minus
583
+ if(node.head.value === 'hyph' || node.head.value === 'hyph.minus') {
584
+ return new TexText(new TexToken(TexTokenType.LITERAL, '-'));
585
+ }
586
+ // special hook for mathbb{R} <-- RR
587
+ if(/^([A-Z])\1$/.test(node.head.value)) {
588
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\mathbb'), [
589
+ new TexToken(TexTokenType.ELEMENT, node.head.value[0]).toNode()
590
+ ]);
591
+ }
592
+ }
593
+ if (node.head.type === TypstTokenType.TEXT) {
594
+ return new TexText(new TexToken(TexTokenType.LITERAL, node.head.value));
595
+ }
596
+ return typst_token_to_tex(node.head).toNode();
530
597
  }
531
- case 'literal':
532
- return new TexNode('literal', node.content);
533
- case 'text':
534
- return new TexNode('text', node.content);
535
- case 'comment':
536
- return new TexNode('comment', node.content);
598
+
537
599
  case 'group': {
538
- const args = node.args!.map(convert_typst_node_to_tex);
539
- if (node.content === 'parenthesis') {
540
- const is_over_high = node.isOverHigh();
541
- const left_delim = is_over_high ? '\\left(' : '(';
542
- const right_delim = is_over_high ? '\\right)' : ')';
543
- args.unshift(new TexNode('element', left_delim));
544
- args.push(new TexNode('element', right_delim));
545
- }
546
- return new TexNode('ordgroup', node.content, args);
600
+ const node = abstractNode as TypstGroup;
601
+ const args = node.items.map(convert_typst_node_to_tex);
602
+ const alignment_char = new TexToken(TexTokenType.CONTROL, '&').toNode();
603
+ const newline_char = new TexToken(TexTokenType.CONTROL, '\\\\').toNode();
604
+ if (array_includes(args, alignment_char)) {
605
+ // wrap the whole math formula with \begin{aligned} and \end{aligned}
606
+ const rows = array_split(args, newline_char);
607
+ const matrix: TexNode[][] = [];
608
+ for(const row of rows) {
609
+ const cells = array_split(row, alignment_char);
610
+ matrix.push(cells.map(cell => new TexGroup(cell)));
611
+ }
612
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'aligned'), matrix);
613
+ }
614
+ return new TexGroup(args);
615
+ }
616
+ case 'leftright': {
617
+ const node = abstractNode as TypstLeftright;
618
+ const body = convert_typst_node_to_tex(node.body);
619
+ let left = node.left? typst_token_to_tex(node.left) : new TexToken(TexTokenType.ELEMENT, '.');
620
+ let right = node.right? typst_token_to_tex(node.right) : new TexToken(TexTokenType.ELEMENT, '.');
621
+ // const is_over_high = node.isOverHigh();
622
+ // const left_delim = is_over_high ? '\\left(' : '(';
623
+ // const right_delim = is_over_high ? '\\right)' : ')';
624
+ if (node.isOverHigh()) {
625
+ left.value = '\\left' + left.value;
626
+ right.value = '\\right' + right.value;
627
+ }
628
+ // TODO: should be TeXLeftRight(...)
629
+ // But currently writer will output `\left |` while people commonly prefer `\left|`.
630
+ return new TexGroup([left.toNode(), body, right.toNode()]);
547
631
  }
548
632
  case 'funcCall': {
549
- // special hook for lr
550
- if (node.content === 'lr') {
551
- const data = node.data as TypstLrData;
552
- if (data.leftDelim !== null) {
553
- let left_delim = apply_escape_if_needed(data.leftDelim);
554
- assert(data.rightDelim !== null, "leftDelim has value but rightDelim not");
555
- let right_delim = apply_escape_if_needed(data.rightDelim!);
556
- // TODO: should be TeXNode('leftright', ...)
557
- // But currently writer will output `\left |` while people commonly prefer `\left|`.
558
- return new TexNode('ordgroup', '', [
559
- new TexNode('element', '\\left' + left_delim),
560
- ...node.args!.map(convert_typst_node_to_tex),
561
- new TexNode('element', '\\right' + right_delim)
562
- ]);
563
- } else {
564
- return new TexNode('ordgroup', '', node.args!.map(convert_typst_node_to_tex));
633
+ const node = abstractNode as TypstFuncCall;
634
+ switch (node.head.value) {
635
+ // special hook for norm
636
+ // `\| a \|` <- `norm(a)`
637
+ // `\left\| a + \frac{1}{3} \right\|` <- `norm(a + 1/3)`
638
+ case 'norm': {
639
+ const arg0 = node.args[0];
640
+ const body = convert_typst_node_to_tex(arg0);
641
+ if (node.isOverHigh()) {
642
+ return new TexLeftRight({
643
+ body: body,
644
+ left: new TexToken(TexTokenType.COMMAND, "\\|"),
645
+ right: new TexToken(TexTokenType.COMMAND, "\\|")
646
+ });
647
+ } else {
648
+ return body;
649
+ }
650
+ }
651
+ // special hook for floor, ceil
652
+ // `\lfloor a \rfloor` <- `floor(a)`
653
+ // `\lceil a \rceil` <- `ceil(a)`
654
+ // `\left\lfloor a \right\rfloor` <- `floor(a)`
655
+ // `\left\lceil a \right\rceil` <- `ceil(a)`
656
+ case 'floor':
657
+ case 'ceil': {
658
+ const left = "\\l" + node.head.value;
659
+ const right = "\\r" + node.head.value;
660
+ const arg0 = node.args[0];
661
+ const body = convert_typst_node_to_tex(arg0);
662
+ const left_node = new TexToken(TexTokenType.COMMAND, left);
663
+ const right_node = new TexToken(TexTokenType.COMMAND, right);
664
+ if (node.isOverHigh()) {
665
+ return new TexLeftRight({
666
+ body: body,
667
+ left: left_node,
668
+ right: right_node
669
+ });
670
+ } else {
671
+ return new TexGroup([left_node.toNode(), body, right_node.toNode()]);
672
+ }
673
+ }
674
+ // special hook for root
675
+ case 'root': {
676
+ const [degree, radicand] = node.args;
677
+ const data = convert_typst_node_to_tex(degree);
678
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\sqrt'), [convert_typst_node_to_tex(radicand)], data);
679
+ }
680
+ // special hook for overbrace and underbrace
681
+ case 'overbrace':
682
+ case 'underbrace': {
683
+ const [body, label] = node.args;
684
+ const base = new TexFuncCall(typst_token_to_tex(node.head), [convert_typst_node_to_tex(body)]);
685
+ const script = convert_typst_node_to_tex(label);
686
+ const data = node.head.value === 'overbrace' ? { base, sup: script, sub: null } : { base, sub: script, sup: null };
687
+ return new TexSupSub(data);
688
+ }
689
+ // special hook for vec
690
+ // "vec(a, b, c)" -> "\begin{pmatrix}a\\ b\\ c\end{pmatrix}"
691
+ case 'vec': {
692
+ const tex_matrix = node.args.map(convert_typst_node_to_tex).map((n) => [n]);
693
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'pmatrix'), tex_matrix);
694
+ }
695
+ // special hook for op
696
+ case 'op': {
697
+ const arg0 = node.args[0];
698
+ assert(arg0.head.type === TypstTokenType.TEXT);
699
+ return new TexFuncCall(typst_token_to_tex(node.head), [new TexToken(TexTokenType.LITERAL, arg0.head.value).toNode()]);
700
+ }
701
+ // general case
702
+ default: {
703
+ const func_name_tex = typst_token_to_tex(node.head);
704
+ const is_known_func = TEX_UNARY_COMMANDS.includes(func_name_tex.value.substring(1))
705
+ || TEX_BINARY_COMMANDS.includes(func_name_tex.value.substring(1));
706
+ if (func_name_tex.value.length > 0 && is_known_func) {
707
+ return new TexFuncCall(func_name_tex, node.args.map(convert_typst_node_to_tex));
708
+ } else {
709
+ return new TexGroup([
710
+ typst_token_to_tex(node.head).toNode(),
711
+ new TexToken(TexTokenType.ELEMENT, '(').toNode(),
712
+ ...array_intersperse(node.args.map(convert_typst_node_to_tex), TEX_NODE_COMMA),
713
+ new TexToken(TexTokenType.ELEMENT, ')').toNode()
714
+ ]);
715
+ }
565
716
  }
566
717
  }
567
- // special hook for norm
568
- // `\| a \|` <- `norm(a)`
569
- // `\left\| a + \frac{1}{3} \right\|` <- `norm(a + 1/3)`
570
- if (node.content === 'norm') {
571
- const arg0 = node.args![0];
572
- const tex_node_type = node.isOverHigh() ? 'leftright' : 'ordgroup';
573
- return new TexNode(tex_node_type, '', [
574
- new TexNode('symbol', "\\|"),
575
- convert_typst_node_to_tex(arg0),
576
- new TexNode('symbol', "\\|")
577
- ]);
578
- }
579
- // special hook for floor, ceil
580
- // `\lfloor a \rfloor` <- `floor(a)`
581
- // `\lceil a \rceil` <- `ceil(a)`
582
- // `\left\lfloor a \right\rfloor` <- `floor(a)`
583
- // `\left\lceil a \right\rceil` <- `ceil(a)`
584
- if (node.content === 'floor' || node.content === 'ceil') {
585
- const left = "\\l" + node.content;
586
- const right = "\\r" + node.content;
587
- const arg0 = node.args![0];
588
- const tex_node_type = node.isOverHigh() ? 'leftright' : 'ordgroup';
589
- return new TexNode(tex_node_type, '', [
590
- new TexNode('symbol', left),
591
- convert_typst_node_to_tex(arg0),
592
- new TexNode('symbol', right)
593
- ]);
594
- }
595
- // special hook for root
596
- if (node.content === 'root') {
597
- const [degree, radicand] = node.args!;
598
- const data: TexSqrtData = convert_typst_node_to_tex(degree);
599
- return new TexNode('unaryFunc', '\\sqrt', [convert_typst_node_to_tex(radicand)], data);
600
- }
601
- // special hook for overbrace and underbrace
602
- if (node.content === 'overbrace' || node.content === 'underbrace') {
603
- const [body, label] = node.args!;
604
- const base = new TexNode('unaryFunc', '\\' + node.content, [convert_typst_node_to_tex(body)]);
605
- const script = convert_typst_node_to_tex(label);
606
- const data = node.content === 'overbrace' ? { base, sup: script } : { base, sub: script };
607
- return new TexNode('supsub', '', [], data);
608
- }
609
-
610
- // special hook for vec
611
- // "vec(a, b, c)" -> "\begin{pmatrix}a\\ b\\ c\end{pmatrix}"
612
- if (node.content === 'vec') {
613
- const tex_data = node.args!.map(convert_typst_node_to_tex).map((n) => [n]) as TexArrayData;
614
- return new TexNode('beginend', 'pmatrix', [], tex_data);
615
- }
616
-
617
- // general case
618
- const func_name_tex = typst_token_to_tex(node.content);
619
- if (func_name_tex.length > 0 && TEX_UNARY_COMMANDS.includes(func_name_tex.substring(1))) {
620
- return new TexNode('unaryFunc', func_name_tex, node.args!.map(convert_typst_node_to_tex));
621
- } else if (func_name_tex.length > 0 && TEX_BINARY_COMMANDS.includes(func_name_tex.substring(1))) {
622
- return new TexNode('binaryFunc', func_name_tex, node.args!.map(convert_typst_node_to_tex));
623
- } else {
624
- return new TexNode('ordgroup', '', [
625
- new TexNode('symbol', typst_token_to_tex(node.content)),
626
- new TexNode('element', '('),
627
- ...array_intersperse(node.args!.map(convert_typst_node_to_tex), TEX_NODE_COMMA),
628
- new TexNode('element', ')')
629
- ]);
718
+ }
719
+ case 'markupFunc': {
720
+ const node = abstractNode as TypstMarkupFunc;
721
+ switch (node.head.value) {
722
+ case '#text': {
723
+ // `\textcolor{red}{2y}` <- `#text(fill: red)[$2 y$]`
724
+ if (node.options && node.options['fill']) {
725
+ const color = node.options['fill'];
726
+ return new TexFuncCall(
727
+ new TexToken(TexTokenType.COMMAND, '\\textcolor'),
728
+ [convert_typst_node_to_tex(color), convert_typst_node_to_tex(node.fragments[0])]
729
+ )
730
+ }
731
+ }
732
+ case '#heading':
733
+ default:
734
+ throw new Error(`Unimplemented markup function: ${node.head.value}`);
630
735
  }
631
736
  }
632
737
  case 'supsub': {
633
- const { base, sup, sub } = node.data as TypstSupsubData;
634
- let sup_tex: TexNode | undefined;
635
- let sub_tex: TexNode | undefined;
636
-
637
- if (sup) {
638
- sup_tex = convert_typst_node_to_tex(sup);
639
- }
640
- if (sub) {
641
- sub_tex = convert_typst_node_to_tex(sub);
642
- }
738
+ const node = abstractNode as TypstSupsub;
739
+ const { base, sup, sub } = node;
740
+ const sup_tex = sup? convert_typst_node_to_tex(sup) : null;
741
+ const sub_tex = sub? convert_typst_node_to_tex(sub) : null;
643
742
 
644
743
  // special hook for limits
645
744
  // `limits(+)^a` -> `\overset{a}{+}`
646
745
  // `limits(+)_a` -> `\underset{a}{+}`
647
746
  // `limits(+)_a^b` -> `\overset{b}{\underset{a}{+}}`
648
- if (base.eq(new TypstNode('funcCall', 'limits'))) {
649
- const body_in_limits = convert_typst_node_to_tex(base.args![0]);
650
- if (sup_tex !== undefined && sub_tex === undefined) {
651
- return new TexNode('binaryFunc', '\\overset', [sup_tex, body_in_limits]);
652
- } else if (sup_tex === undefined && sub_tex !== undefined) {
653
- return new TexNode('binaryFunc', '\\underset', [sub_tex, body_in_limits]);
747
+ if (base.head.eq(new TypstToken(TypstTokenType.SYMBOL, 'limits'))) {
748
+ const limits = base as TypstFuncCall;
749
+ const body_in_limits = convert_typst_node_to_tex(limits.args[0]);
750
+ if (sup_tex !== null && sub_tex === null) {
751
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [sup_tex, body_in_limits]);
752
+ } else if (sup_tex === null && sub_tex !== null) {
753
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\underset'), [sub_tex, body_in_limits]);
654
754
  } else {
655
- const underset_call = new TexNode('binaryFunc', '\\underset', [sub_tex!, body_in_limits]);
656
- return new TexNode('binaryFunc', '\\overset', [sup_tex!, underset_call]);
755
+ const underset_call = new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\underset'), [sub_tex!, body_in_limits]);
756
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [sup_tex!, underset_call]);
657
757
  }
658
758
  }
659
759
 
660
760
  const base_tex = convert_typst_node_to_tex(base);
661
761
 
662
- const res = new TexNode('supsub', '', [], {
762
+ const res = new TexSupSub({
663
763
  base: base_tex,
664
764
  sup: sup_tex,
665
765
  sub: sub_tex
666
766
  });
667
767
  return res;
668
768
  }
669
- case 'matrix': {
670
- const typst_data = node.data as TypstNode[][];
671
- const tex_data = typst_data.map(row => row.map(convert_typst_node_to_tex));
672
- let env_type = 'pmatrix'; // typst mat use delim:"(" by default
673
- if (node.options) {
674
- if ('delim' in node.options) {
675
- const delim = node.options.delim;
676
- switch (delim.content) {
677
- case '#none':
678
- env_type = 'matrix';
679
- break;
680
- case '[':
681
- case ']':
682
- env_type = 'bmatrix';
683
- break;
684
- case '(':
685
- case ')':
686
- env_type = 'pmatrix';
687
- break;
688
- case '{':
689
- case '}':
690
- env_type = 'Bmatrix';
691
- break;
692
- case '|':
693
- env_type = 'vmatrix';
694
- break;
695
- case 'bar':
696
- case 'bar.v':
697
- env_type = 'vmatrix';
698
- break;
699
- case 'bar.v.double':
700
- env_type = 'Vmatrix';
701
- break;
702
- default:
703
- throw new Error(`Unexpected delimiter ${delim.content}`);
769
+ case 'matrixLike': {
770
+ const node = abstractNode as TypstMatrixLike;
771
+ const tex_matrix = node.matrix.map(row => row.map(convert_typst_node_to_tex));
772
+ if (node.head.eq(TypstMatrixLike.MAT)) {
773
+ let env_type = 'pmatrix'; // typst mat use delim:"(" by default
774
+ if (node.options) {
775
+ if ('delim' in node.options) {
776
+ const delim = node.options.delim;
777
+ switch (delim.head.value) {
778
+ case '#none':
779
+ env_type = 'matrix';
780
+ break;
781
+ case '[':
782
+ case ']':
783
+ env_type = 'bmatrix';
784
+ break;
785
+ case '(':
786
+ case ')':
787
+ env_type = 'pmatrix';
788
+ break;
789
+ case '{':
790
+ case '}':
791
+ env_type = 'Bmatrix';
792
+ break;
793
+ case '|':
794
+ env_type = 'vmatrix';
795
+ break;
796
+ case 'bar':
797
+ case 'bar.v':
798
+ env_type = 'vmatrix';
799
+ break;
800
+ case 'bar.v.double':
801
+ env_type = 'Vmatrix';
802
+ break;
803
+ default:
804
+ throw new Error(`Unexpected delimiter ${delim.head}`);
805
+ }
704
806
  }
705
807
  }
706
- }
707
- return new TexNode('beginend', env_type, [], tex_data);
708
- }
709
- case 'cases': {
710
- const typst_data = node.data as TypstNode[][];
711
- const tex_data = typst_data.map(row => row.map(convert_typst_node_to_tex));
712
- return new TexNode('beginend', 'cases', [], tex_data);
713
- }
714
- case 'control': {
715
- switch (node.content) {
716
- case '\\':
717
- return new TexNode('control', '\\\\');
718
- case '&':
719
- return new TexNode('control', '&');
720
- default:
721
- throw new Error('[convert_typst_node_to_tex] Unimplemented control: ' + node.content);
808
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, env_type), tex_matrix);
809
+ } else if (node.head.eq(TypstMatrixLike.CASES)) {
810
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'cases'), tex_matrix);
811
+ } else {
812
+ throw new Error(`Unexpected matrix type ${node.head}`);
722
813
  }
723
814
  }
724
815
  case 'fraction': {
725
- const [numerator, denominator] = node.args!;
816
+ const node = abstractNode as TypstFraction;
817
+ const [numerator, denominator] = node.args;
726
818
  const num_tex = convert_typst_node_to_tex(numerator);
727
819
  const den_tex = convert_typst_node_to_tex(denominator);
728
- return new TexNode('binaryFunc', '\\frac', [num_tex, den_tex]);
820
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\frac'), [num_tex, den_tex]);
729
821
  }
730
822
  default:
731
- throw new Error('[convert_typst_node_to_tex] Unimplemented type: ' + node.type);
823
+ throw new Error('[convert_typst_node_to_tex] Unimplemented type: ' + abstractNode.type);
732
824
  }
733
825
  }
734
826