tex2typst 0.3.0 → 0.3.2
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 +8 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +451 -241
- package/dist/tex-parser.d.ts +1 -0
- package/dist/tex2typst.min.js +20 -19
- package/dist/types.d.ts +8 -4
- package/dist/typst-shorthands.d.ts +3 -0
- package/dist/typst-writer.d.ts +9 -3
- package/docs/api-reference.md +64 -0
- package/package.json +5 -5
- package/src/convert.ts +43 -6
- package/src/index.ts +12 -15
- package/src/map.ts +13 -36
- package/src/tex-parser.ts +33 -44
- package/src/tex-writer.ts +0 -1
- package/src/types.ts +10 -2
- package/src/typst-parser.ts +24 -5
- package/src/typst-shorthands.ts +51 -0
- package/src/typst-writer.ts +52 -25
- package/tools/make-shorthand-map.py +33 -0
- package/tools/make-symbol-map.py +4 -3
package/dist/typst-writer.d.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { TexNode, TypstNode, TypstToken } from "./types";
|
|
2
|
-
export declare const TYPST_INTRINSIC_SYMBOLS: string[];
|
|
3
2
|
export declare class TypstWriterError extends Error {
|
|
4
3
|
node: TexNode | TypstNode | TypstToken;
|
|
5
4
|
constructor(message: string, node: TexNode | TypstNode | TypstToken);
|
|
6
5
|
}
|
|
6
|
+
export interface TypstWriterOptions {
|
|
7
|
+
nonStrict: boolean;
|
|
8
|
+
preferShorthands: boolean;
|
|
9
|
+
keepSpaces: boolean;
|
|
10
|
+
inftyToOo: boolean;
|
|
11
|
+
}
|
|
7
12
|
export declare class TypstWriter {
|
|
8
13
|
private nonStrict;
|
|
9
|
-
private
|
|
14
|
+
private preferShorthands;
|
|
10
15
|
private keepSpaces;
|
|
16
|
+
private inftyToOo;
|
|
11
17
|
protected buffer: string;
|
|
12
18
|
protected queue: TypstToken[];
|
|
13
19
|
private insideFunctionDepth;
|
|
14
|
-
constructor(
|
|
20
|
+
constructor(opt: TypstWriterOptions);
|
|
15
21
|
private writeBuffer;
|
|
16
22
|
serialize(node: TypstNode): void;
|
|
17
23
|
private appendWithBracketsIfNeeded;
|
|
@@ -0,0 +1,64 @@
|
|
|
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 arrow.r 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
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tex2typst",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "JavaScript library for converting TeX code to Typst",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"prebuild": "rimraf dist/",
|
|
18
|
-
"build:node": "
|
|
19
|
-
"build:browser": "
|
|
18
|
+
"build:node": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --format=esm",
|
|
19
|
+
"build:browser": "esbuild src/tex2typst.ts --bundle --platform=browser --outfile=dist/tex2typst.min.js --minify",
|
|
20
20
|
"build:types": "tsc --project ./tsconfig.json",
|
|
21
21
|
"build": "npm run build:node && npm run build:browser && npm run build:types",
|
|
22
22
|
"test": "vitest run",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"js-yaml": "^4.1.0",
|
|
28
28
|
"rimraf": "^3.0.2",
|
|
29
29
|
"toml": "^3.0.0",
|
|
30
|
-
"ts-node": "^10.9.2",
|
|
31
30
|
"typescript": "^5.5.3",
|
|
32
|
-
"vitest": "^2.0.2"
|
|
31
|
+
"vitest": "^2.0.2",
|
|
32
|
+
"esbuild": "^0.25.1"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {}
|
|
35
35
|
}
|
package/src/convert.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions } from "./types";
|
|
2
|
-
import { TypstWriterError
|
|
1
|
+
import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType } from "./types";
|
|
2
|
+
import { TypstWriterError } from "./typst-writer";
|
|
3
3
|
import { symbolMap, reverseSymbolMap } from "./map";
|
|
4
4
|
|
|
5
|
+
// symbols that are supported by Typst but not by KaTeX
|
|
6
|
+
const TYPST_INTRINSIC_SYMBOLS = [
|
|
7
|
+
'dim',
|
|
8
|
+
'id',
|
|
9
|
+
'im',
|
|
10
|
+
'mod',
|
|
11
|
+
'Pr',
|
|
12
|
+
'sech',
|
|
13
|
+
'csch',
|
|
14
|
+
// 'sgn
|
|
15
|
+
];
|
|
5
16
|
|
|
6
17
|
function tex_token_to_typst(token: string): string {
|
|
7
18
|
if (/^[a-zA-Z0-9]$/.test(token)) {
|
|
@@ -60,7 +71,7 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
|
|
|
60
71
|
'op',
|
|
61
72
|
[convert_tex_node_to_typst(base, options)]
|
|
62
73
|
);
|
|
63
|
-
op_call.setOptions({ limits:
|
|
74
|
+
op_call.setOptions({ limits: TYPST_TRUE });
|
|
64
75
|
return new TypstNode(
|
|
65
76
|
'supsub',
|
|
66
77
|
'',
|
|
@@ -240,11 +251,37 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
|
|
|
240
251
|
if (node.content!.startsWith('align')) {
|
|
241
252
|
// align, align*, alignat, alignat*, aligned, etc.
|
|
242
253
|
return new TypstNode('align', '', [], data);
|
|
243
|
-
}
|
|
254
|
+
}
|
|
255
|
+
if (node.content!.endsWith('matrix')) {
|
|
256
|
+
let delim: TypstPrimitiveValue = null;
|
|
257
|
+
switch (node.content) {
|
|
258
|
+
case 'matrix':
|
|
259
|
+
delim = TYPST_NONE;
|
|
260
|
+
break;
|
|
261
|
+
case 'pmatrix':
|
|
262
|
+
delim = '(';
|
|
263
|
+
break;
|
|
264
|
+
case 'bmatrix':
|
|
265
|
+
delim = '[';
|
|
266
|
+
break;
|
|
267
|
+
case 'Bmatrix':
|
|
268
|
+
delim = '{';
|
|
269
|
+
break;
|
|
270
|
+
case 'vmatrix':
|
|
271
|
+
delim = '|';
|
|
272
|
+
break;
|
|
273
|
+
case 'Vmatrix': {
|
|
274
|
+
delim = new TypstToken(TypstTokenType.SYMBOL, 'bar.v.double');
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
default:
|
|
278
|
+
throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
|
|
279
|
+
}
|
|
244
280
|
const res = new TypstNode('matrix', '', [], data);
|
|
245
|
-
res.setOptions({ 'delim':
|
|
281
|
+
res.setOptions({ 'delim': delim });
|
|
246
282
|
return res;
|
|
247
283
|
}
|
|
284
|
+
throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
|
|
248
285
|
}
|
|
249
286
|
case 'unknownMacro':
|
|
250
287
|
return new TypstNode('unknown', tex_token_to_typst(node.content));
|
|
@@ -416,7 +453,7 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
|
|
|
416
453
|
if (node.options) {
|
|
417
454
|
if ('delim' in node.options) {
|
|
418
455
|
switch (node.options.delim) {
|
|
419
|
-
case
|
|
456
|
+
case TYPST_NONE:
|
|
420
457
|
return matrix;
|
|
421
458
|
case '[':
|
|
422
459
|
left_delim = "\\left[";
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseTex } from "./tex-parser";
|
|
2
|
-
import { Tex2TypstOptions } from "./types";
|
|
3
|
-
import { TypstWriter } from "./typst-writer";
|
|
2
|
+
import type { Tex2TypstOptions } from "./types";
|
|
3
|
+
import { TypstWriter, type TypstWriterOptions } from "./typst-writer";
|
|
4
4
|
import { convert_tex_node_to_typst, convert_typst_node_to_tex } from "./convert";
|
|
5
5
|
import { symbolMap } from "./map";
|
|
6
6
|
import { parseTypst } from "./typst-parser";
|
|
@@ -11,27 +11,24 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
|
|
|
11
11
|
const opt: Tex2TypstOptions = {
|
|
12
12
|
nonStrict: true,
|
|
13
13
|
preferTypstIntrinsic: true,
|
|
14
|
+
preferShorthands: true,
|
|
14
15
|
keepSpaces: false,
|
|
15
16
|
fracToSlash: true,
|
|
17
|
+
inftyToOo: false,
|
|
16
18
|
customTexMacros: {}
|
|
17
19
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
if (options.customTexMacros) {
|
|
26
|
-
opt.customTexMacros = options.customTexMacros;
|
|
27
|
-
}
|
|
28
|
-
if (options.fracToSlash !== undefined) {
|
|
29
|
-
opt.fracToSlash = options.fracToSlash;
|
|
20
|
+
|
|
21
|
+
if(options !== undefined) {
|
|
22
|
+
for (const key in opt) {
|
|
23
|
+
if (options[key] !== undefined) {
|
|
24
|
+
opt[key] = options[key];
|
|
25
|
+
}
|
|
30
26
|
}
|
|
31
27
|
}
|
|
28
|
+
|
|
32
29
|
const texTree = parseTex(tex, opt.customTexMacros!);
|
|
33
30
|
const typstTree = convert_tex_node_to_typst(texTree, opt);
|
|
34
|
-
const writer = new TypstWriter(opt
|
|
31
|
+
const writer = new TypstWriter(opt as TypstWriterOptions);
|
|
35
32
|
writer.serialize(typstTree);
|
|
36
33
|
return writer.finalize();
|
|
37
34
|
}
|
package/src/map.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
const symbolMap = new Map<string, string>([
|
|
2
|
+
['cos', 'cos'],
|
|
3
|
+
['sin', 'sin'],
|
|
4
|
+
['tan', 'tan'],
|
|
5
|
+
['cot', 'cot'],
|
|
6
|
+
['sec', 'sec'],
|
|
7
|
+
['csc', 'csc'],
|
|
8
|
+
['mod', 'mod'],
|
|
9
|
+
['omicron', 'omicron'],
|
|
10
|
+
['Xi', 'Xi'],
|
|
11
|
+
['Upsilon', 'Upsilon'],
|
|
12
|
+
['lim', 'lim'],
|
|
13
|
+
|
|
2
14
|
['nonumber', ''],
|
|
3
15
|
['vec', 'arrow'],
|
|
4
16
|
['neq', 'eq.not'],
|
|
@@ -59,37 +71,11 @@ const symbolMap = new Map<string, string>([
|
|
|
59
71
|
|
|
60
72
|
/* arrows */
|
|
61
73
|
['gets', 'arrow.l'],
|
|
62
|
-
['hookleftarrow', 'arrow.l.hook'],
|
|
63
|
-
['leftharpoonup', 'harpoon.lt'],
|
|
64
|
-
['leftharpoondown', 'harpoon.lb'],
|
|
65
|
-
['rightleftharpoons', 'harpoons.rtlb'],
|
|
66
|
-
['longleftarrow', 'arrow.l.long'],
|
|
67
|
-
['longrightarrow', 'arrow.r.long'],
|
|
68
|
-
['longleftrightarrow', 'arrow.l.r.long'],
|
|
69
|
-
['Longleftarrow', 'arrow.l.double.long'],
|
|
70
|
-
['Longrightarrow', 'arrow.r.double.long'],
|
|
71
|
-
['Longleftrightarrow', 'arrow.l.r.double.long'],
|
|
72
74
|
// ['longmapsto', 'arrow.r.bar'],
|
|
73
|
-
['hookrightarrow', 'arrow.r.hook'],
|
|
74
|
-
['rightharpoonup', 'harpoon.rt'],
|
|
75
|
-
['rightharpoondown', 'harpoon.rb'],
|
|
76
75
|
['iff', 'arrow.l.r.double.long'],
|
|
77
76
|
['implies', 'arrow.r.double.long'],
|
|
78
|
-
['uparrow', 'arrow.t'],
|
|
79
|
-
['downarrow', 'arrow.b'],
|
|
80
|
-
['updownarrow', 'arrow.t.b'],
|
|
81
|
-
['Uparrow', 'arrow.t.double'],
|
|
82
|
-
['Downarrow', 'arrow.b.double'],
|
|
83
|
-
['Updownarrow', 'arrow.t.b.double'],
|
|
84
|
-
['nearrow', 'arrow.tr'],
|
|
85
|
-
['searrow', 'arrow.br'],
|
|
86
|
-
['swarrow', 'arrow.bl'],
|
|
87
|
-
['nwarrow', 'arrow.tl'],
|
|
88
77
|
['leadsto', 'arrow.squiggly'],
|
|
89
78
|
|
|
90
|
-
['leftleftarrows', 'arrows.ll'],
|
|
91
|
-
['rightrightarrows', 'arrows.rr'],
|
|
92
|
-
|
|
93
79
|
|
|
94
80
|
['Cap', 'sect.double'],
|
|
95
81
|
['Cup', 'union.double'],
|
|
@@ -97,9 +83,6 @@ const symbolMap = new Map<string, string>([
|
|
|
97
83
|
['Gamma', 'Gamma'],
|
|
98
84
|
['Join', 'join'],
|
|
99
85
|
['Lambda', 'Lambda'],
|
|
100
|
-
['Leftarrow', 'arrow.l.double'],
|
|
101
|
-
['Leftrightarrow', 'arrow.l.r.double'],
|
|
102
|
-
['Longrightarrow', 'arrow.r.double.long'],
|
|
103
86
|
['Omega', 'Omega'],
|
|
104
87
|
['P', 'pilcrow'],
|
|
105
88
|
['Phi', 'Phi'],
|
|
@@ -153,7 +136,6 @@ const symbolMap = new Map<string, string>([
|
|
|
153
136
|
['div', 'div'],
|
|
154
137
|
['divideontimes', 'times.div'],
|
|
155
138
|
['dotplus', 'plus.dot'],
|
|
156
|
-
['downarrow', 'arrow.b'],
|
|
157
139
|
['ell', 'ell'],
|
|
158
140
|
['emptyset', 'nothing'],
|
|
159
141
|
['epsilon', 'epsilon.alt'],
|
|
@@ -186,8 +168,6 @@ const symbolMap = new Map<string, string>([
|
|
|
186
168
|
['lbrack', 'bracket.l'],
|
|
187
169
|
['ldots', 'dots.h'],
|
|
188
170
|
['le', 'lt.eq'],
|
|
189
|
-
['leadsto', 'arrow.squiggly'],
|
|
190
|
-
['leftarrow', 'arrow.l'],
|
|
191
171
|
['leftthreetimes', 'times.three.l'],
|
|
192
172
|
['leftrightarrow', 'arrow.l.r'],
|
|
193
173
|
['leq', 'lt.eq'],
|
|
@@ -224,7 +204,6 @@ const symbolMap = new Map<string, string>([
|
|
|
224
204
|
['nu', 'nu'],
|
|
225
205
|
['ntriangleleft', 'lt.tri.not'],
|
|
226
206
|
['ntriangleright', 'gt.tri.not'],
|
|
227
|
-
['nwarrow', 'arrow.tl'],
|
|
228
207
|
['odot', 'dot.circle'],
|
|
229
208
|
['oint', 'integral.cont'],
|
|
230
209
|
['oiint', 'integral.surf'],
|
|
@@ -277,7 +256,6 @@ const symbolMap = new Map<string, string>([
|
|
|
277
256
|
['supset', 'supset'],
|
|
278
257
|
['supseteq', 'supset.eq'],
|
|
279
258
|
['supsetneq', 'supset.neq'],
|
|
280
|
-
['swarrow', 'arrow.bl'],
|
|
281
259
|
['tau', 'tau'],
|
|
282
260
|
['theta', 'theta'],
|
|
283
261
|
['times', 'times'],
|
|
@@ -288,8 +266,6 @@ const symbolMap = new Map<string, string>([
|
|
|
288
266
|
// ['triangleleft', 'triangle.l.small'],
|
|
289
267
|
// ['triangleright', 'triangle.r.small'],
|
|
290
268
|
['twoheadrightarrow', 'arrow.r.twohead'],
|
|
291
|
-
['uparrow', 'arrow.t'],
|
|
292
|
-
['updownarrow', 'arrow.t.b'],
|
|
293
269
|
['upharpoonright', 'harpoon.tr'],
|
|
294
270
|
['uplus', 'union.plus'],
|
|
295
271
|
['upsilon', 'upsilon'],
|
|
@@ -1086,6 +1062,7 @@ for(const [key, value] of Array.from(symbolMap.entries()).reverse()) {
|
|
|
1086
1062
|
reverseSymbolMap.set(value, key);
|
|
1087
1063
|
}
|
|
1088
1064
|
reverseSymbolMap.set('dif', 'mathrm{d}');
|
|
1065
|
+
reverseSymbolMap.set('oo', 'infty');
|
|
1089
1066
|
|
|
1090
1067
|
// force override some one-to-multiple mappings
|
|
1091
1068
|
const typst_to_tex_map = new Map<string, string>([
|
package/src/tex-parser.ts
CHANGED
|
@@ -296,46 +296,39 @@ export class LatexParser {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
parse(tokens: TexToken[]): TexNode {
|
|
299
|
+
const [tree, _] = this.parseGroup(tokens, 0, tokens.length);
|
|
300
|
+
return tree;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
parseGroup(tokens: TexToken[], start: number, end: number): ParseResult {
|
|
299
304
|
const results: TexNode[] = [];
|
|
300
|
-
let pos =
|
|
301
|
-
while (pos <
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
pos = newPos;
|
|
308
|
-
if(res.type === 'whitespace') {
|
|
309
|
-
if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
if (!this.newline_sensitive && res.content === '\n') {
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
305
|
+
let pos = start;
|
|
306
|
+
while (pos < end) {
|
|
307
|
+
const [res, newPos] = this.parseNextExpr(tokens, pos);
|
|
308
|
+
pos = newPos;
|
|
309
|
+
if(res.type === 'whitespace') {
|
|
310
|
+
if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
|
|
311
|
+
continue;
|
|
315
312
|
}
|
|
316
|
-
if (
|
|
317
|
-
|
|
313
|
+
if (!this.newline_sensitive && res.content === '\n') {
|
|
314
|
+
continue;
|
|
318
315
|
}
|
|
319
|
-
results.push(res);
|
|
320
316
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return EMPTY_NODE;
|
|
324
|
-
} else if (results.length === 1) {
|
|
325
|
-
return results[0];
|
|
326
|
-
} else {
|
|
327
|
-
return new TexNode('ordgroup', '', results);
|
|
317
|
+
if (res.type === 'control' && res.content === '&') {
|
|
318
|
+
throw new LatexParserError('Unexpected & outside of an alignment');
|
|
328
319
|
}
|
|
320
|
+
results.push(res);
|
|
329
321
|
}
|
|
330
322
|
|
|
331
|
-
|
|
323
|
+
let node: TexNode;
|
|
332
324
|
if (results.length === 0) {
|
|
333
|
-
|
|
325
|
+
node = EMPTY_NODE;
|
|
334
326
|
} else if (results.length === 1) {
|
|
335
|
-
|
|
327
|
+
node = results[0];
|
|
336
328
|
} else {
|
|
337
|
-
|
|
329
|
+
node = new TexNode('ordgroup', '', results);
|
|
338
330
|
}
|
|
331
|
+
return [node, end + 1];
|
|
339
332
|
}
|
|
340
333
|
|
|
341
334
|
parseNextExpr(tokens: TexToken[], start: number): ParseResult {
|
|
@@ -396,8 +389,7 @@ export class LatexParser {
|
|
|
396
389
|
|
|
397
390
|
parseNextExprWithoutSupSub(tokens: TexToken[], start: number): ParseResult {
|
|
398
391
|
const firstToken = tokens[start];
|
|
399
|
-
|
|
400
|
-
switch (tokenType) {
|
|
392
|
+
switch (firstToken.type) {
|
|
401
393
|
case TexTokenType.ELEMENT:
|
|
402
394
|
return [new TexNode('element', firstToken.value), start + 1];
|
|
403
395
|
case TexTokenType.TEXT:
|
|
@@ -423,8 +415,7 @@ export class LatexParser {
|
|
|
423
415
|
if(posClosingBracket === -1) {
|
|
424
416
|
throw new LatexParserError("Unmatched '{'");
|
|
425
417
|
}
|
|
426
|
-
|
|
427
|
-
return [this.parse(exprInside), posClosingBracket + 1];
|
|
418
|
+
return this.parseGroup(tokens, start + 1, posClosingBracket);
|
|
428
419
|
case '}':
|
|
429
420
|
throw new LatexParserError("Unmatched '}'");
|
|
430
421
|
case '\\\\':
|
|
@@ -453,7 +444,7 @@ export class LatexParser {
|
|
|
453
444
|
|
|
454
445
|
if (['left', 'right', 'begin', 'end'].includes(command.slice(1))) {
|
|
455
446
|
throw new LatexParserError('Unexpected command: ' + command);
|
|
456
|
-
}
|
|
447
|
+
}
|
|
457
448
|
|
|
458
449
|
|
|
459
450
|
const paramNum = get_command_param_num(command.slice(1));
|
|
@@ -475,8 +466,7 @@ export class LatexParser {
|
|
|
475
466
|
if (posRightSquareBracket === -1) {
|
|
476
467
|
throw new LatexParserError('No matching right square bracket for [');
|
|
477
468
|
}
|
|
478
|
-
const
|
|
479
|
-
const exponent = this.parse(exprInside);
|
|
469
|
+
const [exponent, _] = this.parseGroup(tokens, posLeftSquareBracket + 1, posRightSquareBracket);
|
|
480
470
|
const [arg1, newPos] = this.parseNextExprWithoutSupSub(tokens, posRightSquareBracket + 1);
|
|
481
471
|
return [new TexNode('unaryFunc', command, [arg1], exponent), newPos];
|
|
482
472
|
} else if (command === '\\text') {
|
|
@@ -529,15 +519,14 @@ export class LatexParser {
|
|
|
529
519
|
if (pos >= tokens.length) {
|
|
530
520
|
throw new LatexParserError('Expecting \\right after \\left');
|
|
531
521
|
}
|
|
532
|
-
|
|
522
|
+
|
|
533
523
|
const rightDelimiter = eat_parenthesis(tokens, pos);
|
|
534
524
|
if (rightDelimiter === null) {
|
|
535
525
|
throw new LatexParserError('Invalid delimiter after \\right');
|
|
536
526
|
}
|
|
537
527
|
pos++;
|
|
538
528
|
|
|
539
|
-
const
|
|
540
|
-
const body = this.parse(exprInside);
|
|
529
|
+
const [body, _] = this.parseGroup(tokens, exprInsideStart, exprInsideEnd);
|
|
541
530
|
const args: TexNode[] = [
|
|
542
531
|
new TexNode('element', leftDelimiter.value),
|
|
543
532
|
body,
|
|
@@ -556,9 +545,9 @@ export class LatexParser {
|
|
|
556
545
|
assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
|
|
557
546
|
const envName = tokens[pos + 1].value;
|
|
558
547
|
pos += 3;
|
|
559
|
-
|
|
548
|
+
|
|
560
549
|
pos += eat_whitespaces(tokens, pos).length; // ignore whitespaces and '\n' after \begin{envName}
|
|
561
|
-
|
|
550
|
+
|
|
562
551
|
const exprInsideStart = pos;
|
|
563
552
|
|
|
564
553
|
const endIdx = find_closing_end_command(tokens, start);
|
|
@@ -567,7 +556,7 @@ export class LatexParser {
|
|
|
567
556
|
}
|
|
568
557
|
const exprInsideEnd = endIdx;
|
|
569
558
|
pos = endIdx + 1;
|
|
570
|
-
|
|
559
|
+
|
|
571
560
|
assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
|
|
572
561
|
assert(tokens[pos + 1].type === TexTokenType.TEXT);
|
|
573
562
|
assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
|
|
@@ -575,7 +564,7 @@ export class LatexParser {
|
|
|
575
564
|
throw new LatexParserError('Mismatched \\begin and \\end environments');
|
|
576
565
|
}
|
|
577
566
|
pos += 3;
|
|
578
|
-
|
|
567
|
+
|
|
579
568
|
const exprInside = tokens.slice(exprInsideStart, exprInsideEnd);
|
|
580
569
|
// ignore spaces and '\n' before \end{envName}
|
|
581
570
|
while(exprInside.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(exprInside[exprInside.length - 1].type)) {
|
|
@@ -606,7 +595,7 @@ export class LatexParser {
|
|
|
606
595
|
continue;
|
|
607
596
|
}
|
|
608
597
|
}
|
|
609
|
-
|
|
598
|
+
|
|
610
599
|
if (res.type === 'control' && res.content === '\\\\') {
|
|
611
600
|
row = [];
|
|
612
601
|
group = new TexNode('ordgroup', '', []);
|
package/src/tex-writer.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -352,7 +352,13 @@ export type TypstArrayData = TypstNode[][];
|
|
|
352
352
|
type TypstNodeType = 'atom' | 'symbol' | 'text' | 'control' | 'comment' | 'whitespace'
|
|
353
353
|
| 'empty' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'align' | 'matrix' | 'unknown';
|
|
354
354
|
|
|
355
|
-
export type
|
|
355
|
+
export type TypstPrimitiveValue = string | boolean | null | TypstToken;
|
|
356
|
+
export type TypstNamedParams = { [key: string]: TypstPrimitiveValue };
|
|
357
|
+
|
|
358
|
+
// #none
|
|
359
|
+
export const TYPST_NONE: TypstPrimitiveValue = null;
|
|
360
|
+
export const TYPST_TRUE: TypstPrimitiveValue = true;
|
|
361
|
+
export const TYPST_FALSE: TypstPrimitiveValue = false;
|
|
356
362
|
|
|
357
363
|
export class TypstNode {
|
|
358
364
|
type: TypstNodeType;
|
|
@@ -370,7 +376,7 @@ export class TypstNode {
|
|
|
370
376
|
this.data = data;
|
|
371
377
|
}
|
|
372
378
|
|
|
373
|
-
public setOptions(options:
|
|
379
|
+
public setOptions(options: TypstNamedParams) {
|
|
374
380
|
this.options = options;
|
|
375
381
|
}
|
|
376
382
|
|
|
@@ -383,8 +389,10 @@ export class TypstNode {
|
|
|
383
389
|
export interface Tex2TypstOptions {
|
|
384
390
|
nonStrict?: boolean; // default is true
|
|
385
391
|
preferTypstIntrinsic?: boolean; // default is true,
|
|
392
|
+
preferShorthands?: boolean; // default is true
|
|
386
393
|
keepSpaces?: boolean; // default is false
|
|
387
394
|
fracToSlash?: boolean; // default is true
|
|
395
|
+
inftyToOo?: boolean; // default is false
|
|
388
396
|
customTexMacros?: { [key: string]: string };
|
|
389
397
|
// TODO: custom typst functions
|
|
390
398
|
}
|
package/src/typst-parser.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
import { array_find } from "./generic";
|
|
3
|
-
import { TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
|
|
3
|
+
import { TYPST_NONE, TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
|
|
4
4
|
import { assert, isalpha, isdigit } from "./util";
|
|
5
|
+
import { reverseShorthandMap } from "./typst-shorthands";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const TYPST_EMPTY_NODE = new TypstNode('empty', '');
|
|
10
|
+
|
|
11
|
+
const TYPST_SHORTHANDS = Array.from(reverseShorthandMap.keys());
|
|
5
12
|
|
|
6
13
|
// TODO: In Typst, y' ' is not the same as y''.
|
|
7
14
|
// The parser should be able to parse the former correctly.
|
|
@@ -22,8 +29,14 @@ function eat_identifier_name(typst: string, start: number): string {
|
|
|
22
29
|
return typst.substring(start, pos);
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
const
|
|
32
|
+
function try_eat_shorthand(typst: string, start: number): string | null {
|
|
33
|
+
for (const shorthand of TYPST_SHORTHANDS) {
|
|
34
|
+
if (typst.startsWith(shorthand, start)) {
|
|
35
|
+
return shorthand;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
27
40
|
|
|
28
41
|
|
|
29
42
|
export function tokenize_typst(typst: string): TypstToken[] {
|
|
@@ -115,6 +128,13 @@ export function tokenize_typst(typst: string): TypstToken[] {
|
|
|
115
128
|
break;
|
|
116
129
|
}
|
|
117
130
|
default: {
|
|
131
|
+
const shorthand = try_eat_shorthand(typst, pos);
|
|
132
|
+
if (shorthand !== null) {
|
|
133
|
+
token = new TypstToken(TypstTokenType.SYMBOL, reverseShorthandMap.get(shorthand)!);
|
|
134
|
+
pos += shorthand.length;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
118
138
|
if (isdigit(firstChar)) {
|
|
119
139
|
let newPos = pos;
|
|
120
140
|
while (newPos < typst.length && isdigit(typst[newPos])) {
|
|
@@ -450,7 +470,6 @@ export class TypstParser {
|
|
|
450
470
|
// start: the position of the left parentheses
|
|
451
471
|
parseArguments(tokens: TypstToken[], start: number): [TypstNode[], number] {
|
|
452
472
|
const end = find_closing_match(tokens, start);
|
|
453
|
-
|
|
454
473
|
return [this.parseCommaSeparatedArguments(tokens, start + 1, end), end + 1];
|
|
455
474
|
}
|
|
456
475
|
|
|
@@ -501,7 +520,7 @@ export class TypstParser {
|
|
|
501
520
|
if(g.args!.length !== 4 || !g.args![pos_colon + 2].eq(new TypstNode('symbol', 'none'))) {
|
|
502
521
|
throw new TypstParserError('Invalid number of arguments for delim');
|
|
503
522
|
}
|
|
504
|
-
np['delim'] =
|
|
523
|
+
np['delim'] = TYPST_NONE;
|
|
505
524
|
} else {
|
|
506
525
|
throw new TypstParserError('Not implemented for other types of delim');
|
|
507
526
|
}
|