tex2typst 0.3.0-beta-4 → 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/tex-writer.ts CHANGED
@@ -1,39 +1,6 @@
1
1
  import { array_includes, array_split } from "./generic";
2
2
  import { reverseSymbolMap } from "./map";
3
- import { TexNode, TexToken, TexSqrtData, TexSupsubData, TexTokenType, TypstNode, TypstSupsubData } from "./types";
4
-
5
- const TYPST_UNARY_FUNCTIONS: string[] = [
6
- 'sqrt',
7
- 'bold',
8
- 'arrow',
9
- 'upright',
10
- 'lr',
11
- 'op',
12
- 'macron',
13
- 'dot',
14
- 'dot.double',
15
- 'hat',
16
- 'tilde',
17
- 'overline',
18
- 'underline',
19
- 'bb',
20
- 'cal',
21
- 'frak',
22
- ];
23
-
24
- const TYPST_BINARY_FUNCTIONS: string[] = [
25
- 'frac',
26
- 'root',
27
- 'overbrace',
28
- 'underbrace',
29
- ];
30
-
31
- function apply_escape_if_needed(c: string) {
32
- if (['{', '}', '%'].includes(c)) {
33
- return '\\' + c;
34
- }
35
- return c;
36
- }
3
+ import { TexNode, TexToken, TexSupsubData, TexTokenType } from "./types";
37
4
 
38
5
 
39
6
  export class TexWriter {
@@ -106,184 +73,3 @@ export class TexWriter {
106
73
  }
107
74
  }
108
75
 
