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