tex2typst 0.3.0-beta-5 → 0.3.0-beta-6

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/src/convert.ts ADDED
@@ -0,0 +1,478 @@
1
+ import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions } from "./types";
2
+ import { TypstWriterError, TYPST_INTRINSIC_SYMBOLS } from "./typst-writer";
3
+ import { symbolMap, reverseSymbolMap } from "./map";
4
+
5
+
6
+ function tex_token_to_typst(token: string): string {
7
+ if (/^[a-zA-Z0-9]$/.test(token)) {
8
+ return token;
9
+ } else if (token === '/') {
10
+ return '\\/';
11
+ } else if (token === '\\|') {
12
+ // \| in LaTeX is double vertical bar looks like ||
13
+ return 'parallel';
14
+ } else if (token === '\\\\') {
15
+ return '\\';
16
+ } else if (['\\$', '\\#', '\\&', '\\_'].includes(token)) {
17
+ return token;
18
+ } else if (token.startsWith('\\')) {
19
+ const symbol = token.slice(1);
20
+ if (symbolMap.has(symbol)) {
21
+ return symbolMap.get(symbol)!;
22
+ } else {
23
+ // Fall back to the original macro.
24
+ // This works for \alpha, \beta, \gamma, etc.
25
+ // If this.nonStrict is true, this also works for all unknown macros.
26
+ return symbol;
27
+ }
28
+ }
29
+ return token;
30
+ }
31
+
32
+
33
+ // \overset{X}{Y} -> op(Y, limits: #true)^X
34
+ // and with special case \overset{\text{def}}{=} -> eq.def
35
+ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
36
+ const [sup, base] = node.args!;
37
+
38
+ const is_def = (n: TexNode): boolean => {
39
+ if (n.eq(new TexNode('text', 'def'))) {
40
+ return true;
41
+ }
42
+ // \overset{def}{=} is also considered as eq.def
43
+ if (n.type === 'ordgroup' && n.args!.length === 3) {
44
+ const [a1, a2, a3] = n.args!;
45
+ const d = new TexNode('element', 'd');
46
+ const e = new TexNode('element', 'e');
47
+ const f = new TexNode('element', 'f');
48
+ if (a1.eq(d) && a2.eq(e) && a3.eq(f)) {
49
+ return true;
50
+ }
51
+ }
52
+ return false;
53
+ };
54
+ const is_eq = (n: TexNode): boolean => n.eq(new TexNode('element', '='));
55
+ if (is_def(sup) && is_eq(base)) {
56
+ return new TypstNode('symbol', 'eq.def');
57
+ }
58
+ const op_call = new TypstNode(
59
+ 'funcCall',
60
+ 'op',
61
+ [convert_tex_node_to_typst(base, options)]
62
+ );
63
+ op_call.setOptions({ limits: '#true' });
64
+ return new TypstNode(
65
+ 'supsub',
66
+ '',
67
+ [],
68
+ {
69
+ base: op_call,
70
+ sup: convert_tex_node_to_typst(sup, options),
71
+ }
72
+ );
73
+ }
74
+
75
+
76
+ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptions = {}): TypstNode {
77
+ switch (node.type) {
78
+ case 'empty':
79
+ return new TypstNode('empty', '');
80
+ case 'whitespace':
81
+ return new TypstNode('whitespace', node.content);
82
+ case 'ordgroup':
83
+ return new TypstNode(
84
+ 'group',
85
+ '',
86
+ node.args!.map((n) => convert_tex_node_to_typst(n, options))
87
+ );
88
+ case 'element':
89
+ return new TypstNode('atom', tex_token_to_typst(node.content));
90
+ case 'symbol':
91
+ return new TypstNode('symbol', tex_token_to_typst(node.content));
92
+ case 'text':
93
+ return new TypstNode('text', node.content);
94
+ case 'comment':
95
+ return new TypstNode('comment', node.content);
96
+ case 'supsub': {
97
+ let { base, sup, sub } = node.data as TexSupsubData;
98
+
99
+ // Special logic for overbrace
100
+ if (base && base.type === 'unaryFunc' && base.content === '\\overbrace' && sup) {
101
+ return new TypstNode(
102
+ 'funcCall',
103
+ 'overbrace',
104
+ [convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sup, options)]
105
+ );
106
+ } else if (base && base.type === 'unaryFunc' && base.content === '\\underbrace' && sub) {
107
+ return new TypstNode(
108
+ 'funcCall',
109
+ 'underbrace',
110
+ [convert_tex_node_to_typst(base.args![0], options), convert_tex_node_to_typst(sub, options)]
111
+ );
112
+ }
113
+
114
+ const data: TypstSupsubData = {
115
+ base: convert_tex_node_to_typst(base, options),
116
+ };
117
+ if (data.base.type === 'empty') {
118
+ data.base = new TypstNode('text', '');
119
+ }
120
+
121
+ if (sup) {
122
+ data.sup = convert_tex_node_to_typst(sup, options);
123
+ }
124
+
125
+ if (sub) {
126
+ data.sub = convert_tex_node_to_typst(sub, options);
127
+ }
128
+
129
+ return new TypstNode('supsub', '', [], data);
130
+ }
131
+ case 'leftright': {
132
+ const [left, body, right] = node.args!;
133
+ // These pairs will be handled by Typst compiler by default. No need to add lr()
134
+ const group: TypstNode = new TypstNode(
135
+ 'group',
136
+ '',
137
+ node.args!.map((n) => convert_tex_node_to_typst(n, options))
138
+ );
139
+ if ([
140
+ "[]", "()", "\\{\\}",
141
+ "\\lfloor\\rfloor",
142
+ "\\lceil\\rceil",
143
+ "\\lfloor\\rceil",
144
+ ].includes(left.content + right.content)) {
145
+ return group;
146
+ }
147
+ // "\left\{ A \right." -> "{A"
148
+ // "\left. A \right\}" -> "lr( A} )"
149
+ if (right.content === '.') {
150
+ group.args!.pop();
151
+ return group;
152
+ } else if (left.content === '.') {
153
+ group.args!.shift();
154
+ return new TypstNode('funcCall', 'lr', [group]);
155
+ }
156
+ return new TypstNode(
157
+ 'funcCall',
158
+ 'lr',
159
+ [group]
160
+ );
161
+ }
162
+ case 'binaryFunc': {
163
+ if (node.content === '\\overset') {
164
+ return convert_overset(node, options);
165
+ }
166
+ // \frac{a}{b} -> a / b
167
+ if (node.content === '\\frac') {
168
+ if(options.fracToSlash) {
169
+ return new TypstNode(
170
+ 'fraction',
171
+ '',
172
+ node.args!.map((n) => convert_tex_node_to_typst(n, options))
173
+ );
174
+ }
175
+ }
176
+ return new TypstNode(
177
+ 'funcCall',
178
+ tex_token_to_typst(node.content),
179
+ node.args!.map((n) => convert_tex_node_to_typst(n, options))
180
+ );
181
+ }
182
+ case 'unaryFunc': {
183
+ const arg0 = convert_tex_node_to_typst(node.args![0], options);
184
+ // \sqrt{3}{x} -> root(3, x)
185
+ if (node.content === '\\sqrt' && node.data) {
186
+ const data = convert_tex_node_to_typst(node.data as TexSqrtData, options); // the number of times to take the root
187
+ return new TypstNode(
188
+ 'funcCall',
189
+ 'root',
190
+ [data, arg0]
191
+ );
192
+ }
193
+ // \mathbf{a} -> upright(mathbf(a))
194
+ if (node.content === '\\mathbf') {
195
+ const inner: TypstNode = new TypstNode(
196
+ 'funcCall',
197
+ 'bold',
198
+ [arg0]
199
+ );
200
+ return new TypstNode(
201
+ 'funcCall',
202
+ 'upright',
203
+ [inner]
204
+ );
205
+ }
206
+ // \mathbb{R} -> RR
207
+ if (node.content === '\\mathbb' && arg0.type === 'atom' && /^[A-Z]$/.test(arg0.content)) {
208
+ return new TypstNode('symbol', arg0.content + arg0.content);
209
+ }
210
+ // \operatorname{opname} -> op("opname")
211
+ if (node.content === '\\operatorname') {
212
+ const body = node.args!;
213
+ if (body.length !== 1 || body[0].type !== 'text') {
214
+ throw new TypstWriterError(`Expecting body of \\operatorname to be text but got`, node);
215
+ }
216
+ const text = body[0].content;
217
+
218
+ if (TYPST_INTRINSIC_SYMBOLS.includes(text)) {
219
+ return new TypstNode('symbol', text);
220
+ } else {
221
+ return new TypstNode(
222
+ 'funcCall',
223
+ 'op',
224
+ [new TypstNode('text', text)]
225
+ );
226
+ }
227
+ }
228
+
229
+ // generic case
230
+ return new TypstNode(
231
+ 'funcCall',
232
+ tex_token_to_typst(node.content),
233
+ node.args!.map((n) => convert_tex_node_to_typst(n, options))
234
+ );
235
+ }
236
+ case 'beginend': {
237
+ const matrix = node.data as TexNode[][];
238
+ const data = matrix.map((row) => row.map((n) => convert_tex_node_to_typst(n, options)));
239
+
240
+ if (node.content!.startsWith('align')) {
241
+ // align, align*, alignat, alignat*, aligned, etc.
242
+ return new TypstNode('align', '', [], data);
243
+ } else {
244
+ const res = new TypstNode('matrix', '', [], data);
245
+ res.setOptions({ 'delim': '#none' });
246
+ return res;
247
+ }
248
+ }
249
+ case 'unknownMacro':
250
+ return new TypstNode('unknown', tex_token_to_typst(node.content));
251
+ case 'control':
252
+ if (node.content === '\\\\') {
253
+ return new TypstNode('symbol', '\\');
254
+ } else if (node.content === '\\,') {
255
+ return new TypstNode('symbol', 'thin');
256
+ } else {
257
+ throw new TypstWriterError(`Unknown control sequence: ${node.content}`, node);
258
+ }
259
+ default:
260
+ throw new TypstWriterError(`Unimplemented node type: ${node.type}`, node);
261
+ }
262
+ }
263
+
264
+
265
+
266
+ const TYPST_UNARY_FUNCTIONS: string[] = [
267
+ 'sqrt',
268
+ 'bold',
269
+ 'arrow',
270
+ 'upright',
271
+ 'lr',
272
+ 'op',
273
+ 'macron',
274
+ 'dot',
275
+ 'dot.double',
276
+ 'hat',
277
+ 'tilde',
278
+ 'overline',
279
+ 'underline',
280
+ 'bb',
281
+ 'cal',
282
+ 'frak',
283
+ ];
284
+
285
+ const TYPST_BINARY_FUNCTIONS: string[] = [
286
+ 'frac',
287
+ 'root',
288
+ 'overbrace',
289
+ 'underbrace',
290
+ ];
291
+
292
+ function apply_escape_if_needed(c: string) {
293
+ if (['{', '}', '%'].includes(c)) {
294
+ return '\\' + c;
295
+ }
296
+ return c;
297
+ }
298
+
299
+ function typst_token_to_tex(token: string): string {
300
+ if (/^[a-zA-Z0-9]$/.test(token)) {
301
+ return token;
302
+ } else if (token === 'thin') {
303
+ return '\\,';
304
+ } else if (reverseSymbolMap.has(token)) {
305
+ return '\\' + reverseSymbolMap.get(token)!;
306
+ }
307
+ return '\\' + token;
308
+ }
309
+
310
+
311
+
312
+ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
313
+ // special hook for eq.def
314
+ if (node.eq(new TypstNode('symbol', 'eq.def'))) {
315
+ return new TexNode('binaryFunc', '\\overset', [
316
+ new TexNode('text', 'def'),
317
+ new TexNode('element', '=')
318
+ ]);
319
+ }
320
+ switch (node.type) {
321
+ case 'empty':
322
+ return new TexNode('empty', '');
323
+ case 'whitespace':
324
+ return new TexNode('whitespace', node.content);
325
+ case 'atom':
326
+ return new TexNode('element', node.content);
327
+ case 'symbol':
328
+ switch (node.content) {
329
+ // special hook for comma
330
+ case 'comma':
331
+ return new TexNode('element', ',');
332
+ // special hook for hyph and hyph.minus
333
+ case 'hyph':
334
+ case 'hyph.minus':
335
+ return new TexNode('text', '-');
336
+ default:
337
+ return new TexNode('symbol', typst_token_to_tex(node.content));
338
+ }
339
+ case 'text':
340
+ return new TexNode('text', node.content);
341
+ case 'comment':
342
+ return new TexNode('comment', node.content);
343
+ case 'group': {
344
+ const args = node.args!.map(convert_typst_node_to_tex);
345
+ return new TexNode('ordgroup', node.content, args);
346
+ }
347
+ case 'funcCall': {
348
+ if (TYPST_UNARY_FUNCTIONS.includes(node.content)) {
349
+ // special hook for lr
350
+ if (node.content === 'lr') {
351
+ const body = node.args![0];
352
+ if (body.type === 'group') {
353
+ let left_delim = body.args![0].content;
354
+ let right_delim = body.args![body.args!.length - 1].content;
355
+ left_delim = apply_escape_if_needed(left_delim);
356
+ right_delim = apply_escape_if_needed(right_delim);
357
+ return new TexNode('ordgroup', '', [
358
+ new TexNode('element', '\\left' + left_delim),
359
+ ...body.args!.slice(1, body.args!.length - 1).map(convert_typst_node_to_tex),
360
+ new TexNode('element', '\\right' + right_delim)
361
+ ]);
362
+ }
363
+ }
364
+ const command = typst_token_to_tex(node.content);
365
+ return new TexNode('unaryFunc', command, node.args!.map(convert_typst_node_to_tex));
366
+ } else if (TYPST_BINARY_FUNCTIONS.includes(node.content)) {
367
+ // special hook for root
368
+ if (node.content === 'root') {
369
+ const [degree, radicand] = node.args!;
370
+ const data: TexSqrtData = convert_typst_node_to_tex(degree);
371
+ return new TexNode('unaryFunc', '\\sqrt', [convert_typst_node_to_tex(radicand)], data);
372
+ }
373
+ // special hook for overbrace and underbrace
374
+ if (node.content === 'overbrace' || node.content === 'underbrace') {
375
+ const [body, label] = node.args!;
376
+ const base = new TexNode('unaryFunc', '\\' + node.content, [convert_typst_node_to_tex(body)]);
377
+ const script = convert_typst_node_to_tex(label);
378
+ const data = node.content === 'overbrace' ? { base, sup: script } : { base, sub: script };
379
+ return new TexNode('supsub', '', [], data);
380
+ }
381
+ const command = typst_token_to_tex(node.content);
382
+ return new TexNode('binaryFunc', command, node.args!.map(convert_typst_node_to_tex));
383
+ } else {
384
+ return new TexNode('ordgroup', '', [
385
+ new TexNode('symbol', typst_token_to_tex(node.content)),
386
+ new TexNode('element', '('),
387
+ ...node.args!.map(convert_typst_node_to_tex),
388
+ new TexNode('element', ')')
389
+ ]);
390
+ }
391
+ }
392
+ case 'supsub': {
393
+ const { base, sup, sub } = node.data as TypstSupsubData;
394
+ const base_tex = convert_typst_node_to_tex(base);
395
+ let sup_tex: TexNode | undefined;
396
+ let sub_tex: TexNode | undefined;
397
+ if (sup) {
398
+ sup_tex = convert_typst_node_to_tex(sup);
399
+ }
400
+ if (sub) {
401
+ sub_tex = convert_typst_node_to_tex(sub);
402
+ }
403
+ const res = new TexNode('supsub', '', [], {
404
+ base: base_tex,
405
+ sup: sup_tex,
406
+ sub: sub_tex
407
+ });
408
+ return res;
409
+ }
410
+ case 'matrix': {
411
+ const typst_data = node.data as TypstNode[][];
412
+ const tex_data = typst_data.map(row => row.map(convert_typst_node_to_tex));
413
+ const matrix = new TexNode('beginend', 'matrix', [], tex_data);
414
+ let left_delim = "\\left(";
415
+ let right_delim = "\\right)";
416
+ if (node.options) {
417
+ if ('delim' in node.options) {
418
+ switch (node.options.delim) {
419
+ case '#none':
420
+ return matrix;
421
+ case '[':
422
+ left_delim = "\\left[";
423
+ right_delim = "\\right]";
424
+ break;
425
+ case ']':
426
+ left_delim = "\\left]";
427
+ right_delim = "\\right[";
428
+ break;
429
+ case '{':
430
+ left_delim = "\\left\\{";
431
+ right_delim = "\\right\\}";
432
+ break;
433
+ case '}':
434
+ left_delim = "\\left\\}";
435
+ right_delim = "\\right\\{";
436
+ break;
437
+ case '|':
438
+ left_delim = "\\left|";
439
+ right_delim = "\\right|";
440
+ break;
441
+ case ')':
442
+ left_delim = "\\left)";
443
+ right_delim = "\\right(";
444
+ case '(':
445
+ default:
446
+ left_delim = "\\left(";
447
+ right_delim = "\\right)";
448
+ break;
449
+ }
450
+ }
451
+ }
452
+ return new TexNode('ordgroup', '', [
453
+ new TexNode('element', left_delim),
454
+ matrix,
455
+ new TexNode('element', right_delim)
456
+ ]);
457
+ }
458
+ case 'control': {
459
+ switch (node.content) {
460
+ case '\\':
461
+ return new TexNode('control', '\\\\');
462
+ case '&':
463
+ return new TexNode('control', '&');
464
+ default:
465
+ throw new Error('[convert_typst_node_to_tex] Unimplemented control: ' + node.content);
466
+ }
467
+ }
468
+ case 'fraction': {
469
+ const [numerator, denominator] = node.args!;
470
+ const num_tex = convert_typst_node_to_tex(numerator);
471
+ const den_tex = convert_typst_node_to_tex(denominator);
472
+ return new TexNode('binaryFunc', '\\frac', [num_tex, den_tex]);
473
+ }
474
+ default:
475
+ throw new Error('[convert_typst_node_to_tex] Unimplemented type: ' + node.type);
476
+ }
477
+ }
478
+
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { parseTex } from "./tex-parser";
2
2
  import { Tex2TypstOptions } from "./types";
3
- import { convertTree, TypstWriter } from "./writer";
3
+ import { TypstWriter } from "./typst-writer";
4
+ import { convert_tex_node_to_typst, convert_typst_node_to_tex } from "./convert";
4
5
  import { symbolMap } from "./map";
5
6
  import { parseTypst } from "./typst-parser";
6
- import { convert_typst_node_to_tex, TexWriter } from "./tex-writer";
7
+ import { TexWriter } from "./tex-writer";
7
8
 
8
9
 
9
10
  export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
@@ -11,6 +12,7 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
11
12
  nonStrict: true,
12
13
  preferTypstIntrinsic: true,
13
14
  keepSpaces: false,
15
+ fracToSlash: true,
14
16
  customTexMacros: {}
15
17
  };
16
18
  if (options) {
@@ -25,7 +27,7 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
25
27
  }
26
28
  }
27
29
  const texTree = parseTex(tex, opt.customTexMacros!);
28
- const typstTree = convertTree(texTree);
30
+ const typstTree = convert_tex_node_to_typst(texTree, opt);
29
31
  const writer = new TypstWriter(opt.nonStrict!, opt.preferTypstIntrinsic!, opt.keepSpaces!);
30
32
  writer.serialize(typstTree);
31
33
  return writer.finalize();