109
- export function convert_typst_node_to_tex(node: TypstNode): TexNode {
110
- // special hook for eq.def
111
- if(node.eq(new TypstNode('symbol', 'eq.def'))) {
112
- return new TexNode('binaryFunc', '\\overset', [
113
- new TexNode('text', 'def'),
114
- new TexNode('element', '=')
115
- ]);
116
- }
117
- switch (node.type) {
118
- case 'empty':
119
- return new TexNode('empty', '');
120
- case 'whitespace':
121
- return new TexNode('whitespace', node.content);
122
- case 'atom':
123
- return new TexNode('element', node.content);
124
- case 'symbol':
125
- switch(node.content) {
126
- // special hook for comma
127
- case 'comma':
128
- return new TexNode('element', ',');
129
- // special hook for hyph and hyph.minus
130
- case 'hyph':
131
- case 'hyph.minus':
132
- return new TexNode('text', '-');
133
- default:
134
- return new TexNode('symbol', typst_token_to_tex(node.content));
135
- }
136
- case 'text':
137
- return new TexNode('text', node.content);
138
- case 'comment':
139
- return new TexNode('comment', node.content);
140
- case 'group': {
141
- const args = node.args!.map(convert_typst_node_to_tex);
142
- if(node.content === 'parenthesis') {
143
- args.unshift(new TexNode('element', '('));
144
- args.push(new TexNode('element', ')'));
145
- }
146
- return new TexNode('ordgroup', '', args);
147
- }
148
- case 'funcCall': {
149
- if (TYPST_UNARY_FUNCTIONS.includes(node.content)) {
150
- // special hook for lr
151
- if (node.content === 'lr') {
152
- const body = node.args![0];
153
- if (body.type === 'group') {
154
- let left_delim = body.args![0].content;
155
- let right_delim = body.args![body.args!.length - 1].content;
156
- left_delim = apply_escape_if_needed(left_delim);
157
- right_delim = apply_escape_if_needed(right_delim);
158
- return new TexNode('ordgroup', '', [
159
- new TexNode('element', '\\left' + left_delim),
160
- ...body.args!.slice(1, body.args!.length - 1).map(convert_typst_node_to_tex),
161
- new TexNode('element', '\\right' + right_delim)
162
- ]);
163
- }
164
- }
165
- const command = typst_token_to_tex(node.content);
166
- return new TexNode('unaryFunc', command, node.args!.map(convert_typst_node_to_tex));
167
- } else if (TYPST_BINARY_FUNCTIONS.includes(node.content)) {
168
- // special hook for root
169
- if (node.content === 'root') {
170
- const [degree, radicand] = node.args!;
171
- const data: TexSqrtData = convert_typst_node_to_tex(degree);
172
- return new TexNode('unaryFunc', '\\sqrt', [convert_typst_node_to_tex(radicand)], data);
173
- }
174
- // special hook for overbrace and underbrace
175
- if (node.content === 'overbrace' || node.content === 'underbrace') {
176
- const [body, label] = node.args!;
177
- const base = new TexNode('unaryFunc', '\\' + node.content, [convert_typst_node_to_tex(body)]);
178
- const script = convert_typst_node_to_tex(label);
179
- const data = node.content === 'overbrace' ? { base, sup: script } : { base, sub: script };
180
- return new TexNode('supsub', '', [], data);
181
- }
182
- const command = typst_token_to_tex(node.content);
183
- return new TexNode('binaryFunc', command, node.args!.map(convert_typst_node_to_tex));
184
- } else {
185
- return new TexNode('ordgroup', '', [
186
- new TexNode('symbol', typst_token_to_tex(node.content)),
187
- new TexNode('element', '('),
188
- ...node.args!.map(convert_typst_node_to_tex),
189
- new TexNode('element', ')')
190
- ])
191
- }
192
- }
193
- case 'supsub': {
194
- const { base, sup, sub } = node.data as TypstSupsubData;
195
- const base_tex = convert_typst_node_to_tex(base);
196
- let sup_tex: TexNode | undefined;
197
- let sub_tex: TexNode | undefined;
198
- if (sup) {
199
- sup_tex = convert_typst_node_to_tex(sup);
200
- }
201
- if (sub) {
202
- sub_tex = convert_typst_node_to_tex(sub);
203
- }
204
- const res = new TexNode('supsub', '', [], {
205
- base: base_tex,
206
- sup: sup_tex,
207
- sub: sub_tex
208
- });
209
- return res;
210
- }
211
- case 'matrix': {
212
- const typst_data = node.data as TypstNode[][];
213
- const tex_data = typst_data.map(row => row.map(convert_typst_node_to_tex));
214
- const matrix = new TexNode('beginend', 'matrix', [], tex_data);
215
- let left_delim = "\\left(";
216
- let right_delim = "\\right)";
217
- if (node.options) {
218
- if('delim' in node.options) {
219
- switch (node.options.delim) {
220
- case '#none':
221
- return matrix;
222
- case '[':
223
- left_delim = "\\left[";
224
- right_delim = "\\right]";
225
- break;
226
- case ']':
227
- left_delim = "\\left]";
228
- right_delim = "\\right[";
229
- break;
230
- case '{':
231
- left_delim = "\\left\\{";
232
- right_delim = "\\right\\}";
233
- break;
234
- case '}':
235
- left_delim = "\\left\\}";
236
- right_delim = "\\right\\{";
237
- break;
238
- case '|':
239
- left_delim = "\\left|";
240
- right_delim = "\\right|";
241
- break;
242
- case ')':
243
- left_delim = "\\left)";
244
- right_delim = "\\right(";
245
- case '(':
246
- default:
247
- left_delim = "\\left(";
248
- right_delim = "\\right)";
249
- break;
250
- }
251
- }
252
- }
253
- return new TexNode('ordgroup', '', [
254
- new TexNode('element', left_delim),
255
- matrix,
256
- new TexNode('element', right_delim)
257
- ]);
258
- }
259
- case 'control': {
260
- switch (node.content) {
261
- case '\\':
262
- return new TexNode('control', '\\\\');
263
- case '&':
264
- return new TexNode('control', '&');
265
- default:
266
- throw new Error('[convert_typst_node_to_tex] Unimplemented control: ' + node.content);
267
- }
268
- }
269
- case 'fraction': {
270
- const [numerator, denominator] = node.args!;
271
- const num_tex = convert_typst_node_to_tex(numerator);
272
- const den_tex = convert_typst_node_to_tex(denominator);
273
- return new TexNode('binaryFunc', '\\frac', [num_tex, den_tex]);
274
- }
275
- default:
276
- throw new Error('[convert_typst_node_to_tex] Unimplemented type: ' + node.type);
277
- }
278
- }
279
-
280
- export function typst_token_to_tex(token: string): string {
281
- if (/^[a-zA-Z0-9]$/.test(token)) {
282
- return token;
283
- } else if (token === 'thin') {
284
- return '\\,';
285
- } else if (reverseSymbolMap.has(token)) {
286
- return '\\' + reverseSymbolMap.get(token)!;
287
- }
288
- return '\\' + token;
289
- }
package/src/types.ts CHANGED
@@ -122,8 +122,17 @@ export class TexNode {
122
122
  }
