tex2typst 0.3.9 → 0.3.11

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 } from "./types";
1
+ import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType, TypstLrData, TexArrayData } from "./types";
2
2
  import { TypstWriterError } from "./typst-writer";
3
3
  import { symbolMap, reverseSymbolMap } from "./map";
4
4
  import { array_join } from "./generic";
@@ -90,7 +90,7 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
90
90
  export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptions = {}): TypstNode {
91
91
  switch (node.type) {
92
92
  case 'empty':
93
- return new TypstNode('empty', '');
93
+ return TYPST_NONE;
94
94
  case 'whitespace':
95
95
  return new TypstNode('whitespace', node.content);
96
96
  case 'ordgroup':
@@ -128,8 +128,8 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
128
128
  const data: TypstSupsubData = {
129
129
  base: convert_tex_node_to_typst(base, options),
130
130
  };
131
- if (data.base.type === 'empty') {
132
- data.base = new TypstNode('text', '');
131
+ if (data.base.type === 'none') {
132
+ data.base = new TypstNode('none', '');
133
133
  }
134
134
 
135
135
  if (sup) {
@@ -281,7 +281,7 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
281
281
  return new TypstNode('cases', '', [], data);
282
282
  }
283
283
  if (node.content!.endsWith('matrix')) {
284
- let delim: TypstPrimitiveValue = null;
284
+ let delim: TypstPrimitiveValue;
285
285
  switch (node.content) {
286
286
  case 'matrix':
287
287
  delim = TYPST_NONE;
@@ -299,7 +299,7 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
299
299
  delim = '|';
300
300
  break;
301
301
  case 'Vmatrix': {
302
- delim = new TypstToken(TypstTokenType.SYMBOL, 'bar.v.double');
302
+ delim = new TypstNode('symbol', 'bar.v.double');
303
303
  break;
304
304
  }
305
305
  default:
@@ -388,7 +388,8 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
388
388
  ]);
389
389
  }
390
390
  switch (node.type) {
391
- case 'empty':
391
+ case 'none':
392
+ // e.g. Typst `#none^2` is converted to TeX `^2`
392
393
  return new TexNode('empty', '');
393
394
  case 'whitespace':
394
395
  return new TexNode('whitespace', node.content);
@@ -475,6 +476,12 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
475
476
  const command = typst_token_to_tex(node.content);
476
477
  return new TexNode('binaryFunc', command, node.args!.map(convert_typst_node_to_tex));
477
478
  } else {
479
+ // special hook for vec
480
+ // "vec(a, b, c)" -> "\begin{pmatrix}a\\ b\\ c\end{pmatrix}"
481
+ if (node.content === 'vec') {
482
+ const tex_data = node.args!.map(convert_typst_node_to_tex).map((n) => [n]) as TexArrayData;
483
+ return new TexNode('beginend', 'pmatrix', [], tex_data);
484
+ }
478
485
  return new TexNode('ordgroup', '', [
479
486
  new TexNode('symbol', typst_token_to_tex(node.content)),
480
487
  new TexNode('element', '('),
@@ -504,50 +511,51 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
504
511
  case 'matrix': {
505
512
  const typst_data = node.data as TypstNode[][];
506
513
  const tex_data = typst_data.map(row => row.map(convert_typst_node_to_tex));
507
- const matrix = new TexNode('beginend', 'matrix', [], tex_data);
508
- let left_delim = "\\left(";
509
- let right_delim = "\\right)";
514
+ let env_type = 'pmatrix';
510
515
  if (node.options) {
511
516
  if ('delim' in node.options) {
512
- switch (node.options.delim) {
513
- case TYPST_NONE:
514
- return matrix;
515
- case '[':
516
- left_delim = "\\left[";
517
- right_delim = "\\right]";
518
- break;
519
- case ']':
520
- left_delim = "\\left]";
521
- right_delim = "\\right[";
522
- break;
523
- case '{':
524
- left_delim = "\\left\\{";
525
- right_delim = "\\right\\}";
526
- break;
527
- case '}':
528
- left_delim = "\\left\\}";
529
- right_delim = "\\right\\{";
530
- break;
531
- case '|':
532
- left_delim = "\\left|";
533
- right_delim = "\\right|";
534
- break;
535
- case ')':
536
- left_delim = "\\left)";
537
- right_delim = "\\right(";
538
- case '(':
539
- default:
540
- left_delim = "\\left(";
541
- right_delim = "\\right)";
542
- break;
517
+ const delim = node.options.delim;
518
+ if (delim instanceof TypstNode) {
519
+ switch (delim.content) {
520
+ case '#none':
521
+ env_type = 'matrix';
522
+ break;
523
+ case 'bar.v.double':
524
+ env_type = 'Vmatrix';
525
+ break;
526
+ case 'bar':
527
+ case 'bar.v':
528
+ env_type = 'vmatrix';
529
+ break;
530
+ default:
531
+ throw new Error(`Unexpected delimiter ${delim.content}`);
532
+ }
533
+ } else {
534
+ switch (delim) {
535
+ case '[':
536
+ env_type = 'bmatrix';
537
+ break;
538
+ case ']':
539
+ env_type = 'bmatrix';
540
+ break;
541
+ case '{':
542
+ env_type = 'Bmatrix';
543
+ break;
544
+ case '}':
545
+ env_type = 'Bmatrix';
546
+ break;
547
+ case '|':
548
+ env_type = 'vmatrix';
549
+ break;
550
+ case ')':
551
+ case '(':
552
+ default:
553
+ env_type = 'pmatrix';
554
+ }
543
555
  }
544
556
  }
545
557
  }
546
- return new TexNode('ordgroup', '', [
547
- new TexNode('element', left_delim),
548
- matrix,
549
- new TexNode('element', right_delim)
550
- ]);
558
+ return new TexNode('beginend', env_type, [], tex_data);
551
559
  }
552
560
  case 'cases': {
553
561
  const typst_data = node.data as TypstNode[][];
package/src/tex-parser.ts CHANGED
@@ -209,7 +209,8 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
209
209
  const command = s.text()!;
210
210
  return [ new TexToken(TexTokenType.COMMAND, command), ];
211
211
  }],
212
- [String.raw`[0-9]+`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
212
+ // Numbers like "123", "3.14"
213
+ [String.raw`[0-9]+(\.[0-9]+)?`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
213
214
  [String.raw`[a-zA-Z]`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
214
215
  [String.raw`[+\-*/='<>!.,;:?()\[\]|]`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
215
216
  [String.raw`.`, (s) => new TexToken(TexTokenType.UNKNOWN, s.text()!)],
@@ -273,9 +274,7 @@ export class LatexParser {
273
274
  }
274
275
 
275
276
  let node: TexNode;
276
- if (results.length === 0) {
277
- node = EMPTY_NODE;
278
- } else if (results.length === 1) {
277
+ if (results.length === 1) {
279
278
  node = results[0];
280
279
  } else {
281
280
  node = new TexNode('ordgroup', '', results);
package/src/types.ts CHANGED
@@ -233,12 +233,12 @@ export class TexNode {
233
233
  }
234
234
 
235
235
  export enum TypstTokenType {
236
+ NONE,
236
237
  SYMBOL,
237
238
  ELEMENT,
238
239
  TEXT,
239
240
  COMMENT,
240
241
  SPACE,
241
- SOFT_SPACE,
242
242
  CONTROL,
243
243
  NEWLINE,
244
244
  }
@@ -262,6 +262,8 @@ export class TypstToken {
262
262
 
263
263
  public toNode(): TypstNode {
264
264
  switch(this.type) {
265
+ case TypstTokenType.NONE:
266
+ return new TypstNode('none', '#none');
265
267
  case TypstTokenType.TEXT:
266
268
  return new TypstNode('text', this.value);
267
269
  case TypstTokenType.COMMENT:
@@ -279,7 +281,7 @@ export class TypstToken {
279
281
  case '':
280
282
  case '_':
281
283
  case '^':
282
- return new TypstNode('empty', '');
284
+ throw new Error(`Should not convert ${controlChar} to a node`);
283
285
  case '&':
284
286
  return new TypstNode('control', '&');
285
287
  case '\\':
@@ -318,15 +320,11 @@ export interface TypstLrData {
318
320
  }
319
321
 
320
322
  type TypstNodeType = 'atom' | 'symbol' | 'text' | 'control' | 'comment' | 'whitespace'
321
- | 'empty' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'align' | 'matrix' | 'cases' | 'unknown';
323
+ | 'none' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'align' | 'matrix' | 'cases' | 'unknown';
322
324
 
323
- export type TypstPrimitiveValue = string | boolean | null | TypstToken;
325
+ export type TypstPrimitiveValue = string | boolean | TypstNode;
324
326
  export type TypstNamedParams = { [key: string]: TypstPrimitiveValue };
325
327
 
326
- // #none
327
- export const TYPST_NONE: TypstPrimitiveValue = null;
328
- export const TYPST_TRUE: TypstPrimitiveValue = true;
329
- export const TYPST_FALSE: TypstPrimitiveValue = false;
330
328
 
331
329
  export class TypstNode {
332
330
  type: TypstNodeType;
@@ -379,6 +377,11 @@ export class TypstNode {
379
377
  }
380
378
  }
381
379
 
380
+ // #none
381
+ export const TYPST_NONE = new TypstNode('none', '#none');
382
+ export const TYPST_TRUE: TypstPrimitiveValue = true;
383
+ export const TYPST_FALSE: TypstPrimitiveValue = false;
384
+
382
385
  export interface Tex2TypstOptions {
383
386
  nonStrict?: boolean; // default is true
384
387
  preferTypstIntrinsic?: boolean; // default is true,
@@ -5,9 +5,6 @@ import { assert, isalpha } from "./util";
5
5
  import { reverseShorthandMap } from "./typst-shorthands";
6
6
  import { JSLex, Scanner } from "./jslex";
7
7
 
8
-
9
- const TYPST_EMPTY_NODE = new TypstNode('empty', '');
10
-
11
8
  const TYPST_SHORTHANDS = Array.from(reverseShorthandMap.keys());
12
9
 
13
10
  // TODO: In Typst, y' ' is not the same as y''.
@@ -78,6 +75,7 @@ const rules_map = new Map<string, (a: Scanner<TypstToken>) => TypstToken | Typst
78
75
  [String.raw`[a-zA-Z\.]+`, (s) => {
79
76
  return new TypstToken(s.text()!.length === 1? TypstTokenType.ELEMENT: TypstTokenType.SYMBOL, s.text()!);
80
77
  }],
78
+ [String.raw`#none`, (s) => new TypstToken(TypstTokenType.NONE, s.text()!)],
81
79
  [String.raw`.`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
82
80
  ]);
83
81
 
@@ -168,12 +166,12 @@ const DIV = new TypstNode('atom', '/');
168
166
 
169
167
 
170
168
 
171
- function next_non_whitespace(nodes: TypstNode[], start: number): TypstNode {
169
+ function next_non_whitespace(nodes: TypstNode[], start: number): TypstNode | null {
172
170
  let pos = start;
173
171
  while (pos < nodes.length && nodes[pos].type === 'whitespace') {
174
172
  pos++;
175
173
  }
176
- return pos === nodes.length ? TYPST_EMPTY_NODE : nodes[pos];
174
+ return pos === nodes.length ? null : nodes[pos];
177
175
  }
178
176
 
179
177
  function trim_whitespace_around_operators(nodes: TypstNode[]): TypstNode[] {
@@ -185,7 +183,7 @@ function trim_whitespace_around_operators(nodes: TypstNode[]): TypstNode[] {
185
183
  if(after_operator) {
186
184
  continue;
187
185
  }
188
- if(next_non_whitespace(nodes, i + 1).eq(DIV)) {
186
+ if(next_non_whitespace(nodes, i + 1)?.eq(DIV)) {
189
187
  continue;
190
188
  }
191
189
  }
@@ -253,9 +251,7 @@ function process_operators(nodes: TypstNode[], parenthesis = false): TypstNode {
253
251
  if(parenthesis) {
254
252
  return new TypstNode('group', 'parenthesis', args);
255
253
  } else {
256
- if(args.length === 0) {
257
- return TYPST_EMPTY_NODE;
258
- } else if(args.length === 1) {
254
+ if(args.length === 1) {
259
255
  return args[0];
260
256
  } else {
261
257
  return new TypstNode('group', '', args);
@@ -322,9 +318,7 @@ export class TypstParser {
322
318
  if(parentheses) {
323
319
  node = process_operators(results, true);
324
320
  } else {
325
- if (results.length === 0) {
326
- node = TYPST_EMPTY_NODE;
327
- } else if (results.length === 1) {
321
+ if (results.length === 1) {
328
322
  node = results[0];
329
323
  } else {
330
324
  node = process_operators(results);
@@ -489,19 +483,24 @@ export class TypstParser {
489
483
  to_delete.push(i);
490
484
  const param_name = g.args![pos_colon - 1];
491
485
  if(param_name.eq(new TypstNode('symbol', 'delim'))) {
492
- if(g.args![pos_colon + 1].type === 'text') {
493
- np['delim'] = g.args![pos_colon + 1].content;
494
- if(g.args!.length !== 3) {
495
- throw new TypstParserError('Invalid number of arguments for delim');
486
+ if(g.args!.length !== 3) {
487
+ throw new TypstParserError('Invalid number of arguments for delim');
488
+ }
489
+ switch (g.args![pos_colon + 1].type) {
490
+ case 'text': {
491
+ np['delim'] = g.args![pos_colon + 1].content;
492
+ break;
493
+ }
494
+ case 'none': {
495
+ np['delim'] = TYPST_NONE;
496
+ break;
496
497
  }
497
- } else if(g.args![pos_colon + 1].eq(new TypstNode('atom', '#'))) {
498
- // TODO: should parse #none properly
499
- if(g.args!.length !== 4 || !g.args![pos_colon + 2].eq(new TypstNode('symbol', 'none'))) {
500
- throw new TypstParserError('Invalid number of arguments for delim');
498
+ case 'symbol': {
499
+ np['delim'] = g.args![pos_colon + 1];
500
+ break;
501
501
  }
502
- np['delim'] = TYPST_NONE;
503
- } else {
504
- throw new TypstParserError('Not implemented for other types of delim');
502
+ default:
503
+ throw new TypstParserError('Not implemented for other types of delim');
505
504
  }
506
505
  } else {
507
506
  throw new TypstParserError('Not implemented for other named parameters');
@@ -543,9 +542,7 @@ export class TypstParser {
543
542
  }
544
543
 
545
544
  let arg: TypstNode;
546
- if (nodes.length === 0) {
547
- arg = TYPST_EMPTY_NODE;
548
- } else if (nodes.length === 1) {
545
+ if (nodes.length === 1) {
549
546
  arg = nodes[0];
550
547
  } else {
551
548
  arg = process_operators(nodes);
@@ -1,5 +1,6 @@
1
1
  import { TexNode, TypstNode, TypstPrimitiveValue, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
2
2
  import { shorthandMap } from "./typst-shorthands";
3
+ import { assert } from "./util";
3
4
 
4
5
  function is_delimiter(c: TypstNode): boolean {
5
6
  return c.type === 'atom' && ['(', ')', '[', ']', '{', '}', '|', '⌊', '⌋', '⌈', '⌉'].includes(c.content);
@@ -19,12 +20,8 @@ function typst_primitive_to_string(value: TypstPrimitiveValue) {
19
20
  case 'boolean':
20
21
  return (value as boolean) ? '#true' : '#false';
21
22
  default:
22
- if (value === null) {
23
- return '#none';
24
- } else if (value instanceof TypstToken) {
25
- return value.toString();
26
- }
27
- throw new TypstWriterError(`Invalid primitive value: ${value}`, value);
23
+ assert(value instanceof TypstNode, 'Not a valid primitive value');
24
+ return (value as TypstNode).content;
28
25
  }
29
26
  }
30
27
 
@@ -108,7 +105,8 @@ export class TypstWriter {
108
105
  // Serialize a tree of TypstNode into a list of TypstToken
109
106
  public serialize(node: TypstNode) {
110
107
  switch (node.type) {
111
- case 'empty':
108
+ case 'none':
109
+ this.queue.push(new TypstToken(TypstTokenType.NONE, '#none'));
112
110
  break;
113
111
  case 'atom': {
114
112
  if (node.content === ',' && this.insideFunctionDepth > 0) {
@@ -163,7 +161,7 @@ export class TypstWriter {
163
161
  const has_prime = (sup && sup.type === 'atom' && sup.content === '\'');
164
162
  if (has_prime) {
165
163
  // Put prime symbol before '_'. Because $y_1'$ is not displayed properly in Typst (so far)
166
- // e.g.
164
+ // e.g.
167
165
  // y_1' -> y'_1
168
166
  // y_{a_1}' -> y'_{a_1}
169
167
  this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '\''));
@@ -185,7 +183,9 @@ export class TypstWriter {
185
183
  case 'funcCall': {
186
184
  const func_symbol: TypstToken = new TypstToken(TypstTokenType.SYMBOL, node.content);
187
185
  this.queue.push(func_symbol);
188
- this.insideFunctionDepth++;
186
+ if (node.content !== 'lr') {
187
+ this.insideFunctionDepth++;
188
+ }
189
189
  this.queue.push(TYPST_LEFT_PARENTHESIS);
190
190
  for (let i = 0; i < node.args!.length; i++) {
191
191
  this.serialize(node.args![i]);
@@ -200,7 +200,9 @@ export class TypstWriter {
200
200
  }
201
201
  }
202
202
  this.queue.push(TYPST_RIGHT_PARENTHESIS);
203
- this.insideFunctionDepth--;
203
+ if (node.content !== 'lr') {
204
+ this.insideFunctionDepth--;
205
+ }
204
206
  break;
205
207
  }
206
208
  case 'fraction': {
@@ -317,10 +319,15 @@ export class TypstWriter {
317
319
  let need_to_wrap = ['group', 'supsub', 'empty'].includes(node.type);
318
320
 
319
321
  if (node.type === 'group') {
320
- const first = node.args![0];
321
- const last = node.args![node.args!.length - 1];
322
- if (is_delimiter(first) && is_delimiter(last)) {
323
- need_to_wrap = false;
322
+ if (node.args!.length === 0) {
323
+ // e.g. TeX `P_{}` converts to Typst `P_()`
324
+ need_to_wrap = true;
325
+ } else {
326
+ const first = node.args![0];
327
+ const last = node.args![node.args!.length - 1];
328
+ if (is_delimiter(first) && is_delimiter(last)) {
329
+ need_to_wrap = false;
330
+ }
324
331
  }
325
332
  }
326
333