tex2typst 0.3.17 → 0.3.19

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/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,20 @@ 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 be not working at present or 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
+ preferShorthands?: boolean; /** default is true */
401
+ keepSpaces?: boolean; /** default is false */
402
+ fracToSlash?: boolean; /** default is true */
403
+ inftyToOo?: boolean; /** default is false */
404
+ optimize?: boolean; /** default is true */
405
+ nonAsciiWrapper?: string; /** default is "" */
393
406
  customTexMacros?: { [key: string]: string };
394
407
  // TODO: custom typst functions
395
408
  }
@@ -1,11 +1,9 @@
1
1
 
2
2
  import { array_find } from "./generic";
3
3
  import { TYPST_NONE, TypstLrData, TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
4
+ import { tokenize_typst } from "./typst-tokenizer";
4
5
  import { assert, isalpha } from "./util";
5
- import { reverseShorthandMap } from "./typst-shorthands";
6
- import { JSLex, Scanner } from "./jslex";
7
6
 
8
- const TYPST_SHORTHANDS = Array.from(reverseShorthandMap.keys());
9
7
 
10
8
  // TODO: In Typst, y' ' is not the same as y''.
11
9
  // The parser should be able to parse the former correctly.
@@ -18,77 +16,6 @@ function eat_primes(tokens: TypstToken[], start: number): number {
18
16
  }
19
17
 
20
18
 
21
- function generate_regex_for_shorthands(): string {
22
- const regex_list = TYPST_SHORTHANDS.map((s) => {
23
- s = s.replaceAll('|', '\\|');
24
- s = s.replaceAll('.', '\\.');
25
- s = s.replaceAll('[', '\\[');
26
- s = s.replaceAll(']', '\\]');
27
- return s;
28
- });
29
- return `(${regex_list.join('|')})`;
30
- }
31
-
32
-
33
- const REGEX_SHORTHANDS = generate_regex_for_shorthands();
34
-
35
- const rules_map = new Map<string, (a: Scanner<TypstToken>) => TypstToken | TypstToken[]>([
36
- [String.raw`//[^\n]*`, (s) => new TypstToken(TypstTokenType.COMMENT, s.text()!.substring(2))],
37
- [String.raw`/`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
38
- [String.raw`[_^&]`, (s) => new TypstToken(TypstTokenType.CONTROL, s.text()!)],
39
- [String.raw`\r?\n`, (_s) => new TypstToken(TypstTokenType.NEWLINE, "\n")],
40
- [String.raw`\s+`, (s) => new TypstToken(TypstTokenType.SPACE, s.text()!)],
41
- [String.raw`\\[$&#_]`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
42
- [String.raw`\\\n`, (s) => {
43
- return [
44
- new TypstToken(TypstTokenType.CONTROL, "\\"),
45
- new TypstToken(TypstTokenType.NEWLINE, "\n"),
46
- ]
47
- }],
48
- [String.raw`\\\s`, (s) => {
49
- return [
50
- new TypstToken(TypstTokenType.CONTROL, "\\"),
51
- new TypstToken(TypstTokenType.SPACE, " "),
52
- ]
53
- }],
54
- // this backslash is dummy and will be ignored in later stages
55
- [String.raw`\\\S`, (_s) => new TypstToken(TypstTokenType.CONTROL, "")],
56
- [
57
- String.raw`"([^"]|(\\"))*"`,
58
- (s) => {
59
- const text = s.text()!.substring(1, s.text()!.length - 1);
60
- // replace all escape characters with their actual characters
61
- text.replaceAll('\\"', '"');
62
- return new TypstToken(TypstTokenType.TEXT, text);
63
- }
64
- ],
65
- [
66
- REGEX_SHORTHANDS,
67
- (s) => {
68
- const shorthand = s.text()!;
69
- const symbol = reverseShorthandMap.get(shorthand)!;
70
- return new TypstToken(TypstTokenType.SYMBOL, symbol);
71
- }
72
- ],
73
- [String.raw`[0-9]+(\.[0-9]+)?`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
74
- [String.raw`[+\-*/=\'<>!.,;?()\[\]|]`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
75
- [String.raw`[a-zA-Z\.]+`, (s) => {
76
- return new TypstToken(s.text()!.length === 1? TypstTokenType.ELEMENT: TypstTokenType.SYMBOL, s.text()!);
77
- }],
78
- [String.raw`#none`, (s) => new TypstToken(TypstTokenType.NONE, s.text()!)],
79
- [String.raw`.`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
80
- ]);
81
-
82
- const spec = {
83
- "start": rules_map
84
- };
85
-
86
- export function tokenize_typst(input: string): TypstToken[] {
87
- const lexer = new JSLex<TypstToken>(spec);
88
- return lexer.collect(input);
89
- }
90
-
91
-
92
19
  function _find_closing_match(tokens: TypstToken[], start: number,
93
20
  leftBrackets: TypstToken[], rightBrackets: TypstToken[]): number {
94
21
  assert(tokens[start].isOneOf(leftBrackets));
@@ -0,0 +1,76 @@
1
+ import { TypstToken, TypstTokenType } from "./types";
2
+ import { reverseShorthandMap } from "./typst-shorthands";
3
+ import { JSLex, Scanner } from "./jslex";
4
+
5
+ const TYPST_SHORTHANDS = Array.from(reverseShorthandMap.keys());
6
+
7
+
8
+ function generate_regex_for_shorthands(): string {
9
+ const regex_list = TYPST_SHORTHANDS.map((s) => {
10
+ s = s.replaceAll('|', '\\|');
11
+ s = s.replaceAll('.', '\\.');
12
+ s = s.replaceAll('[', '\\[');
13
+ s = s.replaceAll(']', '\\]');
14
+ return s;
15
+ });
16
+ return `(${regex_list.join('|')})`;
17
+ }
18
+
19
+
20
+ const REGEX_SHORTHANDS = generate_regex_for_shorthands();
21
+
22
+ const rules_map = new Map<string, (a: Scanner<TypstToken>) => TypstToken | TypstToken[]>([
23
+ [String.raw`//[^\n]*`, (s) => new TypstToken(TypstTokenType.COMMENT, s.text()!.substring(2))],
24
+ [String.raw`/`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
25
+ [String.raw`[_^&]`, (s) => new TypstToken(TypstTokenType.CONTROL, s.text()!)],
26
+ [String.raw`\r?\n`, (_s) => new TypstToken(TypstTokenType.NEWLINE, "\n")],
27
+ [String.raw`\s+`, (s) => new TypstToken(TypstTokenType.SPACE, s.text()!)],
28
+ [String.raw`\\[$&#_]`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
29
+ [String.raw`\\\n`, (s) => {
30
+ return [
31
+ new TypstToken(TypstTokenType.CONTROL, "\\"),
32
+ new TypstToken(TypstTokenType.NEWLINE, "\n"),
33
+ ]
34
+ }],
35
+ [String.raw`\\\s`, (s) => {
36
+ return [
37
+ new TypstToken(TypstTokenType.CONTROL, "\\"),
38
+ new TypstToken(TypstTokenType.SPACE, " "),
39
+ ]
40
+ }],
41
+ // this backslash is dummy and will be ignored in later stages
42
+ [String.raw`\\\S`, (_s) => new TypstToken(TypstTokenType.CONTROL, "")],
43
+ [
44
+ String.raw`"([^"]|(\\"))*"`,
45
+ (s) => {
46
+ const text = s.text()!.substring(1, s.text()!.length - 1);
47
+ // replace all escape characters with their actual characters
48
+ text.replaceAll('\\"', '"');
49
+ return new TypstToken(TypstTokenType.TEXT, text);
50
+ }
51
+ ],
52
+ [
53
+ REGEX_SHORTHANDS,
54
+ (s) => {
55
+ const shorthand = s.text()!;
56
+ const symbol = reverseShorthandMap.get(shorthand)!;
57
+ return new TypstToken(TypstTokenType.SYMBOL, symbol);
58
+ }
59
+ ],
60
+ [String.raw`[0-9]+(\.[0-9]+)?`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
61
+ [String.raw`[+\-*/=\'<>!.,;?()\[\]|]`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
62
+ [String.raw`[a-zA-Z\.]+`, (s) => {
63
+ return new TypstToken(s.text()!.length === 1? TypstTokenType.ELEMENT: TypstTokenType.SYMBOL, s.text()!);
64
+ }],
65
+ [String.raw`#none`, (s) => new TypstToken(TypstTokenType.NONE, s.text()!)],
66
+ [String.raw`.`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
67
+ ]);
68
+
69
+ const spec = {
70
+ "start": rules_map
71
+ };
72
+
73
+ export function tokenize_typst(input: string): TypstToken[] {
74
+ const lexer = new JSLex<TypstToken>(spec);
75
+ return lexer.collect(input);
76
+ }
@@ -11,6 +11,8 @@ const TYPST_RIGHT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMEN
11
11
  const TYPST_COMMA: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ',');
12
12
  const TYPST_NEWLINE: TypstToken = new TypstToken(TypstTokenType.SYMBOL, '\n');
13
13
 
14
+ const SOFT_SPACE = new TypstToken(TypstTokenType.CONTROL, ' ');
15
+
14
16
  function typst_primitive_to_string(value: TypstPrimitiveValue) {
15
17
  switch (typeof value) {
16
18
  case 'string':
@@ -40,6 +42,7 @@ export interface TypstWriterOptions {
40
42
  preferShorthands: boolean;
41
43
  keepSpaces: boolean;
42
44
  inftyToOo: boolean;
45
+ optimize: boolean;
43
46
  }
44
47
 
45
48
  export class TypstWriter {
@@ -47,17 +50,19 @@ export class TypstWriter {
47
50
  private preferShorthands: boolean;
48
51
  private keepSpaces: boolean;
49
52
  private inftyToOo: boolean;
53
+ private optimize: boolean;
50
54
 
51
55
  protected buffer: string = "";
52
56
  protected queue: TypstToken[] = [];
53
57
 
54
58
  private insideFunctionDepth = 0;
55
59
 
56
- constructor(opt: TypstWriterOptions) {
57
- this.nonStrict = opt.nonStrict;
58
- this.preferShorthands = opt.preferShorthands;
59
- this.keepSpaces = opt.keepSpaces;
60
- this.inftyToOo = opt.inftyToOo;
60
+ constructor(options: TypstWriterOptions) {
61
+ this.nonStrict = options.nonStrict;
62
+ this.preferShorthands = options.preferShorthands;
63
+ this.keepSpaces = options.keepSpaces;
64
+ this.inftyToOo = options.inftyToOo;
65
+ this.optimize = options.optimize;
61
66
  }
62
67
 
63
68
 
@@ -68,8 +73,6 @@ export class TypstWriter {
68
73
  return;
69
74
  }
70
75
 
71
- // TODO: "C \frac{xy}{z}" should translate to "C (x y)/z" instead of "C(x y)/z"
72
-
73
76
  let no_need_space = false;
74
77
  // putting the first token in clause
75
78
  no_need_space ||= /[\(\[\|]$/.test(this.buffer) && /^\w/.test(str);
@@ -163,7 +166,7 @@ export class TypstWriter {
163
166
  // Put prime symbol before '_'. Because $y_1'$ is not displayed properly in Typst (so far)
164
167
  // e.g.
165
168
  // y_1' -> y'_1
166
- // y_{a_1}' -> y'_{a_1}
169
+ // y_{a_1}' -> y'_(a_1)
167
170
  this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '\''));
168
171
  trailing_space_needed = false;
169
172
  }
@@ -176,7 +179,7 @@ export class TypstWriter {
176
179
  trailing_space_needed = this.appendWithBracketsIfNeeded(sup);
177
180
  }
178
181
  if (trailing_space_needed) {
179
- this.queue.push(new TypstToken(TypstTokenType.CONTROL, ' '));
182
+ this.queue.push(SOFT_SPACE);
180
183
  }
181
184
  break;
182
185
  }
@@ -207,7 +210,16 @@ export class TypstWriter {
207
210
  }
208
211
  case 'fraction': {
209
212
  const [numerator, denominator] = node.args!;
210
- this.appendWithBracketsIfNeeded(numerator);
213
+ const pos = this.queue.length;
214
+ const no_wrap = this.appendWithBracketsIfNeeded(numerator);
215
+
216
+ // This is a dirty hack to force `C \frac{xy}{z}`to translate to `C (x y)/z` instead of `C(x y)/z`
217
+ // To solve this properly, we should implement a Typst formatter
218
+ const wrapped = !no_wrap;
219
+ if (wrapped) {
220
+ this.queue.splice(pos, 0, SOFT_SPACE);
221
+ }
222
+
211
223
  this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '/'));
212
224
  this.appendWithBracketsIfNeeded(denominator);
213
225
  break;
@@ -331,20 +343,24 @@ export class TypstWriter {
331
343
  }
332
344
 
333
345
  protected flushQueue() {
334
- const SOFT_SPACE = new TypstToken(TypstTokenType.CONTROL, ' ');
346
+ const dummy_token = new TypstToken(TypstTokenType.SYMBOL, '');
335
347
 
336
348
  // delete soft spaces if they are not needed
337
349
  for(let i = 0; i < this.queue.length; i++) {
338
350
  let token = this.queue[i];
339
351
  if (token.eq(SOFT_SPACE)) {
340
- if (i === this.queue.length - 1) {
341
- this.queue[i].value = '';
342
- } else if (this.queue[i + 1].isOneOf([TYPST_RIGHT_PARENTHESIS, TYPST_COMMA, TYPST_NEWLINE])) {
343
- this.queue[i].value = '';
352
+ const to_delete = (i === 0)
353
+ || (i === this.queue.length - 1)
354
+ || this.queue[i - 1].isOneOf([TYPST_NEWLINE])
355
+ || this.queue[i + 1].isOneOf([TYPST_RIGHT_PARENTHESIS, TYPST_COMMA, TYPST_NEWLINE]);
356
+ if (to_delete) {
357
+ this.queue[i] = dummy_token;
344
358
  }
345
359
  }
346
360
  }
347
361
 
362
+ this.queue = this.queue.filter((token) => !token.eq(dummy_token));
363
+
348
364
  this.queue.forEach((token) => {
349
365
  this.writeBuffer(token)
350
366
  });
@@ -375,9 +391,11 @@ export class TypstWriter {
375
391
  res = res.replace(/round\(\)/g, 'round("")');
376
392
  return res;
377
393
  }
378
- const all_passes = [smartFloorPass, smartCeilPass, smartRoundPass];
379
- for (const pass of all_passes) {
380
- this.buffer = pass(this.buffer);
394
+ if (this.optimize) {
395
+ const all_passes = [smartFloorPass, smartCeilPass, smartRoundPass];
396
+ for (const pass of all_passes) {
397
+ this.buffer = pass(this.buffer);
398
+ }
381
399
  }
382
400
  return this.buffer;
383
401
  }
package/TODO.md DELETED
@@ -1 +0,0 @@
1
- - Typst math `limits(Y)^X` to TeX `\overset{X}{Y}`
@@ -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
- ```
@@ -1,33 +0,0 @@
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,35 +0,0 @@
1
- import urllib.request
2
- from bs4 import BeautifulSoup
3
-
4
- if __name__ == '__main__':
5
- symbol_map = {}
6
-
7
- url = "https://typst.app/docs/reference/symbols/sym/"
8
- with urllib.request.urlopen(url) as response:
9
- html_text = response.read().decode('utf-8')
10
-
11
- soup = BeautifulSoup(html_text, 'html.parser')
12
- # <ul class="symbol-grid">
13
- ul = soup.find('ul', class_='symbol-grid')
14
- li_list = ul.find_all('li')
15
- for li in li_list:
16
- # e.g. <li id="symbol-brace.r.double" data-latex-name="\rBrace" data-codepoint="10628"><button>...</button></li>
17
- # ==> latex = rBrace
18
- # ==> typst = brace.r.double
19
- # ==> unicode = 10628 = \u2984
20
- latex = li.get('data-latex-name', None)
21
- typst = li['id'][7:]
22
- unicode = int(li['data-codepoint'])
23
- if latex is not None:
24
- # some latex macro can be associated with multiple typst
25
- # e.g. \equiv can be mapped to equal or equiv.triple
26
- # We only keep the first one
27
- if latex not in symbol_map:
28
- symbol_map[latex] = typst
29
-
30
- # sort the pairs with alphabetical order of latex
31
- sorted_keys = sorted(list(symbol_map.keys()), key=str.lower)
32
- sorted_symbol_map = [(key, symbol_map[key]) for key in sorted_keys]
33
- for latex, typst in sorted_symbol_map:
34
- print(f" ['{latex[1:]}', '{typst}'],")
35
- # print(f'{latex[1:]} = "{typst}"')