tex2typst 0.3.16 → 0.3.18

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType, TypstLrData, TexArrayData } from "./types";
1
+ import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType, TypstLrData, TexArrayData, TypstNamedParams } from "./types";
2
2
  import { TypstWriterError } from "./typst-writer";
3
3
  import { symbolMap, reverseSymbolMap } from "./map";
4
4
  import { array_join } from "./generic";
@@ -22,9 +22,6 @@ function tex_token_to_typst(token: string): string {
22
22
  return token;
23
23
  } else if (token === '/') {
24
24
  return '\\/';
25
- } else if (token === '\\|') {
26
- // \| in LaTeX is double vertical bar looks like ||
27
- return 'parallel';
28
25
  } else if (token === '\\\\') {
29
26
  return '\\';
30
27
  } else if (['\\$', '\\#', '\\&', '\\_'].includes(token)) {
@@ -49,40 +46,52 @@ function tex_token_to_typst(token: string): string {
49
46
  function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
50
47
  const [sup, base] = node.args!;
51
48
 
52
- const is_def = (n: TexNode): boolean => {
53
- if (n.eq(new TexNode('text', 'def'))) {
54
- return true;
55
- }
56
- // \overset{def}{=} is also considered as eq.def
57
- if (n.type === 'ordgroup' && n.args!.length === 3) {
58
- const [a1, a2, a3] = n.args!;
59
- const d = new TexNode('element', 'd');
60
- const e = new TexNode('element', 'e');
61
- const f = new TexNode('element', 'f');
62
- if (a1.eq(d) && a2.eq(e) && a3.eq(f)) {
49
+ if (options.optimize) {
50
+ const is_def = (n: TexNode): boolean => {
51
+ if (n.eq(new TexNode('text', 'def'))) {
63
52
  return true;
64
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');
65
69
  }
66
- return false;
67
- };
68
- const is_eq = (n: TexNode): boolean => n.eq(new TexNode('element', '='));
69
- if (is_def(sup) && is_eq(base)) {
70
- return new TypstNode('symbol', 'eq.def');
71
70
  }
72
71
  const limits_call = new TypstNode(
73
72
  'funcCall',
74
73
  'limits',
75
74
  [convert_tex_node_to_typst(base, options)]
76
75
  );
77
- return new TypstNode(
78
- 'supsub',
79
- '',
80
- [],
81
- {
76
+ return new TypstNode('supsub', '', [], {
82
77
  base: limits_call,
83
78
  sup: convert_tex_node_to_typst(sup, options),
84
- }
79
+ });
80
+ }
81
+
82
+ // \underset{X}{Y} -> limits(Y)_X
83
+ function convert_underset(node: TexNode, options: Tex2TypstOptions): TypstNode {
84
+ const [sub, base] = node.args!;
85
+
86
+ const limits_call = new TypstNode(
87
+ 'funcCall',
88
+ 'limits',
89
+ [convert_tex_node_to_typst(base, options)]
85
90
  );
91
+ return new TypstNode('supsub', '', [], {
92
+ base: limits_call,
93
+ sub: convert_tex_node_to_typst(sub, options),
94
+ });
86
95
  }
87
96
 
88
97
 
@@ -117,7 +126,7 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
117
126
  case 'supsub': {
118
127
  let { base, sup, sub } = node.data as TexSupsubData;
119
128
 
120
- // Special logic for overbrace
129
+ // special hook for overbrace
121
130
  if (base && base.type === 'unaryFunc' && base.content === '\\overbrace' && sup) {
122
131
  return new TypstNode(
123
132
  'funcCall',
@@ -150,43 +159,64 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
150
159
  return new TypstNode('supsub', '', [], data);
151
160
  }
152
161
  case 'leftright': {
153
- const [left, body, right] = node.args!;
154
- // These pairs will be handled by Typst compiler by default. No need to add lr()
155
- const group: TypstNode = new TypstNode(
162
+ const [left, _body, right] = node.args!;
163
+ const [typ_left, typ_body, typ_right] = node.args!.map((n) => convert_tex_node_to_typst(n, options));
164
+
165
+ if (options.optimize) {
166
+ // optimization off: "lr(bar.v.double a + 1/2 bar.v.double)"
167
+ // optimization on : "norm(a + 1/2)"
168
+ if (left.content === '\\|' && right.content === '\\|') {
169
+ return new TypstNode('funcCall', 'norm', [typ_body]);
170
+ }
171
+
172
+ // These pairs will be handled by Typst compiler by default. No need to add lr()
173
+ if ([
174
+ "[]", "()", "\\{\\}",
175
+ "\\lfloor\\rfloor",
176
+ "\\lceil\\rceil",
177
+ "\\lfloor\\rceil",
178
+ ].includes(left.content + right.content)) {
179
+ return new TypstNode('group', '', [typ_left, typ_body, typ_right]);
180
+ }
181
+ }
182
+
183
+ const group = new TypstNode(
156
184
  'group',
157
185
  '',
158
- node.args!.map((n) => convert_tex_node_to_typst(n, options))
186
+ [typ_left, typ_body, typ_right]
159
187
  );
160
- if ([
161
- "[]", "()", "\\{\\}",
162
- "\\lfloor\\rfloor",
163
- "\\lceil\\rceil",
164
- "\\lfloor\\rceil",
165
- ].includes(left.content + right.content)) {
166
- return group;
167
- }
168
- // "\left\{ A \right." -> "{A"
169
- // "\left. A \right\}" -> "lr( A} )"
188
+
189
+ // "\left\{ a + \frac{1}{3} \right." -> "lr(\{ a + 1/3)"
190
+ // "\left. a + \frac{1}{3} \right\}" -> "lr( a + \frac{1}{3} \})"
191
+ // Note that: In lr(), if one side of delimiter doesn't present (i.e. derived from "\\left." or "\\right."),
192
+ // "(", ")", "{", "[", should be escaped with "\" to be the other side of delimiter.
193
+ // Simple "lr({ a+1/3)" doesn't compile in Typst.
194
+ const escape_curly_or_paren = function(s: string): string {
195
+ if (["(", ")", "{", "["].includes(s)) {
196
+ return "\\" + s;
197
+ } else {
198
+ return s;
199
+ }
200
+ };
170
201
  if (right.content === '.') {
171
- group.args!.pop();
172
- return group;
202
+ typ_left.content = escape_curly_or_paren(typ_left.content);
203
+ group.args = [typ_left, typ_body];
173
204
  } else if (left.content === '.') {
174
- group.args!.shift();
175
- return new TypstNode('funcCall', 'lr', [group]);
205
+ typ_right.content = escape_curly_or_paren(typ_right.content);
206
+ group.args = [typ_body, typ_right];
176
207
  }
177
- return new TypstNode(
178
- 'funcCall',
179
- 'lr',
180
- [group]
181
- );
208
+ return new TypstNode('funcCall', 'lr', [group]);
182
209
  }
183
210
  case 'binaryFunc': {
184
211
  if (node.content === '\\overset') {
185
212
  return convert_overset(node, options);
186
213
  }
214
+ if (node.content === '\\underset') {
215
+ return convert_underset(node, options);
216
+ }
187
217
  // \frac{a}{b} -> a / b
188
218
  if (node.content === '\\frac') {
189
- if(options.fracToSlash) {
219
+ if (options.fracToSlash) {
190
220
  return new TypstNode(
191
221
  'fraction',
192
222
  '',
@@ -287,6 +317,59 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
287
317
  if (node.content! === 'cases') {
288
318
  return new TypstNode('cases', '', [], data);
289
319
  }
320
+ if (node.content! === 'array') {
321
+ const res = new TypstNode('matrix', '', [], data);
322
+ const options: TypstNamedParams = { 'delim': TYPST_NONE };
323
+
324
+ const align_args = node.args!;
325
+ if (align_args.length > 0) {
326
+ const align_node = align_args[0];
327
+ const align_str = (() => {
328
+ if (align_node.type === 'element') return align_node.content;
329
+ if (align_node.type === 'ordgroup') {
330
+ return align_node.args!.map(n => n.type === 'element' ? n.content : '').join('');
331
+ }
332
+ return '';
333
+ })();
334
+
335
+ if (align_str) {
336
+ const alignMap: Record<string, string> = { l: '#left', c: '#center', r: '#right' };
337
+ const chars = Array.from(align_str);
338
+
339
+ const alignments = chars
340
+ .map(c => alignMap[c])
341
+ .filter(Boolean)
342
+ .map(s => new TypstNode('symbol', s!));
343
+
344
+ const vlinePositions: number[] = [];
345
+ let columnIndex = 0;
346
+ for (const c of chars) {
347
+ if (c === '|') {
348
+ vlinePositions.push(columnIndex);
349
+ } else if (c === 'l' || c === 'c' || c === 'r') {
350
+ columnIndex++;
351
+ }
352
+ }
353
+
354
+ if (vlinePositions.length > 0) {
355
+ if (vlinePositions.length === 1) {
356
+ options['augment'] = new TypstNode('symbol', `#${vlinePositions[0]}`);
357
+ } else {
358
+ options['augment'] = new TypstNode('symbol', `#(vline: (${vlinePositions.join(', ')}))`);
359
+ }
360
+ }
361
+
362
+ if (alignments.length > 0) {
363
+ const first_align = alignments[0].content;
364
+ const all_same = alignments.every(item => item.content === first_align);
365
+ options['align'] = all_same ? alignments[0] : new TypstNode('symbol', '#center');
366
+ }
367
+ }
368
+ }
369
+
370
+ res.setOptions(options);
371
+ return res;
372
+ }
290
373
  if (node.content!.endsWith('matrix')) {
291
374
  let delim: TypstPrimitiveValue;
292
375
  switch (node.content) {
@@ -356,6 +439,8 @@ const TYPST_UNARY_FUNCTIONS: string[] = [
356
439
  'frak',
357
440
  'floor',
358
441
  'ceil',
442
+ 'norm',
443
+ 'limits',
359
444
  ];
360
445
 
361
446
  const TYPST_BINARY_FUNCTIONS: string[] = [
@@ -375,8 +460,6 @@ function apply_escape_if_needed(c: string) {
375
460
  function typst_token_to_tex(token: string): string {
376
461
  if (/^[a-zA-Z0-9]$/.test(token)) {
377
462
  return token;
378
- } else if (token === 'thin') {
379
- return '\\,';
380
463
  } else if (reverseSymbolMap.has(token)) {
381
464
  return '\\' + reverseSymbolMap.get(token)!;
382
465
  }
@@ -438,6 +521,8 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
438
521
  let left_delim = apply_escape_if_needed(data.leftDelim);
439
522
  assert(data.rightDelim !== null, "leftDelim has value but rightDelim not");
440
523
  let right_delim = apply_escape_if_needed(data.rightDelim!);
524
+ // TODO: should be TeXNode('leftright', ...)
525
+ // But currently writer will output `\left |` while people commonly prefer `\left|`.
441
526
  return new TexNode('ordgroup', '', [
442
527
  new TexNode('element', '\\left' + left_delim),
443
528
  ...node.args!.map(convert_typst_node_to_tex),
@@ -447,17 +532,29 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
447
532
  return new TexNode('ordgroup', '', node.args!.map(convert_typst_node_to_tex));
448
533
  }
449
534
  }
535
+ // special hook for norm
536
+ // `\| a \|` <- `norm(a)`
537
+ // `\left\| a + \frac{1}{3} \right\|` <- `norm(a + 1/3)`
538
+ if (node.content === 'norm') {
539
+ const arg0 = node.args![0];
540
+ const tex_node_type = node.isOverHigh() ? 'leftright' : 'ordgroup';
541
+ return new TexNode(tex_node_type, '', [
542
+ new TexNode('symbol', "\\|"),
543
+ convert_typst_node_to_tex(arg0),
544
+ new TexNode('symbol', "\\|")
545
+ ]);
546
+ }
450
547
  // special hook for floor, ceil
451
- // Typst "floor(a) + ceil(b)" should converts to Tex "\lfloor a \rfloor + \lceil b \rceil"
548
+ // `\lfloor a \rfloor` <- `floor(a)`
549
+ // `\lceil a \rceil` <- `ceil(a)`
550
+ // `\left\lfloor a \right\rfloor` <- `floor(a)`
551
+ // `\left\lceil a \right\rceil` <- `ceil(a)`
452
552
  if (node.content === 'floor' || node.content === 'ceil') {
453
- let left = "\\l" + node.content;
454
- let right = "\\r" + node.content;
553
+ const left = "\\l" + node.content;
554
+ const right = "\\r" + node.content;
455
555
  const arg0 = node.args![0];
456
- if (arg0.isOverHigh()) {
457
- left = "\\left" + left;
458
- right = "\\right" + right;
459
- }
460
- return new TexNode('ordgroup', '', [
556
+ const tex_node_type = node.isOverHigh() ? 'leftright' : 'ordgroup';
557
+ return new TexNode(tex_node_type, '', [
461
558
  new TexNode('symbol', left),
462
559
  convert_typst_node_to_tex(arg0),
463
560
  new TexNode('symbol', right)
@@ -499,15 +596,34 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
499
596
  }
500
597
  case 'supsub': {
501
598
  const { base, sup, sub } = node.data as TypstSupsubData;
502
- const base_tex = convert_typst_node_to_tex(base);
503
599
  let sup_tex: TexNode | undefined;
504
600
  let sub_tex: TexNode | undefined;
601
+
505
602
  if (sup) {
506
603
  sup_tex = convert_typst_node_to_tex(sup);
507
604
  }
508
605
  if (sub) {
509
606
  sub_tex = convert_typst_node_to_tex(sub);
510
607
  }
608
+
609
+ // special hook for limits
610
+ // `limits(+)^a` -> `\overset{a}{+}`
611
+ // `limits(+)_a` -> `\underset{a}{+}`
612
+ // `limits(+)_a^b` -> `\overset{b}{\underset{a}{+}}`
613
+ if (base.eq(new TypstNode('funcCall', 'limits'))) {
614
+ const body_in_limits = convert_typst_node_to_tex(base.args![0]);
615
+ if (sup_tex !== undefined && sub_tex === undefined) {
616
+ return new TexNode('binaryFunc', '\\overset', [sup_tex, body_in_limits]);
617
+ } else if (sup_tex === undefined && sub_tex !== undefined) {
618
+ return new TexNode('binaryFunc', '\\underset', [sub_tex, body_in_limits]);
619
+ } else {
620
+ const underset_call = new TexNode('binaryFunc', '\\underset', [sub_tex!, body_in_limits]);
621
+ return new TexNode('binaryFunc', '\\overset', [sup_tex!, underset_call]);
622
+ }
623
+ }
624
+
625
+ const base_tex = convert_typst_node_to_tex(base);
626
+
511
627
  const res = new TexNode('supsub', '', [], {
512
628
  base: base_tex,
513
629
  sup: sup_tex,
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
16
16
  keepSpaces: false,
17
17
  fracToSlash: true,
18
18
  inftyToOo: false,
19
+ optimize: true,
19
20
  nonAsciiWrapper: "",
20
21
  customTexMacros: {}
21
22
  };
package/src/map.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  const symbolMap = new Map<string, string>([
2
2
  ['displaystyle', 'display'],
3
3
 
4
+ ['|', 'bar.v.double'],
5
+ ['!', '#h(-math.thin.amount)'],
4
6
  [',', 'thin'],
5
7
  [':', 'med'],
6
8
  [';', 'thick'],
package/src/tex-parser.ts CHANGED
@@ -47,6 +47,7 @@ const BINARY_COMMANDS = [
47
47
  'dfrac',
48
48
  'tbinom',
49
49
  'overset',
50
+ 'underset',
50
51
  ]
51
52
 
52
53
  const IGNORED_COMMANDS = [
@@ -86,7 +87,7 @@ function eat_whitespaces(tokens: TexToken[], start: number): TexToken[] {
86
87
 
87
88
  function eat_parenthesis(tokens: TexToken[], start: number): TexToken | null {
88
89
  const firstToken = tokens[start];
89
- if (firstToken.type === TexTokenType.ELEMENT && ['(', ')', '[', ']', '|', '\\{', '\\}', '.'].includes(firstToken.value)) {
90
+ if (firstToken.type === TexTokenType.ELEMENT && ['(', ')', '[', ']', '|', '\\{', '\\}', '.', '\\|'].includes(firstToken.value)) {
90
91
  return firstToken;
91
92
  } else if (firstToken.type === TexTokenType.COMMAND && ['lfloor', 'rfloor', 'lceil', 'rceil', 'langle', 'rangle'].includes(firstToken.value.slice(1))) {
92
93
  return firstToken;
@@ -166,7 +167,7 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
166
167
  ],
167
168
  [String.raw`%[^\n]*`, (s) => new TexToken(TexTokenType.COMMENT, s.text()!.substring(1))],
168
169
  [String.raw`[{}_^&]`, (s) => new TexToken(TexTokenType.CONTROL, s.text()!)],
169
- [String.raw`\\[\\,:; ]`, (s) => new TexToken(TexTokenType.CONTROL, s.text()!)],
170
+ [String.raw`\\[\\,:;! ]`, (s) => new TexToken(TexTokenType.CONTROL, s.text()!)],
170
171
  [String.raw`\r?\n`, (_s) => new TexToken(TexTokenType.NEWLINE, "\n")],
171
172
  [String.raw`\s+`, (s) => new TexToken(TexTokenType.SPACE, s.text()!)],
172
173
  [String.raw`\\[{}%$&#_|]`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
@@ -394,6 +395,7 @@ export class LatexParser {
394
395
  case '}':
395
396
  throw new LatexParserError("Unmatched '}'");
396
397
  case '\\\\':
398
+ case '\\!':
397
399
  case '\\,':
398
400
  case '\\:':
399
401
  case '\\;':
@@ -548,7 +550,18 @@ export class LatexParser {
548
550
  const envName = tokens[pos + 1].value;
549
551
  pos += 3;
550
552
 
551
- pos += eat_whitespaces(tokens, pos).length; // ignore whitespaces and '\n' after \begin{envName}
553
+ const args: TexNode[] = [];
554
+ while (pos < tokens.length) {
555
+ const whitespaceCount = eat_whitespaces(tokens, pos).length;
556
+ pos += whitespaceCount;
557
+
558
+ if (pos >= tokens.length || !tokens[pos].eq(LEFT_CURLY_BRACKET)) {
559
+ break;
560
+ }
561
+ const [arg, newPos] = this.parseNextArg(tokens, pos);
562
+ args.push(arg);
563
+ pos = newPos;
564
+ }
552
565
 
553
566
  const exprInsideStart = pos;
554
567
 
@@ -573,7 +586,7 @@ export class LatexParser {
573
586
  exprInside.pop();
574
587
  }
575
588
  const body = this.parseAligned(exprInside);
576
- const res = new TexNode('beginend', envName, [], body);
589
+ const res = new TexNode('beginend', envName, args, body);
577
590
  return [res, pos];
578
591
  }
579
592
 
package/src/types.ts CHANGED
@@ -125,6 +125,13 @@ export class TexNode {
125
125
  case 'ordgroup': {
126
126
  return this.args!.map((n) => n.serialize()).flat();
127
127
  }
128
+ case 'leftright': {
129
+ let tokens = this.args!.map((n) => n.serialize()).flat();
130
+ tokens.splice(0, 0, new TexToken(TexTokenType.COMMAND, '\\left'));
131
+ tokens.splice(tokens.length - 1, 0, new TexToken(TexTokenType.COMMAND, '\\right'));
132
+
133
+ return tokens;
134
+ }
128
135
  case 'unaryFunc': {
129
136
  let tokens: TexToken[] = [];
130
137
  tokens.push(new TexToken(TexTokenType.COMMAND, this.content));
@@ -382,14 +389,21 @@ export const TYPST_NONE = new TypstNode('none', '#none');
382
389
  export const TYPST_TRUE: TypstPrimitiveValue = true;
383
390
  export const TYPST_FALSE: TypstPrimitiveValue = false;
384
391
 
392
+ /**
393
+ * ATTENTION:
394
+ * Don't use any options except those explicitly documented in
395
+ * https://github.com/qwinsi/tex2typst/blob/main/docs/api-reference.md
396
+ * Any undocumented options may break in the future!
397
+ */
385
398
  export interface Tex2TypstOptions {
386
- nonStrict?: boolean; // default is true
387
- preferTypstIntrinsic?: boolean; // default is true,
388
- preferShorthands?: boolean; // default is true
389
- keepSpaces?: boolean; // default is false
390
- fracToSlash?: boolean; // default is true
391
- inftyToOo?: boolean; // default is false
392
- nonAsciiWrapper?: string; // default is ""
399
+ nonStrict?: boolean; /** default is true */
400
+ preferTypstIntrinsic?: boolean; /** default is true */
401
+ preferShorthands?: boolean; /** default is true */
402
+ keepSpaces?: boolean; /** default is false */
403
+ fracToSlash?: boolean; /** default is true */
404
+ inftyToOo?: boolean; /** default is false */
405
+ optimize?: boolean; /** default is true */
406
+ nonAsciiWrapper?: string; /** default is "" */
393
407
  customTexMacros?: { [key: string]: string };
394
408
  // TODO: custom typst functions
395
409
  }
@@ -40,6 +40,7 @@ export interface TypstWriterOptions {
40
40
  preferShorthands: boolean;
41
41
  keepSpaces: boolean;
42
42
  inftyToOo: boolean;
43
+ optimize: boolean;
43
44
  }
44
45
 
45
46
  export class TypstWriter {
@@ -47,17 +48,19 @@ export class TypstWriter {
47
48
  private preferShorthands: boolean;
48
49
  private keepSpaces: boolean;
49
50
  private inftyToOo: boolean;
51
+ private optimize: boolean;
50
52
 
51
53
  protected buffer: string = "";
52
54
  protected queue: TypstToken[] = [];
53
55
 
54
56
  private insideFunctionDepth = 0;
55
57
 
56
- constructor(opt: TypstWriterOptions) {
57
- this.nonStrict = opt.nonStrict;
58
- this.preferShorthands = opt.preferShorthands;
59
- this.keepSpaces = opt.keepSpaces;
60
- this.inftyToOo = opt.inftyToOo;
58
+ constructor(options: TypstWriterOptions) {
59
+ this.nonStrict = options.nonStrict;
60
+ this.preferShorthands = options.preferShorthands;
61
+ this.keepSpaces = options.keepSpaces;
62
+ this.inftyToOo = options.inftyToOo;
63
+ this.optimize = options.optimize;
61
64
  }
62
65
 
63
66
 
@@ -68,8 +71,6 @@ export class TypstWriter {
68
71
  return;
69
72
  }
70
73
 
71
- // TODO: "C \frac{xy}{z}" should translate to "C (x y)/z" instead of "C(x y)/z"
72
-
73
74
  let no_need_space = false;
74
75
  // putting the first token in clause
75
76
  no_need_space ||= /[\(\[\|]$/.test(this.buffer) && /^\w/.test(str);
@@ -163,7 +164,7 @@ export class TypstWriter {
163
164
  // Put prime symbol before '_'. Because $y_1'$ is not displayed properly in Typst (so far)
164
165
  // e.g.
165
166
  // y_1' -> y'_1
166
- // y_{a_1}' -> y'_{a_1}
167
+ // y_{a_1}' -> y'_(a_1)
167
168
  this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '\''));
168
169
  trailing_space_needed = false;
169
170
  }
@@ -207,21 +208,9 @@ export class TypstWriter {
207
208
  }
208
209
  case 'fraction': {
209
210
  const [numerator, denominator] = node.args!;
210
- if(numerator.type === 'group') {
211
- this.queue.push(TYPST_LEFT_PARENTHESIS);
212
- this.serialize(numerator);
213
- this.queue.push(TYPST_RIGHT_PARENTHESIS);
214
- } else {
215
- this.serialize(numerator);
216
- }
211
+ this.appendWithBracketsIfNeeded(numerator);
217
212
  this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '/'));
218
- if(denominator.type === 'group') {
219
- this.queue.push(TYPST_LEFT_PARENTHESIS);
220
- this.serialize(denominator);
221
- this.queue.push(TYPST_RIGHT_PARENTHESIS);
222
- } else {
223
- this.serialize(denominator);
224
- }
213
+ this.appendWithBracketsIfNeeded(denominator);
225
214
  break;
226
215
  }
227
216
  case 'align': {
@@ -316,7 +305,7 @@ export class TypstWriter {
316
305
  }
317
306
 
318
307
  private appendWithBracketsIfNeeded(node: TypstNode): boolean {
319
- let need_to_wrap = ['group', 'supsub', 'empty'].includes(node.type);
308
+ let need_to_wrap = ['group', 'supsub', 'fraction','empty'].includes(node.type);
320
309
 
321
310
  if (node.type === 'group') {
322
311
  if (node.args!.length === 0) {
@@ -387,9 +376,11 @@ export class TypstWriter {
387
376
  res = res.replace(/round\(\)/g, 'round("")');
388
377
  return res;
389
378
  }
390
- const all_passes = [smartFloorPass, smartCeilPass, smartRoundPass];
391
- for (const pass of all_passes) {
392
- this.buffer = pass(this.buffer);
379
+ if (this.optimize) {
380
+ const all_passes = [smartFloorPass, smartCeilPass, smartRoundPass];
381
+ for (const pass of all_passes) {
382
+ this.buffer = pass(this.buffer);
383
+ }
393
384
  }
394
385
  return this.buffer;
395
386
  }
package/TODO.md DELETED
@@ -1 +0,0 @@
1
- - Tex math `\frac{X}{\frac{Y}{2}}` should converts to Typst `X/(Y/2)`, not `X/Y/2`.
@@ -1,64 +0,0 @@
1
- # API Reference of tex2typst.js
2
-
3
- ## Basic usage
4
-
5
- ```javascript
6
- import { tex2typst, typst2tex } from 'tex2typst';
7
-
8
- let tex = "e \\overset{\\text{def}}{=} \\lim_{{n \\to \\infty}} \left(1 + \\frac{1}{n}\\right)^n";
9
- let typst = tex2typst(tex);
10
- console.log(typst);
11
- // e eq.def lim_(n -> infinity)(1 + 1/n)^n
12
-
13
- let tex_recovered = typst2tex(typst);
14
- console.log(tex_recovered);
15
- // e \overset{\text{def}}{=} \lim_{n \rightarrow \infty} \left(1 + \frac{1}{n} \right)^n
16
- ```
17
-
18
- ## Advanced options
19
-
20
- `tex2typst` function accepts an optional second argument, which is an object containing options to customize the conversion.
21
-
22
- ```typescript
23
- interface Tex2TypstOptions {
24
- preferShorthands?: boolean;
25
- fracToSlash?: boolean;
26
- inftyToOo?: boolean;
27
- }
28
- ```
29
-
30
- - `preferShorthands`: If set to `true`, the function will prefer using shorthands in Typst (e.g., `->` instead of `arrow.r`, `<<` instead of `lt.double`) when converting TeX to Typst. Default is `ture`.
31
-
32
- ```javascript
33
- let tex = "a \\rightarrow b \\ll c";
34
- let typst1 = tex2typst(tex, { preferShorthands: false });
35
- console.log(typst1);
36
- // a arrow.r b lt.double c
37
- let typst2 = tex2typst(tex, { preferShorthands: true });
38
- console.log(typst2);
39
- // a -> b << c
40
- ```
41
-
42
- - `fracToSlash`: If set to `true`, the Typst result will use the slash notation for fractions. Default is `true`.
43
-
44
- ```javascript
45
- let tex = "\\frac{a}{b}";
46
- let tpyst1 = tex2typst(tex, { fracToSlash: false });
47
- console.log(typst1);
48
- // frac(a, b)
49
- let typst2 = tex2typst(tex, { fracToSlash: true });
50
- console.log(typst2);
51
- // a / b
52
- ```
53
-
54
- - `inftyToOo`: If set to `true`, `\infty` converts to `oo` instead of `infinity`. Default is `false`.
55
-
56
- ```javascript
57
- let tex = "\\infty";
58
- let typst1 = tex2typst(tex, { inftyToOo: false });
59
- console.log(typst1);
60
- // infinity
61
- let typst2 = tex2typst(tex, { inftyToOo: true });
62
- console.log(typst2);
63
- // oo
64
- ```