tex2typst 0.3.7 → 0.3.9

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,6 +1,9 @@
1
- import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType } from "./types";
1
+ import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue, TypstToken, TypstTokenType, TypstLrData } from "./types";
2
2
  import { TypstWriterError } from "./typst-writer";
3
3
  import { symbolMap, reverseSymbolMap } from "./map";
4
+ import { array_join } from "./generic";
5
+ import { assert } from "./util";
6
+
4
7
 
5
8
  // symbols that are supported by Typst but not by KaTeX
6
9
  const TYPST_INTRINSIC_SYMBOLS = [
@@ -344,6 +347,8 @@ const TYPST_UNARY_FUNCTIONS: string[] = [
344
347
  'bb',
345
348
  'cal',
346
349
  'frak',
350
+ 'floor',
351
+ 'ceil',
347
352
  ];
348
353
 
349
354
  const TYPST_BINARY_FUNCTIONS: string[] = [
@@ -372,6 +377,7 @@ function typst_token_to_tex(token: string): string {
372
377
  }
373
378
 
374
379
 
380
+ const TEX_NODE_COMMA = new TexNode('element', ',');
375
381
 
376
382
  export function convert_typst_node_to_tex(node: TypstNode): TexNode {
377
383
  // special hook for eq.def
@@ -406,24 +412,48 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
406
412
  return new TexNode('comment', node.content);
407
413
  case 'group': {
408
414
  const args = node.args!.map(convert_typst_node_to_tex);
415
+ if (node.content === 'parenthesis') {
416
+ const is_over_high = node.isOverHigh();
417
+ const left_delim = is_over_high ? '\\left(' : '(';
418
+ const right_delim = is_over_high ? '\\right)' : ')';
419
+ args.unshift(new TexNode('element', left_delim));
420
+ args.push(new TexNode('element', right_delim));
421
+ }
409
422
  return new TexNode('ordgroup', node.content, args);
410
423
  }
411
424
  case 'funcCall': {
412
425
  if (TYPST_UNARY_FUNCTIONS.includes(node.content)) {
413
426
  // special hook for lr
414
427
  if (node.content === 'lr') {
415
- const body = node.args![0];
416
- if (body.type === 'group') {
417
- let left_delim = body.args![0].content;
418
- let right_delim = body.args![body.args!.length - 1].content;
419
- left_delim = apply_escape_if_needed(left_delim);
420
- right_delim = apply_escape_if_needed(right_delim);
428
+ const data = node.data as TypstLrData;
429
+ if (data.leftDelim !== null) {
430
+ let left_delim = apply_escape_if_needed(data.leftDelim);
431
+ assert(data.rightDelim !== null, "leftDelim has value but rightDelim not");
432
+ let right_delim = apply_escape_if_needed(data.rightDelim!);
421
433
  return new TexNode('ordgroup', '', [
422
434
  new TexNode('element', '\\left' + left_delim),
423
- ...body.args!.slice(1, body.args!.length - 1).map(convert_typst_node_to_tex),
435
+ ...node.args!.map(convert_typst_node_to_tex),
424
436
  new TexNode('element', '\\right' + right_delim)
425
437
  ]);
438
+ } else {
439
+ return new TexNode('ordgroup', '', node.args!.map(convert_typst_node_to_tex));
440
+ }
441
+ }
442
+ // special hook for floor, ceil
443
+ // Typst "floor(a) + ceil(b)" should converts to Tex "\lfloor a \rfloor + \lceil b \rceil"
444
+ if (node.content === 'floor' || node.content === 'ceil') {
445
+ let left = "\\l" + node.content;
446
+ let right = "\\r" + node.content;
447
+ const arg0 = node.args![0];
448
+ if (arg0.isOverHigh()) {
449
+ left = "\\left" + left;
450
+ right = "\\right" + right;
426
451
  }
452
+ return new TexNode('ordgroup', '', [
453
+ new TexNode('symbol', left),
454
+ convert_typst_node_to_tex(arg0),
455
+ new TexNode('symbol', right)
456
+ ]);
427
457
  }
428
458
  const command = typst_token_to_tex(node.content);
429
459
  return new TexNode('unaryFunc', command, node.args!.map(convert_typst_node_to_tex));
@@ -448,7 +478,7 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
448
478
  return new TexNode('ordgroup', '', [
449
479
  new TexNode('symbol', typst_token_to_tex(node.content)),
450
480
  new TexNode('element', '('),
451
- ...node.args!.map(convert_typst_node_to_tex),
481
+ ...array_join(node.args!.map(convert_typst_node_to_tex), TEX_NODE_COMMA),
452
482
  new TexNode('element', ')')
453
483
  ]);
454
484
  }
package/src/generic.ts CHANGED
@@ -34,4 +34,17 @@ export function array_split<T extends IEquatable>(array: T[], sep: T): T[][] {
34
34
  }
35
35
  res.push(current_slice);
36
36
  return res;
37
+ }
38
+
39
+ // e.g. input array=['a', 'b', 'c'], sep = '+'
40
+ // return ['a','+', 'b', '+','c']
41
+ export function array_join<T>(array: T[], sep: T): T[] {
42
+ const res: T[] = [];
43
+ for (let i = 0; i < array.length; i++) {
44
+ res.push(array[i]);
45
+ if (i != array.length - 1) {
46
+ res.push(sep);
47
+ }
48
+ }
49
+ return res;
37
50
  }
package/src/tex-parser.ts CHANGED
@@ -175,25 +175,35 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
175
175
  const match = text.match(regex);
176
176
  assert(match !== null);
177
177
  const command = match![1];
178
- const arg1 = match![2].trimStart();
179
- const arg2 = match![3];
180
- return [
181
- new TexToken(TexTokenType.COMMAND, command),
182
- new TexToken(TexTokenType.ELEMENT, arg1),
183
- new TexToken(TexTokenType.ELEMENT, arg2),
184
- ];
185
- }],
178
+ if (BINARY_COMMANDS.includes(command.substring(1))) {
179
+ const arg1 = match![2].trimStart();
180
+ const arg2 = match![3];
181
+ return [
182
+ new TexToken(TexTokenType.COMMAND, command),
183
+ new TexToken(TexTokenType.ELEMENT, arg1),
184
+ new TexToken(TexTokenType.ELEMENT, arg2),
185
+ ];
186
+ } else {
187
+ s.reject();
188
+ return [];
189
+ }
190
+ }],
186
191
  [String.raw`(\\[a-zA-Z]+)(\s*\d|\s+[a-zA-Z])`, (s) => {
187
192
  const text = s.text()!;
188
193
  const regex = RegExp(String.raw`(\\[a-zA-Z]+)(\s*\d|\s+[a-zA-Z])`);
189
194
  const match = text.match(regex);
190
195
  assert(match !== null);
191
196
  const command = match![1];
192
- const arg1 = match![2].trimStart();
193
- return [
194
- new TexToken(TexTokenType.COMMAND, command),
195
- new TexToken(TexTokenType.ELEMENT, arg1),
196
- ];
197
+ if (UNARY_COMMANDS.includes(command.substring(1))) {
198
+ const arg1 = match![2].trimStart();
199
+ return [
200
+ new TexToken(TexTokenType.COMMAND, command),
201
+ new TexToken(TexTokenType.ELEMENT, arg1),
202
+ ];
203
+ } else {
204
+ s.reject();
205
+ return [];
206
+ }
197
207
  }],