123
123
  return tokens;
124
124
  }
125
- case 'ordgroup':
126
- return this.args!.map((n) => n.serialize()).flat();
125
+ case 'ordgroup': {
126
+ let tokens = this.args!.map((n) => n.serialize()).flat();
127
+ if(this.content === 'parenthesis') {
128
+ const is_over_high = this.isOverHigh();
129
+ const left_delim = is_over_high ? '\\left(' : '(';
130
+ const right_delim = is_over_high ? '\\right)' : ')';
131
+ tokens.unshift(new TexToken(TexTokenType.ELEMENT, left_delim));
132
+ tokens.push(new TexToken(TexTokenType.ELEMENT, right_delim));
133
+ }
134
+ return tokens;
135
+ }
127
136
  case 'unaryFunc': {
128
137
  let tokens: TexToken[] = [];
129
138
  tokens.push(new TexToken(TexTokenType.COMMAND, this.content));
@@ -229,6 +238,34 @@ export class TexNode {
229
238
  throw new Error('[TexNode.serialize] Unimplemented type: ' + this.type);
230
239
  }
231
240
  }
241
+
242
+ // whether the node is over high so that if it's wrapped in braces, \left and \right should be used.
243
+ // e.g. \frac{1}{2} is over high, "2" is not.
244
+ public isOverHigh(): boolean {
245
+ switch (this.type) {
246
+ case 'element':
247
+ case 'symbol':
248
+ case 'text':
249
+ case 'control':
250
+ case 'empty':
251
+ return false;
252
+ case 'binaryFunc':
253
+ if(this.content === '\\frac') {
254
+ return true;
255
+ }
256
+ // fall through
257
+ case 'unaryFunc':
258
+ case 'ordgroup':
259
+ return this.args!.some((n) => n.isOverHigh());
260
+ case 'supsub': {
261
+ return (this.data as TexSupsubData).base.isOverHigh();
262
+ }
263
+ case 'beginend':
264
+ return true;
265
+ default:
266
+ return false;
267
+ }
268
+ }
232
269
  }
233
270
 
