tex2typst 0.3.1 → 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.
package/src/convert.ts CHANGED
@@ -1,7 +1,18 @@
1
- import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue } 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)) {
@@ -260,22 +271,8 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
260
271
  delim = '|';
261
272
  break;
262
273
  case 'Vmatrix': {
263
- // mat(delim: "||") does not compile in Typst.
264
- // For a workaround, translate
265
- // \begin{Vmatrix}
266
- // a & b \\
267
- // c & d
268
- // \end{Vmatrix}
269
- // to
270
- // lr(||mat(delim: #none, a, b; c, d)||)
271
- const matrix = new TypstNode('matrix', '', [], data);
272
- matrix.setOptions({ 'delim': TYPST_NONE });
273
- const group = new TypstNode('group', '', [
274
- new TypstNode('symbol', '||'),
275
- matrix,
276
- new TypstNode('symbol', '||'),
277
- ]);
278
- return new TypstNode('funcCall', 'lr', [ group ]);
274
+ delim = new TypstToken(TypstTokenType.SYMBOL, 'bar.v.double');
275
+ break;
279
276
  }
280
277
  default:
281
278
  throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { parseTex } from "./tex-parser";
2
2
  import type { Tex2TypstOptions } from "./types";
3
- import { TypstWriter } from "./typst-writer";
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/types.ts CHANGED
@@ -352,7 +352,7 @@ 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 TypstPrimitiveValue = string | boolean | null;
355
+ export type TypstPrimitiveValue = string | boolean | null | TypstToken;
356
356
  export type TypstNamedParams = { [key: string]: TypstPrimitiveValue };
357
357
 
358
358
  // #none
@@ -389,8 +389,10 @@ export class TypstNode {
389
389
  export interface Tex2TypstOptions {
390
390
  nonStrict?: boolean; // default is true
391
391
  preferTypstIntrinsic?: boolean; // default is true,
392
+ preferShorthands?: boolean; // default is true
392
393
  keepSpaces?: boolean; // default is false
393
394
  fracToSlash?: boolean; // default is true
395
+ inftyToOo?: boolean; // default is false
394
396
  customTexMacros?: { [key: string]: string };
395
397
  // TODO: custom typst functions
396
398
  }
@@ -2,6 +2,13 @@
2
2
  import { array_find } from "./generic";
3
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])) {
@@ -0,0 +1,51 @@
1
+ const shorthandMap = new Map<string, string>([
2
+ ['arrow.l.r.double.long', '<==>'],
3
+ ['arrow.l.r.long', '<-->'],
4
+ ['arrow.r.bar', '|->'],
5
+ ['arrow.r.double.bar', '|=>'],
6
+ ['arrow.r.double.long', '==>'],
7
+ ['arrow.r.long', '-->'],
8
+ ['arrow.r.long.squiggly', '~~>'],
9
+ ['arrow.r.tail', '>->'],
10
+ ['arrow.r.twohead', '->>'],
11
+ ['arrow.l.double.long', '<=='],
12
+ ['arrow.l.long', '<--'],
13
+ ['arrow.l.long.squiggly', '<~~'],
14
+ ['arrow.l.tail', '<-<'],
15
+ ['arrow.l.twohead', '<<-'],
16
+ ['arrow.l.r', '<->'],
17
+ ['arrow.l.r.double', '<=>'],
18
+ ['colon.double.eq', '::='],
19
+ ['dots.h', '...'],
20
+ ['gt.triple', '>>>'],
21
+ ['lt.triple', '<<<'],
22
+ ['arrow.r', '->'],
23
+ ['arrow.r.double', '=>'],
24
+ ['arrow.r.squiggly', '~>'],
25
+ ['arrow.l', '<-'],
26
+ ['arrow.l.squiggly', '<~'],
27
+ ['bar.v.double', '||'],
28
+ ['bracket.l.double', '[|'],
29
+ ['bracket.r.double', '|]'],
30
+ ['colon.eq', ':='],
31
+ ['eq.colon', '=:'],
32
+ ['eq.not', '!='],
33
+ ['gt.double', '>>'],
34
+ ['gt.eq', '>='],
35
+ ['lt.double', '<<'],
36
+ ['lt.eq', '<='],
37
+ ['ast.op', '*'],
38
+ ['minus', '-'],
39
+ ['tilde.op', '~'],
40
+ ]);
41
+
42
+
43
+ const reverseShorthandMap = new Map<string, string>();
44
+ for (const [key, value] of shorthandMap.entries()) {
45
+ // filter out single character values ('-', '~', '*')
46
+ if(value.length > 1) {
47
+ reverseShorthandMap.set(value, key);
48
+ }
49
+ }
50
+
51
+ export { shorthandMap, reverseShorthandMap };
@@ -1,24 +1,10 @@
1
1
  import { TexNode, TypstNode, TypstPrimitiveValue, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
2
-
3
-
4
- // symbols that are supported by Typst but not by KaTeX
5
- export const TYPST_INTRINSIC_SYMBOLS = [
6
- 'dim',
7
- 'id',
8
- 'im',
9
- 'mod',
10
- 'Pr',
11
- 'sech',
12
- 'csch',
13
- // 'sgn
14
- ];
15
-
2
+ import { shorthandMap } from "./typst-shorthands";
16
3
 
17
4
  function is_delimiter(c: TypstNode): boolean {
18
5
  return c.type === 'atom' && ['(', ')', '[', ']', '{', '}', '|', '⌊', '⌋', '⌈', '⌉'].includes(c.content);
19
6
  }
20
7
 
21
-
22
8
  const TYPST_LEFT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '(');
23
9
  const TYPST_RIGHT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ')');
