tex2typst 0.2.12 → 0.2.15

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/writer.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { symbolMap } from "./map";
2
- import { TexNode, TexSqrtData, TexSupsubData, TypstNode, TypstSupsubData } from "./types";
2
+ import { TexNode, TexSqrtData, TexSupsubData, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
3
3
 
4
4
 
5
5
  // symbols that are supported by Typst but not by KaTeX
@@ -19,10 +19,58 @@ function is_delimiter(c: TypstNode): boolean {
19
19
  return c.type === 'atom' && ['(', ')', '[', ']', '{', '}', '|', '⌊', '⌋', '⌈', '⌉'].includes(c.content);
20
20
  }
21
21
 
22
+
23
+ // \overset{X}{Y} -> op(Y, limits: #true)^X
24
+ // and with special case \overset{\text{def}}{=} -> eq.def
25
+ function convert_overset(node: TexNode): TypstNode {
26
+ const [sup, base] = node.args!;
27
+
28
+ const is_def = (n: TexNode): boolean => {
29
+ if (n.eq_shallow(new TexNode('text', 'def'))) {
30
+ return true;
31
+ }
32
+ // \overset{def}{=} is also considered as eq.def
33
+ if (n.type === 'ordgroup' && n.args!.length === 3) {
34
+ const [a1, a2, a3] = n.args!;
35
+ const d = new TexNode('element', 'd');
36
+ const e = new TexNode('element', 'e');
37
+ const f = new TexNode('element', 'f');
38
+ if (a1.eq_shallow(d) && a2.eq_shallow(e) && a3.eq_shallow(f)) {
39
+ return true;
40
+ }
41
+ }
42
+ return false;
43
+ };
44
+ const is_eq = (n: TexNode): boolean => n.eq_shallow(new TexNode('element', '='));
45
+ if (is_def(sup) && is_eq(base)) {
46
+ return new TypstNode('symbol', 'eq.def');
47
+ }
48
+ const op_call = new TypstNode(
49
+ 'unaryFunc',
50
+ 'op',
51
+ [convertTree(base)]
52
+ );
53
+ op_call.setOptions({ limits: '#true' });
54
+ return new TypstNode(
55
+ 'supsub',
56
+ '',
57
+ [],
58
+ {
59
+ base: op_call,
60
+ sup: convertTree(sup),
61
+ }
62
+ );
63
+ }
64
+
65
+ const TYPST_LEFT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ATOM, '(');
66
+ const TYPST_RIGHT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ATOM, ')');
67
+ const TYPST_COMMA: TypstToken = new TypstToken(TypstTokenType.ATOM, ',');
68
+ const TYPST_NEWLINE: TypstToken = new TypstToken(TypstTokenType.SYMBOL, '\n');
69
+
22
70
  export class TypstWriterError extends Error {
23
- node: TexNode | TypstNode;
71
+ node: TexNode | TypstNode | TypstToken;
24
72
 
25
- constructor(message: string, node: TexNode | TypstNode) {
73
+ constructor(message: string, node: TexNode | TypstNode | TypstToken) {
26
74
  super(message);
27
75
  this.name = "TypstWriterError";
28
76
  this.node = node;
@@ -32,73 +80,91 @@ export class TypstWriterError extends Error {
32
80
  export class TypstWriter {
33
81
  private nonStrict: boolean;
34
82
  private preferTypstIntrinsic: boolean;
83
+ private keepSpaces: boolean;
35
84
 
36
85
  protected buffer: string = "";
37
- protected queue: TypstNode[] = [];
86
+ protected queue: TypstToken[] = [];
38
87
 
39
- private needSpaceAfterSingleItemScript = false;
40
88
  private insideFunctionDepth = 0;
41
89
 
42
- constructor(nonStrict: boolean, preferTypstIntrinsic: boolean) {
90
+ constructor(nonStrict: boolean, preferTypstIntrinsic: boolean, keepSpaces: boolean) {
43
91
  this.nonStrict = nonStrict;
44
92
  this.preferTypstIntrinsic = preferTypstIntrinsic;
93
+ this.keepSpaces = keepSpaces;
45
94
  }
46
95
 
47
96
 
48
- private writeBuffer(str: string) {
49
- if (this.needSpaceAfterSingleItemScript && /^[0-9a-zA-Z\(]/.test(str)) {
50
- this.buffer += ' ';
51
- } else {
52
- let no_need_space = false;
53
- // starting clause
54
- no_need_space ||= /[\(\|]$/.test(this.buffer) && /^\w/.test(str);
55
- // putting punctuation
56
- no_need_space ||= /^[}()_^,;!\|]$/.test(str);
57
- // putting a prime
58
- no_need_space ||= str === "'";
59
- // continue a number
60
- no_need_space ||= /[0-9]$/.test(this.buffer) && /^[0-9]/.test(str);
61
- // leading sign
62
- no_need_space ||= /[\(\[{]\s*(-|\+)$/.test(this.buffer) || this.buffer === "-" || this.buffer === "+";
63
- // new line
64
- no_need_space ||= str.startsWith('\n');
65
- // buffer is empty
66
- no_need_space ||= this.buffer === "";
67
- // other cases
68
- no_need_space ||= /[\s_^{\(]$/.test(this.buffer);
69
- if(!no_need_space) {
70
- this.buffer += ' ';
71
- }
97
+ private writeBuffer(token: TypstToken) {
98
+ const str = token.content;
99
+
100
+ if (str === '') {
101
+ return;
72
102
  }
73
103
 
74
- if (this.needSpaceAfterSingleItemScript) {
75
- this.needSpaceAfterSingleItemScript = false;
104
+ let no_need_space = false;
105
+ // starting clause
106
+ no_need_space ||= /[\(\|]$/.test(this.buffer) && /^\w/.test(str);
107
+ // putting punctuation
108
+ no_need_space ||= /^[}()_^,;!\|]$/.test(str);
109
+ // putting a prime
110
+ no_need_space ||= str === "'";
111
+ // continue a number
112
+ no_need_space ||= /[0-9]$/.test(this.buffer) && /^[0-9]/.test(str);
113
+ // leading sign. e.g. produce "+1" instead of " +1"
114
+ no_need_space ||= /[\(\[{]\s*(-|\+)$/.test(this.buffer) || this.buffer === "-" || this.buffer === "+";
115
+ // new line
116
+ no_need_space ||= str.startsWith('\n');
117
+ // buffer is empty
118
+ no_need_space ||= this.buffer === "";
119
+ // str is starting with a space itself
120
+ no_need_space ||= /^\s/.test(str);
121
+ // other cases
122
+ no_need_space ||= /[\s_^{\(]$/.test(this.buffer);
123
+ if (!no_need_space) {
124
+ this.buffer += ' ';
76
125
  }
77
126
 
78
127
  this.buffer += str;
79
128
  }
80
129
 
81
- public append(node: TypstNode) {
130
+ // Serialize a tree of TypstNode into a list of TypstToken
131
+ public serialize(node: TypstNode) {
82
132
  switch (node.type) {
83
133
  case 'empty':
84
134
  break;
85
135
  case 'atom': {
86
136
  if (node.content === ',' && this.insideFunctionDepth > 0) {
87
- this.queue.push({ type: 'symbol', content: 'comma' });
137
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, 'comma'));
88
138
  } else {
89
- this.queue.push({ type: 'atom', content: node.content });
139
+ this.queue.push(new TypstToken(TypstTokenType.ATOM, node.content));
90
140
  }
91
141
  break;
92
142
  }
93
143
  case 'symbol':
144
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, node.content));
145
+ break;
94
146
  case 'text':
147
+ this.queue.push(new TypstToken(TypstTokenType.TEXT, `"${node.content}"`));
148
+ break;
95
149
  case 'comment':
96
- case 'newline':
97
- this.queue.push(node);
150
+ this.queue.push(new TypstToken(TypstTokenType.COMMENT, `//${node.content}`));
151
+ break;
152
+ case 'whitespace':
153
+ for (const c of node.content) {
154
+ if (c === ' ') {
155
+ if (this.keepSpaces) {
156
+ this.queue.push(new TypstToken(TypstTokenType.SPACE, c));
157
+ }
158
+ } else if (c === '\n') {
159
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, c));
160
+ } else {
161
+ throw new TypstWriterError(`Unexpected whitespace character: ${c}`, node);
162
+ }
163
+ }
98
164
  break;
99
165
  case 'group':
100
166
  for (const item of node.args!) {
101
- this.append(item);
167
+ this.serialize(item);
102
168
  }
103
169
  break;
104
170
  case 'supsub': {
@@ -112,44 +178,49 @@ export class TypstWriter {
112
178
  // e.g.
113
179
  // y_1' -> y'_1
114
180
  // y_{a_1}' -> y'_{a_1}
115
- this.queue.push({ type: 'atom', content: '\''});
181
+ this.queue.push(new TypstToken(TypstTokenType.ATOM, '\''));
116
182
  trailing_space_needed = false;
117
183
  }
118
184
  if (sub) {
119
- this.queue.push({ type: 'atom', content: '_'});
185
+ this.queue.push(new TypstToken(TypstTokenType.ATOM, '_'));
120
186
  trailing_space_needed = this.appendWithBracketsIfNeeded(sub);
121
187
  }
122
188
  if (sup && !has_prime) {
123
- this.queue.push({ type: 'atom', content: '^'});
189
+ this.queue.push(new TypstToken(TypstTokenType.ATOM, '^'));
124
190
  trailing_space_needed = this.appendWithBracketsIfNeeded(sup);
125
191
  }
126
192
  if (trailing_space_needed) {
127
- this.queue.push({ type: 'softSpace', content: ''});
193
+ this.queue.push(new TypstToken(TypstTokenType.CONTROL, ' '));
128
194
  }
129
195
  break;
130
196
  }
131
197
  case 'binaryFunc': {
132
- const func_symbol: TypstNode = { type: 'symbol', content: node.content };
198
+ const func_symbol: TypstToken = new TypstToken(TypstTokenType.SYMBOL, node.content);
133
199
  const [arg0, arg1] = node.args!;
134
200
  this.queue.push(func_symbol);
135
- this.insideFunctionDepth ++;
136
- this.queue.push({ type: 'atom', content: '('});
137
- this.append(arg0);
138
- this.queue.push({ type: 'atom', content: ','});
139
- this.append(arg1);
140
- this.queue.push({ type: 'atom', content: ')'});
141
- this.insideFunctionDepth --;
201
+ this.insideFunctionDepth++;
202
+ this.queue.push(TYPST_LEFT_PARENTHESIS);
203
+ this.serialize(arg0);
204
+ this.queue.push(new TypstToken(TypstTokenType.ATOM, ','));
205
+ this.serialize(arg1);
206
+ this.queue.push(TYPST_RIGHT_PARENTHESIS);
207
+ this.insideFunctionDepth--;
142
208
  break;
143
209
  }
144
210
  case 'unaryFunc': {
145
- const func_symbol: TypstNode = { type: 'symbol', content: node.content };
211
+ const func_symbol: TypstToken = new TypstToken(TypstTokenType.SYMBOL, node.content);
146
212
  const arg0 = node.args![0];
147
213
  this.queue.push(func_symbol);
148
- this.insideFunctionDepth ++;
149
- this.queue.push({ type: 'atom', content: '('});
150
- this.append(arg0);
151
- this.queue.push({ type: 'atom', content: ')'});
152
- this.insideFunctionDepth --;
214
+ this.insideFunctionDepth++;
215
+ this.queue.push(TYPST_LEFT_PARENTHESIS);
216
+ this.serialize(arg0);
217
+ if (node.options) {
218
+ for (const [key, value] of Object.entries(node.options)) {
219
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `, ${key}: ${value}`));
220
+ }
221
+ }
222
+ this.queue.push(TYPST_RIGHT_PARENTHESIS);
223
+ this.insideFunctionDepth--;
153
224
  break;
154
225
  }
155
226
  case 'align': {
@@ -157,50 +228,54 @@ export class TypstWriter {
157
228
  matrix.forEach((row, i) => {
158
229
  row.forEach((cell, j) => {
159
230
  if (j > 0) {
160
- this.queue.push({ type: 'atom', content: '&' });
231
+ this.queue.push(new TypstToken(TypstTokenType.ATOM, '&'));
161
232
  }
162
- this.append(cell);
233
+ this.serialize(cell);
163
234
  });
164
235
  if (i < matrix.length - 1) {
165
- this.queue.push({ type: 'symbol', content: '\\' });
236
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, '\\'));
166
237
  }
167
238
  });
168
239
  break;
169
240
  }
170
241
  case 'matrix': {
171
242
  const matrix = node.data as TypstNode[][];
172
- this.queue.push({ type: 'symbol', content: 'mat' });
173
- this.insideFunctionDepth ++;
174
- this.queue.push({ type: 'atom', content: '('});
175
- this.queue.push({type: 'symbol', content: 'delim: #none, '});
243
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, 'mat'));
244
+ this.insideFunctionDepth++;
245
+ this.queue.push(TYPST_LEFT_PARENTHESIS);
246
+ if (node.options) {
247
+ for (const [key, value] of Object.entries(node.options)) {
248
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `${key}: ${value}, `));
249
+ }
250
+ }
176
251
  matrix.forEach((row, i) => {
177
252
  row.forEach((cell, j) => {
178
253
  // There is a leading & in row
179
254
  // if (cell.type === 'ordgroup' && cell.args!.length === 0) {
180
- // this.queue.push({ type: 'atom', content: ',' });
181
- // return;
255
+ // this.queue.push(new TypstNode('atom', ','));
256
+ // return;
182
257
  // }
183
258
  // if (j == 0 && cell.type === 'newline' && cell.content === '\n') {
184
- // return;
259
+ // return;
185
260
  // }
186
- this.append(cell);
261
+ this.serialize(cell);
187
262
  // cell.args!.forEach((n) => this.append(n));
188
263
  if (j < row.length - 1) {
189
- this.queue.push({ type: 'atom', content: ',' });
264
+ this.queue.push(new TypstToken(TypstTokenType.ATOM, ','));
190
265
  } else {
191
266
  if (i < matrix.length - 1) {
192
- this.queue.push({ type: 'atom', content: ';' });
267
+ this.queue.push(new TypstToken(TypstTokenType.ATOM, ';'));
193
268
  }
194
269
  }
195
270
  });
196
271
  });
197
- this.queue.push({ type: 'atom', content: ')'});
198
- this.insideFunctionDepth --;
272
+ this.queue.push(TYPST_RIGHT_PARENTHESIS);
273
+ this.insideFunctionDepth--;
199
274
  break;
200
275
  }
201
276
  case 'unknown': {
202
277
  if (this.nonStrict) {
203
- this.queue.push({ type: 'symbol', content: node.content });
278
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, node.content));
204
279
  } else {
205
280
  throw new TypstWriterError(`Unknown macro: ${node.content}`, node);
206
281
  }
@@ -223,44 +298,35 @@ export class TypstWriter {
223
298
  }
224
299
 
225
300
  if (need_to_wrap) {
226
- this.queue.push({ type: 'atom', content: '(' });
227
- this.append(node);
228
- this.queue.push({ type: 'atom', content: ')' });
301
+ this.queue.push(TYPST_LEFT_PARENTHESIS);
302
+ this.serialize(node);
303
+ this.queue.push(TYPST_RIGHT_PARENTHESIS);
229
304
  } else {
230
- this.append(node);
305
+ this.serialize(node);
231
306
  }
232
307
 
233
308
  return !need_to_wrap;
234
309
  }
235
310
 
236
311
  protected flushQueue() {
237
- this.queue.forEach((node) => {
238
- let str = "";
239
- switch (node.type) {
240
- case 'atom':
241
- case 'symbol':
242
- str = node.content;
243
- break;
244
- case 'text':
245
- str = `"${node.content}"`;
246
- break;
247
- case 'softSpace':
248
- this.needSpaceAfterSingleItemScript = true;
249
- str = '';
250
- break;
251
- case 'comment':
252
- str = `//${node.content}`;
253
- break;
254
- case 'newline':
255
- str = '\n';
256
- break;
257
- default:
258
- throw new TypstWriterError(`Unexpected node type to stringify: ${node.type}`, node)
259
- }
260
- if (str !== '') {
261
- this.writeBuffer(str);
312
+ const SOFT_SPACE = new TypstToken(TypstTokenType.CONTROL, ' ');
313
+
314
+ // delete soft spaces if they are not needed
315
+ for(let i = 0; i < this.queue.length; i++) {
316
+ let token = this.queue[i];
317
+ if (token.eq(SOFT_SPACE)) {
318
+ if (i === this.queue.length - 1) {
319
+ this.queue[i].content = '';
320
+ } else if (this.queue[i + 1].isOneOf([TYPST_RIGHT_PARENTHESIS, TYPST_COMMA, TYPST_NEWLINE])) {
321
+ this.queue[i].content = '';
322
+ }
262
323
  }
324
+ }
325
+
326
+ this.queue.forEach((token) => {
327
+ this.writeBuffer(token)
263
328
  });
329
+
264
330
  this.queue = [];
265
331
  }
266
332
 
@@ -295,48 +361,50 @@ export class TypstWriter {
295
361
  }
296
362
  }
297
363
 
364
+ // Convert a tree of TexNode into a tree of TypstNode
298
365
  export function convertTree(node: TexNode): TypstNode {
299
366
  switch (node.type) {
300
367
  case 'empty':
368
+ return new TypstNode('empty', '');
301
369
  case 'whitespace':
302
- return { type: 'empty', content: '' };
370
+ return new TypstNode('whitespace', node.content);
303
371
  case 'ordgroup':
304
- return {
305
- type: 'group',
306
- content: '',
307
- args: node.args!.map(convertTree),
308
- };
372
+ return new TypstNode(
373
+ 'group',
374
+ '',
375
+ node.args!.map(convertTree),
376
+ );
309
377
  case 'element':
310
- return { type: 'atom', content: convertToken(node.content) };
378
+ return new TypstNode('atom', convertToken(node.content));
311
379
  case 'symbol':
312
- return { type: 'symbol', content: convertToken(node.content) };
380
+ return new TypstNode('symbol', convertToken(node.content));
313
381
  case 'text':
314
- return { type: 'text', content: node.content };
382
+ return new TypstNode('text', node.content);
315
383
  case 'comment':
316
- return { type: 'comment', content: node.content };
384
+ return new TypstNode('comment', node.content);
317
385
  case 'supsub': {
318
386
  let { base, sup, sub } = node.data as TexSupsubData;
319
387
 
320
388
  // Special logic for overbrace
321
389
  if (base && base.type === 'unaryFunc' && base.content === '\\overbrace' && sup) {
322
- return {
323
- type: 'binaryFunc',
324
- content: 'overbrace',
325
- args: [convertTree(base.args![0]), convertTree(sup)],
326
- };
390
+ return new TypstNode(
391
+ 'binaryFunc',
392
+ 'overbrace',
393
+ [convertTree(base.args![0]), convertTree(sup)],
394
+ );
327
395
  } else if (base && base.type === 'unaryFunc' && base.content === '\\underbrace' && sub) {
328
- return {
329
- type: 'binaryFunc',
330
- content: 'underbrace',
331
- args: [convertTree(base.args![0]), convertTree(sub)],
332
- };
396
+ return new TypstNode(
397
+ 'binaryFunc',
398
+ 'underbrace',
399
+ [convertTree(base.args![0]), convertTree(sub)],
400
+ );
333
401
  }
334
402
 
335
403
  const data: TypstSupsubData = {
336
404
  base: convertTree(base),
337
405
  };
338
406
  if (data.base.type === 'empty') {
339
- data.base = { type: 'text', content: '' };
407
+ data.base = new TypstNode('text', '');
340
408
  }
341
409
 
342
410
  if (sup) {
@@ -347,71 +415,67 @@ export function convertTree(node: TexNode): TypstNode {
347
415
  data.sub = convertTree(sub);
348
416
  }
349
417
 
350
- return {
351
- type: 'supsub',
352
- content: '',
353
- data: data,
354
- };
418
+ return new TypstNode('supsub', '', [], data);
355
419
  }
356
420
  case 'leftright': {
357
421
  const [left, body, right] = node.args!;
358
422
  // These pairs will be handled by Typst compiler by default. No need to add lr()
359
- const group: TypstNode = {
360
- type: 'group',
361
- content: '',
362
- args: node.args!.map(convertTree),
363
- };
423
+ const group: TypstNode = new TypstNode(
424
+ 'group',
425
+ '',
426
+ node.args!.map(convertTree),
427
+ );
364
428
  if ([
365
- "[]", "()", "\\{\\}",
366
- "\\lfloor\\rfloor",
367
- "\\lceil\\rceil",
368
- "\\lfloor\\rceil",
369
- ].includes(left.content + right.content)) {
429
+ "[]", "()", "\\{\\}",
430
+ "\\lfloor\\rfloor",
431
+ "\\lceil\\rceil",
432
+ "\\lfloor\\rceil",
433
+ ].includes(left.content + right.content)) {
370
434
  return group;
371
435
  }
372
- return {
373
- type: 'unaryFunc',
374
- content: 'lr',
375
- args: [group],
376
- };
436
+ return new TypstNode(
437
+ 'unaryFunc',
438
+ 'lr',
439
+ [group],
440
+ );
377
441
  }
378
442
  case 'binaryFunc': {
379
- return {
380
- type: 'binaryFunc',
381
- content: convertToken(node.content),
382
- args: node.args!.map(convertTree),
383
- };
443
+ if (node.content === '\\overset') {
444
+ return convert_overset(node);
445
+ }
446
+ return new TypstNode(
447
+ 'binaryFunc',
448
+ convertToken(node.content),
449
+ node.args!.map(convertTree),
450
+ );
384
451
  }
385
452
  case 'unaryFunc': {
386
453
  const arg0 = convertTree(node.args![0]);
387
454
  // \sqrt{3}{x} -> root(3, x)
388
455
  if (node.content === '\\sqrt' && node.data) {
389
456
  const data = convertTree(node.data as TexSqrtData); // the number of times to take the root
390
- return {
391
- type: 'binaryFunc',
392
- content: 'root',
393
- args: [data, arg0],
394
- };
457
+ return new TypstNode(
458
+ 'binaryFunc',
459
+ 'root',
460
+ [data, arg0],
461
+ );
395
462
  }
396
463
  // \mathbf{a} -> upright(mathbf(a))
397
464
  if (node.content === '\\mathbf') {
398
- const inner: TypstNode = {
399
- type: 'unaryFunc',
400
- content: 'bold',
401
- args: [arg0],
402
- };
403
- return {
404
- type: 'unaryFunc',
405
- content: 'upright',
406
- args: [inner],
407
- };
465
+ const inner: TypstNode = new TypstNode(
466
+ 'unaryFunc',
467
+ 'bold',
468
+ [arg0],
469
+ );
470
+ return new TypstNode(
471
+ 'unaryFunc',
472
+ 'upright',
473
+ [inner],
474
+ );
408
475
  }
409
476
  // \mathbb{R} -> RR
410
477
  if (node.content === '\\mathbb' && arg0.type === 'atom' && /^[A-Z]$/.test(arg0.content)) {
411
- return {
412
- type: 'symbol',
413
- content: arg0.content + arg0.content,
414
- };
478
+ return new TypstNode('symbol', arg0.content + arg0.content);
415
479
  }
416
480
  // \operatorname{opname} -> op("opname")
417
481
  if (node.content === '\\operatorname') {
@@ -422,54 +486,43 @@ export function convertTree(node: TexNode): TypstNode {
422
486
  const text = body[0].content;
423
487
 
424
488
  if (TYPST_INTRINSIC_SYMBOLS.includes(text)) {
425
- return {
426
- type: 'symbol',
427
- content: text,
428
- };
489
+ return new TypstNode('symbol', text);
429
490
  } else {
430
- return {
431
- type: 'unaryFunc',
432
- content: 'op',
433
- args: [{ type: 'text', content: text }],
434
- };
491
+ return new TypstNode(
492
+ 'unaryFunc',
493
+ 'op',
494
+ [new TypstNode('text', text)],
495
+ );
435
496
  }
436
497
  }
437
498
 
438
499
  // generic case
439
- return {
440
- type: 'unaryFunc',
441
- content: convertToken(node.content),
442
- args: node.args!.map(convertTree),
443
- };
500
+ return new TypstNode(
501
+ 'unaryFunc',
502
+ convertToken(node.content),
503
+ node.args!.map(convertTree),
504
+ );
444
505
  }
445
- case 'newline':
446
- return { type: 'newline', content: '\n' };
447
506
  case 'beginend': {
448
507
  const matrix = node.data as TexNode[][];
449
508
  const data = matrix.map((row) => row.map(convertTree));
450
509
 
451
510
  if (node.content!.startsWith('align')) {
452
511
  // align, align*, alignat, alignat*, aligned, etc.
453
- return {
454
- type: 'align',
455
- content: '',
456
- data: data,
457
- };
512
+ return new TypstNode( 'align', '', [], data);
458
513
  } else {
459
- return {
460
- type: 'matrix',
461
- content: 'mat',
462
- data: data,
463
- };
514
+ const res = new TypstNode('matrix', '', [], data);
515
+ res.setOptions({'delim': '#none'});
516
+ return res;
464
517
  }
465
518
  }
466
519
  case 'unknownMacro':
467
- return { type: 'unknown', content: convertToken(node.content) };
520
+ return new TypstNode('unknown', convertToken(node.content));
468
521
  case 'control':
469
522
  if (node.content === '\\\\') {
470
- return { type: 'symbol', content: '\\' };
523
+ return new TypstNode('symbol', '\\');
471
524
  } else if (node.content === '\\,') {
472
- return { type: 'symbol', content: 'thin' };
525
+ return new TypstNode('symbol', 'thin');
473
526
  } else {
474
527
  throw new TypstWriterError(`Unknown control sequence: ${node.content}`, node);
475
528
  }
@@ -482,10 +535,15 @@ export function convertTree(node: TexNode): TypstNode {
482
535
  function convertToken(token: string): string {
483
536
  if (/^[a-zA-Z0-9]$/.test(token)) {
484
537
  return token;
538
+ } else if (token === '/') {
539
+ return '\\/';
540
+ } else if (token === '\\|') {
541
+ // \| in LaTeX is double vertical bar looks like ||
542
+ return 'parallel';
543
+ } else if (token === '\\colon') {
544
+ return ':';
485
545
  } else if (token === '\\\\') {
486
546
  return '\\';
487
- } else if (token == '/') {
488
- return '\\/';
489
547
  } else if (['\\$', '\\#', '\\&', '\\_'].includes(token)) {
490
548
  return token;
491
549
  } else if (token.startsWith('\\')) {