tex2typst 0.3.24 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ export declare enum TypstTokenType {
10
10
  NEWLINE = 8
11
11
  }
12
12
  export declare class TypstToken {
13
- type: TypstTokenType;
13
+ readonly type: TypstTokenType;
14
14
  value: string;
15
15
  constructor(type: TypstTokenType, content: string);
16
16
  eq(other: TypstToken): boolean;
@@ -25,6 +25,7 @@ export interface TypstSupsubData {
25
25
  sub: TypstNode | null;
26
26
  }
27
27
  export interface TypstLeftRightData {
28
+ body: TypstNode;
28
29
  left: TypstToken | null;
29
30
  right: TypstToken | null;
30
31
  }
@@ -32,17 +33,17 @@ export interface TypstLeftRightData {
32
33
  * fraction: `1/2`, `(x + y)/2`, `(1+x)/(1-x)`
33
34
  * group: `a + 1/3`
34
35
  * leftright: `(a + 1/3)`, `[a + 1/3)`, `lr(]sum_(x=1)^n])`
36
+ * markupFunc: `#heading(level: 2)[something]`, `#text(fill: red)[some text and math $x + y$]`
35
37
  */
36
- export type TypstNodeType = 'terminal' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'leftright' | 'align' | 'matrix' | 'cases';
38
+ export type TypstNodeType = 'terminal' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'leftright' | 'matrixLike' | 'markupFunc';
37
39
  export type TypstNamedParams = {
38
40
  [key: string]: TypstNode;
39
41
  };
40
42
  export declare abstract class TypstNode {
41
43
  readonly type: TypstNodeType;
42
44
  head: TypstToken;
43
- args?: TypstNode[];
44
45
  options?: TypstNamedParams;
45
- constructor(type: TypstNodeType, head: TypstToken | null, args?: TypstNode[]);
46
+ constructor(type: TypstNodeType, head: TypstToken | null);
46
47
  abstract isOverHigh(): boolean;
47
48
  setOptions(options: TypstNamedParams): void;
48
49
  eq(other: TypstNode): boolean;
@@ -54,7 +55,8 @@ export declare class TypstTerminal extends TypstNode {
54
55
  toString(): string;
55
56
  }
56
57
  export declare class TypstGroup extends TypstNode {
57
- constructor(args: TypstNode[]);
58
+ items: TypstNode[];
59
+ constructor(items: TypstNode[]);
58
60
  isOverHigh(): boolean;
59
61
  }
60
62
  export declare class TypstSupsub extends TypstNode {
@@ -65,31 +67,31 @@ export declare class TypstSupsub extends TypstNode {
65
67
  isOverHigh(): boolean;
66
68
  }
67
69
  export declare class TypstFuncCall extends TypstNode {
70
+ args: TypstNode[];
68
71
  constructor(head: TypstToken, args: TypstNode[]);
69
72
  isOverHigh(): boolean;
70
73
  }
71
74
  export declare class TypstFraction extends TypstNode {
75
+ args: TypstNode[];
72
76
  constructor(args: TypstNode[]);
73
77
  isOverHigh(): boolean;
74
78
  }
75
79
  export declare class TypstLeftright extends TypstNode {
80
+ body: TypstNode;
76
81
  left: TypstToken | null;
77
82
  right: TypstToken | null;
78
- constructor(head: TypstToken | null, args: TypstNode[], data: TypstLeftRightData);
83
+ constructor(head: TypstToken | null, data: TypstLeftRightData);
79
84
  isOverHigh(): boolean;
80
85
  }
81
- export declare class TypstAlign extends TypstNode {
86
+ export declare class TypstMatrixLike extends TypstNode {
82
87
  matrix: TypstNode[][];
83
- constructor(data: TypstNode[][]);
88
+ constructor(head: TypstToken | null, data: TypstNode[][]);
84
89
  isOverHigh(): boolean;
90
+ static readonly MAT: TypstToken;
91
+ static readonly CASES: TypstToken;
85
92
  }
86
- export declare class TypstMatrix extends TypstNode {
87
- matrix: TypstNode[][];
88
- constructor(data: TypstNode[][]);
89
- isOverHigh(): boolean;
90
- }
91
- export declare class TypstCases extends TypstNode {
92
- matrix: TypstNode[][];
93
- constructor(data: TypstNode[][]);
93
+ export declare class TypstMarkupFunc extends TypstNode {
94
+ fragments: TypstNode[];
95
+ constructor(head: TypstToken, fragments: TypstNode[]);
94
96
  isOverHigh(): boolean;
95
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tex2typst",
3
- "version": "0.3.24",
3
+ "version": "0.3.25",
4
4
  "description": "JavaScript library for converting TeX code to Typst",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/convert.ts CHANGED
@@ -2,7 +2,7 @@ import { TexNode, Tex2TypstOptions,
2
2
  TexToken, TexTokenType, TexFuncCall, TexGroup, TexSupSub,
3
3
  TexText, TexBeginEnd, TexLeftRight,
4
4
  TexTerminal} from "./tex-types";
5
- import { TypstAlign, TypstCases, TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstMatrix, TypstNode, TypstSupsub, TypstTerminal } from "./typst-types";
5
+ import { TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstMarkupFunc, TypstMatrixLike, TypstNode, TypstSupsub, TypstTerminal } from "./typst-types";
6
6
  import { TypstNamedParams } from "./typst-types";
7
7
  import { TypstSupsubData } from "./typst-types";
8
8
  import { TypstToken } from "./typst-types";
@@ -116,8 +116,8 @@ function tex_token_to_typst(token: TexToken, options: Tex2TypstOptions): TypstTo
116
116
 
117
117
  // \overset{X}{Y} -> limits(Y)^X
118
118
  // and with special case \overset{\text{def}}{=} -> eq.def
119
- function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
120
- const [sup, base] = node.args!;
119
+ function convert_overset(node: TexFuncCall, options: Tex2TypstOptions): TypstNode {
120
+ const [sup, base] = node.args;
121
121
 
122
122
  if (options.optimize) {
123
123
  // \overset{\text{def}}{=} or \overset{def}{=} are considered as eq.def
@@ -137,8 +137,8 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
137
137
  }
138
138
 
139
139
  // \underset{X}{Y} -> limits(Y)_X
140
- function convert_underset(node: TexNode, options: Tex2TypstOptions): TypstNode {
141
- const [sub, base] = node.args!;
140
+ function convert_underset(node: TexFuncCall, options: Tex2TypstOptions): TypstNode {
141
+ const [sub, base] = node.args;
142
142
 
143
143
  const limits_call = new TypstFuncCall(
144
144
  new TypstToken(TypstTokenType.SYMBOL, 'limits'),
@@ -209,22 +209,23 @@ export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2Ty
209
209
  case 'ordgroup':
210
210
  const node = abstractNode as TexGroup;
211
211
  return new TypstGroup(
212
- node.args!.map((n) => convert_tex_node_to_typst(n, options))
212
+ node.items.map((n) => convert_tex_node_to_typst(n, options))
213
213
  );
214
214
  case 'supsub': {
215
215
  const node = abstractNode as TexSupSub;
216
216
  let { base, sup, sub } = node;
217
217
 
218
218
  // special hook for overbrace
219
+ // \overbrace{X}^{Y} -> overbrace(X, Y)
219
220
  if (base && base.type === 'funcCall' && base.head.value === '\\overbrace' && sup) {
220
221
  return new TypstFuncCall(
221
222
  new TypstToken(TypstTokenType.SYMBOL, 'overbrace'),
222
- [convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sup, options)]
223
+ [convert_tex_node_to_typst((base as TexFuncCall).args[0], options), convert_tex_node_to_typst(sup, options)]
223
224
  );
224
225
  } else if (base && base.type === 'funcCall' && base.head.value === '\\underbrace' && sub) {
225
226
  return new TypstFuncCall(
226
227
  new TypstToken(TypstTokenType.SYMBOL, 'underbrace'),
227
- [convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sub, options)]
228
+ [convert_tex_node_to_typst((base as TexFuncCall).args[0], options), convert_tex_node_to_typst(sub, options)]
228
229
  );
229
230
  }
230
231
 
@@ -239,11 +240,8 @@ export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2Ty
239
240
  case 'leftright': {
240
241
  const node = abstractNode as TexLeftRight;
241
242
  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
243
 
244
+ const typ_body = convert_tex_node_to_typst(node.body, options);
247
245
 
248
246
  if (options.optimize) {
249
247
  // optimization off: "lr(bar.v.double a + 1/2 bar.v.double)"
@@ -291,13 +289,12 @@ export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2Ty
291
289
 
292
290
  return new TypstLeftright(
293
291
  new TypstToken(TypstTokenType.SYMBOL, 'lr'),
294
- [typ_body],
295
- { left: typ_left, right: typ_right }
292
+ { body: typ_body, left: typ_left, right: typ_right }
296
293
  );
297
294
  }
298
295
  case 'funcCall': {
299
296
  const node = abstractNode as TexFuncCall;
300
- const arg0 = convert_tex_node_to_typst(node.args![0], options);
297
+ const arg0 = convert_tex_node_to_typst(node.args[0], options);
301
298
  // \sqrt[3]{x} -> root(3, x)
302
299
  if (node.head.value === '\\sqrt' && node.data) {
303
300
  const data = convert_tex_node_to_typst(node.data, options); // the number of times to take the root
@@ -342,6 +339,16 @@ export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2Ty
342
339
  return new TypstFuncCall(new TypstToken(TypstTokenType.SYMBOL, 'op'), [new TypstToken(TypstTokenType.TEXT, arg0.head.value).toNode()]);
343
340
  }
344
341
 
342
+ // \textcolor{red}{2y} -> #text(fill: red)[$2y$]
343
+ if (node.head.value === '\\textcolor') {
344
+ const res = new TypstMarkupFunc(
345
+ new TypstToken(TypstTokenType.SYMBOL, `#text`),
346
+ [convert_tex_node_to_typst(node.args[1], options)]
347
+ );
348
+ res.setOptions({ fill: arg0 });
349
+ return res;
350
+ }
351
+
345
352
  // \substack{a \\ b} -> `a \ b`
346
353
  // as in translation from \sum_{\substack{a \\ b}} to sum_(a \ b)
347
354
  if (node.head.value === '\\substack') {
@@ -358,9 +365,10 @@ export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2Ty
358
365
  // \frac{a}{b} -> a / b
359
366
  if (node.head.value === '\\frac') {
360
367
  if (options.fracToSlash) {
361
- return new TypstFraction(node.args!.map((n) => convert_tex_node_to_typst(n, options)));
368
+ return new TypstFraction(node.args.map((n) => convert_tex_node_to_typst(n, options)));
362
369
  }
363
370
  }
371
+
364
372
  if(options.optimize) {
365
373
  // \mathbb{R} -> RR
366
374
  if (node.head.value === '\\mathbb' && /^\\mathbb{[A-Z]}$/.test(node.toString())) {
@@ -375,47 +383,49 @@ export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2Ty
375
383
  // generic case
376
384
  return new TypstFuncCall(
377
385
  tex_token_to_typst(node.head, options),
378
- node.args!.map((n) => convert_tex_node_to_typst(n, options))
386
+ node.args.map((n) => convert_tex_node_to_typst(n, options))
379
387
  );
380
388
  }
381
389
  case 'beginend': {
382
390
  const node = abstractNode as TexBeginEnd;
383
- const data = node.matrix.map((row) => row.map((n) => convert_tex_node_to_typst(n, options)));
391
+ const matrix = node.matrix.map((row) => row.map((n) => convert_tex_node_to_typst(n, options)));
384
392
 
385
393
  if (node.head.value.startsWith('align')) {
386
394
  // align, align*, alignat, alignat*, aligned, etc.
387
- return new TypstAlign(data);
395
+ return new TypstMatrixLike(null, matrix);
388
396
  }
389
397
  if (node.head.value === 'cases') {
390
- return new TypstCases(data);
398
+ return new TypstMatrixLike(TypstMatrixLike.CASES, matrix);
391
399
  }
392
400
  if (node.head.value === 'subarray') {
393
- const align_node = node.args![0];
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;
401
+ if (node.data) {
402
+ const align_node = node.data;
403
+ switch (align_node.head.value) {
404
+ case 'r':
405
+ matrix.forEach(row => (row[0] as TypstGroup).items.push(new TypstToken(TypstTokenType.CONTROL, '&').toNode()));
406
+ break;
407
+ case 'l':
408
+ matrix.forEach(row => (row[0] as TypstGroup).items.unshift(new TypstToken(TypstTokenType.CONTROL, '&').toNode()));
409
+ break;
410
+ default:
411
+ break;
412
+ }
403
413
  }
404
- return new TypstAlign(data);
414
+ return new TypstMatrixLike(null, matrix);
405
415
  }
406
416
  if (node.head.value === 'array') {
407
417
  const np: TypstNamedParams = { 'delim': TYPST_NONE };
408
418
 
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);
419
+ assert(node.data !== null && node.head.type === TexTokenType.LITERAL);
420
+ const np_new = convert_tex_array_align_literal(node.data!.head.value);
411
421
  Object.assign(np, np_new);
412
422
 
413
- const res = new TypstMatrix(data);
423
+ const res = new TypstMatrixLike(TypstMatrixLike.MAT, matrix);
414
424
  res.setOptions(np);
415
425
  return res;
416
426
  }
417
427
  if (node.head.value.endsWith('matrix')) {
418
- const res = new TypstMatrix(data);
428
+ const res = new TypstMatrixLike(TypstMatrixLike.MAT, matrix);
419
429
  let delim: TypstToken;
420
430
  switch (node.head.value) {
421
431
  case 'matrix':
@@ -588,24 +598,24 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
588
598
 
589
599
  case 'group': {
590
600
  const node = abstractNode as TypstGroup;
591
- const args = node.args!.map(convert_typst_node_to_tex);
601
+ const args = node.items.map(convert_typst_node_to_tex);
592
602
  const alignment_char = new TexToken(TexTokenType.CONTROL, '&').toNode();
593
603
  const newline_char = new TexToken(TexTokenType.CONTROL, '\\\\').toNode();
594
604
  if (array_includes(args, alignment_char)) {
595
605
  // wrap the whole math formula with \begin{aligned} and \end{aligned}
596
606
  const rows = array_split(args, newline_char);
597
- const data: TexNode[][] = [];
607
+ const matrix: TexNode[][] = [];
598
608
  for(const row of rows) {
599
609
  const cells = array_split(row, alignment_char);
600
- data.push(cells.map(cell => new TexGroup(cell)));
610
+ matrix.push(cells.map(cell => new TexGroup(cell)));
601
611
  }
602
- return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'aligned'), [], data);
612
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'aligned'), matrix);
603
613
  }
604
614
  return new TexGroup(args);
605
615
  }
606
616
  case 'leftright': {
607
617
  const node = abstractNode as TypstLeftright;
608
- const args = node.args!.map(convert_typst_node_to_tex);
618
+ const body = convert_typst_node_to_tex(node.body);
609
619
  let left = node.left? typst_token_to_tex(node.left) : new TexToken(TexTokenType.ELEMENT, '.');
610
620
  let right = node.right? typst_token_to_tex(node.right) : new TexToken(TexTokenType.ELEMENT, '.');
611
621
  // const is_over_high = node.isOverHigh();
@@ -615,11 +625,9 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
615
625
  left.value = '\\left' + left.value;
616
626
  right.value = '\\right' + right.value;
617
627
  }
618
- args.unshift(left.toNode());
619
- args.push(right.toNode());
620
628
  // TODO: should be TeXLeftRight(...)
621
629
  // But currently writer will output `\left |` while people commonly prefer `\left|`.
622
- return new TexGroup(args);
630
+ return new TexGroup([left.toNode(), body, right.toNode()]);
623
631
  }
624
632
  case 'funcCall': {
625
633
  const node = abstractNode as TypstFuncCall;
@@ -628,15 +636,16 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
628
636
  // `\| a \|` <- `norm(a)`
629
637
  // `\left\| a + \frac{1}{3} \right\|` <- `norm(a + 1/3)`
630
638
  case 'norm': {
631
- const arg0 = node.args![0];
632
- const args = [ convert_typst_node_to_tex(arg0) ];
639
+ const arg0 = node.args[0];
640
+ const body = convert_typst_node_to_tex(arg0);
633
641
  if (node.isOverHigh()) {
634
- return new TexLeftRight(args, {
642
+ return new TexLeftRight({
643
+ body: body,
635
644
  left: new TexToken(TexTokenType.COMMAND, "\\|"),
636
645
  right: new TexToken(TexTokenType.COMMAND, "\\|")
637
646
  });
638
647
  } else {
639
- return new TexGroup(args);
648
+ return body;
640
649
  }
641
650
  }
642
651
  // special hook for floor, ceil
@@ -648,29 +657,30 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
648
657
  case 'ceil': {
649
658
  const left = "\\l" + node.head.value;
650
659
  const right = "\\r" + node.head.value;
651
- const arg0 = node.args![0];
652
- const typ_arg0 = convert_typst_node_to_tex(arg0);
660
+ const arg0 = node.args[0];
661
+ const body = convert_typst_node_to_tex(arg0);
653
662
  const left_node = new TexToken(TexTokenType.COMMAND, left);
654
663
  const right_node = new TexToken(TexTokenType.COMMAND, right);
655
664
  if (node.isOverHigh()) {
656
- return new TexLeftRight([typ_arg0], {
665
+ return new TexLeftRight({
666
+ body: body,
657
667
  left: left_node,
658
668
  right: right_node
659
669
  });
660
670
  } else {
661
- return new TexGroup([left_node.toNode(), typ_arg0, right_node.toNode()]);
671
+ return new TexGroup([left_node.toNode(), body, right_node.toNode()]);
662
672
  }
663
673
  }
664
674
  // special hook for root
665
675
  case 'root': {
666
- const [degree, radicand] = node.args!;
676
+ const [degree, radicand] = node.args;
667
677
  const data = convert_typst_node_to_tex(degree);
668
678
  return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\sqrt'), [convert_typst_node_to_tex(radicand)], data);
669
679
  }
670
680
  // special hook for overbrace and underbrace
671
681
  case 'overbrace':
672
682
  case 'underbrace': {
673
- const [body, label] = node.args!;
683
+ const [body, label] = node.args;
674
684
  const base = new TexFuncCall(typst_token_to_tex(node.head), [convert_typst_node_to_tex(body)]);
675
685
  const script = convert_typst_node_to_tex(label);
676
686
  const data = node.head.value === 'overbrace' ? { base, sup: script, sub: null } : { base, sub: script, sup: null };
@@ -679,12 +689,12 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
679
689
  // special hook for vec
680
690
  // "vec(a, b, c)" -> "\begin{pmatrix}a\\ b\\ c\end{pmatrix}"
681
691
  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);
692
+ const tex_matrix = node.args.map(convert_typst_node_to_tex).map((n) => [n]);
693
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'pmatrix'), tex_matrix);
684
694
  }
685
695
  // special hook for op
686
696
  case 'op': {
687
- const arg0 = node.args![0];
697
+ const arg0 = node.args[0];
688
698
  assert(arg0.head.type === TypstTokenType.TEXT);
689
699
  return new TexFuncCall(typst_token_to_tex(node.head), [new TexToken(TexTokenType.LITERAL, arg0.head.value).toNode()]);
690
700
  }
@@ -694,18 +704,36 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
694
704
  const is_known_func = TEX_UNARY_COMMANDS.includes(func_name_tex.value.substring(1))
695
705
  || TEX_BINARY_COMMANDS.includes(func_name_tex.value.substring(1));
696
706
  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));
707
+ return new TexFuncCall(func_name_tex, node.args.map(convert_typst_node_to_tex));
698
708
  } else {
699
709
  return new TexGroup([
700
710
  typst_token_to_tex(node.head).toNode(),
701
711
  new TexToken(TexTokenType.ELEMENT, '(').toNode(),
702
- ...array_intersperse(node.args!.map(convert_typst_node_to_tex), TEX_NODE_COMMA),
712
+ ...array_intersperse(node.args.map(convert_typst_node_to_tex), TEX_NODE_COMMA),
703
713
  new TexToken(TexTokenType.ELEMENT, ')').toNode()
704
714
  ]);
705
715
  }
706
716
  }
707
717
  }
708
718
  }
719
+ case 'markupFunc': {
720
+ const node = abstractNode as TypstMarkupFunc;
721
+ switch (node.head.value) {
722
+ case '#text': {
723
+ // `\textcolor{red}{2y}` <- `#text(fill: red)[$2 y$]`
724
+ if (node.options && node.options['fill']) {
725
+ const color = node.options['fill'];
726
+ return new TexFuncCall(
727
+ new TexToken(TexTokenType.COMMAND, '\\textcolor'),
728
+ [convert_typst_node_to_tex(color), convert_typst_node_to_tex(node.fragments[0])]
729
+ )
730
+ }
731
+ }
732
+ case '#heading':
733
+ default:
734
+ throw new Error(`Unimplemented markup function: ${node.head.value}`);
735
+ }
736
+ }
709
737
  case 'supsub': {
710
738
  const node = abstractNode as TypstSupsub;
711
739
  const { base, sup, sub } = node;
@@ -717,7 +745,8 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
717
745
  // `limits(+)_a` -> `\underset{a}{+}`
718
746
  // `limits(+)_a^b` -> `\overset{b}{\underset{a}{+}}`
719
747
  if (base.head.eq(new TypstToken(TypstTokenType.SYMBOL, 'limits'))) {
720
- const body_in_limits = convert_typst_node_to_tex(base.args![0]);
748
+ const limits = base as TypstFuncCall;
749
+ const body_in_limits = convert_typst_node_to_tex(limits.args[0]);
721
750
  if (sup_tex !== null && sub_tex === null) {
722
751
  return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [sup_tex, body_in_limits]);
723
752
  } else if (sup_tex === null && sub_tex !== null) {
@@ -737,54 +766,55 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
737
766
  });
738
767
  return res;
739
768
  }
740
- case 'matrix': {
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
744
- if (node.options) {
745
- if ('delim' in node.options) {
746
- const delim = node.options.delim;
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}`);
769
+ case 'matrixLike': {
770
+ const node = abstractNode as TypstMatrixLike;
771
+ const tex_matrix = node.matrix.map(row => row.map(convert_typst_node_to_tex));
772
+ if (node.head.eq(TypstMatrixLike.MAT)) {
773
+ let env_type = 'pmatrix'; // typst mat use delim:"(" by default
774
+ if (node.options) {
775
+ if ('delim' in node.options) {
776
+ const delim = node.options.delim;
777
+ switch (delim.head.value) {
778
+ case '#none':
779
+ env_type = 'matrix';
780
+ break;
781
+ case '[':
782
+ case ']':
783
+ env_type = 'bmatrix';
784
+ break;
785
+ case '(':
786
+ case ')':
787
+ env_type = 'pmatrix';
788
+ break;
789
+ case '{':
790
+ case '}':
791
+ env_type = 'Bmatrix';
792
+ break;
793
+ case '|':
794
+ env_type = 'vmatrix';
795
+ break;
796
+ case 'bar':
797
+ case 'bar.v':
798
+ env_type = 'vmatrix';
799
+ break;
800
+ case 'bar.v.double':
801
+ env_type = 'Vmatrix';
802
+ break;
803
+ default:
804
+ throw new Error(`Unexpected delimiter ${delim.head}`);
805
+ }
775
806
  }
776
807
  }
808
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, env_type), tex_matrix);
809
+ } else if (node.head.eq(TypstMatrixLike.CASES)) {
810
+ return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'cases'), tex_matrix);
811
+ } else {
812
+ throw new Error(`Unexpected matrix type ${node.head}`);
777
813
  }
778
- return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, env_type), [], tex_data);
779
- }
780
- case 'cases': {
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);
784
814
  }
785
815
  case 'fraction': {
786
816
  const node = abstractNode as TypstFraction;
787
- const [numerator, denominator] = node.args!;
817
+ const [numerator, denominator] = node.args;
788
818
  const num_tex = convert_typst_node_to_tex(numerator);
789
819
  const den_tex = convert_typst_node_to_tex(denominator);
790
820
  return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\frac'), [num_tex, den_tex]);
package/src/tex-parser.ts CHANGED
@@ -204,16 +204,14 @@ export class LatexParser {
204
204
  res.sub = sub;
205
205
  }
206
206
  if (num_prime > 0) {
207
- res.sup = new TexGroup([]);
207
+ const items: TexNode[] = [];
208
208
  for (let i = 0; i < num_prime; i++) {
209
- res.sup.args!.push(new TexToken(TexTokenType.ELEMENT, "'").toNode());
209
+ items.push(new TexToken(TexTokenType.ELEMENT, "'").toNode());
210
210
  }
211
211
  if (sup) {
212
- res.sup.args!.push(sup);
213
- }
214
- if (res.sup.args!.length === 1) {
215
- res.sup = res.sup.args![0];
212
+ items.push(sup);
216
213
  }
214
+ res.sup = items.length === 1 ? items[0] : new TexGroup(items);
217
215
  } else if (sup) {
218
216
  res.sup = sup;
219
217
  }
@@ -393,10 +391,9 @@ export class LatexParser {
393
391
  pos++;
394
392
 
395
393
  const [body, _] = this.parseGroup(tokens, exprInsideStart, exprInsideEnd);
396
- const args = [ body ];
397
394
  const left = leftDelimiter.value === '.'? null: leftDelimiter;
398
395
  const right = rightDelimiter.value === '.'? null: rightDelimiter;
399
- const res = new TexLeftRight(args, {left: left, right: right});
396
+ const res = new TexLeftRight({body: body, left: left, right: right});
400
397
  return [res, pos];
401
398
  }
402
399
 
@@ -411,12 +408,10 @@ export class LatexParser {
411
408
  pos += 3;
412
409
 
413
410
 
414
- const args: TexNode[] = [];
411
+ let data: TexNode | null = null;
415
412
  if(['array', 'subarray'].includes(envName)) {
416
413
  pos += eat_whitespaces(tokens, pos).length;
417
- const [arg, newPos] = this.parseNextArg(tokens, pos);
418
- args.push(arg);
419
- pos = newPos;
414
+ [data, pos] = this.parseNextArg(tokens, pos);
420
415
  }
421
416
 
422
417
  pos += eat_whitespaces(tokens, pos).length; // ignore whitespaces and '\n' after \begin{envName}
@@ -445,7 +440,7 @@ export class LatexParser {
445
440
  exprInside.pop();
446
441
  }
447
442
  const body = this.parseAligned(exprInside);
448
- const res = new TexBeginEnd(new TexToken(TexTokenType.LITERAL, envName), args, body);
443
+ const res = new TexBeginEnd(new TexToken(TexTokenType.LITERAL, envName), body, data);
449
444
  return [res, pos];
450
445
  }
451
446
 
@@ -479,7 +474,7 @@ export class LatexParser {
479
474
  group = new TexGroup([]);
480
475
  row.push(group);
481
476
  } else {
482
- group.args!.push(res);
477
+ group.items.push(res);
483
478
  }
484
479
  }
485
480
  return allRows;
@@ -46,6 +46,7 @@ export const TEX_BINARY_COMMANDS = [
46
46
  'tbinom',
47
47
  'overset',
48
48
  'underset',
49
+ 'textcolor',
49
50
  ]
50
51
 
51
52
 
@@ -58,7 +59,7 @@ function unescape(str: string): string {
58
59
  }
59
60
 
60
61
  const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[]>([
61
- // math `\begin{array}{cc}`
62
+ // match `\begin{array}{cc}`
62
63
  [
63
64
  String.raw`\\begin{(array|subarry)}{(.+?)}`, (s) => {
64
65
  const match = s.reMatchArray()!;
@@ -74,7 +75,7 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
74
75
  }
75
76
  ],
76
77
  [
77
- String.raw`\\(text|operatorname|begin|end|hspace|array){(.+?)}`, (s) => {
78
+ String.raw`\\(text|operatorname|textcolor|begin|end|hspace|array){(.+?)}`, (s) => {
78
79
  const match = s.reMatchArray()!;
79
80
  return [
80
81
  new TexToken(TexTokenType.COMMAND, '\\' + match[1]),