tex2typst 0.2.11 → 0.2.13

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/parser.ts CHANGED
@@ -41,6 +41,7 @@ const BINARY_COMMANDS = [
41
41
  'dbinom',
42
42
  'dfrac',
43
43
  'tbinom',
44
+ 'overset',
44
45
  ]
45
46
 
46
47
 
@@ -295,7 +296,7 @@ export function tokenize(latex: string): Token[] {
295
296
  const firstTwoChars = latex.slice(pos, pos + 2);
296
297
  if (['\\\\', '\\,'].includes(firstTwoChars)) {
297
298
  token = new Token(TokenType.CONTROL, firstTwoChars);
298
- } else if (['\\{','\\}', '\\%', '\\$', '\\&', '\\#', '\\_'].includes(firstTwoChars)) {
299
+ } else if (['\\{','\\}', '\\%', '\\$', '\\&', '\\#', '\\_', '\\|'].includes(firstTwoChars)) {
299
300
  token = new Token(TokenType.ELEMENT, firstTwoChars);
300
301
  } else {
301
302
  const command = eat_command_name(latex, pos + 1);
package/src/types.ts CHANGED
@@ -47,6 +47,8 @@ export interface TypstNode {
47
47
  content: string;
48
48
  args?: TypstNode[];
49
49
  data?: TypstSupsubData | TypstArrayData;
50
+ // Some Typst functions accept additional options. e.g. mat() has option "delim", op() has option "limits"
51
+ options?: { [key: string]: string };
50
52
  }
51
53
 
52
54
  export interface Tex2TypstOptions {
package/src/writer.ts CHANGED
@@ -19,6 +19,54 @@ function is_delimiter(c: TypstNode): boolean {
19
19
  return c.type === 'atom' && ['(', ')', '[', ']', '{', '}', '|', '⌊', '⌋', '⌈', '⌉'].includes(c.content);
20
20
  }
21
21
 
22
+ function text_node_shallow_eq(a: TexNode, b: TexNode): boolean {
23
+ return (a.type === b.type) && (a.content === b.content);
24
+ }
25
+
26
+ // \overset{X}{Y} -> op(Y, limits: #true)^X
27
+ // and with special case \overset{\text{def}}{=} -> eq.def
28
+ function convert_overset(node: TexNode): TypstNode {
29
+ const [sup, base] = node.args!;
30
+
31
+ const is_def = (n: TexNode): boolean => {
32
+ if(n.type === 'text' && n.content === 'def') {
33
+ return true;
34
+ }
35
+ // \overset{def}{=} is also considered as eq.def
36
+ if(n.type === 'ordgroup' && n.args!.length === 3) {
37
+ const [a1, a2, a3] = n.args!;
38
+ const d: TexNode = { type: 'element', content: 'd' };
39
+ const e: TexNode = { type: 'element', content: 'e' };
40
+ const f: TexNode = { type: 'element', content: 'f' };
41
+ if(text_node_shallow_eq(a1, d) && text_node_shallow_eq(a2, e) && text_node_shallow_eq(a3, f)) {
42
+ return true;
43
+ }
44
+ }
45
+ return false;
46
+ };
47
+ const is_eq = (n: TexNode): boolean => (n.type === 'element' && n.content === '=');
48
+ if(is_def(sup) && is_eq(base)) {
49
+ return {
50
+ type: 'symbol',
51
+ content: 'eq.def',
52
+ };
53
+ }
54
+ const op_call: TypstNode = {
55
+ type: 'unaryFunc',
56
+ content: 'op',
57
+ args: [convertTree(base)],
58
+ options: { limits: '#true' },
59
+ };
60
+ return {
61
+ type: 'supsub',
62
+ content: '',
63
+ data: {
64
+ base: op_call,
65
+ sup: convertTree(sup),
66
+ },
67
+ }
68
+ }
69
+
22
70
  export class TypstWriterError extends Error {
23
71
  node: TexNode | TypstNode;
24
72
 
@@ -148,6 +196,11 @@ export class TypstWriter {
148
196
  this.insideFunctionDepth ++;
149
197
  this.queue.push({ type: 'atom', content: '('});
150
198
  this.append(arg0);
199
+ if(node.options) {
200
+ for (const [key, value] of Object.entries(node.options)) {
201
+ this.queue.push({ type: 'symbol', content: `, ${key}: ${value}`});
202
+ }
203
+ }
151
204
  this.queue.push({ type: 'atom', content: ')'});
152
205
  this.insideFunctionDepth --;
153
206
  break;
@@ -267,22 +320,22 @@ export class TypstWriter {
267
320
  public finalize(): string {
268
321
  this.flushQueue();
269
322
  const smartFloorPass = function (input: string): string {
270
- // Use regex to replace all " xxx " with "floor(xxx)"
271
- let res = input.replace(/⌊\s*(.*?)\s*⌋/g, "floor($1)");
323
+ // Use regex to replace all "floor.l xxx floor.r" with "floor(xxx)"
324
+ let res = input.replace(/floor\.l\s*(.*?)\s*floor\.r/g, "floor($1)");
272
325
  // Typst disallow "floor()" with empty argument, so add am empty string inside if it's empty.
273
326
  res = res.replace(/floor\(\)/g, 'floor("")');
274
327
  return res;
275
328
  };
276
329
  const smartCeilPass = function (input: string): string {
277
- // Use regex to replace all " xxx " with "ceil(xxx)"
278
- let res = input.replace(/⌈\s*(.*?)\s*⌉/g, "ceil($1)");
330
+ // Use regex to replace all "ceil.l xxx ceil.r" with "ceil(xxx)"
331
+ let res = input.replace(/ceil\.l\s*(.*?)\s*ceil\.r/g, "ceil($1)");
279
332
  // Typst disallow "ceil()" with empty argument, so add an empty string inside if it's empty.
280
333
  res = res.replace(/ceil\(\)/g, 'ceil("")');
281
334
  return res;
282
335
  }
283
336
  const smartRoundPass = function (input: string): string {
284
- // Use regex to replace all " xxx " with "round(xxx)"
285
- let res = input.replace(/⌊\s*(.*?)\s*⌉/g, "round($1)");
337
+ // Use regex to replace all "floor.l xxx ceil.r" with "round(xxx)"
338
+ let res = input.replace(/floor\.l\s*(.*?)\s*ceil\.r/g, "round($1)");
286
339
  // Typst disallow "round()" with empty argument, so add an empty string inside if it's empty.
287
340
  res = res.replace(/round\(\)/g, 'round("")');
288
341
  return res;
@@ -376,6 +429,9 @@ export function convertTree(node: TexNode): TypstNode {
376
429
  };
377
430
  }
378
431
  case 'binaryFunc': {
432
+ if (node.content === '\\overset') {
433
+ return convert_overset(node);
434
+ }
379
435
  return {
380
436
  type: 'binaryFunc',
381
437
  content: convertToken(node.content),
@@ -482,10 +538,13 @@ export function convertTree(node: TexNode): TypstNode {
482
538
  function convertToken(token: string): string {
483
539
  if (/^[a-zA-Z0-9]$/.test(token)) {
484
540
  return token;
541
+ } else if (token === '/') {
542
+ return '\\/';
543
+ } else if (token === '\\|') {
544
+ // \| in LaTeX is double vertical bar looks like ||
545
+ return 'parallel';
485
546
  } else if (token === '\\\\') {
486
547
  return '\\';
487
- } else if (token == '/') {
488
- return '\\/';
489
548
  } else if (['\\$', '\\#', '\\&', '\\_'].includes(token)) {
490
549
  return token;
491
550
  } else if (token.startsWith('\\')) {
@@ -0,0 +1,29 @@
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+
4
+
5
+ if __name__ == '__main__':
6
+ symbol_map = {}
7
+
8
+ url = "https://typst.app/docs/reference/symbols/sym/"
9
+ html_text = requests.get(url).text
10
+ soup = BeautifulSoup(html_text, 'html.parser')
11
+ # <ul class="symbol-grid">
12
+ ul = soup.find('ul', class_='symbol-grid')
13
+ li_list = ul.find_all('li')
14
+ for li in li_list:
15
+ # e.g. <li id="symbol-brace.r.double" data-latex-name="\rBrace" data-codepoint="10628"><button>...</button></li>
16
+ # ==> latex = rBrace
17
+ # ==> typst = brace.r.double
18
+ # ==> unicode = 10628 = \u2984
19
+ latex = li.get('data-latex-name', None)
20
+ typst = li['id'][7:]
21
+ unicode = int(li['data-codepoint'])
22
+ if latex is not None:
23
+ # some latex macro can be associated with multiple typst
24
+ # e.g. \equiv can be mapped to equal or equiv.triple
25
+ # We only keep the first one
26
+ if latex not in symbol_map:
27
+ symbol_map[latex] = typst
28
+ # print(f" ['{latex[1:]}', '{typst}'],")
29
+ print(f'{latex[1:]} = "{typst}"')