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.
- package/dist/index.js +261 -211
- package/dist/tex-types.d.ts +11 -7
- package/dist/tex2typst.min.js +13 -13
- package/dist/typst-types.d.ts +18 -16
- package/package.json +1 -1
- package/src/convert.ts +131 -101
- package/src/tex-parser.ts +9 -14
- package/src/tex-tokenizer.ts +3 -2
- package/src/tex-types.ts +28 -17
- package/src/typst-parser.ts +39 -19
- package/src/typst-tokenizer.ts +2 -2
- package/src/typst-types.ts +45 -37
- package/src/typst-writer.ts +73 -73
package/dist/typst-types.d.ts
CHANGED
|
@@ -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' | '
|
|
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
|
|
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
|
-
|
|
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,
|
|
83
|
+
constructor(head: TypstToken | null, data: TypstLeftRightData);
|
|
79
84
|
isOverHigh(): boolean;
|
|
80
85
|
}
|
|
81
|
-
export declare class
|
|
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
|
|
87
|
-
|
|
88
|
-
constructor(
|
|
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
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 {
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
395
|
+
return new TypstMatrixLike(null, matrix);
|
|
388
396
|
}
|
|
389
397
|
if (node.head.value === 'cases') {
|
|
390
|
-
return new
|
|
398
|
+
return new TypstMatrixLike(TypstMatrixLike.CASES, matrix);
|
|
391
399
|
}
|
|
392
400
|
if (node.head.value === 'subarray') {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
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.
|
|
410
|
-
const np_new = convert_tex_array_align_literal(node.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
607
|
+
const matrix: TexNode[][] = [];
|
|
598
608
|
for(const row of rows) {
|
|
599
609
|
const cells = array_split(row, alignment_char);
|
|
600
|
-
|
|
610
|
+
matrix.push(cells.map(cell => new TexGroup(cell)));
|
|
601
611
|
}
|
|
602
|
-
return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'aligned'),
|
|
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
|
|
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(
|
|
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
|
|
632
|
-
const
|
|
639
|
+
const arg0 = node.args[0];
|
|
640
|
+
const body = convert_typst_node_to_tex(arg0);
|
|
633
641
|
if (node.isOverHigh()) {
|
|
634
|
-
return new TexLeftRight(
|
|
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
|
|
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
|
|
652
|
-
const
|
|
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(
|
|
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(),
|
|
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
|
|
683
|
-
return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'pmatrix'),
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 '
|
|
741
|
-
const node = abstractNode as
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
207
|
+
const items: TexNode[] = [];
|
|
208
208
|
for (let i = 0; i < num_prime; i++) {
|
|
209
|
-
|
|
209
|
+
items.push(new TexToken(TexTokenType.ELEMENT, "'").toNode());
|
|
210
210
|
}
|
|
211
211
|
if (sup) {
|
|
212
|
-
|
|
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(
|
|
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
|
-
|
|
411
|
+
let data: TexNode | null = null;
|
|
415
412
|
if(['array', 'subarray'].includes(envName)) {
|
|
416
413
|
pos += eat_whitespaces(tokens, pos).length;
|
|
417
|
-
|
|
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),
|
|
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.
|
|
477
|
+
group.items.push(res);
|
|
483
478
|
}
|
|
484
479
|
}
|
|
485
480
|
return allRows;
|
package/src/tex-tokenizer.ts
CHANGED
|
@@ -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
|
-
//
|
|
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]),
|