24
10
  const TYPST_COMMA: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ',');
@@ -35,6 +21,8 @@ function typst_primitive_to_string(value: TypstPrimitiveValue) {
35
21
  default:
36
22
  if (value === null) {
37
23
  return '#none';
24
+ } else if (value instanceof TypstToken) {
25
+ return value.toString();
38
26
  }
39
27
  throw new TypstWriterError(`Invalid primitive value: ${value}`, value);
40
28
  }
@@ -50,20 +38,29 @@ export class TypstWriterError extends Error {
50
38
  }
51
39
  }
52
40
 
41
+ export interface TypstWriterOptions {
42
+ nonStrict: boolean;
43
+ preferShorthands: boolean;
44
+ keepSpaces: boolean;
45
+ inftyToOo: boolean;
46
+ }
47
+
53
48
  export class TypstWriter {
54
49
  private nonStrict: boolean;
55
- private preferTypstIntrinsic: boolean;
50
+ private preferShorthands: boolean;
56
51
  private keepSpaces: boolean;
52
+ private inftyToOo: boolean;
57
53
 
58
54
  protected buffer: string = "";
59
55
  protected queue: TypstToken[] = [];
60
56
 
61
57
  private insideFunctionDepth = 0;
62
58
 
63
- constructor(nonStrict: boolean, preferTypstIntrinsic: boolean, keepSpaces: boolean) {
64
- this.nonStrict = nonStrict;
65
- this.preferTypstIntrinsic = preferTypstIntrinsic;
66
- this.keepSpaces = keepSpaces;
59
+ constructor(opt: TypstWriterOptions) {
60
+ this.nonStrict = opt.nonStrict;
61
+ this.preferShorthands = opt.preferShorthands;
62
+ this.keepSpaces = opt.keepSpaces;
63
+ this.inftyToOo = opt.inftyToOo;
67
64
  }
68
65
 
69
66
 
@@ -123,9 +120,19 @@ export class TypstWriter {
123
120
  }
124
121
  break;
125
122
  }
126
- case 'symbol':
127
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, node.content));
123
+ case 'symbol': {
124
+ let content = node.content;
125
+ if(this.preferShorthands) {
126
+ if (shorthandMap.has(content)) {
127
+ content = shorthandMap.get(content)!;
128
+ }
129
+ }
130
+ if (this.inftyToOo && content === 'infinity') {
131
+ content = 'oo';
132
+ }
133
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, content));
128
134
  break;
135
+ }
129
136
  case 'text':
130
137
  this.queue.push(new TypstToken(TypstTokenType.TEXT, node.content));
131
138
  break;
@@ -0,0 +1,33 @@
1
+ import urllib.request
2
+ import html
3
+ from bs4 import BeautifulSoup
4
+
5
+
6
+ if __name__ == '__main__':
7
+ shorthand_map = []
8
+
9
+
10
+ url = "https://typst.app/docs/reference/symbols/"
11
+ with urllib.request.urlopen(url) as response:
12
+ html_text = response.read().decode('utf-8')
13
+
14
+ soup = BeautifulSoup(html_text, 'html.parser')
15
+
16
+ # <ul class="symbol-grid">
17
+ ul_list = soup.find_all('ul', class_='symbol-grid')
18
+ # ul_shorthands_markup = ul_list[0]
19
+ ul_shorthands_math = ul_list[1]
20
+
21
+ li_list = ul_shorthands_math.find_all('li')
22
+ for li in li_list:
23
+ # e.g. <li id="symbol-arrow.r" data-math-shorthand="-&gt;"><button>...</button></li>
24
+ # ==> typst = "arrow.r"
25
+ # ==> shorthand = "->"
26
+ typst = li['id'][7:]
27
+ shorthand = html.unescape(li['data-math-shorthand'])
28
+ shorthand_map.append((typst, shorthand))
29
+
30
+ # Sort by length of shorthand, order from longest to shortest
31
+ shorthand_map.sort(key=lambda x: len(x[1]), reverse=True)
32
+ for typst, shorthand in shorthand_map:
33
+ print(f"['{typst}', '{shorthand}'],")
@@ -1,12 +1,13 @@
1
- import requests
1
+ import urllib.request
2
2
  from bs4 import BeautifulSoup
3
3
 
4
-
5
4
  if __name__ == '__main__':
6
5
  symbol_map = {}
7
6
 
8
7
  url = "https://typst.app/docs/reference/symbols/sym/"
9
- html_text = requests.get(url).text
8
+ with urllib.request.urlopen(url) as response:
9
+ html_text = response.read().decode('utf-8')
10
+
10
11
  soup = BeautifulSoup(html_text, 'html.parser')
11
12
  # <ul class="symbol-grid">
12
13
  ul = soup.find('ul', class_='symbol-grid')