tex2typst 0.3.23 → 0.3.24

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 { TypstAlign, TypstCases, TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstMatrix, 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,12 +54,65 @@ 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
@@ -48,35 +120,19 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
48
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
 
@@ -84,14 +140,14 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
84
140
  function convert_underset(node: TexNode, options: Tex2TypstOptions): TypstNode {
85
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,280 @@ 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.args!.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',
219
+ if (base && base.type === 'funcCall' && base.head.value === '\\overbrace' && sup) {
220
+ return new TypstFuncCall(
221
+ new TypstToken(TypstTokenType.SYMBOL, 'overbrace'),
176
222
  [convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sup, options)]
177
223
  );
178
- } else if (base && base.type === 'unaryFunc' && base.content === '\\underbrace' && sub) {
179
- return new TypstNode(
180
- 'funcCall',
181
- 'underbrace',
224
+ } else if (base && base.type === 'funcCall' && base.head.value === '\\underbrace' && sub) {
225
+ return new TypstFuncCall(
226
+ new TypstToken(TypstTokenType.SYMBOL, 'underbrace'),
182
227
  [convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sub, options)]
183
228
  );
184
229
  }
185
230
 
186
231
  const data: TypstSupsubData = {
187
232
  base: convert_tex_node_to_typst(base, options),
233
+ sup: sup? convert_tex_node_to_typst(sup, options) : null,
234
+ sub: sub? convert_tex_node_to_typst(sub, options) : null,
188
235
  };
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
236
 
197
- if (sub) {
198
- data.sub = convert_tex_node_to_typst(sub, options);
199
- }
200
-
201
- return new TypstNode('supsub', '', [], data);
237
+ return new TypstSupsub(data);
202
238
  }
203
239
  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));
240
+ const node = abstractNode as TexLeftRight;
241
+ const { left, right } = node;
242
+ const [_body] = node.args!;
243
+ // const [typ_left, typ_body, typ_right] = node.args!.map((n) => convert_tex_node_to_typst(n, options));
244
+ const typ_body = convert_tex_node_to_typst(_body, options);
245
+
246
+
206
247
 
207
248
  if (options.optimize) {
208
249
  // optimization off: "lr(bar.v.double a + 1/2 bar.v.double)"
209
250
  // optimization on : "norm(a + 1/2)"
210
- if (left.content === '\\|' && right.content === '\\|') {
211
- return new TypstNode('funcCall', 'norm', [typ_body]);
212
- }
251
+ if (left !== null && right !== null) {
252
+ const typ_left = tex_token_to_typst(left, options);
253
+ const typ_right = tex_token_to_typst(right, options);
254
+ if (left.value === '\\|' && right.value === '\\|') {
255
+ return new TypstFuncCall(new TypstToken(TypstTokenType.SYMBOL, 'norm'), [typ_body]);
256
+ }
213
257
 
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]);
258
+ // These pairs will be handled by Typst compiler by default. No need to add lr()
259
+ if ([
260
+ "[]", "()", "\\{\\}",
261
+ "\\lfloor\\rfloor",
262
+ "\\lceil\\rceil",
263
+ "\\lfloor\\rceil",
264
+ ].includes(left.value + right.value)) {
265
+ return new TypstGroup([typ_left.toNode(), typ_body, typ_right.toNode()]);
266
+ }
222
267
  }
223
268
  }
224
269
 
225
- const group = new TypstNode(
226
- 'group',
227
- '',
228
- [typ_left, typ_body, typ_right]
229
- );
230
-
231
270
  // "\left\{ a + \frac{1}{3} \right." -> "lr(\{ a + 1/3)"
232
- // "\left. a + \frac{1}{3} \right\}" -> "lr( a + \frac{1}{3} \})"
271
+ // "\left. a + \frac{1}{3} \right\}" -> "lr( a + 1/3 \})"
233
272
  // Note that: In lr(), if one side of delimiter doesn't present (i.e. derived from "\\left." or "\\right."),
234
273
  // "(", ")", "{", "[", should be escaped with "\" to be the other side of delimiter.
235
274
  // 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;
