tex2typst 0.3.22 → 0.3.24
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/convert.d.ts +9 -3
- package/dist/generic.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2219 -2181
- package/dist/jslex.d.ts +2 -0
- package/dist/tex-parser.d.ts +1 -1
- package/dist/tex-tokenizer.d.ts +1 -1
- package/dist/tex-types.d.ts +103 -0
- package/dist/tex-writer.d.ts +1 -2
- package/dist/tex2typst.min.js +13 -13
- package/dist/typst-parser.d.ts +6 -4
- package/dist/typst-tokenizer.d.ts +1 -1
- package/dist/typst-types.d.ts +95 -0
- package/dist/typst-writer.d.ts +4 -2
- package/package.json +1 -1
- package/src/convert.ts +515 -457
- package/src/generic.ts +29 -3
- package/src/index.ts +1 -1
- package/src/jslex.ts +19 -12
- package/src/map.ts +6 -4
- package/src/tex-parser.ts +54 -62
- package/src/tex-tokenizer.ts +27 -24
- package/src/tex-types.ts +358 -0
- package/src/tex-writer.ts +3 -51
- package/src/typst-parser.ts +54 -71
- package/src/typst-tokenizer.ts +86 -76
- package/src/typst-types.ts +221 -0
- package/src/typst-writer.ts +83 -84
- package/src/util.ts +1 -1
- package/dist/types.d.ts +0 -111
- package/src/types.ts +0 -408
package/src/convert.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
import { TexNode,
|
|
2
|
-
|
|
1
|
+
import { TexNode, Tex2TypstOptions,
|
|
2
|
+
TexToken, TexTokenType, TexFuncCall, TexGroup, TexSupSub,
|
|
3
|
+
TexText, TexBeginEnd, TexLeftRight,
|
|
4
|
+
TexTerminal} from "./tex-types";
|
|
5
|
+
import { TypstAlign, TypstCases, TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstMatrix, TypstNode, TypstSupsub, TypstTerminal } from "./typst-types";
|
|
6
|
+
import { TypstNamedParams } from "./typst-types";
|
|
7
|
+
import { TypstSupsubData } from "./typst-types";
|
|
8
|
+
import { TypstToken } from "./typst-types";
|
|
9
|
+
import { TypstTokenType } from "./typst-types";
|
|
3
10
|
import { symbolMap, reverseSymbolMap } from "./map";
|
|
4
|
-
import {
|
|
11
|
+
import { array_includes, array_intersperse, array_split } from "./generic";
|
|
5
12
|
import { assert } from "./util";
|
|
13
|
+
import { TEX_BINARY_COMMANDS, TEX_UNARY_COMMANDS } from "./tex-tokenizer";
|
|
6
14
|
|
|
7
15
|
|
|
16
|
+
export class ConverterError extends Error {
|
|
17
|
+
node: TexNode | TypstNode | TexToken | TypstToken | null;
|
|
18
|
+
|
|
19
|
+
constructor(message: string, node: TexNode | TypstNode | TexToken | TypstToken | null = null) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "ConverterError";
|
|
22
|
+
this.node = node;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TYPST_NONE = TypstToken.NONE.toNode();
|
|
27
|
+
|
|
8
28
|
// native textual operators in Typst
|
|
9
29
|
const TYPST_INTRINSIC_OP = [
|
|
10
30
|
'dim',
|
|
@@ -17,13 +37,13 @@ const TYPST_INTRINSIC_OP = [
|
|
|
17
37
|
// 'sgn
|
|
18
38
|
];
|
|
19
39
|
|
|
20
|
-
function
|
|
40
|
+
function _tex_token_str_to_typst(token: string): string | null {
|
|
21
41
|
if (/^[a-zA-Z0-9]$/.test(token)) {
|
|
22
42
|
return token;
|
|
23
43
|
} else if (token === '/') {
|
|
24
44
|
return '\\/';
|
|
25
|
-
} else if (
|
|
26
|
-
return
|
|
45
|
+
} else if (['\\\\', '\\{', '\\}', '\\%'].includes(token)) {
|
|
46
|
+
return token.substring(1);
|
|
27
47
|
} else if (['\\$', '\\#', '\\&', '\\_'].includes(token)) {
|
|
28
48
|
return token;
|
|
29
49
|
} else if (token.startsWith('\\')) {
|
|
@@ -34,12 +54,65 @@ function tex_token_to_typst(token: string): string {
|
|
|
34
54
|
// Fall back to the original macro.
|
|
35
55
|
// This works for \alpha, \beta, \gamma, etc.
|
|
36
56
|
// If this.nonStrict is true, this also works for all unknown macros.
|
|
37
|
-
return
|
|
57
|
+
return null;
|
|
38
58
|
}
|
|
39
59
|
}
|
|
40
60
|
return token;
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
function tex_token_to_typst(token: TexToken, options: Tex2TypstOptions): TypstToken {
|
|
64
|
+
let token_type: TypstTokenType;
|
|
65
|
+
switch (token.type) {
|
|
66
|
+
case TexTokenType.EMPTY:
|
|
67
|
+
return TypstToken.NONE;
|
|
68
|
+
case TexTokenType.COMMAND:
|
|
69
|
+
token_type = TypstTokenType.SYMBOL;
|
|
70
|
+
break;
|
|
71
|
+
case TexTokenType.ELEMENT:
|
|
72
|
+
token_type = TypstTokenType.ELEMENT;
|
|
73
|
+
break;
|
|
74
|
+
case TexTokenType.LITERAL:
|
|
75
|
+
// This happens, for example, node={type: 'literal', content: 'myop'} as in `\operatorname{myop}`
|
|
76
|
+
token_type = TypstTokenType.LITERAL;
|
|
77
|
+
break;
|
|
78
|
+
case TexTokenType.COMMENT:
|
|
79
|
+
token_type = TypstTokenType.COMMENT;
|
|
80
|
+
break;
|
|
81
|
+
case TexTokenType.SPACE:
|
|
82
|
+
token_type = TypstTokenType.SPACE;
|
|
83
|
+
break;
|
|
84
|
+
case TexTokenType.NEWLINE:
|
|
85
|
+
token_type = TypstTokenType.NEWLINE;
|
|
86
|
+
break;
|
|
87
|
+
case TexTokenType.CONTROL: {
|
|
88
|
+
if (token.value === '\\\\') {
|
|
89
|
+
// \\ -> \
|
|
90
|
+
return new TypstToken(TypstTokenType.CONTROL, '\\');
|
|
91
|
+
} else if (token.value === '\\!') {
|
|
92
|
+
// \! -> #h(-math.thin.amount)
|
|
93
|
+
return new TypstToken(TypstTokenType.SYMBOL, '#h(-math.thin.amount)');
|
|
94
|
+
} else if (symbolMap.has(token.value.substring(1))) {
|
|
95
|
+
// node.content is one of \, \: \;
|
|
96
|
+
const typst_symbol = symbolMap.get(token.value.substring(1))!;
|
|
97
|
+
return new TypstToken(TypstTokenType.SYMBOL, typst_symbol);
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error(`Unknown control sequence: ${token.value}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
default:
|
|
103
|
+
throw Error(`Unknown token type: ${token.type}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const typst_str = _tex_token_str_to_typst(token.value);
|
|
107
|
+
if (typst_str === null) {
|
|
108
|
+
if (options.nonStrict) {
|
|
109
|
+
return new TypstToken(token_type, token.value.substring(1));
|
|
110
|
+
} else {
|
|
111
|
+
throw new ConverterError(`Unknown token: ${token.value}`, token);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return new TypstToken(token_type, typst_str);
|
|
115
|
+
}
|
|
43
116
|
|
|
44
117
|
// \overset{X}{Y} -> limits(Y)^X
|
|
45
118
|
// and with special case \overset{\text{def}}{=} -> eq.def
|
|
@@ -47,35 +120,19 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
|
|
|
47
120
|
const [sup, base] = node.args!;
|
|
48
121
|
|
|
49
122
|
if (options.optimize) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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');
|
|
123
|
+
// \overset{\text{def}}{=} or \overset{def}{=} are considered as eq.def
|
|
124
|
+
if (["\\overset{\\text{def}}{=}", "\\overset{d e f}{=}"].includes(node.toString())) {
|
|
125
|
+
return new TypstToken(TypstTokenType.SYMBOL, 'eq.def').toNode();
|
|
69
126
|
}
|
|
70
127
|
}
|
|
71
|
-
const limits_call = new
|
|
72
|
-
'
|
|
73
|
-
'limits',
|
|
128
|
+
const limits_call = new TypstFuncCall(
|
|
129
|
+
new TypstToken(TypstTokenType.SYMBOL, 'limits'),
|
|
74
130
|
[convert_tex_node_to_typst(base, options)]
|
|
75
131
|
);
|
|
76
|
-
return new
|
|
132
|
+
return new TypstSupsub({
|
|
77
133
|
base: limits_call,
|
|
78
134
|
sup: convert_tex_node_to_typst(sup, options),
|
|
135
|
+
sub: null,
|
|
79
136
|
});
|
|
80
137
|
}
|
|
81
138
|
|
|
@@ -83,368 +140,319 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
|
|
|
83
140
|
function convert_underset(node: TexNode, options: Tex2TypstOptions): TypstNode {
|
|
84
141
|
const [sub, base] = node.args!;
|
|
85
142
|
|
|
86
|
-
const limits_call = new
|
|
87
|
-
'
|
|
88
|
-
'limits',
|
|
143
|
+
const limits_call = new TypstFuncCall(
|
|
144
|
+
new TypstToken(TypstTokenType.SYMBOL, 'limits'),
|
|
89
145
|
[convert_tex_node_to_typst(base, options)]
|
|
90
146
|
);
|
|
91
|
-
return new
|
|
147
|
+
return new TypstSupsub({
|
|
92
148
|
base: limits_call,
|
|
93
149
|
sub: convert_tex_node_to_typst(sub, options),
|
|
150
|
+
sup: null,
|
|
94
151
|
});
|
|
95
152
|
}
|
|
96
153
|
|
|
154
|
+
function convert_tex_array_align_literal(alignLiteral: string): TypstNamedParams {
|
|
155
|
+
const np: TypstNamedParams = {};
|
|
156
|
+
const alignMap: Record<string, string> = { l: '#left', c: '#center', r: '#right' };
|
|
157
|
+
const chars = Array.from(alignLiteral);
|
|
158
|
+
|
|
159
|
+
const vlinePositions: number[] = [];
|
|
160
|
+
let columnIndex = 0;
|
|
161
|
+
for (const c of chars) {
|
|
162
|
+
if (c === '|') {
|
|
163
|
+
vlinePositions.push(columnIndex);
|
|
164
|
+
} else if (c === 'l' || c === 'c' || c === 'r') {
|
|
165
|
+
columnIndex++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
97
168
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
169
|
+
if (vlinePositions.length > 0) {
|
|
170
|
+
let augment_str: string;
|
|
171
|
+
if (vlinePositions.length === 1) {
|
|
172
|
+
augment_str = `#${vlinePositions[0]}`;
|
|
173
|
+
} else {
|
|
174
|
+
augment_str = `#(vline: (${vlinePositions.join(', ')}))`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
np['augment'] = new TypstToken(TypstTokenType.LITERAL, augment_str).toNode();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const alignments = chars
|
|
181
|
+
.map(c => alignMap[c])
|
|
182
|
+
.filter((x) => x !== undefined)
|
|
183
|
+
.map(s => new TypstToken(TypstTokenType.LITERAL, s!).toNode());
|
|
184
|
+
|
|
185
|
+
if (alignments.length > 0) {
|
|
186
|
+
const all_same = alignments.every(item => item.eq(alignments[0]));
|
|
187
|
+
np['align'] = all_same ? alignments[0] : new TypstToken(TypstTokenType.LITERAL, '#center').toNode();
|
|
188
|
+
}
|
|
189
|
+
return np;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2TypstOptions = {}): TypstNode {
|
|
194
|
+
switch (abstractNode.type) {
|
|
195
|
+
case 'terminal': {
|
|
196
|
+
const node = abstractNode as TexTerminal;
|
|
197
|
+
return tex_token_to_typst(node.head, options).toNode();
|
|
198
|
+
}
|
|
114
199
|
case 'text': {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
options.nonAsciiWrapper
|
|
119
|
-
[new
|
|
200
|
+
const node = abstractNode as TexText;
|
|
201
|
+
if ((/[^\x00-\x7F]+/).test(node.head.value) && options.nonAsciiWrapper !== "") {
|
|
202
|
+
return new TypstFuncCall(
|
|
203
|
+
new TypstToken(TypstTokenType.SYMBOL, options.nonAsciiWrapper!),
|
|
204
|
+
[new TypstToken(TypstTokenType.TEXT, node.head.value).toNode()]
|
|
120
205
|
);
|
|
121
206
|
}
|
|
122
|
-
return new
|
|
207
|
+
return new TypstToken(TypstTokenType.TEXT, node.head.value).toNode();
|
|
123
208
|
}
|
|
124
|
-
case '
|
|
125
|
-
|
|
209
|
+
case 'ordgroup':
|
|
210
|
+
const node = abstractNode as TexGroup;
|
|
211
|
+
return new TypstGroup(
|
|
212
|
+
node.args!.map((n) => convert_tex_node_to_typst(n, options))
|
|
213
|
+
);
|
|
126
214
|
case 'supsub': {
|
|
127
|
-
|
|
215
|
+
const node = abstractNode as TexSupSub;
|
|
216
|
+
let { base, sup, sub } = node;
|
|
128
217
|
|
|
129
218
|
// special hook for overbrace
|
|
130
|
-
if (base && base.type === '
|
|
131
|
-
return new
|
|
132
|
-
'
|
|
133
|
-
'overbrace',
|
|
219
|
+
if (base && base.type === 'funcCall' && base.head.value === '\\overbrace' && sup) {
|
|
220
|
+
return new TypstFuncCall(
|
|
221
|
+
new TypstToken(TypstTokenType.SYMBOL, 'overbrace'),
|
|
134
222
|
[convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sup, options)]
|
|
135
223
|
);
|
|
136
|
-
} else if (base && base.type === '
|
|
137
|
-
return new
|
|
138
|
-
'
|
|
139
|
-
'underbrace',
|
|
224
|
+
} else if (base && base.type === 'funcCall' && base.head.value === '\\underbrace' && sub) {
|
|
225
|
+
return new TypstFuncCall(
|
|
226
|
+
new TypstToken(TypstTokenType.SYMBOL, 'underbrace'),
|
|
140
227
|
[convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sub, options)]
|
|
141
228
|
);
|
|
142
229
|
}
|
|
143
230
|
|
|
144
231
|
const data: TypstSupsubData = {
|
|
145
232
|
base: convert_tex_node_to_typst(base, options),
|
|
233
|
+
sup: sup? convert_tex_node_to_typst(sup, options) : null,
|
|
234
|
+
sub: sub? convert_tex_node_to_typst(sub, options) : null,
|
|
146
235
|
};
|
|
147
|
-
if (data.base.type === 'none') {
|
|
148
|
-
data.base = new TypstNode('none', '');
|
|
149
|
-
}
|
|
150
236
|
|
|
151
|
-
|
|
152
|
-
data.sup = convert_tex_node_to_typst(sup, options);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (sub) {
|
|
156
|
-
data.sub = convert_tex_node_to_typst(sub, options);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return new TypstNode('supsub', '', [], data);
|
|
237
|
+
return new TypstSupsub(data);
|
|
160
238
|
}
|
|
161
239
|
case 'leftright': {
|
|
162
|
-
const
|
|
163
|
-
const
|
|
240
|
+
const node = abstractNode as TexLeftRight;
|
|
241
|
+
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
|
+
|
|
164
247
|
|
|
165
248
|
if (options.optimize) {
|
|
166
249
|
// optimization off: "lr(bar.v.double a + 1/2 bar.v.double)"
|
|
167
250
|
// optimization on : "norm(a + 1/2)"
|
|
168
|
-
if (left
|
|
169
|
-
|
|
170
|
-
|
|
251
|
+
if (left !== null && right !== null) {
|
|
252
|
+
const typ_left = tex_token_to_typst(left, options);
|
|
253
|
+
const typ_right = tex_token_to_typst(right, options);
|
|
254
|
+
if (left.value === '\\|' && right.value === '\\|') {
|
|
255
|
+
return new TypstFuncCall(new TypstToken(TypstTokenType.SYMBOL, 'norm'), [typ_body]);
|
|
256
|
+
}
|
|
171
257
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
258
|
+
// These pairs will be handled by Typst compiler by default. No need to add lr()
|
|
259
|
+
if ([
|
|
260
|
+
"[]", "()", "\\{\\}",
|
|
261
|
+
"\\lfloor\\rfloor",
|
|
262
|
+
"\\lceil\\rceil",
|
|
263
|
+
"\\lfloor\\rceil",
|
|
264
|
+
].includes(left.value + right.value)) {
|
|
265
|
+
return new TypstGroup([typ_left.toNode(), typ_body, typ_right.toNode()]);
|
|
266
|
+
}
|
|
180
267
|
}
|
|
181
268
|
}
|
|
182
269
|
|
|
183
|
-
const group = new TypstNode(
|
|
184
|
-
'group',
|
|
185
|
-
'',
|
|
186
|
-
[typ_left, typ_body, typ_right]
|
|
187
|
-
);
|
|
188
|
-
|
|
189
270
|
// "\left\{ a + \frac{1}{3} \right." -> "lr(\{ a + 1/3)"
|
|
190
|
-
// "\left. a + \frac{1}{3} \right\}" -> "lr( a +
|
|
271
|
+
// "\left. a + \frac{1}{3} \right\}" -> "lr( a + 1/3 \})"
|
|
191
272
|
// Note that: In lr(), if one side of delimiter doesn't present (i.e. derived from "\\left." or "\\right."),
|
|
192
273
|
// "(", ")", "{", "[", should be escaped with "\" to be the other side of delimiter.
|
|
193
274
|
// Simple "lr({ a+1/3)" doesn't compile in Typst.
|
|
194
|
-
const escape_curly_or_paren = function(s:
|
|
195
|
-
if (["(", ")", "{", "["].includes(s)) {
|
|
196
|
-
return "\\" + s;
|
|
275
|
+
const escape_curly_or_paren = function(s: TypstToken): TypstToken {
|
|
276
|
+
if (["(", ")", "{", "["].includes(s.value)) {
|
|
277
|
+
return new TypstToken(TypstTokenType.ELEMENT, "\\" + s.value);
|
|
197
278
|
} else {
|
|
198
279
|
return s;
|
|
199
280
|
}
|
|
200
281
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
typ_right
|
|
206
|
-
group.args = [typ_body, typ_right];
|
|
207
|
-
}
|
|
208
|
-
return new TypstNode('funcCall', 'lr', [group]);
|
|
209
|
-
}
|
|
210
|
-
case 'binaryFunc': {
|
|
211
|
-
if (node.content === '\\overset') {
|
|
212
|
-
return convert_overset(node, options);
|
|
213
|
-
}
|
|
214
|
-
if (node.content === '\\underset') {
|
|
215
|
-
return convert_underset(node, options);
|
|
282
|
+
|
|
283
|
+
let typ_left = left? tex_token_to_typst(left, options) : null;
|
|
284
|
+
let typ_right = right? tex_token_to_typst(right, options) : null;
|
|
285
|
+
if (typ_left === null && typ_right !== null) { // left.
|
|
286
|
+
typ_right = escape_curly_or_paren(typ_right);
|
|
216
287
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (options.fracToSlash) {
|
|
220
|
-
return new TypstNode(
|
|
221
|
-
'fraction',
|
|
222
|
-
'',
|
|
223
|
-
node.args!.map((n) => convert_tex_node_to_typst(n, options))
|
|
224
|
-
);
|
|
225
|
-
}
|
|
288
|
+
if (typ_right === null && typ_left !== null) { // right.
|
|
289
|
+
typ_left = escape_curly_or_paren(typ_left);
|
|
226
290
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
291
|
+
|
|
292
|
+
return new TypstLeftright(
|
|
293
|
+
new TypstToken(TypstTokenType.SYMBOL, 'lr'),
|
|
294
|
+
[typ_body],
|
|
295
|
+
{ left: typ_left, right: typ_right }
|
|
231
296
|
);
|
|
232
297
|
}
|
|
233
|
-
case '
|
|
298
|
+
case 'funcCall': {
|
|
299
|
+
const node = abstractNode as TexFuncCall;
|
|
234
300
|
const arg0 = convert_tex_node_to_typst(node.args![0], options);
|
|
235
|
-
// \sqrt
|
|
236
|
-
if (node.
|
|
237
|
-
const data = convert_tex_node_to_typst(node.data
|
|
238
|
-
return new
|
|
239
|
-
'
|
|
240
|
-
'root',
|
|
301
|
+
// \sqrt[3]{x} -> root(3, x)
|
|
302
|
+
if (node.head.value === '\\sqrt' && node.data) {
|
|
303
|
+
const data = convert_tex_node_to_typst(node.data, options); // the number of times to take the root
|
|
304
|
+
return new TypstFuncCall(
|
|
305
|
+
new TypstToken(TypstTokenType.SYMBOL, 'root'),
|
|
241
306
|
[data, arg0]
|
|
242
307
|
);
|
|
243
308
|
}
|
|
244
309
|
// \mathbf{a} -> upright(bold(a))
|
|
245
|
-
if (node.
|
|
246
|
-
const inner: TypstNode = new
|
|
247
|
-
'
|
|
248
|
-
'bold',
|
|
310
|
+
if (node.head.value === '\\mathbf') {
|
|
311
|
+
const inner: TypstNode = new TypstFuncCall(
|
|
312
|
+
new TypstToken(TypstTokenType.SYMBOL, 'bold'),
|
|
249
313
|
[arg0]
|
|
250
314
|
);
|
|
251
|
-
return new
|
|
252
|
-
'
|
|
253
|
-
'upright',
|
|
315
|
+
return new TypstFuncCall(
|
|
316
|
+
new TypstToken(TypstTokenType.SYMBOL, 'upright'),
|
|
254
317
|
[inner]
|
|
255
318
|
);
|
|
256
319
|
}
|
|
257
320
|
// \overrightarrow{AB} -> arrow(A B)
|
|
258
|
-
if (node.
|
|
259
|
-
return new
|
|
260
|
-
'
|
|
261
|
-
'arrow',
|
|
321
|
+
if (node.head.value === '\\overrightarrow') {
|
|
322
|
+
return new TypstFuncCall(
|
|
323
|
+
new TypstToken(TypstTokenType.SYMBOL, 'arrow'),
|
|
262
324
|
[arg0]
|
|
263
325
|
);
|
|
264
326
|
}
|
|
265
327
|
// \overleftarrow{AB} -> accent(A B, arrow.l)
|
|
266
|
-
if (node.
|
|
267
|
-
return new
|
|
268
|
-
'
|
|
269
|
-
'
|
|
270
|
-
[arg0, new TypstNode('symbol', 'arrow.l')]
|
|
328
|
+
if (node.head.value === '\\overleftarrow') {
|
|
329
|
+
return new TypstFuncCall(
|
|
330
|
+
new TypstToken(TypstTokenType.SYMBOL, 'accent'),
|
|
331
|
+
[arg0, new TypstToken(TypstTokenType.SYMBOL, 'arrow.l').toNode()]
|
|
271
332
|
);
|
|
272
333
|
}
|
|
273
334
|
// \operatorname{opname} -> op("opname")
|
|
274
|
-
if (node.
|
|
335
|
+
if (node.head.value === '\\operatorname') {
|
|
336
|
+
// arg0 must be of type 'literal' in this situation
|
|
275
337
|
if (options.optimize) {
|
|
276
|
-
if (TYPST_INTRINSIC_OP.includes(arg0.
|
|
277
|
-
return new
|
|
338
|
+
if (TYPST_INTRINSIC_OP.includes(arg0.head.value)) {
|
|
339
|
+
return new TypstToken(TypstTokenType.SYMBOL, arg0.head.value).toNode();
|
|
278
340
|
}
|
|
279
341
|
}
|
|
280
|
-
return new
|
|
342
|
+
return new TypstFuncCall(new TypstToken(TypstTokenType.SYMBOL, 'op'), [new TypstToken(TypstTokenType.TEXT, arg0.head.value).toNode()]);
|
|
281
343
|
}
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return
|
|
287
|
-
'funcCall',
|
|
288
|
-
'#h',
|
|
289
|
-
[new TypstNode('symbol', text)]
|
|
290
|
-
);
|
|
344
|
+
|
|
345
|
+
// \substack{a \\ b} -> `a \ b`
|
|
346
|
+
// as in translation from \sum_{\substack{a \\ b}} to sum_(a \ b)
|
|
347
|
+
if (node.head.value === '\\substack') {
|
|
348
|
+
return arg0;
|
|
291
349
|
}
|
|
292
350
|
|
|
293
|
-
if (node.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
[arg0]
|
|
299
|
-
);
|
|
351
|
+
if (node.head.value === '\\overset') {
|
|
352
|
+
return convert_overset(node, options);
|
|
353
|
+
}
|
|
354
|
+
if (node.head.value === '\\underset') {
|
|
355
|
+
return convert_underset(node, options);
|
|
300
356
|
}
|
|
301
357
|
|
|
358
|
+
// \frac{a}{b} -> a / b
|
|
359
|
+
if (node.head.value === '\\frac') {
|
|
360
|
+
if (options.fracToSlash) {
|
|
361
|
+
return new TypstFraction(node.args!.map((n) => convert_tex_node_to_typst(n, options)));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
302
364
|
if(options.optimize) {
|
|
303
365
|
// \mathbb{R} -> RR
|
|
304
|
-
if (node.
|
|
305
|
-
return new
|
|
366
|
+
if (node.head.value === '\\mathbb' && /^\\mathbb{[A-Z]}$/.test(node.toString())) {
|
|
367
|
+
return new TypstToken(TypstTokenType.SYMBOL, arg0.head.value.repeat(2)).toNode();
|
|
306
368
|
}
|
|
307
369
|
// \mathrm{d} -> dif
|
|
308
|
-
if (node.
|
|
309
|
-
return new
|
|
370
|
+
if (node.head.value === '\\mathrm' && node.toString() === '\\mathrm{d}') {
|
|
371
|
+
return new TypstToken(TypstTokenType.SYMBOL, 'dif').toNode();
|
|
310
372
|
}
|
|
311
373
|
}
|
|
312
374
|
|
|
313
375
|
// generic case
|
|
314
|
-
return new
|
|
315
|
-
|
|
316
|
-
tex_token_to_typst(node.content),
|
|
376
|
+
return new TypstFuncCall(
|
|
377
|
+
tex_token_to_typst(node.head, options),
|
|
317
378
|
node.args!.map((n) => convert_tex_node_to_typst(n, options))
|
|
318
379
|
);
|
|
319
380
|
}
|
|
320
381
|
case 'beginend': {
|
|
321
|
-
const
|
|
322
|
-
const data = matrix.map((row) => row.map((n) => convert_tex_node_to_typst(n, options)));
|
|
382
|
+
const node = abstractNode as TexBeginEnd;
|
|
383
|
+
const data = node.matrix.map((row) => row.map((n) => convert_tex_node_to_typst(n, options)));
|
|
323
384
|
|
|
324
|
-
if (node.
|
|
385
|
+
if (node.head.value.startsWith('align')) {
|
|
325
386
|
// align, align*, alignat, alignat*, aligned, etc.
|
|
326
|
-
return new
|
|
387
|
+
return new TypstAlign(data);
|
|
327
388
|
}
|
|
328
|
-
if (node.
|
|
329
|
-
return new
|
|
389
|
+
if (node.head.value === 'cases') {
|
|
390
|
+
return new TypstCases(data);
|
|
330
391
|
}
|
|
331
|
-
if (node.
|
|
392
|
+
if (node.head.value === 'subarray') {
|
|
332
393
|
const align_node = node.args![0];
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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;
|
|
338
403
|
}
|
|
339
|
-
return new
|
|
340
|
-
'group',
|
|
341
|
-
'',
|
|
342
|
-
[new TypstNode('align', '', [], data)]
|
|
343
|
-
);
|
|
404
|
+
return new TypstAlign(data);
|
|
344
405
|
}
|
|
345
|
-
if (node.
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
const align_node = align_args[0];
|
|
352
|
-
const align_str = (() => {
|
|
353
|
-
if (align_node.type === 'element') return align_node.content;
|
|
354
|
-
if (align_node.type === 'ordgroup') {
|
|
355
|
-
return align_node.args!.map(n => n.type === 'element' ? n.content : '').join('');
|
|
356
|
-
}
|
|
357
|
-
return '';
|
|
358
|
-
})();
|
|
359
|
-
|
|
360
|
-
if (align_str) {
|
|
361
|
-
const alignMap: Record<string, string> = { l: '#left', c: '#center', r: '#right' };
|
|
362
|
-
const chars = Array.from(align_str);
|
|
363
|
-
|
|
364
|
-
const alignments = chars
|
|
365
|
-
.map(c => alignMap[c])
|
|
366
|
-
.filter(Boolean)
|
|
367
|
-
.map(s => new TypstNode('symbol', s!));
|
|
368
|
-
|
|
369
|
-
const vlinePositions: number[] = [];
|
|
370
|
-
let columnIndex = 0;
|
|
371
|
-
for (const c of chars) {
|
|
372
|
-
if (c === '|') {
|
|
373
|
-
vlinePositions.push(columnIndex);
|
|
374
|
-
} else if (c === 'l' || c === 'c' || c === 'r') {
|
|
375
|
-
columnIndex++;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (vlinePositions.length > 0) {
|
|
380
|
-
if (vlinePositions.length === 1) {
|
|
381
|
-
options['augment'] = new TypstNode('symbol', `#${vlinePositions[0]}`);
|
|
382
|
-
} else {
|
|
383
|
-
options['augment'] = new TypstNode('symbol', `#(vline: (${vlinePositions.join(', ')}))`);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (alignments.length > 0) {
|
|
388
|
-
const first_align = alignments[0].content;
|
|
389
|
-
const all_same = alignments.every(item => item.content === first_align);
|
|
390
|
-
options['align'] = all_same ? alignments[0] : new TypstNode('symbol', '#center');
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
406
|
+
if (node.head.value === 'array') {
|
|
407
|
+
const np: TypstNamedParams = { 'delim': TYPST_NONE };
|
|
408
|
+
|
|
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);
|
|
411
|
+
Object.assign(np, np_new);
|
|
394
412
|
|
|
395
|
-
res
|
|
413
|
+
const res = new TypstMatrix(data);
|
|
414
|
+
res.setOptions(np);
|
|
396
415
|
return res;
|
|
397
416
|
}
|
|
398
|
-
if (node.
|
|
399
|
-
|
|
400
|
-
|
|
417
|
+
if (node.head.value.endsWith('matrix')) {
|
|
418
|
+
const res = new TypstMatrix(data);
|
|
419
|
+
let delim: TypstToken;
|
|
420
|
+
switch (node.head.value) {
|
|
401
421
|
case 'matrix':
|
|
402
|
-
delim =
|
|
422
|
+
delim = TypstToken.NONE;
|
|
403
423
|
break;
|
|
404
424
|
case 'pmatrix':
|
|
405
|
-
delim = '(';
|
|
406
|
-
break;
|
|
425
|
+
// delim = new TypstToken(TypstTokenType.TEXT, '(');
|
|
426
|
+
// break;
|
|
427
|
+
return res; // typst mat use delim:"(" by default
|
|
407
428
|
case 'bmatrix':
|
|
408
|
-
delim = '[';
|
|
429
|
+
delim = new TypstToken(TypstTokenType.TEXT, '[');
|
|
409
430
|
break;
|
|
410
431
|
case 'Bmatrix':
|
|
411
|
-
delim = '{';
|
|
432
|
+
delim = new TypstToken(TypstTokenType.TEXT, '{');
|
|
412
433
|
break;
|
|
413
434
|
case 'vmatrix':
|
|
414
|
-
delim = '|';
|
|
435
|
+
delim = new TypstToken(TypstTokenType.TEXT, '|');
|
|
415
436
|
break;
|
|
416
437
|
case 'Vmatrix': {
|
|
417
|
-
delim = new
|
|
438
|
+
delim = new TypstToken(TypstTokenType.SYMBOL, 'bar.v.double');
|
|
418
439
|
break;
|
|
419
440
|
}
|
|
420
441
|
default:
|
|
421
|
-
throw new
|
|
442
|
+
throw new ConverterError(`Unimplemented beginend: ${node.head}`, node);
|
|
422
443
|
}
|
|
423
|
-
|
|
424
|
-
res.setOptions({ 'delim': delim });
|
|
444
|
+
res.setOptions({ 'delim': delim.toNode()});
|
|
425
445
|
return res;
|
|
426
446
|
}
|
|
427
|
-
throw new
|
|
447
|
+
throw new ConverterError(`Unimplemented beginend: ${node.head}`, node);
|
|
428
448
|
}
|
|
429
|
-
case 'unknownMacro':
|
|
430
|
-
return new TypstNode('unknown', tex_token_to_typst(node.content));
|
|
431
|
-
case 'control':
|
|
432
|
-
if (node.content === '\\\\') {
|
|
433
|
-
return new TypstNode('symbol', '\\');
|
|
434
|
-
} else if (symbolMap.has(node.content.substring(1))) {
|
|
435
|
-
// node.content is one of \, \: \;
|
|
436
|
-
const typst_symbol = symbolMap.get(node.content.substring(1))!;
|
|
437
|
-
return new TypstNode('symbol', typst_symbol);
|
|
438
|
-
} else {
|
|
439
|
-
throw new TypstWriterError(`Unknown control sequence: ${node.content}`, node);
|
|
440
|
-
}
|
|
441
449
|
default:
|
|
442
|
-
throw new
|
|
450
|
+
throw new ConverterError(`Unimplemented node type: ${abstractNode.type}`, abstractNode);
|
|
443
451
|
}
|
|
444
452
|
}
|
|
445
453
|
|
|
446
454
|
|
|
447
|
-
|
|
455
|
+
/*
|
|
448
456
|
const TYPST_UNARY_FUNCTIONS: string[] = [
|
|
449
457
|
'sqrt',
|
|
450
458
|
'bold',
|
|
@@ -466,6 +474,7 @@ const TYPST_UNARY_FUNCTIONS: string[] = [
|
|
|
466
474
|
'ceil',
|
|
467
475
|
'norm',
|
|
468
476
|
'limits',
|
|
477
|
+
'#h',
|
|
469
478
|
];
|
|
470
479
|
|
|
471
480
|
const TYPST_BINARY_FUNCTIONS: string[] = [
|
|
@@ -474,187 +483,254 @@ const TYPST_BINARY_FUNCTIONS: string[] = [
|
|
|
474
483
|
'overbrace',
|
|
475
484
|
'underbrace',
|
|
476
485
|
];
|
|
486
|
+
*/
|
|
477
487
|
|
|
478
|
-
function apply_escape_if_needed(c:
|
|
479
|
-
if (['{', '}', '%'].includes(c)) {
|
|
480
|
-
return '\\' + c;
|
|
488
|
+
function apply_escape_if_needed(c: TexToken): TexToken {
|
|
489
|
+
if (['{', '}', '%'].includes(c.value)) {
|
|
490
|
+
return new TexToken(TexTokenType.ELEMENT, '\\' + c.value);
|
|
481
491
|
}
|
|
482
492
|
return c;
|
|
483
493
|
}
|
|
484
494
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
495
|
+
|
|
496
|
+
function typst_token_to_tex(token: TypstToken): TexToken {
|
|
497
|
+
switch (token.type) {
|
|
498
|
+
case TypstTokenType.NONE:
|
|
499
|
+
// e.g. Typst `#none^2` is converted to TeX `^2`
|
|
500
|
+
return TexToken.EMPTY;
|
|
501
|
+
case TypstTokenType.SYMBOL: {
|
|
502
|
+
const _typst_symbol_to_tex = function(symbol: string): string {
|
|
503
|
+
if (reverseSymbolMap.has(symbol)) {
|
|
504
|
+
return '\\' + reverseSymbolMap.get(symbol)!;
|
|
505
|
+
} else {
|
|
506
|
+
return '\\' + symbol;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return new TexToken(TexTokenType.COMMAND, _typst_symbol_to_tex(token.value));
|
|
510
|
+
}
|
|
511
|
+
case TypstTokenType.ELEMENT: {
|
|
512
|
+
let value: string;
|
|
513
|
+
if (['{', '}', '%'].includes(token.value)) {
|
|
514
|
+
value = '\\' + token.value;
|
|
515
|
+
} else {
|
|
516
|
+
value = token.value;
|
|
517
|
+
}
|
|
518
|
+
return new TexToken(TexTokenType.ELEMENT, value);
|
|
519
|
+
}
|
|
520
|
+
case TypstTokenType.LITERAL:
|
|
521
|
+
return new TexToken(TexTokenType.LITERAL, token.value);
|
|
522
|
+
case TypstTokenType.TEXT:
|
|
523
|
+
return new TexToken(TexTokenType.LITERAL, token.value);
|
|
524
|
+
case TypstTokenType.COMMENT:
|
|
525
|
+
return new TexToken(TexTokenType.COMMENT, token.value);
|
|
526
|
+
case TypstTokenType.SPACE:
|
|
527
|
+
return new TexToken(TexTokenType.SPACE, token.value);
|
|
528
|
+
case TypstTokenType.CONTROL: {
|
|
529
|
+
let value: string;
|
|
530
|
+
switch(token.value) {
|
|
531
|
+
case '\\':
|
|
532
|
+
value = '\\\\';
|
|
533
|
+
break;
|
|
534
|
+
case '&':
|
|
535
|
+
value = '&';
|
|
536
|
+
break;
|
|
537
|
+
default:
|
|
538
|
+
throw new Error(`[typst_token_to_tex]Unimplemented control sequence: ${token.value}`);
|
|
539
|
+
}
|
|
540
|
+
return new TexToken(TexTokenType.CONTROL, value);
|
|
541
|
+
}
|
|
542
|
+
case TypstTokenType.NEWLINE:
|
|
543
|
+
return new TexToken(TexTokenType.NEWLINE, token.value);
|
|
544
|
+
default:
|
|
545
|
+
throw new Error(`Unimplemented token type: ${token.type}`);
|
|
490
546
|
}
|
|
491
|
-
return '\\' + token;
|
|
492
547
|
}
|
|
493
548
|
|
|
494
549
|
|
|
495
|
-
const TEX_NODE_COMMA = new
|
|
550
|
+
const TEX_NODE_COMMA = new TexToken(TexTokenType.ELEMENT, ',').toNode();
|
|
496
551
|
|
|
497
|
-
export function convert_typst_node_to_tex(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
552
|
+
export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
553
|
+
switch (abstractNode.type) {
|
|
554
|
+
case 'terminal': {
|
|
555
|
+
const node = abstractNode as TypstTerminal;
|
|
556
|
+
if (node.head.type === TypstTokenType.SYMBOL) {
|
|
557
|
+
// special hook for eq.def
|
|
558
|
+
if (node.head.value === 'eq.def') {
|
|
559
|
+
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [
|
|
560
|
+
new TexText(new TexToken(TexTokenType.LITERAL, 'def')),
|
|
561
|
+
new TexToken(TexTokenType.ELEMENT, '=').toNode()
|
|
562
|
+
]);
|
|
563
|
+
}
|
|
564
|
+
// special hook for comma
|
|
565
|
+
if(node.head.value === 'comma') {
|
|
566
|
+
return new TexToken(TexTokenType.ELEMENT, ',').toNode();
|
|
567
|
+
}
|
|
568
|
+
// special hook for dif
|
|
569
|
+
if(node.head.value === 'dif') {
|
|
570
|
+
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\mathrm'), [new TexToken(TexTokenType.ELEMENT, 'd').toNode()]);
|
|
571
|
+
}
|
|
572
|
+
// special hook for hyph and hyph.minus
|
|
573
|
+
if(node.head.value === 'hyph' || node.head.value === 'hyph.minus') {
|
|
574
|
+
return new TexText(new TexToken(TexTokenType.LITERAL, '-'));
|
|
575
|
+
}
|
|
576
|
+
// special hook for mathbb{R} <-- RR
|
|
577
|
+
if(/^([A-Z])\1$/.test(node.head.value)) {
|
|
578
|
+
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\mathbb'), [
|
|
579
|
+
new TexToken(TexTokenType.ELEMENT, node.head.value[0]).toNode()
|
|
580
|
+
]);
|
|
581
|
+
}
|
|
521
582
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return new TexNode('unaryFunc', '\\mathbb', [
|
|
525
|
-
new TexNode('element', node.content[0])
|
|
526
|
-
]);
|
|
583
|
+
if (node.head.type === TypstTokenType.TEXT) {
|
|
584
|
+
return new TexText(new TexToken(TexTokenType.LITERAL, node.head.value));
|
|
527
585
|
}
|
|
528
|
-
return
|
|
586
|
+
return typst_token_to_tex(node.head).toNode();
|
|
529
587
|
}
|
|
530
|
-
|
|
531
|
-
return new TexNode('text', node.content);
|
|
532
|
-
case 'comment':
|
|
533
|
-
return new TexNode('comment', node.content);
|
|
588
|
+
|
|
534
589
|
case 'group': {
|
|
590
|
+
const node = abstractNode as TypstGroup;
|
|
535
591
|
const args = node.args!.map(convert_typst_node_to_tex);
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
592
|
+
const alignment_char = new TexToken(TexTokenType.CONTROL, '&').toNode();
|
|
593
|
+
const newline_char = new TexToken(TexTokenType.CONTROL, '\\\\').toNode();
|
|
594
|
+
if (array_includes(args, alignment_char)) {
|
|
595
|
+
// wrap the whole math formula with \begin{aligned} and \end{aligned}
|
|
596
|
+
const rows = array_split(args, newline_char);
|
|
597
|
+
const data: TexNode[][] = [];
|
|
598
|
+
for(const row of rows) {
|
|
599
|
+
const cells = array_split(row, alignment_char);
|
|
600
|
+
data.push(cells.map(cell => new TexGroup(cell)));
|
|
601
|
+
}
|
|
602
|
+
return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'aligned'), [], data);
|
|
542
603
|
}
|
|
543
|
-
return new
|
|
604
|
+
return new TexGroup(args);
|
|
605
|
+
}
|
|
606
|
+
case 'leftright': {
|
|
607
|
+
const node = abstractNode as TypstLeftright;
|
|
608
|
+
const args = node.args!.map(convert_typst_node_to_tex);
|
|
609
|
+
let left = node.left? typst_token_to_tex(node.left) : new TexToken(TexTokenType.ELEMENT, '.');
|
|
610
|
+
let right = node.right? typst_token_to_tex(node.right) : new TexToken(TexTokenType.ELEMENT, '.');
|
|
611
|
+
// const is_over_high = node.isOverHigh();
|
|
612
|
+
// const left_delim = is_over_high ? '\\left(' : '(';
|
|
613
|
+
// const right_delim = is_over_high ? '\\right)' : ')';
|
|
614
|
+
if (node.isOverHigh()) {
|
|
615
|
+
left.value = '\\left' + left.value;
|
|
616
|
+
right.value = '\\right' + right.value;
|
|
617
|
+
}
|
|
618
|
+
args.unshift(left.toNode());
|
|
619
|
+
args.push(right.toNode());
|
|
620
|
+
// TODO: should be TeXLeftRight(...)
|
|
621
|
+
// But currently writer will output `\left |` while people commonly prefer `\left|`.
|
|
622
|
+
return new TexGroup(args);
|
|
544
623
|
}
|
|
545
624
|
case 'funcCall': {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (node.content === 'lr') {
|
|
549
|
-
const data = node.data as TypstLrData;
|
|
550
|
-
if (data.leftDelim !== null) {
|
|
551
|
-
let left_delim = apply_escape_if_needed(data.leftDelim);
|
|
552
|
-
assert(data.rightDelim !== null, "leftDelim has value but rightDelim not");
|
|
553
|
-
let right_delim = apply_escape_if_needed(data.rightDelim!);
|
|
554
|
-
// TODO: should be TeXNode('leftright', ...)
|
|
555
|
-
// But currently writer will output `\left |` while people commonly prefer `\left|`.
|
|
556
|
-
return new TexNode('ordgroup', '', [
|
|
557
|
-
new TexNode('element', '\\left' + left_delim),
|
|
558
|
-
...node.args!.map(convert_typst_node_to_tex),
|
|
559
|
-
new TexNode('element', '\\right' + right_delim)
|
|
560
|
-
]);
|
|
561
|
-
} else {
|
|
562
|
-
return new TexNode('ordgroup', '', node.args!.map(convert_typst_node_to_tex));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
625
|
+
const node = abstractNode as TypstFuncCall;
|
|
626
|
+
switch (node.head.value) {
|
|
565
627
|
// special hook for norm
|
|
566
628
|
// `\| a \|` <- `norm(a)`
|
|
567
629
|
// `\left\| a + \frac{1}{3} \right\|` <- `norm(a + 1/3)`
|
|
568
|
-
|
|
630
|
+
case 'norm': {
|
|
569
631
|
const arg0 = node.args![0];
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
new
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
632
|
+
const args = [ convert_typst_node_to_tex(arg0) ];
|
|
633
|
+
if (node.isOverHigh()) {
|
|
634
|
+
return new TexLeftRight(args, {
|
|
635
|
+
left: new TexToken(TexTokenType.COMMAND, "\\|"),
|
|
636
|
+
right: new TexToken(TexTokenType.COMMAND, "\\|")
|
|
637
|
+
});
|
|
638
|
+
} else {
|
|
639
|
+
return new TexGroup(args);
|
|
640
|
+
}
|
|
576
641
|
}
|
|
577
642
|
// special hook for floor, ceil
|
|
578
643
|
// `\lfloor a \rfloor` <- `floor(a)`
|
|
579
644
|
// `\lceil a \rceil` <- `ceil(a)`
|
|
580
645
|
// `\left\lfloor a \right\rfloor` <- `floor(a)`
|
|
581
646
|
// `\left\lceil a \right\rceil` <- `ceil(a)`
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
647
|
+
case 'floor':
|
|
648
|
+
case 'ceil': {
|
|
649
|
+
const left = "\\l" + node.head.value;
|
|
650
|
+
const right = "\\r" + node.head.value;
|
|
585
651
|
const arg0 = node.args![0];
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
new
|
|
591
|
-
|
|
652
|
+
const typ_arg0 = convert_typst_node_to_tex(arg0);
|
|
653
|
+
const left_node = new TexToken(TexTokenType.COMMAND, left);
|
|
654
|
+
const right_node = new TexToken(TexTokenType.COMMAND, right);
|
|
655
|
+
if (node.isOverHigh()) {
|
|
656
|
+
return new TexLeftRight([typ_arg0], {
|
|
657
|
+
left: left_node,
|
|
658
|
+
right: right_node
|
|
659
|
+
});
|
|
660
|
+
} else {
|
|
661
|
+
return new TexGroup([left_node.toNode(), typ_arg0, right_node.toNode()]);
|
|
662
|
+
}
|
|
592
663
|
}
|
|
593
|
-
const command = typst_token_to_tex(node.content);
|
|
594
|
-
return new TexNode('unaryFunc', command, node.args!.map(convert_typst_node_to_tex));
|
|
595
|
-
} else if (TYPST_BINARY_FUNCTIONS.includes(node.content)) {
|
|
596
664
|
// special hook for root
|
|
597
|
-
|
|
665
|
+
case 'root': {
|
|
598
666
|
const [degree, radicand] = node.args!;
|
|
599
|
-
const data
|
|
600
|
-
return new
|
|
667
|
+
const data = convert_typst_node_to_tex(degree);
|
|
668
|
+
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\sqrt'), [convert_typst_node_to_tex(radicand)], data);
|
|
601
669
|
}
|
|
602
670
|
// special hook for overbrace and underbrace
|
|
603
|
-
|
|
671
|
+
case 'overbrace':
|
|
672
|
+
case 'underbrace': {
|
|
604
673
|
const [body, label] = node.args!;
|
|
605
|
-
const base = new
|
|
674
|
+
const base = new TexFuncCall(typst_token_to_tex(node.head), [convert_typst_node_to_tex(body)]);
|
|
606
675
|
const script = convert_typst_node_to_tex(label);
|
|
607
|
-
const data = node.
|
|
608
|
-
return new
|
|
676
|
+
const data = node.head.value === 'overbrace' ? { base, sup: script, sub: null } : { base, sub: script, sup: null };
|
|
677
|
+
return new TexSupSub(data);
|
|
609
678
|
}
|
|
610
|
-
const command = typst_token_to_tex(node.content);
|
|
611
|
-
return new TexNode('binaryFunc', command, node.args!.map(convert_typst_node_to_tex));
|
|
612
|
-
} else {
|
|
613
679
|
// special hook for vec
|
|
614
680
|
// "vec(a, b, c)" -> "\begin{pmatrix}a\\ b\\ c\end{pmatrix}"
|
|
615
|
-
|
|
616
|
-
const tex_data = node.args!.map(convert_typst_node_to_tex).map((n) => [n])
|
|
617
|
-
return new
|
|
681
|
+
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);
|
|
684
|
+
}
|
|
685
|
+
// special hook for op
|
|
686
|
+
case 'op': {
|
|
687
|
+
const arg0 = node.args![0];
|
|
688
|
+
assert(arg0.head.type === TypstTokenType.TEXT);
|
|
689
|
+
return new TexFuncCall(typst_token_to_tex(node.head), [new TexToken(TexTokenType.LITERAL, arg0.head.value).toNode()]);
|
|
690
|
+
}
|
|
691
|
+
// general case
|
|
692
|
+
default: {
|
|
693
|
+
const func_name_tex = typst_token_to_tex(node.head);
|
|
694
|
+
const is_known_func = TEX_UNARY_COMMANDS.includes(func_name_tex.value.substring(1))
|
|
695
|
+
|| TEX_BINARY_COMMANDS.includes(func_name_tex.value.substring(1));
|
|
696
|
+
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));
|
|
698
|
+
} else {
|
|
699
|
+
return new TexGroup([
|
|
700
|
+
typst_token_to_tex(node.head).toNode(),
|
|
701
|
+
new TexToken(TexTokenType.ELEMENT, '(').toNode(),
|
|
702
|
+
...array_intersperse(node.args!.map(convert_typst_node_to_tex), TEX_NODE_COMMA),
|
|
703
|
+
new TexToken(TexTokenType.ELEMENT, ')').toNode()
|
|
704
|
+
]);
|
|
705
|
+
}
|
|
618
706
|
}
|
|
619
|
-
return new TexNode('ordgroup', '', [
|
|
620
|
-
new TexNode('symbol', typst_token_to_tex(node.content)),
|
|
621
|
-
new TexNode('element', '('),
|
|
622
|
-
...array_join(node.args!.map(convert_typst_node_to_tex), TEX_NODE_COMMA),
|
|
623
|
-
new TexNode('element', ')')
|
|
624
|
-
]);
|
|
625
707
|
}
|
|
626
708
|
}
|
|
627
709
|
case 'supsub': {
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (sup) {
|
|
633
|
-
sup_tex = convert_typst_node_to_tex(sup);
|
|
634
|
-
}
|
|
635
|
-
if (sub) {
|
|
636
|
-
sub_tex = convert_typst_node_to_tex(sub);
|
|
637
|
-
}
|
|
710
|
+
const node = abstractNode as TypstSupsub;
|
|
711
|
+
const { base, sup, sub } = node;
|
|
712
|
+
const sup_tex = sup? convert_typst_node_to_tex(sup) : null;
|
|
713
|
+
const sub_tex = sub? convert_typst_node_to_tex(sub) : null;
|
|
638
714
|
|
|
639
715
|
// special hook for limits
|
|
640
716
|
// `limits(+)^a` -> `\overset{a}{+}`
|
|
641
717
|
// `limits(+)_a` -> `\underset{a}{+}`
|
|
642
718
|
// `limits(+)_a^b` -> `\overset{b}{\underset{a}{+}}`
|
|
643
|
-
if (base.eq(new
|
|
719
|
+
if (base.head.eq(new TypstToken(TypstTokenType.SYMBOL, 'limits'))) {
|
|
644
720
|
const body_in_limits = convert_typst_node_to_tex(base.args![0]);
|
|
645
|
-
if (sup_tex !==
|
|
646
|
-
return new
|
|
647
|
-
} else if (sup_tex ===
|
|
648
|
-
return new
|
|
721
|
+
if (sup_tex !== null && sub_tex === null) {
|
|
722
|
+
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [sup_tex, body_in_limits]);
|
|
723
|
+
} else if (sup_tex === null && sub_tex !== null) {
|
|
724
|
+
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\underset'), [sub_tex, body_in_limits]);
|
|
649
725
|
} else {
|
|
650
|
-
const underset_call = new
|
|
651
|
-
return new
|
|
726
|
+
const underset_call = new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\underset'), [sub_tex!, body_in_limits]);
|
|
727
|
+
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [sup_tex!, underset_call]);
|
|
652
728
|
}
|
|
653
729
|
}
|
|
654
730
|
|
|
655
731
|
const base_tex = convert_typst_node_to_tex(base);
|
|
656
732
|
|
|
657
|
-
const res = new
|
|
733
|
+
const res = new TexSupSub({
|
|
658
734
|
base: base_tex,
|
|
659
735
|
sup: sup_tex,
|
|
660
736
|
sub: sub_tex
|
|
@@ -662,77 +738,59 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
|
|
|
662
738
|
return res;
|
|
663
739
|
}
|
|
664
740
|
case 'matrix': {
|
|
665
|
-
const
|
|
666
|
-
const tex_data =
|
|
667
|
-
let env_type = 'pmatrix';
|
|
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
|
|
668
744
|
if (node.options) {
|
|
669
745
|
if ('delim' in node.options) {
|
|
670
746
|
const delim = node.options.delim;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
break;
|
|
700
|
-
case '|':
|
|
701
|
-
env_type = 'vmatrix';
|
|
702
|
-
break;
|
|
703
|
-
case ')':
|
|
704
|
-
case '(':
|
|
705
|
-
default:
|
|
706
|
-
env_type = 'pmatrix';
|
|
707
|
-
}
|
|
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}`);
|
|
708
775
|
}
|
|
709
776
|
}
|
|
710
777
|
}
|
|
711
|
-
return new
|
|
778
|
+
return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, env_type), [], tex_data);
|
|
712
779
|
}
|
|
713
780
|
case 'cases': {
|
|
714
|
-
const
|
|
715
|
-
const tex_data =
|
|
716
|
-
return new
|
|
717
|
-
}
|
|
718
|
-
case 'control': {
|
|
719
|
-
switch (node.content) {
|
|
720
|
-
case '\\':
|
|
721
|
-
return new TexNode('control', '\\\\');
|
|
722
|
-
case '&':
|
|
723
|
-
return new TexNode('control', '&');
|
|
724
|
-
default:
|
|
725
|
-
throw new Error('[convert_typst_node_to_tex] Unimplemented control: ' + node.content);
|
|
726
|
-
}
|
|
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);
|
|
727
784
|
}
|
|
728
785
|
case 'fraction': {
|
|
786
|
+
const node = abstractNode as TypstFraction;
|
|
729
787
|
const [numerator, denominator] = node.args!;
|
|
730
788
|
const num_tex = convert_typst_node_to_tex(numerator);
|
|
731
789
|
const den_tex = convert_typst_node_to_tex(denominator);
|
|
732
|
-
return new
|
|
790
|
+
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\frac'), [num_tex, den_tex]);
|
|
733
791
|
}
|
|
734
792
|
default:
|
|
735
|
-
throw new Error('[convert_typst_node_to_tex] Unimplemented type: ' +
|
|
793
|
+
throw new Error('[convert_typst_node_to_tex] Unimplemented type: ' + abstractNode.type);
|
|
736
794
|
}
|
|
737
795
|
}
|
|
738
796
|
|