tex2typst 0.2.13 → 0.2.16

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