275
+ const escape_curly_or_paren = function(s: TypstToken): TypstToken {
276
+ if (["(", ")", "{", "["].includes(s.value)) {
277
+ return new TypstToken(TypstTokenType.ELEMENT, "\\" + s.value);
239
278
  } else {
240
279
  return s;
241
280
  }
242
281
  };
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);
282
+
283
+ let typ_left = left? tex_token_to_typst(left, options) : null;
284
+ let typ_right = right? tex_token_to_typst(right, options) : null;
285
+ if (typ_left === null && typ_right !== null) { // left.
286
+ typ_right = escape_curly_or_paren(typ_right);
258
287
  }
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
- }
288
+ if (typ_right === null && typ_left !== null) { // right.
289
+ typ_left = escape_curly_or_paren(typ_left);
268
290
  }
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))
291
+
292
+ return new TypstLeftright(
293
+ new TypstToken(TypstTokenType.SYMBOL, 'lr'),
294
+ [typ_body],
295
+ { left: typ_left, right: typ_right }
273
296
  );
274
297
  }
275
- case 'unaryFunc': {
298
+ case 'funcCall': {
299
+ const node = abstractNode as TexFuncCall;
276
300
  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',
301
+ // \sqrt[3]{x} -> root(3, x)
302
+ if (node.head.value === '\\sqrt' && node.data) {
303
+ const data = convert_tex_node_to_typst(node.data, options); // the number of times to take the root
304
+ return new TypstFuncCall(
305
+ new TypstToken(TypstTokenType.SYMBOL, 'root'),
283
306
  [data, arg0]
284
307
  );
285
308
  }
286
309
  // \mathbf{a} -> upright(bold(a))
287
- if (node.content === '\\mathbf') {
288
- const inner: TypstNode = new TypstNode(
289
- 'funcCall',
290
- 'bold',
310
+ if (node.head.value === '\\mathbf') {
311
+ const inner: TypstNode = new TypstFuncCall(
312
+ new TypstToken(TypstTokenType.SYMBOL, 'bold'),
291
313
  [arg0]
292
314
  );
293
- return new TypstNode(
294
- 'funcCall',
295
- 'upright',
315
+ return new TypstFuncCall(
316
+ new TypstToken(TypstTokenType.SYMBOL, 'upright'),
296
317
  [inner]
297
318
  );
298
319
  }
299
320
  // \overrightarrow{AB} -> arrow(A B)
300
- if (node.content === '\\overrightarrow') {
301
- return new TypstNode(
302
- 'funcCall',
303
- 'arrow',
321
+ if (node.head.value === '\\overrightarrow') {
322
+ return new TypstFuncCall(
323
+ new TypstToken(TypstTokenType.SYMBOL, 'arrow'),
304
324
  [arg0]
305
325
  );
306
326
  }
307
327
  // \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')]
328
+ if (node.head.value === '\\overleftarrow') {
329
+ return new TypstFuncCall(
330
+ new TypstToken(TypstTokenType.SYMBOL, 'accent'),
331
+ [arg0, new TypstToken(TypstTokenType.SYMBOL, 'arrow.l').toNode()]
313
332
  );
314
333
  }
315
334
  // \operatorname{opname} -> op("opname")
316
- if (node.content === '\\operatorname') {
335
+ if (node.head.value === '\\operatorname') {
317
336
  // arg0 must be of type 'literal' in this situation
318
337
  if (options.optimize) {
319
- if (TYPST_INTRINSIC_OP.includes(arg0.content)) {
320
- return new TypstNode('symbol', arg0.content);
338
+ if (TYPST_INTRINSIC_OP.includes(arg0.head.value)) {
339
+ return new TypstToken(TypstTokenType.SYMBOL, arg0.head.value).toNode();
321
340
  }
322
341
  }
323
- return new TypstNode('funcCall', 'op', [new TypstNode('text', arg0.content)]);
342
+ return new TypstFuncCall(new TypstToken(TypstTokenType.SYMBOL, 'op'), [new TypstToken(TypstTokenType.TEXT, arg0.head.value).toNode()]);
324
343
  }
325
344
 
326
345
  // \substack{a \\ b} -> `a \ b`
327
346
  // as in translation from \sum_{\substack{a \\ b}} to sum_(a \ b)
328
- if (node.content === '\\substack') {
347
+ if (node.head.value === '\\substack') {
329
348
  return arg0;
330
349
  }
331
350
 
351
+ if (node.head.value === '\\overset') {
352
+ return convert_overset(node, options);
353
+ }
354
+ if (node.head.value === '\\underset') {
355
+ return convert_underset(node, options);
356
+ }
357
+
358
+ // \frac{a}{b} -> a / b
359
+ if (node.head.value === '\\frac') {
360
+ if (options.fracToSlash) {
361
+ return new TypstFraction(node.args!.map((n) => convert_tex_node_to_typst(n, options)));
362
+ }
363
+ }
332
364
  if(options.optimize) {
333
365
  // \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);
366
+ if (node.head.value === '\\mathbb' && /^\\mathbb{[A-Z]}$/.test(node.toString())) {
367
+ return new TypstToken(TypstTokenType.SYMBOL, arg0.head.value.repeat(2)).toNode();
336
368
  }
337
369
  // \mathrm{d} -> dif
338
- if (node.content === '\\mathrm' && arg0.eq(new TypstNode('atom', 'd'))) {
339
- return new TypstNode('symbol', 'dif');
370
+ if (node.head.value === '\\mathrm' && node.toString() === '\\mathrm{d}') {
371
+ return new TypstToken(TypstTokenType.SYMBOL, 'dif').toNode();
340
372
  }
341
373
  }
342
374
 
343
375
  // generic case
344
- return new TypstNode(
345
- 'funcCall',
346
- tex_token_to_typst(node.content),
376
+ return new TypstFuncCall(
377
+ tex_token_to_typst(node.head, options),
347
378
  node.args!.map((n) => convert_tex_node_to_typst(n, options))
348
379
  );
349
380
  }
350
381
  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)));
382
+ const node = abstractNode as TexBeginEnd;
383
+ const data = node.matrix.map((row) => row.map((n) => convert_tex_node_to_typst(n, options)));
353
384
 
354
- if (node.content.startsWith('align')) {
385
+ if (node.head.value.startsWith('align')) {
355
386
  // align, align*, alignat, alignat*, aligned, etc.
356
- return new TypstNode('align', '', [], data);
387
+ return new TypstAlign(data);
357
388
  }
358
- if (node.content === 'cases') {
359
- return new TypstNode('cases', '', [], data);
389
+ if (node.head.value === 'cases') {
390
+ return new TypstCases(data);
360
391
  }
361
- if (node.content === 'subarray') {
392
+ if (node.head.value === 'subarray') {
362
393
  const align_node = node.args![0];
363
- switch (align_node.content) {
394
+ switch (align_node.head.value) {
364
395
  case 'r':
365
- data.forEach(row => row[0].args!.push(new TypstNode('symbol', '&')));
396
+ data.forEach(row => row[0].args!.push(new TypstToken(TypstTokenType.CONTROL, '&').toNode()));
366
397
  break;
367
398
  case 'l':
368
- data.forEach(row => row[0].args!.unshift(new TypstNode('symbol', '&')));
399
+ data.forEach(row => row[0].args!.unshift(new TypstToken(TypstTokenType.CONTROL, '&').toNode()));
369
400
  break;
370
401
  default:
371
402
  break;
372
403
  }
373
- return new TypstNode('align', '', [], data);
404
+ return new TypstAlign(data);
374
405
  }
375
- if (node.content === 'array') {
406
+ if (node.head.value === 'array') {
376
407
  const np: TypstNamedParams = { 'delim': TYPST_NONE };
377
408
 
378
- assert(node.args!.length > 0 && node.args![0].type === 'literal');
379
- const np_new = convert_tex_array_align_literal(node.args![0].content);
409
+ assert(node.args!.length > 0 && node.args![0].head.type === TexTokenType.LITERAL);
410
+ const np_new = convert_tex_array_align_literal(node.args![0].head.value);
380
411
  Object.assign(np, np_new);
381
412
 
382
- const res = new TypstNode('matrix', '', [], data);
413
+ const res = new TypstMatrix(data);
383
414
  res.setOptions(np);
384
415
  return res;
385
416
  }
386
- if (node.content.endsWith('matrix')) {
387
- const res = new TypstNode('matrix', '', [], data);
388
- let delim: TypstNode;
389
- switch (node.content) {
417
+ if (node.head.value.endsWith('matrix')) {
418
+ const res = new TypstMatrix(data);
419
+ let delim: TypstToken;
420
+ switch (node.head.value) {
390
421
  case 'matrix':
391
- delim = TYPST_NONE;
422
+ delim = TypstToken.NONE;
392
423
  break;
393
424
  case 'pmatrix':
394
- // delim = new TypstNode('text', '(');
425
+ // delim = new TypstToken(TypstTokenType.TEXT, '(');
395
426
  // break;
396
427
  return res; // typst mat use delim:"(" by default
397
428
  case 'bmatrix':
398
- delim = new TypstNode('text', '[');
429
+ delim = new TypstToken(TypstTokenType.TEXT, '[');
399
430
  break;
400
431
  case 'Bmatrix':
401
- delim = new TypstNode('text', '{');
432
+ delim = new TypstToken(TypstTokenType.TEXT, '{');
402
433
  break;
403
434
  case 'vmatrix':
404
- delim = new TypstNode('text', '|');
435
+ delim = new TypstToken(TypstTokenType.TEXT, '|');
405
436
  break;
406
437
  case 'Vmatrix': {
407
- delim = new TypstNode('symbol', 'bar.v.double');
438
+ delim = new TypstToken(TypstTokenType.SYMBOL, 'bar.v.double');
408
439
  break;
409
440
  }
410
441
  default:
411
- throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
442
+ throw new ConverterError(`Unimplemented beginend: ${node.head}`, node);
412
443
  }
413
- res.setOptions({ 'delim': delim });
444
+ res.setOptions({ 'delim': delim.toNode()});
414
445
  return res;
415
446
  }
416
- throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
447
+ throw new ConverterError(`Unimplemented beginend: ${node.head}`, node);
417
448
  }
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
449
  default:
437
- throw new TypstWriterError(`Unimplemented node type: ${node.type}`, node);
450
+ throw new ConverterError(`Unimplemented node type: ${abstractNode.type}`, abstractNode);
438
451
  }
439
452
  }
440
453
 
@@ -472,194 +485,252 @@ const TYPST_BINARY_FUNCTIONS: string[] = [
472
485
  ];
473
486
  */
474
487
 
475
- function apply_escape_if_needed(c: string) {
476
- if (['{', '}', '%'].includes(c)) {
477
- return '\\' + c;
488
+ function apply_escape_if_needed(c: TexToken): TexToken {
489
+ if (['{', '}', '%'].includes(c.value)) {
490
+ return new TexToken(TexTokenType.ELEMENT, '\\' + c.value);
478
491
  }
479
492
  return c;
480
493
  }
481
494
 
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)!;
495
+
496
+ function typst_token_to_tex(token: TypstToken): TexToken {
497
+ switch (token.type) {
498
+ case TypstTokenType.NONE:
499
+ // e.g. Typst `#none^2` is converted to TeX `^2`
500
+ return TexToken.EMPTY;
501
+ case TypstTokenType.SYMBOL: {
502
+ const _typst_symbol_to_tex = function(symbol: string): string {
503
+ if (reverseSymbolMap.has(symbol)) {
504
+ return '\\' + reverseSymbolMap.get(symbol)!;
505
+ } else {
506
+ return '\\' + symbol;
507
+ }
508
+ }
509
+ return new TexToken(TexTokenType.COMMAND, _typst_symbol_to_tex(token.value));
510
+ }
511
+ case TypstTokenType.ELEMENT: {
512
+ let value: string;
513
+ if (['{', '}', '%'].includes(token.value)) {
514
+ value = '\\' + token.value;
515
+ } else {
516
+ value = token.value;
517
+ }
518
+ return new TexToken(TexTokenType.ELEMENT, value);
519
+ }
520
+ case TypstTokenType.LITERAL:
521
+ return new TexToken(TexTokenType.LITERAL, token.value);
522
+ case TypstTokenType.TEXT:
523
+ return new TexToken(TexTokenType.LITERAL, token.value);
524
+ case TypstTokenType.COMMENT:
525
+ return new TexToken(TexTokenType.COMMENT, token.value);
526
+ case TypstTokenType.SPACE:
527
+ return new TexToken(TexTokenType.SPACE, token.value);
528
+ case TypstTokenType.CONTROL: {
529
+ let value: string;
530
+ switch(token.value) {
531
+ case '\\':
532
+ value = '\\\\';
533
+ break;
534
+ case '&':
535
+ value = '&';
536
+ break;
537
+ default:
538
+ throw new Error(`[typst_token_to_tex]Unimplemented control sequence: ${token.value}`);
539
+ }
540
+ return new TexToken(TexTokenType.CONTROL, value);
541
+ }
542
+ case TypstTokenType.NEWLINE:
543
+ return new TexToken(TexTokenType.NEWLINE, token.value);
544
+ default:
545
+ throw new Error(`Unimplemented token type: ${token.type}`);
487
546
  }
488
- return '\\' + token;
489
547
  }
490
548
 
491
549
 
492
- const TEX_NODE_COMMA = new TexNode('element', ',');
550
+ const TEX_NODE_COMMA = new TexToken(TexTokenType.ELEMENT, ',').toNode();
493
551
 
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', '-');
552
+ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
553
+ switch (abstractNode.type) {
554
+ case 'terminal': {
555
+ const node = abstractNode as TypstTerminal;
556
+ if (node.head.type === TypstTokenType.SYMBOL) {
557
+ // special hook for eq.def
558
+ if (node.head.value === 'eq.def') {
559
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [
560
+ new TexText(new TexToken(TexTokenType.LITERAL, 'def')),
561
+ new TexToken(TexTokenType.ELEMENT, '=').toNode()
562
+ ]);
563
+ }
564
+ // special hook for comma
565
+ if(node.head.value === 'comma') {
566
+ return new TexToken(TexTokenType.ELEMENT, ',').toNode();
567
+ }
568
+ // special hook for dif
569
+ if(node.head.value === 'dif') {
570
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\mathrm'), [new TexToken(TexTokenType.ELEMENT, 'd').toNode()]);
571
+ }
572
+ // special hook for hyph and hyph.minus
573
+ if(node.head.value === 'hyph' || node.head.value === 'hyph.minus') {
574
+ return new TexText(new TexToken(TexTokenType.LITERAL, '-'));
575
+ }
576
+ // special hook for mathbb{R} <-- RR
577
+ if(/^([A-Z])\1$/.test(node.head.value)) {
578
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\mathbb'), [
579
+ new TexToken(TexTokenType.ELEMENT, node.head.value[0]).toNode()
580
+ ]);
581
+ }
522
582
  }
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
- ]);
583
+ if (node.head.type === TypstTokenType.TEXT) {
584
+ return new TexText(new TexToken(TexTokenType.LITERAL, node.head.value));
528
585
  }
529
- return new TexNode('symbol', typst_token_to_tex(node.content));
586
+ return typst_token_to_tex(node.head).toNode();
530
587
  }
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);
588
+
537
589
  case 'group': {
590
+ const node = abstractNode as TypstGroup;
538
591
  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));
592
+ const alignment_char = new TexToken(TexTokenType.CONTROL, '&').toNode();
593
+ const newline_char = new TexToken(TexTokenType.CONTROL, '\\\\').toNode();
594
+ if (array_includes(args, alignment_char)) {
595
+ // wrap the whole math formula with \begin{aligned} and \end{aligned}
596
+ const rows = array_split(args, newline_char);
597
+ const data: TexNode[][] = [];
598
+ for(const row of rows) {
599
+ const cells = array_split(row, alignment_char);
600
+ data.push(cells.map(cell => new TexGroup(cell)));
601
+ }
602
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'aligned'), [], data);
545
603
  }
546
- return new TexNode('ordgroup', node.content, args);
604
+ return new TexGroup(args);
605
+ }
606
+ case 'leftright': {
607
+ const node = abstractNode as TypstLeftright;
608
+ const args = node.args!.map(convert_typst_node_to_tex);
609
+ let left = node.left? typst_token_to_tex(node.left) : new TexToken(TexTokenType.ELEMENT, '.');
610
+ let right = node.right? typst_token_to_tex(node.right) : new TexToken(TexTokenType.ELEMENT, '.');
611
+ // const is_over_high = node.isOverHigh();
612
+ // const left_delim = is_over_high ? '\\left(' : '(';
613
+ // const right_delim = is_over_high ? '\\right)' : ')';
614
+ if (node.isOverHigh()) {
615
+ left.value = '\\left' + left.value;
616
+ right.value = '\\right' + right.value;
617
+ }
618
+ args.unshift(left.toNode());
619
+ args.push(right.toNode());
620
+ // TODO: should be TeXLeftRight(...)
621
+ // But currently writer will output `\left |` while people commonly prefer `\left|`.
622
+ return new TexGroup(args);
547
623
  }
548
624
  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));
625
+ const node = abstractNode as TypstFuncCall;
626
+ switch (node.head.value) {
627
+ // special hook for norm
628
+ // `\| a \|` <- `norm(a)`
629
+ // `\left\| a + \frac{1}{3} \right\|` <- `norm(a + 1/3)`
630
+ case 'norm': {
631
+ const arg0 = node.args![0];
632
+ const args = [ convert_typst_node_to_tex(arg0) ];
633
+ if (node.isOverHigh()) {
634
+ return new TexLeftRight(args, {
635
+ left: new TexToken(TexTokenType.COMMAND, "\\|"),
636
+ right: new TexToken(TexTokenType.COMMAND, "\\|")
637
+ });
638
+ } else {
639
+ return new TexGroup(args);
640
+ }
641
+ }
642
+ // special hook for floor, ceil
643
+ // `\lfloor a \rfloor` <- `floor(a)`
644
+ // `\lceil a \rceil` <- `ceil(a)`
645
+ // `\left\lfloor a \right\rfloor` <- `floor(a)`
646
+ // `\left\lceil a \right\rceil` <- `ceil(a)`
647
+ case 'floor':
648
+ case 'ceil': {
649
+ const left = "\\l" + node.head.value;
650
+ const right = "\\r" + node.head.value;
651
+ const arg0 = node.args![0];
652
+ const typ_arg0 = convert_typst_node_to_tex(arg0);
653
+ const left_node = new TexToken(TexTokenType.COMMAND, left);
654
+ const right_node = new TexToken(TexTokenType.COMMAND, right);
655
+ if (node.isOverHigh()) {
656
+ return new TexLeftRight([typ_arg0], {
657
+ left: left_node,
658
+ right: right_node
659
+ });
660
+ } else {
661
+ return new TexGroup([left_node.toNode(), typ_arg0, right_node.toNode()]);
662
+ }
663
+ }
664
+ // special hook for root
665
+ case 'root': {
666
+ const [degree, radicand] = node.args!;
667
+ const data = convert_typst_node_to_tex(degree);
668
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\sqrt'), [convert_typst_node_to_tex(radicand)], data);
669
+ }
670
+ // special hook for overbrace and underbrace
671
+ case 'overbrace':
672
+ case 'underbrace': {
673
+ const [body, label] = node.args!;
674
+ const base = new TexFuncCall(typst_token_to_tex(node.head), [convert_typst_node_to_tex(body)]);
675
+ const script = convert_typst_node_to_tex(label);
676
+ const data = node.head.value === 'overbrace' ? { base, sup: script, sub: null } : { base, sub: script, sup: null };
677
+ return new TexSupSub(data);
678
+ }
679
+ // special hook for vec
680
+ // "vec(a, b, c)" -> "\begin{pmatrix}a\\ b\\ c\end{pmatrix}"
681
+ case 'vec': {
682
+ const tex_data = node.args!.map(convert_typst_node_to_tex).map((n) => [n]);
683
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'pmatrix'), [], tex_data);
684
+ }
685
+ // special hook for op
686
+ case 'op': {
687
+ const arg0 = node.args![0];
688
+ assert(arg0.head.type === TypstTokenType.TEXT);
689
+ return new TexFuncCall(typst_token_to_tex(node.head), [new TexToken(TexTokenType.LITERAL, arg0.head.value).toNode()]);
690
+ }
691
+ // general case
692
+ default: {
693
+ const func_name_tex = typst_token_to_tex(node.head);
694
+ const is_known_func = TEX_UNARY_COMMANDS.includes(func_name_tex.value.substring(1))
695
+ || TEX_BINARY_COMMANDS.includes(func_name_tex.value.substring(1));
696
+ if (func_name_tex.value.length > 0 && is_known_func) {
697
+ return new TexFuncCall(func_name_tex, node.args!.map(convert_typst_node_to_tex));
698
+ } else {
699
+ return new TexGroup([
700
+ typst_token_to_tex(node.head).toNode(),
701
+ new TexToken(TexTokenType.ELEMENT, '(').toNode(),
702
+ ...array_intersperse(node.args!.map(convert_typst_node_to_tex), TEX_NODE_COMMA),
703
+ new TexToken(TexTokenType.ELEMENT, ')').toNode()
704
+ ]);
705
+ }
565
706
  }
566
- }
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
- ]);
630
707
  }
