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