tex2typst 0.3.10 → 0.3.12

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_NULL, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType, TypstLrData, TexArrayData } 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':
@@ -103,8 +103,16 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
103
103
  return new TypstNode('atom', tex_token_to_typst(node.content));
104
104
  case 'symbol':
105
105
  return new TypstNode('symbol', tex_token_to_typst(node.content));
106
- case 'text':
106
+ case 'text': {
107
+ if ((/[^\x00-\x7F]+/).test(node.content) && options.nonAsciiWrapper !== "") {
108
+ return new TypstNode(
109
+ 'funcCall',
110
+ options.nonAsciiWrapper!,
111
+ [new TypstNode('text', node.content)]
112
+ );
113
+ }
107
114
  return new TypstNode('text', node.content);
115
+ }
108
116
  case 'comment':
109
117
  return new TypstNode('comment', node.content);
110
118
  case 'supsub': {
@@ -128,8 +136,8 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
128
136
  const data: TypstSupsubData = {
129
137
  base: convert_tex_node_to_typst(base, options),
130
138
  };
131
- if (data.base.type === 'empty') {
132
- data.base = new TypstNode('text', '');
139
+ if (data.base.type === 'none') {
140
+ data.base = new TypstNode('none', '');
133
141
  }
134
142
 
135
143
  if (sup) {
@@ -281,10 +289,10 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
281
289
  return new TypstNode('cases', '', [], data);
282
290
  }
283
291
  if (node.content!.endsWith('matrix')) {
284
- let delim: TypstPrimitiveValue = null;
292
+ let delim: TypstPrimitiveValue;
285
293
  switch (node.content) {
286
294
  case 'matrix':
287
- delim = TYPST_NULL;
295
+ delim = TYPST_NONE;
288
296
  break;
289
297
  case 'pmatrix':
290
298
  delim = '(';
@@ -299,7 +307,7 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
299
307
  delim = '|';
300
308
  break;
301
309
  case 'Vmatrix': {
302
- delim = new TypstToken(TypstTokenType.SYMBOL, 'bar.v.double');
310
+ delim = new TypstNode('symbol', 'bar.v.double');
303
311
  break;
304
312
  }
305
313
  default:
@@ -388,7 +396,8 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
388
396
  ]);
389
397
  }
390
398
  switch (node.type) {
391
- case 'empty':
399
+ case 'none':
400
+ // e.g. Typst `#none^2` is converted to TeX `^2`
392
401
  return new TexNode('empty', '');
393
402
  case 'whitespace':
394
403
  return new TexNode('whitespace', node.content);
@@ -513,29 +522,44 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
513
522
  let env_type = 'pmatrix';
514
523
  if (node.options) {
515
524
  if ('delim' in node.options) {
516
- switch (node.options.delim) {
517
- case TYPST_NULL:
518
- env_type = 'matrix';
519
- break;
520
- case '[':
521
- env_type = 'bmatrix';
522
- break;
523
- case ']':
524
- env_type = 'bmatrix';
525
- break;
526
- case '{':
527
- env_type = 'Bmatrix';
528
- break;
529
- case '}':
530
- env_type = 'Bmatrix';
531
- break;
532
- case '|':
533
- env_type = 'vmatrix';
534
- break;
535
- case ')':
536
- case '(':
537
- default:
538
- env_type = 'pmatrix';
525
+ const delim = node.options.delim;
526
+ if (delim instanceof TypstNode) {
527
+ switch (delim.content) {
528
+ case '#none':
529
+ env_type = 'matrix';
530
+ break;
531
+ case 'bar.v.double':
532
+ env_type = 'Vmatrix';
533
+ break;
534
+ case 'bar':
535
+ case 'bar.v':
536
+ env_type = 'vmatrix';
537
+ break;
538
+ default:
539
+ throw new Error(`Unexpected delimiter ${delim.content}`);
540
+ }
541
+ } else {
542
+ switch (delim) {
543
+ case '[':
544
+ env_type = 'bmatrix';
545
+ break;
546
+ case ']':
547
+ env_type = 'bmatrix';
548
+ break;
549
+ case '{':
550
+ env_type = 'Bmatrix';
551
+ break;
552
+ case '}':
553
+ env_type = 'Bmatrix';
554
+ break;
555
+ case '|':
556
+ env_type = 'vmatrix';
557
+ break;
558
+ case ')':
559
+ case '(':
560
+ default:
561
+ env_type = 'pmatrix';
562
+ }
539
563
  }
540
564
  }
541
565
  }
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
16
16
  keepSpaces: false,
17
17
  fracToSlash: true,
18
18
  inftyToOo: false,
19
+ nonAsciiWrapper: "",
19
20
  customTexMacros: {}
20
21
  };
21
22
 
package/src/tex-parser.ts CHANGED
@@ -209,9 +209,12 @@ 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()!)],
216
+ // non-ASCII characters
217
+ [String.raw`[^\x00-\x7F]`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
215
218
  [String.raw`.`, (s) => new TexToken(TexTokenType.UNKNOWN, s.text()!)],
216
219
  ]);
217
220
 
@@ -273,9 +276,7 @@ export class LatexParser {
273
276
  }
274
277
 
275
278
  let node: TexNode;
276
- if (results.length === 0) {
277
- node = EMPTY_NODE;
278
- } else if (results.length === 1) {
279
+ if (results.length === 1) {
279
280
  node = results[0];
280
281
  } else {
281
282
  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_NULL: 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,
@@ -386,6 +389,7 @@ export interface Tex2TypstOptions {
386
389
  keepSpaces?: boolean; // default is false
387
390
  fracToSlash?: boolean; // default is true
388
391
  inftyToOo?: boolean; // default is false
392
+ nonAsciiWrapper?: string; // default is ""
389
393
  customTexMacros?: { [key: string]: string };
390
394
  // TODO: custom typst functions
391
395
  }
@@ -1,13 +1,10 @@
1
1
 
2
2
  import { array_find } from "./generic";
3
- import { TYPST_NULL, TypstLrData, TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
3
+ import { TYPST_NONE, TypstLrData, TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
4
4
  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_NULL;
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