198
208
  [String.raw`\\[a-zA-Z]+`, (s) => {
199
209
  const command = s.text()!;
package/src/types.ts CHANGED
@@ -123,15 +123,7 @@ export class TexNode {
123
123
  return tokens;
124
124
  }
125
125
  case 'ordgroup': {
126
- let tokens = this.args!.map((n) => n.serialize()).flat();
127
- if(this.content === 'parenthesis') {
128
- const is_over_high = this.isOverHigh();
129
- const left_delim = is_over_high ? '\\left(' : '(';
130
- const right_delim = is_over_high ? '\\right)' : ')';
131
- tokens.unshift(new TexToken(TexTokenType.ELEMENT, left_delim));
132
- tokens.push(new TexToken(TexTokenType.ELEMENT, right_delim));
133
- }
134
- return tokens;
126
+ return this.args!.map((n) => n.serialize()).flat();
135
127
  }
136
128
  case 'unaryFunc': {
137
129
  let tokens: TexToken[] = [];
@@ -238,34 +230,6 @@ export class TexNode {
238
230
  throw new Error('[TexNode.serialize] Unimplemented type: ' + this.type);
239
231
  }
240
232
  }
241
-
242
- // whether the node is over high so that if it's wrapped in braces, \left and \right should be used.
243
- // e.g. \frac{1}{2} is over high, "2" is not.
244
- public isOverHigh(): boolean {
245
- switch (this.type) {
246
- case 'element':
247
- case 'symbol':
248
- case 'text':
249
- case 'control':
250
- case 'empty':
251
- return false;
252
- case 'binaryFunc':
253
- if(this.content === '\\frac') {
254
- return true;
255
- }
256
- // fall through
257
- case 'unaryFunc':
258
- case 'ordgroup':
259
- return this.args!.some((n) => n.isOverHigh());
260
- case 'supsub': {
261
- return (this.data as TexSupsubData).base.isOverHigh();
262
- }
263
- case 'beginend':
264
- return true;
265
- default:
266
- return false;
267
- }
268
- }
269
233
  }
270
234
 
271
235
  export enum TypstTokenType {
@@ -348,6 +312,10 @@ export interface TypstSupsubData {
348
312
  }
349
313
 
350
314
  export type TypstArrayData = TypstNode[][];
315
+ export interface TypstLrData {
316
+ leftDelim: string | null;
317
+ rightDelim: string | null;
318
+ }
351
319
 
352
320
  type TypstNodeType = 'atom' | 'symbol' | 'text' | 'control' | 'comment' | 'whitespace'
353
321
  | 'empty' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'align' | 'matrix' | 'cases' | 'unknown';
@@ -364,12 +332,12 @@ export class TypstNode {
364
332
  type: TypstNodeType;
365
333
  content: string;
366
334
  args?: TypstNode[];
367
- data?: TypstSupsubData | TypstArrayData;
335
+ data?: TypstSupsubData | TypstArrayData | TypstLrData;
368
336
  // Some Typst functions accept additional options. e.g. mat() has option "delim", op() has option "limits"
369
337
  options?: TypstNamedParams;
370
338
 
371
339
  constructor(type: TypstNodeType, content: string, args?: TypstNode[],
372
- data?: TypstSupsubData | TypstArrayData) {
340
+ data?: TypstSupsubData | TypstArrayData| TypstLrData) {
373
341
  this.type = type;
374
342
  this.content = content;
375
343
  this.args = args;
@@ -384,6 +352,31 @@ export class TypstNode {
384
352
  public eq(other: TypstNode): boolean {
385
353
  return this.type === other.type && this.content === other.content;
386
354
  }
355
+
356
+ // whether the node is over high so that if it's wrapped in braces, \left and \right should be used in its TeX form
357
+ // e.g. 1/2 is over high, "2" is not.
358
+ public isOverHigh(): boolean {
359
+ switch (this.type) {
360
+ case 'fraction':
361
+ return true;
362
+ case 'funcCall': {
363
+ if (this.content === 'frac') {
364
+ return true;
365
+ }
366
+ return this.args!.some((n) => n.isOverHigh());
367
+ }
368
+ case 'group':
369
+ return this.args!.some((n) => n.isOverHigh());
370
+ case 'supsub':
371
+ return (this.data as TypstSupsubData).base.isOverHigh();
372
+ case 'align':
373
+ case 'cases':
374
+ case 'matrix':
375
+ return true;
376
+ default:
377
+ return false;
378
+ }
379
+ }
387
380
  }
388
381
 
389
382
  export interface Tex2TypstOptions {
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { array_find } from "./generic";
3
- import { TYPST_NONE, 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";
@@ -91,8 +91,9 @@ export function tokenize_typst(input: string): TypstToken[] {
91
91
  }
92
92
 
93
93
 
94
- function find_closing_match(tokens: TypstToken[], start: number): number {
95
- assert(tokens[start].isOneOf([LEFT_PARENTHESES, LEFT_BRACKET, LEFT_CURLY_BRACKET]));
94
+ function _find_closing_match(tokens: TypstToken[], start: number,
95
+ leftBrackets: TypstToken[], rightBrackets: TypstToken[]): number {
96
+ assert(tokens[start].isOneOf(leftBrackets));
96
97
  let count = 1;
97
98
  let pos = start + 1;
98
99
 
@@ -100,10 +101,10 @@ function find_closing_match(tokens: TypstToken[], start: number): number {
100
101
  if (pos >= tokens.length) {
101
102
  throw new Error('Unmatched brackets');
102
103
  }
103
- if (tokens[pos].isOneOf([LEFT_PARENTHESES, LEFT_BRACKET, LEFT_CURLY_BRACKET])) {
104
- count += 1;
105
- } else if (tokens[pos].isOneOf([RIGHT_PARENTHESES, RIGHT_BRACKET, RIGHT_CURLY_BRACKET])) {
104
+ if (tokens[pos].isOneOf(rightBrackets)) {
106
105
  count -= 1;
106
+ }else if (tokens[pos].isOneOf(leftBrackets)) {
107
+ count += 1;
107
108
  }
108
109
  pos += 1;
109
110
  }
@@ -111,6 +112,25 @@ function find_closing_match(tokens: TypstToken[], start: number): number {
111
112
  return pos - 1;
112
113
  }
113
114
 
115
+ function find_closing_match(tokens: TypstToken[], start: number): number {
116
+ return _find_closing_match(
117
+ tokens,
118
+ start,
119
+ [LEFT_PARENTHESES, LEFT_BRACKET, LEFT_CURLY_BRACKET],
120
+ [RIGHT_PARENTHESES, RIGHT_BRACKET, RIGHT_CURLY_BRACKET]
121
+ );
122
+ }
123
+
124
+ function find_closing_delim(tokens: TypstToken[], start: number): number {
125
+ return _find_closing_match(
126
+ tokens,
127
+ start,
128
+ [LEFT_PARENTHESES, LEFT_BRACKET, LEFT_CURLY_BRACKET, VERTICAL_BAR],
129
+ [RIGHT_PARENTHESES, RIGHT_BRACKET, RIGHT_CURLY_BRACKET, VERTICAL_BAR]
130
+ );
131
+ }
132
+
133
+
114
134
 
115
135
  function find_closing_parenthesis(nodes: TypstNode[], start: number): number {
116
136
  const left_parenthesis = new TypstNode('atom', '(');
@@ -261,6 +281,7 @@ const LEFT_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '[');
261
281
  const RIGHT_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ']');
262
282
  const LEFT_CURLY_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '{');
263
283
  const RIGHT_CURLY_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '}');
284
+ const VERTICAL_BAR = new TypstToken(TypstTokenType.ELEMENT, '|');
264
285
  const COMMA = new TypstToken(TypstTokenType.ELEMENT, ',');
265
286
  const SEMICOLON = new TypstToken(TypstTokenType.ELEMENT, ';');
266
287
  const SINGLE_SPACE = new TypstToken(TypstTokenType.SPACE, ' ');
@@ -389,9 +410,13 @@ export class TypstParser {
389
410
  casesNode.setOptions(named_params);
390
411
  return [casesNode, newPos];
391
412
  }
413
+ if (firstToken.value === 'lr') {
414
+ const [args, newPos, lrData] = this.parseLrArguments(tokens, start + 1);
415
+ const func_call = new TypstNode('funcCall', firstToken.value, args, lrData);
416
+ return [func_call, newPos];
417
+ }
392
418
  const [args, newPos] = this.parseArguments(tokens, start + 1);
393
- const func_call = new TypstNode('funcCall', firstToken.value);
394
- func_call.args = args;
419
+ const func_call = new TypstNode('funcCall', firstToken.value, args);
395
420
  return [func_call, newPos];
396
421
  }
397
422
  }
@@ -405,6 +430,28 @@ export class TypstParser {
405
430
  return [this.parseCommaSeparatedArguments(tokens, start + 1, end), end + 1];
406
431
  }
407
432
 
433
+ // start: the position of the left parentheses
434
+ parseLrArguments(tokens: TypstToken[], start: number): [TypstNode[], number, TypstLrData] {
435
+ if (tokens[start + 1].isOneOf([LEFT_PARENTHESES, LEFT_BRACKET, LEFT_CURLY_BRACKET, VERTICAL_BAR])) {
436
+ const end = find_closing_match(tokens, start);
437
+ const inner_start = start + 1;
438
+ const inner_end = find_closing_delim(tokens, inner_start);
439
+ const inner_args= this.parseCommaSeparatedArguments(tokens, inner_start + 1, inner_end);
440
+ return [
441
+ inner_args,
442
+ end + 1,
443
+ {leftDelim: tokens[inner_start].value, rightDelim: tokens[inner_end].value} as TypstLrData
444
+ ];
445
+ } else {
446
+ const [args, end] = this.parseArguments(tokens, start);
447
+ return [
448
+ args,
449
+ end,
450
+ {leftDelim: null, rightDelim: null} as TypstLrData,
451
+ ];
452
+ }
453
+ }
454
+
408
455
  // start: the position of the left parentheses
409
456
  parseGroupsOfArguments(tokens: TypstToken[], start: number, newline_token = SEMICOLON): [TypstNode[][], TypstNamedParams, number] {
410
457
  const end = find_closing_match(tokens, start);
@@ -472,7 +519,7 @@ export class TypstParser {
472
519
  pos = next_stop + 1;
473
520
  }
474
521
  }
475
-
522
+
476
523
  return [matrix, named_params, end + 1];
477
524
  }
478
525
 
@@ -481,8 +528,7 @@ export class TypstParser {
481
528
  const args: TypstNode[] = [];
482
529
  let pos = start;
483
530
  while (pos < end) {
484
- let arg = new TypstNode('group', '', []);
485
-
531
+ let nodes: TypstNode[] = [];
486
532
  while(pos < end) {
487
533
  if(tokens[pos].eq(COMMA)) {
488
534
  pos += 1;
@@ -493,14 +539,18 @@ export class TypstParser {
493
539
  }
494
540
  const [argItem, newPos] = this.parseNextExpr(tokens, pos);
495
541
  pos = newPos;
496
- arg.args!.push(argItem);
542
+ nodes.push(argItem);
497
543
  }
498
544
 
499
- if(arg.args!.length === 0) {
545
+ let arg: TypstNode;
546
+ if (nodes.length === 0) {
500
547
  arg = TYPST_EMPTY_NODE;
501
- } else if (arg.args!.length === 1) {
502
- arg = arg.args![0];
548
+ } else if (nodes.length === 1) {
549
+ arg = nodes[0];
550
+ } else {
551
+ arg = process_operators(nodes);
503
552
  }
553
+
504
554
  args.push(arg);
505
555
  }
506
556
  return args;