tex2typst 0.3.0 → 0.3.2

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.
@@ -1,17 +1,23 @@
1
1
  import { TexNode, TypstNode, TypstToken } from "./types";
2
- export declare const TYPST_INTRINSIC_SYMBOLS: string[];
3
2
  export declare class TypstWriterError extends Error {
4
3
  node: TexNode | TypstNode | TypstToken;
5
4
  constructor(message: string, node: TexNode | TypstNode | TypstToken);
6
5
  }
6
+ export interface TypstWriterOptions {
7
+ nonStrict: boolean;
8
+ preferShorthands: boolean;
9
+ keepSpaces: boolean;
10
+ inftyToOo: boolean;
11
+ }
7
12
  export declare class TypstWriter {
8
13
  private nonStrict;
9
- private preferTypstIntrinsic;
14
+ private preferShorthands;
10
15
  private keepSpaces;
16
+ private inftyToOo;
11
17
  protected buffer: string;
12
18
  protected queue: TypstToken[];
13
19
  private insideFunctionDepth;
14
- constructor(nonStrict: boolean, preferTypstIntrinsic: boolean, keepSpaces: boolean);
20
+ constructor(opt: TypstWriterOptions);
15
21
  private writeBuffer;
16
22
  serialize(node: TypstNode): void;
17
23
  private appendWithBracketsIfNeeded;
@@ -0,0 +1,64 @@
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 arrow.r 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
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tex2typst",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "JavaScript library for converting TeX code to Typst",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,8 +15,8 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "prebuild": "rimraf dist/",
18
- "build:node": "bun build --entrypoints src/index.ts --outdir ./dist --target node",
19
- "build:browser": "bun build --entrypoints src/tex2typst.ts --outdir ./dist --target browser --entry-naming [dir]/[name].min.[ext] --minify",
18
+ "build:node": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --format=esm",
19
+ "build:browser": "esbuild src/tex2typst.ts --bundle --platform=browser --outfile=dist/tex2typst.min.js --minify",
20
20
  "build:types": "tsc --project ./tsconfig.json",
21
21
  "build": "npm run build:node && npm run build:browser && npm run build:types",
22
22
  "test": "vitest run",
@@ -27,9 +27,9 @@
27
27
  "js-yaml": "^4.1.0",
28
28
  "rimraf": "^3.0.2",
29
29
  "toml": "^3.0.0",
30
- "ts-node": "^10.9.2",
31
30
  "typescript": "^5.5.3",
32
- "vitest": "^2.0.2"
31
+ "vitest": "^2.0.2",
32
+ "esbuild": "^0.25.1"
33
33
  },
34
34
  "dependencies": {}
35
35
  }
package/src/convert.ts CHANGED
@@ -1,7 +1,18 @@
1
- import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions } from "./types";
2
- import { TypstWriterError, TYPST_INTRINSIC_SYMBOLS } from "./typst-writer";
1
+ import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType } from "./types";
2
+ import { TypstWriterError } from "./typst-writer";
3
3
  import { symbolMap, reverseSymbolMap } from "./map";
4
4
 
5
+ // symbols that are supported by Typst but not by KaTeX
6
+ const TYPST_INTRINSIC_SYMBOLS = [
7
+ 'dim',
8
+ 'id',
9
+ 'im',
10
+ 'mod',
11
+ 'Pr',
12
+ 'sech',
13
+ 'csch',
14
+ // 'sgn
15
+ ];
5
16
 