234
271
  export enum TypstTokenType {
@@ -347,6 +384,7 @@ export interface Tex2TypstOptions {
347
384
  nonStrict?: boolean; // default is true
348
385
  preferTypstIntrinsic?: boolean; // default is true,
349
386
  keepSpaces?: boolean; // default is false
387
+ fracToSlash?: boolean; // default is true
350
388
  customTexMacros?: { [key: string]: string };
351
389
  // TODO: custom typst functions
352
390
  }
@@ -1,9 +1,8 @@
1
- import { symbolMap } from "./map";
2
- import { TexNode, TexSqrtData, TexSupsubData, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
1
+ import { TexNode, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
3
2
 
4
3
 
5
4
  // symbols that are supported by Typst but not by KaTeX
6
- const TYPST_INTRINSIC_SYMBOLS = [
5
+ export const TYPST_INTRINSIC_SYMBOLS = [
7
6
  'dim',
8
7
  'id',
9
8
  'im',
@@ -20,48 +19,6 @@ function is_delimiter(c: TypstNode): boolean {
20
19
  }
21
20
 
22
21
 
23
- // \overset{X}{Y} -> op(Y, limits: #true)^X
24
- // and with special case \overset{\text{def}}{=} -> eq.def
25
- function convert_overset(node: TexNode): TypstNode {
26
- const [sup, base] = node.args!;
27
-
28
- const is_def = (n: TexNode): boolean => {
29
- if (n.eq(new TexNode('text', 'def'))) {
30
- return true;
31
- }
32
- // \overset{def}{=} is also considered as eq.def
33
- if (n.type === 'ordgroup' && n.args!.length === 3) {
34
- const [a1, a2, a3] = n.args!;
35
- const d = new TexNode('element', 'd');
36
- const e = new TexNode('element', 'e');
37
- const f = new TexNode('element', 'f');
38
- if (a1.eq(d) && a2.eq(e) && a3.eq(f)) {
39
- return true;
40
- }
41
- }
42
- return false;
43
- };
44
- const is_eq = (n: TexNode): boolean => n.eq(new TexNode('element', '='));
45
- if (is_def(sup) && is_eq(base)) {
46
- return new TypstNode('symbol', 'eq.def');
47
- }
48
- const op_call = new TypstNode(
49
- 'funcCall',
50
- 'op',
51
- [convertTree(base)]
52
- );
53
- op_call.setOptions({ limits: '#true' });
54
- return new TypstNode(
55
- 'supsub',
56
- '',
57
- [],
58
- {
59
- base: op_call,
60
- sup: convertTree(sup),
61
- }
62
- );
63
- }
64
-
65
22
  const TYPST_LEFT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '(');
66
23
  const TYPST_RIGHT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ')');
67
24
  const TYPST_COMMA: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ',');
@@ -106,8 +63,10 @@ export class TypstWriter {
106
63
  no_need_space ||= /[\(\[\|]$/.test(this.buffer) && /^\w/.test(str);
107
64
  // closing a clause
108
65
  no_need_space ||= /^[})\]\|]$/.test(str);
66
+ // putting the opening '(' for a function
67
+ no_need_space ||= /[^=]$/.test(this.buffer) && str === '(';
109
68
  // putting punctuation
110
- no_need_space ||= /^[(_^,;!]$/.test(str);
69
+ no_need_space ||= /^[_^,;!]$/.test(str);
111
70
  // putting a prime
112
71
  no_need_space ||= str === "'";
113
72
  // continue a number
@@ -122,6 +81,8 @@ export class TypstWriter {
122
81
  no_need_space ||= /^\s/.test(str);
123
82
  // "&=" instead of "& ="
124
83
  no_need_space ||= this.buffer.endsWith('&') && str === '=';
84
+ // before or after a slash e.g. "a/b" instead of "a / b"
85
+ no_need_space ||= this.buffer.endsWith('/') || str === '/';
125
86
  // other cases
126
87
  no_need_space ||= /[\s_^{\(]$/.test(this.buffer);
127
88
  if (!no_need_space) {
@@ -218,6 +179,25 @@ export class TypstWriter {
218
179
  this.insideFunctionDepth--;
219
180
  break;
220
181
  }
182
+ case 'fraction': {
183
+ const [numerator, denominator] = node.args!;
184
+ if(numerator.type === 'group') {
185
+ this.queue.push(TYPST_LEFT_PARENTHESIS);
186
+ this.serialize(numerator);
187
+ this.queue.push(TYPST_RIGHT_PARENTHESIS);
188
+ } else {
189
+ this.serialize(numerator);
190
+ }
191
+ this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '/'));
192
+ if(denominator.type === 'group') {
193
+ this.queue.push(TYPST_LEFT_PARENTHESIS);
194
+ this.serialize(denominator);
195
+ this.queue.push(TYPST_RIGHT_PARENTHESIS);
196
+ } else {
197
+ this.serialize(denominator);
198
+ }
199
+ break;
200
+ }
221
201
  case 'align': {
222
202
  const matrix = node.data as TypstNode[][];
223
203
  matrix.forEach((row, i) => {
@@ -355,210 +335,3 @@ export class TypstWriter {
355
335
  return this.buffer;
356
336
  }
357
337
  }
358
-
359
- // Convert a tree of TexNode into a tree of TypstNode
360
- export function convertTree(node: TexNode): TypstNode {
361
- switch (node.type) {
362
- case 'empty':
363
- return new TypstNode('empty', '');
364
- case 'whitespace':
365
- return new TypstNode('whitespace', node.content);
366
- case 'ordgroup':
367
- return new TypstNode(
368
- 'group',
369
- '',
370
- node.args!.map(convertTree),
371
- );
372
- case 'element':
373
- return new TypstNode('atom', convertToken(node.content));
374
- case 'symbol':
375
- return new TypstNode('symbol', convertToken(node.content));
376
- case 'text':
377
- return new TypstNode('text', node.content);
378
- case 'comment':
379
- return new TypstNode('comment', node.content);
380
- case 'supsub': {
381
- let { base, sup, sub } = node.data as TexSupsubData;
382
-
383
- // Special logic for overbrace
384
- if (base && base.type === 'unaryFunc' && base.content === '\\overbrace' && sup) {
385
- return new TypstNode(
386
- 'funcCall',
387
- 'overbrace',
388
- [convertTree(base.args![0]), convertTree(sup)],
389
- );
390
- } else if (base && base.type === 'unaryFunc' && base.content === '\\underbrace' && sub) {
391
- return new TypstNode(
392
- 'funcCall',
393
- 'underbrace',
394
- [convertTree(base.args![0]), convertTree(sub)],
395
- );
396
- }
397
-
398
- const data: TypstSupsubData = {
399
- base: convertTree(base),
400
- };
401
- if (data.base.type === 'empty') {
402
- data.base = new TypstNode('text', '');
403
- }
404
-
405
- if (sup) {
406
- data.sup = convertTree(sup);
407
- }
408
-
409
- if (sub) {
410
- data.sub = convertTree(sub);
411
- }
412
-
413
- return new TypstNode('supsub', '', [], data);
414
- }
415
- case 'leftright': {
416
- const [left, body, right] = node.args!;
417
- // These pairs will be handled by Typst compiler by default. No need to add lr()
418
- const group: TypstNode = new TypstNode(
419
- 'group',
420
- '',
421
- node.args!.map(convertTree),
422
- );
423
- if ([
424
- "[]", "()", "\\{\\}",
425
- "\\lfloor\\rfloor",
426
- "\\lceil\\rceil",
427
- "\\lfloor\\rceil",
428
- ].includes(left.content + right.content)) {
429
- return group;
430
- }
431
- // "\left\{ A \right." -> "{A"
432
- // "\left. A \right\}" -> "lr( A} )"
433
- if(right.content === '.') {
434
- group.args!.pop();
435
- return group;
436
- } else if(left.content === '.') {
437
- group.args!.shift();
438
- return new TypstNode( 'funcCall', 'lr', [group]);
439
- }
440
- return new TypstNode(
441
- 'funcCall',
442
- 'lr',
443
- [group],
444
- );
445
- }
446
- case 'binaryFunc': {
447
- if (node.content === '\\overset') {
448
- return convert_overset(node);
449
- }
450
- return new TypstNode(
451
- 'funcCall',
452
- convertToken(node.content),
453
- node.args!.map(convertTree),
454
- );
455
- }
456
- case 'unaryFunc': {
457
- const arg0 = convertTree(node.args![0]);
458
- // \sqrt{3}{x} -> root(3, x)
459
- if (node.content === '\\sqrt' && node.data) {
460
- const data = convertTree(node.data as TexSqrtData); // the number of times to take the root
461
- return new TypstNode(
462
- 'funcCall',
463
- 'root',
464
- [data, arg0],
465
- );
466
- }
467
- // \mathbf{a} -> upright(mathbf(a))
468
- if (node.content === '\\mathbf') {
469
- const inner: TypstNode = new TypstNode(
470
- 'funcCall',
471
- 'bold',
472
- [arg0],
473
- );
474
- return new TypstNode(
475
- 'funcCall',
476
- 'upright',
477
- [inner],
478
- );
479
- }
480
- // \mathbb{R} -> RR
481
- if (node.content === '\\mathbb' && arg0.type === 'atom' && /^[A-Z]$/.test(arg0.content)) {
482
- return new TypstNode('symbol', arg0.content + arg0.content);
483
- }
484
- // \operatorname{opname} -> op("opname")
485
- if (node.content === '\\operatorname') {
486
- const body = node.args!;
487
- if (body.length !== 1 || body[0].type !== 'text') {
488
- throw new TypstWriterError(`Expecting body of \\operatorname to be text but got`, node);
489
- }
490
- const text = body[0].content;
491
-
492
- if (TYPST_INTRINSIC_SYMBOLS.includes(text)) {
493
- return new TypstNode('symbol', text);
494
- } else {
495
- return new TypstNode(
496
- 'funcCall',
497
- 'op',
498
- [new TypstNode('text', text)],
499
- );
500
- }
501
- }
502
-
503
- // generic case
504
- return new TypstNode(
505
- 'funcCall',
506
- convertToken(node.content),
507
- node.args!.map(convertTree),
508
- );
509
- }
510
- case 'beginend': {
511
- const matrix = node.data as TexNode[][];
512
- const data = matrix.map((row) => row.map(convertTree));
513
-
514
- if (node.content!.startsWith('align')) {
515
- // align, align*, alignat, alignat*, aligned, etc.
516
- return new TypstNode( 'align', '', [], data);
517
- } else {
518
- const res = new TypstNode('matrix', '', [], data);
519
- res.setOptions({'delim': '#none'});
520
- return res;
521
- }
522
- }
523
- case 'unknownMacro':
524
- return new TypstNode('unknown', convertToken(node.content));
525
- case 'control':
526
- if (node.content === '\\\\') {
527
- return new TypstNode('symbol', '\\');
528
- } else if (node.content === '\\,') {
529
- return new TypstNode('symbol', 'thin');
530
- } else {
531
- throw new TypstWriterError(`Unknown control sequence: ${node.content}`, node);
532
- }
533
- default:
534
- throw new TypstWriterError(`Unimplemented node type: ${node.type}`, node);
535
- }
536
- }
537
-
538
-
539
- function convertToken(token: string): string {
540
- if (/^[a-zA-Z0-9]$/.test(token)) {
541
- return token;
542
- } else if (token === '/') {
543
- return '\\/';
544
- } else if (token === '\\|') {
545
- // \| in LaTeX is double vertical bar looks like ||
546
- return 'parallel';
547
- } else if (token === '\\\\') {
548
- return '\\';
549
- } else if (['\\$', '\\#', '\\&', '\\_'].includes(token)) {
550
- return token;
551
- } else if (token.startsWith('\\')) {
552
- const symbol = token.slice(1);
553
- if (symbolMap.has(symbol)) {
554
- return symbolMap.get(symbol)!;
555
- } else {
556
- // Fall back to the original macro.
557
- // This works for \alpha, \beta, \gamma, etc.
558
- // If this.nonStrict is true, this also works for all unknown macros.
559
- return symbol;
560
- }
561
- }
562
- return token;
563
- }
564
-