631
708
  }
632
709
  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
- }
710
+ const node = abstractNode as TypstSupsub;
711
+ const { base, sup, sub } = node;
712
+ const sup_tex = sup? convert_typst_node_to_tex(sup) : null;
713
+ const sub_tex = sub? convert_typst_node_to_tex(sub) : null;
643
714
 
644
715
  // special hook for limits
645
716
  // `limits(+)^a` -> `\overset{a}{+}`
646
717
  // `limits(+)_a` -> `\underset{a}{+}`
647
718
  // `limits(+)_a^b` -> `\overset{b}{\underset{a}{+}}`
648
- if (base.eq(new TypstNode('funcCall', 'limits'))) {
719
+ if (base.head.eq(new TypstToken(TypstTokenType.SYMBOL, 'limits'))) {
649
720
  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]);
721
+ if (sup_tex !== null && sub_tex === null) {
722
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [sup_tex, body_in_limits]);
723
+ } else if (sup_tex === null && sub_tex !== null) {
724
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\underset'), [sub_tex, body_in_limits]);
654
725
  } else {
655
- const underset_call = new TexNode('binaryFunc', '\\underset', [sub_tex!, body_in_limits]);
656
- return new TexNode('binaryFunc', '\\overset', [sup_tex!, underset_call]);
726
+ const underset_call = new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\underset'), [sub_tex!, body_in_limits]);
727
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [sup_tex!, underset_call]);
657
728
  }