6
17
  function tex_token_to_typst(token: string): string {
7
18
  if (/^[a-zA-Z0-9]$/.test(token)) {
@@ -60,7 +71,7 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
60
71
  'op',
61
72
  [convert_tex_node_to_typst(base, options)]
62
73
  );
63
- op_call.setOptions({ limits: '#true' });
74
+ op_call.setOptions({ limits: TYPST_TRUE });
64
75
  return new TypstNode(
65
76
  'supsub',
66
77
  '',
@@ -240,11 +251,37 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
240
251
  if (node.content!.startsWith('align')) {
241
252
  // align, align*, alignat, alignat*, aligned, etc.
242
253
  return new TypstNode('align', '', [], data);
243
- } else {
254
+ }
255
+ if (node.content!.endsWith('matrix')) {
256
+ let delim: TypstPrimitiveValue = null;
257
+ switch (node.content) {
258
+ case 'matrix':
259
+ delim = TYPST_NONE;
260
+ break;
261
+ case 'pmatrix':
262
+ delim = '(';
263
+ break;
264
+ case 'bmatrix':
265
+ delim = '[';
266
+ break;
267
+ case 'Bmatrix':
268
+ delim = '{';
269
+ break;
270
+ case 'vmatrix':
271
+ delim = '|';
272
+ break;
273
+ case 'Vmatrix': {
274
+ delim = new TypstToken(TypstTokenType.SYMBOL, 'bar.v.double');
275
+ break;
276
+ }
277
+ default:
278
+ throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
279
+ }
244
280
  const res = new TypstNode('matrix', '', [], data);
245
- res.setOptions({ 'delim': '#none' });
281
+ res.setOptions({ 'delim': delim });
246
282
  return res;
247
283
  }
284
+ throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
248
285
  }
249
286
  case 'unknownMacro':
250
287
  return new TypstNode('unknown', tex_token_to_typst(node.content));
@@ -416,7 +453,7 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
416
453
  if (node.options) {
417
454
  if ('delim' in node.options) {
418
455
  switch (node.options.delim) {
419
- case '#none':
456
+ case TYPST_NONE:
420
457
  return matrix;
421
458
  case '[':
422
459
  left_delim = "\\left[";
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { parseTex } from "./tex-parser";
2
- import { Tex2TypstOptions } from "./types";
3
- import { TypstWriter } from "./typst-writer";
2
+ import type { Tex2TypstOptions } from "./types";
3
+ import { TypstWriter, type TypstWriterOptions } from "./typst-writer";
4
4
  import { convert_tex_node_to_typst, convert_typst_node_to_tex } from "./convert";
5
5
  import { symbolMap } from "./map";
6
6
  import { parseTypst } from "./typst-parser";
@@ -11,27 +11,24 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
11
11
  const opt: Tex2TypstOptions = {
12
12
  nonStrict: true,
13
13
  preferTypstIntrinsic: true,
14
+ preferShorthands: true,
14
15
  keepSpaces: false,
15
16
  fracToSlash: true,
17
+ inftyToOo: false,
16
18
  customTexMacros: {}
17
19
  };
18
- if (options) {
19
- if (options.nonStrict) {
20
- opt.nonStrict = options.nonStrict;
21
- }
22
- if (options.preferTypstIntrinsic) {
23
- opt.preferTypstIntrinsic = options.preferTypstIntrinsic;
24
- }
25
- if (options.customTexMacros) {
26
- opt.customTexMacros = options.customTexMacros;
27
- }
28
- if (options.fracToSlash !== undefined) {
29
- opt.fracToSlash = options.fracToSlash;
20
+
21
+ if(options !== undefined) {
22
+ for (const key in opt) {
23
+ if (options[key] !== undefined) {
24
+ opt[key] = options[key];
25
+ }
30
26
  }
31
27
  }
28
+
32
29
  const texTree = parseTex(tex, opt.customTexMacros!);
33
30
  const typstTree = convert_tex_node_to_typst(texTree, opt);
34
- const writer = new TypstWriter(opt.nonStrict!, opt.preferTypstIntrinsic!, opt.keepSpaces!);
31
+ const writer = new TypstWriter(opt as TypstWriterOptions);
35
32
  writer.serialize(typstTree);
36
33
  return writer.finalize();
37
34
  }
package/src/map.ts CHANGED
@@ -1,4 +1,16 @@
1
1
  const symbolMap = new Map<string, string>([
2
+ ['cos', 'cos'],
3
+ ['sin', 'sin'],
4
+ ['tan', 'tan'],
5
+ ['cot', 'cot'],
6
+ ['sec', 'sec'],
7
+ ['csc', 'csc'],
8
+ ['mod', 'mod'],
9
+ ['omicron', 'omicron'],
10
+ ['Xi', 'Xi'],
11
+ ['Upsilon', 'Upsilon'],
12
+ ['lim', 'lim'],
13
+
2
14
  ['nonumber', ''],
3
15
  ['vec', 'arrow'],
4
16
  ['neq', 'eq.not'],
@@ -59,37 +71,11 @@ const symbolMap = new Map<string, string>([
59
71
 
60
72
  /* arrows */
61
73
  ['gets', 'arrow.l'],
62
- ['hookleftarrow', 'arrow.l.hook'],
63
- ['leftharpoonup', 'harpoon.lt'],
64
- ['leftharpoondown', 'harpoon.lb'],
65
- ['rightleftharpoons', 'harpoons.rtlb'],
66
- ['longleftarrow', 'arrow.l.long'],
67
- ['longrightarrow', 'arrow.r.long'],
68
- ['longleftrightarrow', 'arrow.l.r.long'],
69
- ['Longleftarrow', 'arrow.l.double.long'],
70
- ['Longrightarrow', 'arrow.r.double.long'],
71
- ['Longleftrightarrow', 'arrow.l.r.double.long'],
72
74
  // ['longmapsto', 'arrow.r.bar'],
73
- ['hookrightarrow', 'arrow.r.hook'],
74
- ['rightharpoonup', 'harpoon.rt'],
75
- ['rightharpoondown', 'harpoon.rb'],
76
75
  ['iff', 'arrow.l.r.double.long'],
77
76
  ['implies', 'arrow.r.double.long'],
78
- ['uparrow', 'arrow.t'],
79
- ['downarrow', 'arrow.b'],
80
- ['updownarrow', 'arrow.t.b'],
81
- ['Uparrow', 'arrow.t.double'],
82
- ['Downarrow', 'arrow.b.double'],
83
- ['Updownarrow', 'arrow.t.b.double'],
84
- ['nearrow', 'arrow.tr'],
85
- ['searrow', 'arrow.br'],
86
- ['swarrow', 'arrow.bl'],
87
- ['nwarrow', 'arrow.tl'],
88
77
  ['leadsto', 'arrow.squiggly'],
89
78
 
90
- ['leftleftarrows', 'arrows.ll'],
91
- ['rightrightarrows', 'arrows.rr'],
92
-
93
79
 
94
80
  ['Cap', 'sect.double'],
95
81
  ['Cup', 'union.double'],
@@ -97,9 +83,6 @@ const symbolMap = new Map<string, string>([
97
83
  ['Gamma', 'Gamma'],
98
84
  ['Join', 'join'],
99
85
  ['Lambda', 'Lambda'],
100
- ['Leftarrow', 'arrow.l.double'],
101
- ['Leftrightarrow', 'arrow.l.r.double'],
102
- ['Longrightarrow', 'arrow.r.double.long'],
103
86
  ['Omega', 'Omega'],
104
87
  ['P', 'pilcrow'],
105
88
  ['Phi', 'Phi'],
@@ -153,7 +136,6 @@ const symbolMap = new Map<string, string>([
153
136
  ['div', 'div'],
154
137
  ['divideontimes', 'times.div'],
155
138
  ['dotplus', 'plus.dot'],
156
- ['downarrow', 'arrow.b'],
157
139
  ['ell', 'ell'],
158
140
  ['emptyset', 'nothing'],
159
141
  ['epsilon', 'epsilon.alt'],
@@ -186,8 +168,6 @@ const symbolMap = new Map<string, string>([
186
168
  ['lbrack', 'bracket.l'],
187
169
  ['ldots', 'dots.h'],
188
170
  ['le', 'lt.eq'],
189
- ['leadsto', 'arrow.squiggly'],
190
- ['leftarrow', 'arrow.l'],
191
171
  ['leftthreetimes', 'times.three.l'],
192
172
  ['leftrightarrow', 'arrow.l.r'],
193
173
  ['leq', 'lt.eq'],
@@ -224,7 +204,6 @@ const symbolMap = new Map<string, string>([
224
204
  ['nu', 'nu'],
225
205
  ['ntriangleleft', 'lt.tri.not'],
226
206
  ['ntriangleright', 'gt.tri.not'],
227
- ['nwarrow', 'arrow.tl'],
228
207
  ['odot', 'dot.circle'],
229
208
  ['oint', 'integral.cont'],
230
209
  ['oiint', 'integral.surf'],
@@ -277,7 +256,6 @@ const symbolMap = new Map<string, string>([
277
256
  ['supset', 'supset'],
278
257
  ['supseteq', 'supset.eq'],
279
258
  ['supsetneq', 'supset.neq'],
280
- ['swarrow', 'arrow.bl'],
281
259
  ['tau', 'tau'],
282
260
  ['theta', 'theta'],
283
261
  ['times', 'times'],
@@ -288,8 +266,6 @@ const symbolMap = new Map<string, string>([
288
266
  // ['triangleleft', 'triangle.l.small'],
289
267
  // ['triangleright', 'triangle.r.small'],
290
268
  ['twoheadrightarrow', 'arrow.r.twohead'],
291
- ['uparrow', 'arrow.t'],
292
- ['updownarrow', 'arrow.t.b'],
293
269
  ['upharpoonright', 'harpoon.tr'],
294
270
  ['uplus', 'union.plus'],
295
271
  ['upsilon', 'upsilon'],
@@ -1086,6 +1062,7 @@ for(const [key, value] of Array.from(symbolMap.entries()).reverse()) {
1086
1062
  reverseSymbolMap.set(value, key);
1087
1063
  }
1088
1064
  reverseSymbolMap.set('dif', 'mathrm{d}');
1065
+ reverseSymbolMap.set('oo', 'infty');
1089
1066
 
1090
1067
  // force override some one-to-multiple mappings
1091
1068
  const typst_to_tex_map = new Map<string, string>([
package/src/tex-parser.ts CHANGED
@@ -296,46 +296,39 @@ export class LatexParser {
296
296
  }
297
297
 
298
298
  parse(tokens: TexToken[]): TexNode {
299
+ const [tree, _] = this.parseGroup(tokens, 0, tokens.length);
300
+ return tree;
301
+ }
302
+
303
+ parseGroup(tokens: TexToken[], start: number, end: number): ParseResult {
299
304
  const results: TexNode[] = [];
300
- let pos = 0;
301
- while (pos < tokens.length) {
302
- const results: TexNode[] = [];
303
- let pos = 0;
304
-
305
- while (pos < tokens.length) {
306
- const [res, newPos] = this.parseNextExpr(tokens, pos);
307
- pos = newPos;
308
- if(res.type === 'whitespace') {
309
- if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
310
- continue;
311
- }
312
- if (!this.newline_sensitive && res.content === '\n') {
313
- continue;
314
- }
305
+ let pos = start;
306
+ while (pos < end) {
307
+ const [res, newPos] = this.parseNextExpr(tokens, pos);
308
+ pos = newPos;
309
+ if(res.type === 'whitespace') {
310
+ if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
311
+ continue;
315
312
  }
316
- if (res.type === 'control' && res.content === '&') {
317
- throw new LatexParserError('Unexpected & outside of an alignment');
313
+ if (!this.newline_sensitive && res.content === '\n') {
314
+ continue;
318
315
  }
319
- results.push(res);
320
316
  }
321
-
322
- if (results.length === 0) {
323
- return EMPTY_NODE;
324
- } else if (results.length === 1) {
325
- return results[0];
326
- } else {
327
- return new TexNode('ordgroup', '', results);
317
+ if (res.type === 'control' && res.content === '&') {
318
+ throw new LatexParserError('Unexpected & outside of an alignment');
328
319
  }
320
+ results.push(res);
329
321
  }
330
322
 
331
-
323
+ let node: TexNode;
332
324
  if (results.length === 0) {
333
- return EMPTY_NODE;
325
+ node = EMPTY_NODE;
334
326
  } else if (results.length === 1) {
335
- return results[0];
327
+ node = results[0];
336
328
  } else {
337
- return new TexNode('ordgroup', '', results);
329
+ node = new TexNode('ordgroup', '', results);
338
330
  }
331
+ return [node, end + 1];
339
332
  }
340
333
 
341
334
  parseNextExpr(tokens: TexToken[], start: number): ParseResult {
@@ -396,8 +389,7 @@ export class LatexParser {
396
389
 
397
390
  parseNextExprWithoutSupSub(tokens: TexToken[], start: number): ParseResult {
398
391
  const firstToken = tokens[start];
399
- const tokenType = firstToken.type;
400
- switch (tokenType) {
392
+ switch (firstToken.type) {
401
393
  case TexTokenType.ELEMENT:
402
394
  return [new TexNode('element', firstToken.value), start + 1];
403
395
  case TexTokenType.TEXT:
@@ -423,8 +415,7 @@ export class LatexParser {
423
415
  if(posClosingBracket === -1) {
424
416
  throw new LatexParserError("Unmatched '{'");
425
417
  }
426
- const exprInside = tokens.slice(start + 1, posClosingBracket);
427
- return [this.parse(exprInside), posClosingBracket + 1];
418
+ return this.parseGroup(tokens, start + 1, posClosingBracket);
428
419
  case '}':
429
420
  throw new LatexParserError("Unmatched '}'");
430
421
  case '\\\\':
@@ -453,7 +444,7 @@ export class LatexParser {
453
444
 
454
445
  if (['left', 'right', 'begin', 'end'].includes(command.slice(1))) {
455
446
  throw new LatexParserError('Unexpected command: ' + command);
456
- }
447
+ }
457
448
 
458
449
 
459
450
  const paramNum = get_command_param_num(command.slice(1));
@@ -475,8 +466,7 @@ export class LatexParser {
475
466
  if (posRightSquareBracket === -1) {
476
467
  throw new LatexParserError('No matching right square bracket for [');
477
468
  }
478
- const exprInside = tokens.slice(posLeftSquareBracket + 1, posRightSquareBracket);
479
- const exponent = this.parse(exprInside);
469
+ const [exponent, _] = this.parseGroup(tokens, posLeftSquareBracket + 1, posRightSquareBracket);
480
470
  const [arg1, newPos] = this.parseNextExprWithoutSupSub(tokens, posRightSquareBracket + 1);
481
471
  return [new TexNode('unaryFunc', command, [arg1], exponent), newPos];
482
472
  } else if (command === '\\text') {
@@ -529,15 +519,14 @@ export class LatexParser {
529
519
  if (pos >= tokens.length) {
530
520
  throw new LatexParserError('Expecting \\right after \\left');
531
521
  }
532
-
522
+
533
523
  const rightDelimiter = eat_parenthesis(tokens, pos);
534
524
  if (rightDelimiter === null) {
535
525
  throw new LatexParserError('Invalid delimiter after \\right');
536
526
  }
537
527
  pos++;
538
528
 
539
- const exprInside = tokens.slice(exprInsideStart, exprInsideEnd);
540
- const body = this.parse(exprInside);
529
+ const [body, _] = this.parseGroup(tokens, exprInsideStart, exprInsideEnd);
541
530
  const args: TexNode[] = [
542
531
  new TexNode('element', leftDelimiter.value),
543
532
  body,
@@ -556,9 +545,9 @@ export class LatexParser {
556
545
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
557
546
  const envName = tokens[pos + 1].value;
558
547
  pos += 3;
559
-
548
+
560
549
  pos += eat_whitespaces(tokens, pos).length; // ignore whitespaces and '\n' after \begin{envName}
561
-
550
+
562
551
  const exprInsideStart = pos;
563
552
 
564
553
  const endIdx = find_closing_end_command(tokens, start);
@@ -567,7 +556,7 @@ export class LatexParser {
567
556
  }
568
557
  const exprInsideEnd = endIdx;
569
558
  pos = endIdx + 1;
570
-
559
+
571
560
  assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
572
561
  assert(tokens[pos + 1].type === TexTokenType.TEXT);
573
562
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
@@ -575,7 +564,7 @@ export class LatexParser {
575
564
  throw new LatexParserError('Mismatched \\begin and \\end environments');
576
565
  }
577
566
  pos += 3;
578
-
567
+
579
568
  const exprInside = tokens.slice(exprInsideStart, exprInsideEnd);
580
569
  // ignore spaces and '\n' before \end{envName}
581
570
  while(exprInside.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(exprInside[exprInside.length - 1].type)) {
@@ -606,7 +595,7 @@ export class LatexParser {
606
595
  continue;
607
596
  }
608
597
  }
609
-
598
+
610
599
  if (res.type === 'control' && res.content === '\\\\') {
611
600
  row = [];
612
601
  group = new TexNode('ordgroup', '', []);
package/src/tex-writer.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { array_includes, array_split } from "./generic";
2
- import { reverseSymbolMap } from "./map";
3
2
  import { TexNode, TexToken, TexSupsubData, TexTokenType } from "./types";
4
3
 
5
4
 
package/src/types.ts CHANGED
@@ -352,7 +352,13 @@ export type TypstArrayData = TypstNode[][];
352
352
  type TypstNodeType = 'atom' | 'symbol' | 'text' | 'control' | 'comment' | 'whitespace'
353
353
  | 'empty' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'align' | 'matrix' | 'unknown';
354
354
 
355
- export type TypstNamedParams = { [key: string]: string };
355
+ export type TypstPrimitiveValue = string | boolean | null | TypstToken;
356
+ export type TypstNamedParams = { [key: string]: TypstPrimitiveValue };
357
+
358
+ // #none
359
+ export const TYPST_NONE: TypstPrimitiveValue = null;
360
+ export const TYPST_TRUE: TypstPrimitiveValue = true;
361
+ export const TYPST_FALSE: TypstPrimitiveValue = false;
356
362
 
357
363
  export class TypstNode {
358
364
  type: TypstNodeType;
@@ -370,7 +376,7 @@ export class TypstNode {
370
376
  this.data = data;
371
377
  }
372
378
 
373
- public setOptions(options: { [key: string]: string }) {
379
+ public setOptions(options: TypstNamedParams) {
374
380
  this.options = options;
375
381
  }
376
382
 
@@ -383,8 +389,10 @@ export class TypstNode {
383
389
  export interface Tex2TypstOptions {
384
390
  nonStrict?: boolean; // default is true
385
391
  preferTypstIntrinsic?: boolean; // default is true,
392
+ preferShorthands?: boolean; // default is true
386
393
  keepSpaces?: boolean; // default is false
387
394
  fracToSlash?: boolean; // default is true
395
+ inftyToOo?: boolean; // default is false
388
396
  customTexMacros?: { [key: string]: string };
389
397
  // TODO: custom typst functions
390
398
  }
@@ -1,7 +1,14 @@
1
1
 
2
2
  import { array_find } from "./generic";
3
- import { TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
3
+ import { TYPST_NONE, TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
4
4
  import { assert, isalpha, isdigit } from "./util";
5
+ import { reverseShorthandMap } from "./typst-shorthands";
6
+
7
+
8
+
9
+ const TYPST_EMPTY_NODE = new TypstNode('empty', '');
10
+
11
+ const TYPST_SHORTHANDS = Array.from(reverseShorthandMap.keys());
5
12
 
6
13
  // TODO: In Typst, y' ' is not the same as y''.
7
14
  // The parser should be able to parse the former correctly.
@@ -22,8 +29,14 @@ function eat_identifier_name(typst: string, start: number): string {
22
29
  return typst.substring(start, pos);
23
30
  }
24
31
 
25
-
26
- const TYPST_EMPTY_NODE = new TypstNode('empty', '');
32
+ function try_eat_shorthand(typst: string, start: number): string | null {
33
+ for (const shorthand of TYPST_SHORTHANDS) {
34
+ if (typst.startsWith(shorthand, start)) {
35
+ return shorthand;
36
+ }
37
+ }
38
+ return null;
39
+ }
27
40
 
28
41
 
29
42
  export function tokenize_typst(typst: string): TypstToken[] {
@@ -115,6 +128,13 @@ export function tokenize_typst(typst: string): TypstToken[] {
115
128
  break;
116
129
  }
117
130
  default: {
131
+ const shorthand = try_eat_shorthand(typst, pos);
132
+ if (shorthand !== null) {
133
+ token = new TypstToken(TypstTokenType.SYMBOL, reverseShorthandMap.get(shorthand)!);
134
+ pos += shorthand.length;
135
+ break;
136
+ }
137
+
118
138
  if (isdigit(firstChar)) {
119
139
  let newPos = pos;
120
140
  while (newPos < typst.length && isdigit(typst[newPos])) {
@@ -450,7 +470,6 @@ export class TypstParser {
450
470
  // start: the position of the left parentheses
451
471
  parseArguments(tokens: TypstToken[], start: number): [TypstNode[], number] {
452
472
  const end = find_closing_match(tokens, start);
453
-
454
473
  return [this.parseCommaSeparatedArguments(tokens, start + 1, end), end + 1];
455
474
  }
456
475
 
@@ -501,7 +520,7 @@ export class TypstParser {
501
520
  if(g.args!.length !== 4 || !g.args![pos_colon + 2].eq(new TypstNode('symbol', 'none'))) {
502
521
  throw new TypstParserError('Invalid number of arguments for delim');
503
522
  }
504
- np['delim'] = "#none";
523
+ np['delim'] = TYPST_NONE;
505
524
  } else {
506
525
  throw new TypstParserError('Not implemented for other types of delim');
507
526
  }