tex2typst 0.3.16 → 0.3.18
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/README.md +2 -2
- package/dist/index.js +176 -85
- package/dist/tex2typst.min.js +12 -12
- package/dist/types.d.ts +14 -7
- package/dist/typst-writer.d.ts +3 -1
- package/package.json +1 -1
- package/src/convert.ts +178 -62
- package/src/index.ts +1 -0
- package/src/map.ts +2 -0
- package/src/tex-parser.ts +17 -4
- package/src/types.ts +21 -7
- package/src/typst-writer.ts +17 -26
- package/TODO.md +0 -1
- package/docs/api-reference.md +0 -64
- package/tools/make-shorthand-map.py +0 -33
- package/tools/make-symbol-map.py +0 -35
package/src/convert.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType, TypstLrData, TexArrayData } from "./types";
|
|
1
|
+
import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType, TypstLrData, TexArrayData, TypstNamedParams } from "./types";
|
|
2
2
|
import { TypstWriterError } from "./typst-writer";
|
|
3
3
|
import { symbolMap, reverseSymbolMap } from "./map";
|
|
4
4
|
import { array_join } from "./generic";
|
|
@@ -22,9 +22,6 @@ function tex_token_to_typst(token: string): string {
|
|
|
22
22
|
return token;
|
|
23
23
|
} else if (token === '/') {
|
|
24
24
|
return '\\/';
|
|
25
|
-
} else if (token === '\\|') {
|
|
26
|
-
// \| in LaTeX is double vertical bar looks like ||
|
|
27
|
-
return 'parallel';
|
|
28
25
|
} else if (token === '\\\\') {
|
|
29
26
|
return '\\';
|
|
30
27
|
} else if (['\\$', '\\#', '\\&', '\\_'].includes(token)) {
|
|
@@ -49,40 +46,52 @@ function tex_token_to_typst(token: string): string {
|
|
|
49
46
|
function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
|
|
50
47
|
const [sup, base] = node.args!;
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
// \overset{def}{=} is also considered as eq.def
|
|
57
|
-
if (n.type === 'ordgroup' && n.args!.length === 3) {
|
|
58
|
-
const [a1, a2, a3] = n.args!;
|
|
59
|
-
const d = new TexNode('element', 'd');
|
|
60
|
-
const e = new TexNode('element', 'e');
|
|
61
|
-
const f = new TexNode('element', 'f');
|
|
62
|
-
if (a1.eq(d) && a2.eq(e) && a3.eq(f)) {
|
|
49
|
+
if (options.optimize) {
|
|
50
|
+
const is_def = (n: TexNode): boolean => {
|
|
51
|
+
if (n.eq(new TexNode('text', 'def'))) {
|
|
63
52
|
return true;
|
|
64
53
|
}
|
|
54
|
+
// \overset{def}{=} is also considered as eq.def
|
|
55
|
+
if (n.type === 'ordgroup' && n.args!.length === 3) {
|
|
56
|
+
const [a1, a2, a3] = n.args!;
|
|
57
|
+
const d = new TexNode('element', 'd');
|
|
58
|
+
const e = new TexNode('element', 'e');
|
|
59
|
+
const f = new TexNode('element', 'f');
|
|
60
|
+
if (a1.eq(d) && a2.eq(e) && a3.eq(f)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
};
|
|
66
|
+
const is_eq = (n: TexNode): boolean => n.eq(new TexNode('element', '='));
|
|
67
|
+
if (is_def(sup) && is_eq(base)) {
|
|
68
|
+
return new TypstNode('symbol', 'eq.def');
|
|
65
69
|
}
|
|
66
|
-
return false;
|
|
67
|
-
};
|
|
68
|
-
const is_eq = (n: TexNode): boolean => n.eq(new TexNode('element', '='));
|
|
69
|
-
if (is_def(sup) && is_eq(base)) {
|
|
70
|
-
return new TypstNode('symbol', 'eq.def');
|
|
71
70
|
}
|
|
72
71
|
const limits_call = new TypstNode(
|
|
73
72
|
'funcCall',
|
|
74
73
|
'limits',
|
|
75
74
|
[convert_tex_node_to_typst(base, options)]
|
|
76
75
|
);
|
|
77
|
-
return new TypstNode(
|
|
78
|
-
'supsub',
|
|
79
|
-
'',
|
|
80
|
-
[],
|
|
81
|
-
{
|
|
76
|
+
return new TypstNode('supsub', '', [], {
|
|
82
77
|
base: limits_call,
|
|
83
78
|
sup: convert_tex_node_to_typst(sup, options),
|
|
84
|
-
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// \underset{X}{Y} -> limits(Y)_X
|
|
83
|
+
function convert_underset(node: TexNode, options: Tex2TypstOptions): TypstNode {
|
|
84
|
+
const [sub, base] = node.args!;
|
|
85
|
+
|
|
86
|
+
const limits_call = new TypstNode(
|
|
87
|
+
'funcCall',
|
|
88
|
+
'limits',
|
|
89
|
+
[convert_tex_node_to_typst(base, options)]
|
|
85
90
|
);
|
|
91
|
+
return new TypstNode('supsub', '', [], {
|
|
92
|
+
base: limits_call,
|
|
93
|
+
sub: convert_tex_node_to_typst(sub, options),
|
|
94
|
+
});
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
|
|
@@ -117,7 +126,7 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
|
|
|
117
126
|
case 'supsub': {
|
|
118
127
|
let { base, sup, sub } = node.data as TexSupsubData;
|
|
119
128
|
|
|
120
|
-
//
|
|
129
|
+
// special hook for overbrace
|
|
121
130
|
if (base && base.type === 'unaryFunc' && base.content === '\\overbrace' && sup) {
|
|
122
131
|
return new TypstNode(
|
|
123
132
|
'funcCall',
|
|
@@ -150,43 +159,64 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
|
|
|
150
159
|
return new TypstNode('supsub', '', [], data);
|
|
151
160
|
}
|
|
152
161
|
case 'leftright': {
|
|
153
|
-
const [left,
|
|
154
|
-
|
|
155
|
-
|
|
162
|
+
const [left, _body, right] = node.args!;
|
|
163
|
+
const [typ_left, typ_body, typ_right] = node.args!.map((n) => convert_tex_node_to_typst(n, options));
|
|
164
|
+
|
|
165
|
+
if (options.optimize) {
|
|
166
|
+
// optimization off: "lr(bar.v.double a + 1/2 bar.v.double)"
|
|
167
|
+
// optimization on : "norm(a + 1/2)"
|
|
168
|
+
if (left.content === '\\|' && right.content === '\\|') {
|
|
169
|
+
return new TypstNode('funcCall', 'norm', [typ_body]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// These pairs will be handled by Typst compiler by default. No need to add lr()
|
|
173
|
+
if ([
|
|
174
|
+
"[]", "()", "\\{\\}",
|
|
175
|
+
"\\lfloor\\rfloor",
|
|
176
|
+
"\\lceil\\rceil",
|
|
177
|
+
"\\lfloor\\rceil",
|
|
178
|
+
].includes(left.content + right.content)) {
|
|
179
|
+
return new TypstNode('group', '', [typ_left, typ_body, typ_right]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const group = new TypstNode(
|
|
156
184
|
'group',
|
|
157
185
|
'',
|
|
158
|
-
|
|
186
|
+
[typ_left, typ_body, typ_right]
|
|
159
187
|
);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
188
|
+
|
|
189
|
+
// "\left\{ a + \frac{1}{3} \right." -> "lr(\{ a + 1/3)"
|
|
190
|
+
// "\left. a + \frac{1}{3} \right\}" -> "lr( a + \frac{1}{3} \})"
|
|
191
|
+
// Note that: In lr(), if one side of delimiter doesn't present (i.e. derived from "\\left." or "\\right."),
|
|
192
|
+
// "(", ")", "{", "[", should be escaped with "\" to be the other side of delimiter.
|
|
193
|
+
// Simple "lr({ a+1/3)" doesn't compile in Typst.
|
|
194
|
+
const escape_curly_or_paren = function(s: string): string {
|
|
195
|
+
if (["(", ")", "{", "["].includes(s)) {
|
|
196
|
+
return "\\" + s;
|
|
197
|
+
} else {
|
|
198
|
+
return s;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
170
201
|
if (right.content === '.') {
|
|
171
|
-
|
|
172
|
-
|
|
202
|
+
typ_left.content = escape_curly_or_paren(typ_left.content);
|
|
203
|
+
group.args = [typ_left, typ_body];
|
|
173
204
|
} else if (left.content === '.') {
|
|
174
|
-
|
|
175
|
-
|
|
205
|
+
typ_right.content = escape_curly_or_paren(typ_right.content);
|
|
206
|
+
group.args = [typ_body, typ_right];
|
|
176
207
|
}
|
|
177
|
-
return new TypstNode(
|
|
178
|
-
'funcCall',
|
|
179
|
-
'lr',
|
|
180
|
-
[group]
|
|
181
|
-
);
|
|
208
|
+
return new TypstNode('funcCall', 'lr', [group]);
|
|
182
209
|
}
|
|
183
210
|
case 'binaryFunc': {
|
|
184
211
|
if (node.content === '\\overset') {
|
|
185
212
|
return convert_overset(node, options);
|
|
186
213
|
}
|
|
214
|
+
if (node.content === '\\underset') {
|
|
215
|
+
return convert_underset(node, options);
|
|
216
|
+
}
|
|
187
217
|
// \frac{a}{b} -> a / b
|
|
188
218
|
if (node.content === '\\frac') {
|
|
189
|
-
if(options.fracToSlash) {
|
|
219
|
+
if (options.fracToSlash) {
|
|
190
220
|
return new TypstNode(
|
|
191
221
|
'fraction',
|
|
192
222
|
'',
|
|
@@ -287,6 +317,59 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
|
|
|
287
317
|
if (node.content! === 'cases') {
|
|
288
318
|
return new TypstNode('cases', '', [], data);
|
|
289
319
|
}
|
|
320
|
+
if (node.content! === 'array') {
|
|
321
|
+
const res = new TypstNode('matrix', '', [], data);
|
|
322
|
+
const options: TypstNamedParams = { 'delim': TYPST_NONE };
|
|
323
|
+
|
|
324
|
+
const align_args = node.args!;
|
|
325
|
+
if (align_args.length > 0) {
|
|
326
|
+
const align_node = align_args[0];
|
|
327
|
+
const align_str = (() => {
|
|
328
|
+
if (align_node.type === 'element') return align_node.content;
|
|
329
|
+
if (align_node.type === 'ordgroup') {
|
|
330
|
+
return align_node.args!.map(n => n.type === 'element' ? n.content : '').join('');
|
|
331
|
+
}
|
|
332
|
+
return '';
|
|
333
|
+
})();
|
|
334
|
+
|
|
335
|
+
if (align_str) {
|
|
336
|
+
const alignMap: Record<string, string> = { l: '#left', c: '#center', r: '#right' };
|
|
337
|
+
const chars = Array.from(align_str);
|
|
338
|
+
|
|
339
|
+
const alignments = chars
|
|
340
|
+
.map(c => alignMap[c])
|
|
341
|
+
.filter(Boolean)
|
|
342
|
+
.map(s => new TypstNode('symbol', s!));
|
|
343
|
+
|
|
344
|
+
const vlinePositions: number[] = [];
|
|
345
|
+
let columnIndex = 0;
|
|
346
|
+
for (const c of chars) {
|
|
347
|
+
if (c === '|') {
|
|
348
|
+
vlinePositions.push(columnIndex);
|
|
349
|
+
} else if (c === 'l' || c === 'c' || c === 'r') {
|
|
350
|
+
columnIndex++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (vlinePositions.length > 0) {
|
|
355
|
+
if (vlinePositions.length === 1) {
|
|
356
|
+
options['augment'] = new TypstNode('symbol', `#${vlinePositions[0]}`);
|
|
357
|
+
} else {
|
|
358
|
+
options['augment'] = new TypstNode('symbol', `#(vline: (${vlinePositions.join(', ')}))`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (alignments.length > 0) {
|
|
363
|
+
const first_align = alignments[0].content;
|
|
364
|
+
const all_same = alignments.every(item => item.content === first_align);
|
|
365
|
+
options['align'] = all_same ? alignments[0] : new TypstNode('symbol', '#center');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
res.setOptions(options);
|
|
371
|
+
return res;
|
|
372
|
+
}
|
|
290
373
|
if (node.content!.endsWith('matrix')) {
|
|
291
374
|
let delim: TypstPrimitiveValue;
|
|
292
375
|
switch (node.content) {
|
|
@@ -356,6 +439,8 @@ const TYPST_UNARY_FUNCTIONS: string[] = [
|
|
|
356
439
|
'frak',
|
|
357
440
|
'floor',
|
|
358
441
|
'ceil',
|
|
442
|
+
'norm',
|
|
443
|
+
'limits',
|
|
359
444
|
];
|
|
360
445
|
|
|
361
446
|
const TYPST_BINARY_FUNCTIONS: string[] = [
|
|
@@ -375,8 +460,6 @@ function apply_escape_if_needed(c: string) {
|
|
|
375
460
|
function typst_token_to_tex(token: string): string {
|
|
376
461
|
if (/^[a-zA-Z0-9]$/.test(token)) {
|
|
377
462
|
return token;
|
|
378
|
-
} else if (token === 'thin') {
|
|
379
|
-
return '\\,';
|
|
380
463
|
} else if (reverseSymbolMap.has(token)) {
|
|
381
464
|
return '\\' + reverseSymbolMap.get(token)!;
|
|
382
465
|
}
|
|
@@ -438,6 +521,8 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
|
|
|
438
521
|
let left_delim = apply_escape_if_needed(data.leftDelim);
|
|
439
522
|
assert(data.rightDelim !== null, "leftDelim has value but rightDelim not");
|
|
440
523
|
let right_delim = apply_escape_if_needed(data.rightDelim!);
|
|
524
|
+
// TODO: should be TeXNode('leftright', ...)
|
|
525
|
+
// But currently writer will output `\left |` while people commonly prefer `\left|`.
|
|
441
526
|
return new TexNode('ordgroup', '', [
|
|
442
527
|
new TexNode('element', '\\left' + left_delim),
|
|
443
528
|
...node.args!.map(convert_typst_node_to_tex),
|
|
@@ -447,17 +532,29 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
|
|
|
447
532
|
return new TexNode('ordgroup', '', node.args!.map(convert_typst_node_to_tex));
|
|
448
533
|
}
|
|
449
534
|
}
|
|
535
|
+
// special hook for norm
|
|
536
|
+
// `\| a \|` <- `norm(a)`
|
|
537
|
+
// `\left\| a + \frac{1}{3} \right\|` <- `norm(a + 1/3)`
|
|
538
|
+
if (node.content === 'norm') {
|
|
539
|
+
const arg0 = node.args![0];
|
|
540
|
+
const tex_node_type = node.isOverHigh() ? 'leftright' : 'ordgroup';
|
|
541
|
+
return new TexNode(tex_node_type, '', [
|
|
542
|
+
new TexNode('symbol', "\\|"),
|
|
543
|
+
convert_typst_node_to_tex(arg0),
|
|
544
|
+
new TexNode('symbol', "\\|")
|
|
545
|
+
]);
|
|
546
|
+
}
|
|
450
547
|
// special hook for floor, ceil
|
|
451
|
-
//
|
|
548
|
+
// `\lfloor a \rfloor` <- `floor(a)`
|
|
549
|
+
// `\lceil a \rceil` <- `ceil(a)`
|
|
550
|
+
// `\left\lfloor a \right\rfloor` <- `floor(a)`
|
|
551
|
+
// `\left\lceil a \right\rceil` <- `ceil(a)`
|
|
452
552
|
if (node.content === 'floor' || node.content === 'ceil') {
|
|
453
|
-
|
|
454
|
-
|
|
553
|
+
const left = "\\l" + node.content;
|
|
554
|
+
const right = "\\r" + node.content;
|
|
455
555
|
const arg0 = node.args![0];
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
right = "\\right" + right;
|
|
459
|
-
}
|
|
460
|
-
return new TexNode('ordgroup', '', [
|
|
556
|
+
const tex_node_type = node.isOverHigh() ? 'leftright' : 'ordgroup';
|
|
557
|
+
return new TexNode(tex_node_type, '', [
|
|
461
558
|
new TexNode('symbol', left),
|
|
462
559
|
convert_typst_node_to_tex(arg0),
|
|
463
560
|
new TexNode('symbol', right)
|
|
@@ -499,15 +596,34 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
|
|
|
499
596
|
}
|
|
500
597
|
case 'supsub': {
|
|
501
598
|
const { base, sup, sub } = node.data as TypstSupsubData;
|
|
502
|
-
const base_tex = convert_typst_node_to_tex(base);
|
|
503
599
|
let sup_tex: TexNode | undefined;
|
|
504
600
|
let sub_tex: TexNode | undefined;
|
|
601
|
+
|
|
505
602
|
if (sup) {
|
|
506
603
|
sup_tex = convert_typst_node_to_tex(sup);
|
|
507
604
|
}
|
|
508
605
|
if (sub) {
|
|
509
606
|
sub_tex = convert_typst_node_to_tex(sub);
|
|
510
607
|
}
|
|
608
|
+
|
|
609
|
+
// special hook for limits
|
|
610
|
+
// `limits(+)^a` -> `\overset{a}{+}`
|
|
611
|
+
// `limits(+)_a` -> `\underset{a}{+}`
|
|
612
|
+
// `limits(+)_a^b` -> `\overset{b}{\underset{a}{+}}`
|
|
613
|
+
if (base.eq(new TypstNode('funcCall', 'limits'))) {
|
|
614
|
+
const body_in_limits = convert_typst_node_to_tex(base.args![0]);
|
|
615
|
+
if (sup_tex !== undefined && sub_tex === undefined) {
|
|
616
|
+
return new TexNode('binaryFunc', '\\overset', [sup_tex, body_in_limits]);
|
|
617
|
+
} else if (sup_tex === undefined && sub_tex !== undefined) {
|
|
618
|
+
return new TexNode('binaryFunc', '\\underset', [sub_tex, body_in_limits]);
|
|
619
|
+
} else {
|
|
620
|
+
const underset_call = new TexNode('binaryFunc', '\\underset', [sub_tex!, body_in_limits]);
|
|
621
|
+
return new TexNode('binaryFunc', '\\overset', [sup_tex!, underset_call]);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const base_tex = convert_typst_node_to_tex(base);
|
|
626
|
+
|
|
511
627
|
const res = new TexNode('supsub', '', [], {
|
|
512
628
|
base: base_tex,
|
|
513
629
|
sup: sup_tex,
|
package/src/index.ts
CHANGED
package/src/map.ts
CHANGED
package/src/tex-parser.ts
CHANGED
|
@@ -47,6 +47,7 @@ const BINARY_COMMANDS = [
|
|
|
47
47
|
'dfrac',
|
|
48
48
|
'tbinom',
|
|
49
49
|
'overset',
|
|
50
|
+
'underset',
|
|
50
51
|
]
|
|
51
52
|
|
|
52
53
|
const IGNORED_COMMANDS = [
|
|
@@ -86,7 +87,7 @@ function eat_whitespaces(tokens: TexToken[], start: number): TexToken[] {
|
|
|
86
87
|
|
|
87
88
|
function eat_parenthesis(tokens: TexToken[], start: number): TexToken | null {
|
|
88
89
|
const firstToken = tokens[start];
|
|
89
|
-
if (firstToken.type === TexTokenType.ELEMENT && ['(', ')', '[', ']', '|', '\\{', '\\}', '.'].includes(firstToken.value)) {
|
|
90
|
+
if (firstToken.type === TexTokenType.ELEMENT && ['(', ')', '[', ']', '|', '\\{', '\\}', '.', '\\|'].includes(firstToken.value)) {
|
|
90
91
|
return firstToken;
|
|
91
92
|
} else if (firstToken.type === TexTokenType.COMMAND && ['lfloor', 'rfloor', 'lceil', 'rceil', 'langle', 'rangle'].includes(firstToken.value.slice(1))) {
|
|
92
93
|
return firstToken;
|
|
@@ -166,7 +167,7 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
|
|
|
166
167
|
],
|
|
167
168
|
[String.raw`%[^\n]*`, (s) => new TexToken(TexTokenType.COMMENT, s.text()!.substring(1))],
|
|
168
169
|
[String.raw`[{}_^&]`, (s) => new TexToken(TexTokenType.CONTROL, s.text()!)],
|
|
169
|
-
[String.raw`\\[
|
|
170
|
+
[String.raw`\\[\\,:;! ]`, (s) => new TexToken(TexTokenType.CONTROL, s.text()!)],
|
|
170
171
|
[String.raw`\r?\n`, (_s) => new TexToken(TexTokenType.NEWLINE, "\n")],
|
|
171
172
|
[String.raw`\s+`, (s) => new TexToken(TexTokenType.SPACE, s.text()!)],
|
|
172
173
|
[String.raw`\\[{}%$&#_|]`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
|
|
@@ -394,6 +395,7 @@ export class LatexParser {
|
|
|
394
395
|
case '}':
|
|
395
396
|
throw new LatexParserError("Unmatched '}'");
|
|
396
397
|
case '\\\\':
|
|
398
|
+
case '\\!':
|
|
397
399
|
case '\\,':
|
|
398
400
|
case '\\:':
|
|
399
401
|
case '\\;':
|
|
@@ -548,7 +550,18 @@ export class LatexParser {
|
|
|
548
550
|
const envName = tokens[pos + 1].value;
|
|
549
551
|
pos += 3;
|
|
550
552
|
|
|
551
|
-
|
|
553
|
+
const args: TexNode[] = [];
|
|
554
|
+
while (pos < tokens.length) {
|
|
555
|
+
const whitespaceCount = eat_whitespaces(tokens, pos).length;
|
|
556
|
+
pos += whitespaceCount;
|
|
557
|
+
|
|
558
|
+
if (pos >= tokens.length || !tokens[pos].eq(LEFT_CURLY_BRACKET)) {
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
const [arg, newPos] = this.parseNextArg(tokens, pos);
|
|
562
|
+
args.push(arg);
|
|
563
|
+
pos = newPos;
|
|
564
|
+
}
|
|
552
565
|
|
|
553
566
|
const exprInsideStart = pos;
|
|
554
567
|
|
|
@@ -573,7 +586,7 @@ export class LatexParser {
|
|
|
573
586
|
exprInside.pop();
|
|
574
587
|
}
|
|
575
588
|
const body = this.parseAligned(exprInside);
|
|
576
|
-
const res = new TexNode('beginend', envName,
|
|
589
|
+
const res = new TexNode('beginend', envName, args, body);
|
|
577
590
|
return [res, pos];
|
|
578
591
|
}
|
|
579
592
|
|
package/src/types.ts
CHANGED
|
@@ -125,6 +125,13 @@ export class TexNode {
|
|
|
125
125
|
case 'ordgroup': {
|
|
126
126
|
return this.args!.map((n) => n.serialize()).flat();
|
|
127
127
|
}
|
|
128
|
+
case 'leftright': {
|
|
129
|
+
let tokens = this.args!.map((n) => n.serialize()).flat();
|
|
130
|
+
tokens.splice(0, 0, new TexToken(TexTokenType.COMMAND, '\\left'));
|
|
131
|
+
tokens.splice(tokens.length - 1, 0, new TexToken(TexTokenType.COMMAND, '\\right'));
|
|
132
|
+
|
|
133
|
+
return tokens;
|
|
134
|
+
}
|
|
128
135
|
case 'unaryFunc': {
|
|
129
136
|
let tokens: TexToken[] = [];
|
|
130
137
|
tokens.push(new TexToken(TexTokenType.COMMAND, this.content));
|
|
@@ -382,14 +389,21 @@ export const TYPST_NONE = new TypstNode('none', '#none');
|
|
|
382
389
|
export const TYPST_TRUE: TypstPrimitiveValue = true;
|
|
383
390
|
export const TYPST_FALSE: TypstPrimitiveValue = false;
|
|
384
391
|
|
|
392
|
+
/**
|
|
393
|
+
* ATTENTION:
|
|
394
|
+
* Don't use any options except those explicitly documented in
|
|
395
|
+
* https://github.com/qwinsi/tex2typst/blob/main/docs/api-reference.md
|
|
396
|
+
* Any undocumented options may break in the future!
|
|
397
|
+
*/
|
|
385
398
|
export interface Tex2TypstOptions {
|
|
386
|
-
nonStrict?: boolean;
|
|
387
|
-
preferTypstIntrinsic?: boolean;
|
|
388
|
-
preferShorthands?: boolean;
|
|
389
|
-
keepSpaces?: boolean;
|
|
390
|
-
fracToSlash?: boolean;
|
|
391
|
-
inftyToOo?: boolean;
|
|
392
|
-
|
|
399
|
+
nonStrict?: boolean; /** default is true */
|
|
400
|
+
preferTypstIntrinsic?: boolean; /** default is true */
|
|
401
|
+
preferShorthands?: boolean; /** default is true */
|
|
402
|
+
keepSpaces?: boolean; /** default is false */
|
|
403
|
+
fracToSlash?: boolean; /** default is true */
|
|
404
|
+
inftyToOo?: boolean; /** default is false */
|
|
405
|
+
optimize?: boolean; /** default is true */
|
|
406
|
+
nonAsciiWrapper?: string; /** default is "" */
|
|
393
407
|
customTexMacros?: { [key: string]: string };
|
|
394
408
|
// TODO: custom typst functions
|
|
395
409
|
}
|
package/src/typst-writer.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface TypstWriterOptions {
|
|
|
40
40
|
preferShorthands: boolean;
|
|
41
41
|
keepSpaces: boolean;
|
|
42
42
|
inftyToOo: boolean;
|
|
43
|
+
optimize: boolean;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export class TypstWriter {
|
|
@@ -47,17 +48,19 @@ export class TypstWriter {
|
|
|
47
48
|
private preferShorthands: boolean;
|
|
48
49
|
private keepSpaces: boolean;
|
|
49
50
|
private inftyToOo: boolean;
|
|
51
|
+
private optimize: boolean;
|
|
50
52
|
|
|
51
53
|
protected buffer: string = "";
|
|
52
54
|
protected queue: TypstToken[] = [];
|
|
53
55
|
|
|
54
56
|
private insideFunctionDepth = 0;
|
|
55
57
|
|
|
56
|
-
constructor(
|
|
57
|
-
this.nonStrict =
|
|
58
|
-
this.preferShorthands =
|
|
59
|
-
this.keepSpaces =
|
|
60
|
-
this.inftyToOo =
|
|
58
|
+
constructor(options: TypstWriterOptions) {
|
|
59
|
+
this.nonStrict = options.nonStrict;
|
|
60
|
+
this.preferShorthands = options.preferShorthands;
|
|
61
|
+
this.keepSpaces = options.keepSpaces;
|
|
62
|
+
this.inftyToOo = options.inftyToOo;
|
|
63
|
+
this.optimize = options.optimize;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
|
|
@@ -68,8 +71,6 @@ export class TypstWriter {
|
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
// TODO: "C \frac{xy}{z}" should translate to "C (x y)/z" instead of "C(x y)/z"
|
|
72
|
-
|
|
73
74
|
let no_need_space = false;
|
|
74
75
|
// putting the first token in clause
|
|
75
76
|
no_need_space ||= /[\(\[\|]$/.test(this.buffer) && /^\w/.test(str);
|
|
@@ -163,7 +164,7 @@ export class TypstWriter {
|
|
|
163
164
|
// Put prime symbol before '_'. Because $y_1'$ is not displayed properly in Typst (so far)
|
|
164
165
|
// e.g.
|
|
165
166
|
// y_1' -> y'_1
|
|
166
|
-
// y_{a_1}' -> y'_
|
|
167
|
+
// y_{a_1}' -> y'_(a_1)
|
|
167
168
|
this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '\''));
|
|
168
169
|
trailing_space_needed = false;
|
|
169
170
|
}
|
|
@@ -207,21 +208,9 @@ export class TypstWriter {
|
|
|
207
208
|
}
|
|
208
209
|
case 'fraction': {
|
|
209
210
|
const [numerator, denominator] = node.args!;
|
|
210
|
-
|
|
211
|
-
this.queue.push(TYPST_LEFT_PARENTHESIS);
|
|
212
|
-
this.serialize(numerator);
|
|
213
|
-
this.queue.push(TYPST_RIGHT_PARENTHESIS);
|
|
214
|
-
} else {
|
|
215
|
-
this.serialize(numerator);
|
|
216
|
-
}
|
|
211
|
+
this.appendWithBracketsIfNeeded(numerator);
|
|
217
212
|
this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '/'));
|
|
218
|
-
|
|
219
|
-
this.queue.push(TYPST_LEFT_PARENTHESIS);
|
|
220
|
-
this.serialize(denominator);
|
|
221
|
-
this.queue.push(TYPST_RIGHT_PARENTHESIS);
|
|
222
|
-
} else {
|
|
223
|
-
this.serialize(denominator);
|
|
224
|
-
}
|
|
213
|
+
this.appendWithBracketsIfNeeded(denominator);
|
|
225
214
|
break;
|
|
226
215
|
}
|
|
227
216
|
case 'align': {
|
|
@@ -316,7 +305,7 @@ export class TypstWriter {
|
|
|
316
305
|
}
|
|
317
306
|
|
|
318
307
|
private appendWithBracketsIfNeeded(node: TypstNode): boolean {
|
|
319
|
-
let need_to_wrap = ['group', 'supsub', 'empty'].includes(node.type);
|
|
308
|
+
let need_to_wrap = ['group', 'supsub', 'fraction','empty'].includes(node.type);
|
|
320
309
|
|
|
321
310
|
if (node.type === 'group') {
|
|
322
311
|
if (node.args!.length === 0) {
|
|
@@ -387,9 +376,11 @@ export class TypstWriter {
|
|
|
387
376
|
res = res.replace(/round\(\)/g, 'round("")');
|
|
388
377
|
return res;
|
|
389
378
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
379
|
+
if (this.optimize) {
|
|
380
|
+
const all_passes = [smartFloorPass, smartCeilPass, smartRoundPass];
|
|
381
|
+
for (const pass of all_passes) {
|
|
382
|
+
this.buffer = pass(this.buffer);
|
|
383
|
+
}
|
|
393
384
|
}
|
|
394
385
|
return this.buffer;
|
|
395
386
|
}
|
package/TODO.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
- Tex math `\frac{X}{\frac{Y}{2}}` should converts to Typst `X/(Y/2)`, not `X/Y/2`.
|
package/docs/api-reference.md
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# API Reference of tex2typst.js
|
|
2
|
-
|
|
3
|
-
## Basic usage
|
|
4
|
-
|
|
5
|
-
```javascript
|
|
6
|
-
import { tex2typst, typst2tex } from 'tex2typst';
|
|
7
|
-
|
|
8
|
-
let tex = "e \\overset{\\text{def}}{=} \\lim_{{n \\to \\infty}} \left(1 + \\frac{1}{n}\\right)^n";
|
|
9
|
-
let typst = tex2typst(tex);
|
|
10
|
-
console.log(typst);
|
|
11
|
-
// e eq.def lim_(n -> infinity)(1 + 1/n)^n
|
|
12
|
-
|
|
13
|
-
let tex_recovered = typst2tex(typst);
|
|
14
|
-
console.log(tex_recovered);
|
|
15
|
-
// e \overset{\text{def}}{=} \lim_{n \rightarrow \infty} \left(1 + \frac{1}{n} \right)^n
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Advanced options
|
|
19
|
-
|
|
20
|
-
`tex2typst` function accepts an optional second argument, which is an object containing options to customize the conversion.
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
interface Tex2TypstOptions {
|
|
24
|
-
preferShorthands?: boolean;
|
|
25
|
-
fracToSlash?: boolean;
|
|
26
|
-
inftyToOo?: boolean;
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
- `preferShorthands`: If set to `true`, the function will prefer using shorthands in Typst (e.g., `->` instead of `arrow.r`, `<<` instead of `lt.double`) when converting TeX to Typst. Default is `ture`.
|
|
31
|
-
|
|
32
|
-
```javascript
|
|
33
|
-
let tex = "a \\rightarrow b \\ll c";
|
|
34
|
-
let typst1 = tex2typst(tex, { preferShorthands: false });
|
|
35
|
-
console.log(typst1);
|
|
36
|
-
// a arrow.r b lt.double c
|
|
37
|
-
let typst2 = tex2typst(tex, { preferShorthands: true });
|
|
38
|
-
console.log(typst2);
|
|
39
|
-
// a -> b << c
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
- `fracToSlash`: If set to `true`, the Typst result will use the slash notation for fractions. Default is `true`.
|
|
43
|
-
|
|
44
|
-
```javascript
|
|
45
|
-
let tex = "\\frac{a}{b}";
|
|
46
|
-
let tpyst1 = tex2typst(tex, { fracToSlash: false });
|
|
47
|
-
console.log(typst1);
|
|
48
|
-
// frac(a, b)
|
|
49
|
-
let typst2 = tex2typst(tex, { fracToSlash: true });
|
|
50
|
-
console.log(typst2);
|
|
51
|
-
// a / b
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
- `inftyToOo`: If set to `true`, `\infty` converts to `oo` instead of `infinity`. Default is `false`.
|
|
55
|
-
|
|
56
|
-
```javascript
|
|
57
|
-
let tex = "\\infty";
|
|
58
|
-
let typst1 = tex2typst(tex, { inftyToOo: false });
|
|
59
|
-
console.log(typst1);
|
|
60
|
-
// infinity
|
|
61
|
-
let typst2 = tex2typst(tex, { inftyToOo: true });
|
|
62
|
-
console.log(typst2);
|
|
63
|
-
// oo
|
|
64
|
-
```
|