658
729
  }
659
730
 
660
731
  const base_tex = convert_typst_node_to_tex(base);
661
732
 
662
- const res = new TexNode('supsub', '', [], {
733
+ const res = new TexSupSub({
663
734
  base: base_tex,
664
735
  sup: sup_tex,
665
736
  sub: sub_tex
@@ -667,13 +738,13 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
667
738
  return res;
668
739
  }
669
740
  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));
741
+ const node = abstractNode as TypstMatrix;
742
+ const tex_data = node.matrix.map(row => row.map(convert_typst_node_to_tex));
672
743
  let env_type = 'pmatrix'; // typst mat use delim:"(" by default
673
744
  if (node.options) {
674
745
  if ('delim' in node.options) {
675
746
  const delim = node.options.delim;
676
- switch (delim.content) {
747
+ switch (delim.head.value) {
677
748
  case '#none':
678
749
  env_type = 'matrix';
679
750
  break;
@@ -700,35 +771,26 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
700
771
  env_type = 'Vmatrix';
701
772
  break;
702
773
  default:
703
- throw new Error(`Unexpected delimiter ${delim.content}`);
774
+ throw new Error(`Unexpected delimiter ${delim.head}`);
704
775
  }
705
776
  }
706
777
  }
707
- return new TexNode('beginend', env_type, [], tex_data);
778
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, env_type), [], tex_data);
708
779
  }
709
780
  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);
722
- }
781
+ const node = abstractNode as TypstCases;
782
+ const tex_data = node.matrix.map(row => row.map(convert_typst_node_to_tex));
783
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'cases'), [], tex_data);
723
784
  }
724
785
  case 'fraction': {
786
+ const node = abstractNode as TypstFraction;
725
787
  const [numerator, denominator] = node.args!;
726
788
  const num_tex = convert_typst_node_to_tex(numerator);
727
789
  const den_tex = convert_typst_node_to_tex(denominator);
728
- return new TexNode('binaryFunc', '\\frac', [num_tex, den_tex]);
790
+ return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\frac'), [num_tex, den_tex]);
729
791
  }
730
792
  default:
731
- throw new Error('[convert_typst_node_to_tex] Unimplemented type: ' + node.type);
793
+ throw new Error('[convert_typst_node_to_tex] Unimplemented type: ' + abstractNode.type);
732
794
  }
733
795
  }
734
796