tex2typst 0.3.23 → 0.3.25

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/generic.ts CHANGED
@@ -2,25 +2,48 @@ interface IEquatable {
2
2
  eq(other: IEquatable): boolean;
3
3
  }
4
4
 
5
+ export function array_equal<T extends IEquatable>(a: T[], b: T[]): boolean {
6
+ /*
7
+ if (a.length !== b.length) {
8
+ return false;
9
+ }
10
+ for (let i = 0; i < a.length; i++) {
11
+ if (!a[i].eq(b[i])) {
12
+ return false;
13
+ }
14
+ }
15
+ return true;
16
+ */
17
+ return a.length === b.length && a.every((x, i) => x.eq(b[i]));
18
+ }
5
19
 
6
20
  export function array_find<T extends IEquatable>(array: T[], item: T, start: number = 0): number {
21
+ /*
7
22
  for (let i = start; i < array.length; i++) {
8
23
  if (array[i].eq(item)) {
9
24
  return i;
10
25
  }
11
26
  }
12
27
  return -1;
28
+ */
29
+ const index = array.slice(start).findIndex((x) => x.eq(item));
30
+ return index === -1 ? -1 : index + start;
13
31
  }
14
32
 
15
33
  export function array_includes<T extends IEquatable>(array: T[], item: T): boolean {
16
- for (const i of array) {
17
- if (i.eq(item)) {
34
+ /*
35
+ for (const x of array) {
36
+ if (x.eq(item)) {
18
37
  return true;
19
38
  }
20
39
  }
21
40
  return false;
41
+ */
42
+ return array.some((x) => x.eq(item));
22
43
  }
23
44
 
45
+ // e.g. input array=['a', 'b', '+', 'c', '+', 'd', 'e'], sep = '+'
46
+ // return [['a', 'b'], ['c'], ['d', 'e']]
24
47
  export function array_split<T extends IEquatable>(array: T[], sep: T): T[][] {
25
48
  const res: T[][] = [];
26
49
  let current_slice: T[] = [];
@@ -39,6 +62,7 @@ export function array_split<T extends IEquatable>(array: T[], sep: T): T[][] {
39
62
  // e.g. input array=['a', 'b', 'c'], sep = '+'
40
63
  // return ['a','+', 'b', '+','c']
41
64
  export function array_intersperse<T>(array: T[], sep: T): T[] {
65
+ /*
42
66
  const res: T[] = [];
43
67
  for (let i = 0; i < array.length; i++) {
44
68
  res.push(array[i]);
@@ -47,4 +71,6 @@ export function array_intersperse<T>(array: T[], sep: T): T[] {
47
71
  }
48
72
  }
49
73
  return res;
74
+ */
75
+ return array.flatMap((x, i) => i !== array.length - 1? [x, sep]: [x]);
50
76
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseTex } from "./tex-parser";
2
- import type { Tex2TypstOptions } from "./types";
2
+ import type { Tex2TypstOptions } from "./tex-types";
3
3
  import { TypstWriter, type TypstWriterOptions } from "./typst-writer";
4
4
  import { convert_tex_node_to_typst, convert_typst_node_to_tex } from "./convert";
5
5
  import { symbolMap } from "./map";
package/src/map.ts CHANGED
@@ -5,6 +5,7 @@ const symbolMap = new Map<string, string>([
5
5
  ['|', 'bar.v.double'],
6
6
  [',', 'thin'],
7
7
  [':', 'med'],
8
+ [' ', 'med'],
8
9
  [';', 'thick'],
9
10
 
10
11
  /* textual operators */
@@ -21,6 +22,10 @@ const symbolMap = new Map<string, string>([
21
22
  ['Xi', 'Xi'],
22
23
  ['Upsilon', 'Upsilon'],
23
24
  ['lim', 'lim'],
25
+ ['binom', 'binom'],
26
+ ['tilde', 'tilde'],
27
+ ['hat', 'hat'],
28
+ ['sqrt', 'sqrt'],
24
29
 
25
30
  ['nonumber', ''],
26
31
  ['vec', 'arrow'],
package/src/tex-parser.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { symbolMap } from "./map";
2
- import { TexNode, TexSupsubData, TexToken, TexTokenType } from "./types";
1
+ import { TexBeginEnd, TexFuncCall, TexLeftRight, TexNode, TexGroup, TexSupSub, TexSupsubData, TexText, TexToken, TexTokenType } from "./tex-types";
3
2
  import { assert } from "./util";
4
3
  import { array_find } from "./generic";
5
4
  import { TEX_BINARY_COMMANDS, TEX_UNARY_COMMANDS, tokenize_tex } from "./tex-tokenizer";
@@ -11,7 +10,7 @@ const IGNORED_COMMANDS = [
11
10
  'Biggl', 'Biggr',
12
11
  ];
13
12
 
14
- const EMPTY_NODE: TexNode = new TexNode('empty', '');
13
+ const EMPTY_NODE: TexNode = TexToken.EMPTY.toNode();
15
14
 
16
15
  function get_command_param_num(command: string): number {
17
16
  if (TEX_UNARY_COMMANDS.includes(command)) {
@@ -120,7 +119,8 @@ export class LatexParser {
120
119
  }
121
120
 
122
121
  parse(tokens: TexToken[]): TexNode {
123
- const idx = array_find(tokens, new TexToken(TexTokenType.COMMAND, '\\displaystyle'));
122
+ const token_displaystyle = new TexToken(TexTokenType.COMMAND, '\\displaystyle');
123
+ const idx = array_find(tokens, token_displaystyle);
124
124
  if (idx === -1) {
125
125
  // no \displaystyle, normal execution path
126
126
  const [tree, _] = this.parseGroup(tokens, 0, tokens.length);
@@ -128,13 +128,13 @@ export class LatexParser {
128
128
  } else if (idx === 0) {
129
129
  // \displaystyle at the beginning. Wrap the whole thing in \displaystyle
130
130
  const [tree, _] = this.parseGroup(tokens, 1, tokens.length);
131
- return new TexNode('unaryFunc', '\\displaystyle', [tree]);
131
+ return new TexFuncCall(token_displaystyle, [tree]);
132
132
  } else {
133
133
  // \displaystyle somewhere in the middle. Split the expression to two parts
134
134
  const [tree1, _1] = this.parseGroup(tokens, 0, idx);
135
135
  const [tree2, _2] = this.parseGroup(tokens, idx + 1, tokens.length);
136
- const display = new TexNode('unaryFunc', '\\displaystyle', [tree2]);
137
- return new TexNode('ordgroup', '', [tree1, display]);
136
+ const display = new TexFuncCall(token_displaystyle, [tree2]);
137
+ return new TexGroup([tree1, display]);
138
138
  }
139
139
  }
140
140
 
@@ -144,15 +144,15 @@ export class LatexParser {
144
144
  while (pos < end) {
145
145
  const [res, newPos] = this.parseNextExpr(tokens, pos);
146
146
  pos = newPos;
147
- if(res.type === 'whitespace') {
148
- if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
147
+ if(res.head.type === TexTokenType.SPACE || res.head.type === TexTokenType.NEWLINE) {
148
+ if (!this.space_sensitive && res.head.value.replace(/ /g, '').length === 0) {
149
149
  continue;
150
150
  }
151
- if (!this.newline_sensitive && res.content === '\n') {
151
+ if (!this.newline_sensitive && res.head.value === '\n') {
152
152
  continue;
153
153
  }
154
154
  }
155
- if (res.type === 'control' && res.content === '&') {
155
+ if (res.head.eq(new TexToken(TexTokenType.CONTROL, '&'))) {
156
156
  throw new LatexParserError('Unexpected & outside of an alignment');
157
157
  }
158
158
  results.push(res);
@@ -162,7 +162,7 @@ export class LatexParser {
162
162
  if (results.length === 1) {
163
163
  node = results[0];
164
164
  } else {
165
- node = new TexNode('ordgroup', '', results);
165
+ node = new TexGroup(results);
166
166
  }
167
167
  return [node, end + 1];
168
168
  }
@@ -199,25 +199,23 @@ export class LatexParser {
199
199
  }
200
200
 
201
201
  if (sub !== null || sup !== null || num_prime > 0) {
202
- const res: TexSupsubData = { base };
202
+ const res: TexSupsubData = { base, sup: null, sub: null };
203
203
  if (sub) {
204
204
  res.sub = sub;
205
205
  }
206
206
  if (num_prime > 0) {
207
- res.sup = new TexNode('ordgroup', '', []);
207
+ const items: TexNode[] = [];
208
208
  for (let i = 0; i < num_prime; i++) {
209
- res.sup.args!.push(new TexNode('element', "'"));
209
+ items.push(new TexToken(TexTokenType.ELEMENT, "'").toNode());
210
210
  }
211
211
  if (sup) {
212
- res.sup.args!.push(sup);
213
- }
214
- if (res.sup.args!.length === 1) {
215
- res.sup = res.sup.args![0];
212
+ items.push(sup);
216
213
  }
214
+ res.sup = items.length === 1 ? items[0] : new TexGroup(items);
217
215
  } else if (sup) {
218
216
  res.sup = sup;
219
217
  }
220
- return [new TexNode('supsub', '', [], res), pos];
218
+ return [new TexSupSub(res), pos];
221
219
  } else {
222
220
  return [base, pos];
223
221
  }
@@ -230,14 +228,11 @@ export class LatexParser {
230
228
  const firstToken = tokens[start];
231
229
  switch (firstToken.type) {
232
230
  case TexTokenType.ELEMENT:
233
- return [new TexNode('element', firstToken.value), start + 1];
234
231
  case TexTokenType.LITERAL:
235
- return [new TexNode('literal', firstToken.value), start + 1];
236
232
  case TexTokenType.COMMENT:
237
- return [new TexNode('comment', firstToken.value), start + 1];
238
233
  case TexTokenType.SPACE:
239
234
  case TexTokenType.NEWLINE:
240
- return [new TexNode('whitespace', firstToken.value), start + 1];
235
+ return [firstToken.toNode(), start + 1];
241
236
  case TexTokenType.COMMAND:
242
237
  const commandName = firstToken.value.slice(1);
243
238
  if (IGNORED_COMMANDS.includes(commandName)) {
@@ -266,14 +261,14 @@ export class LatexParser {
266
261
  case '\\,':
267
262
  case '\\:':
268
263
  case '\\;':
269
- return [new TexNode('control', controlChar), start + 1];
264
+ return [firstToken.toNode(), start + 1];
270
265
  case '\\ ':
271
- return [new TexNode('control', '\\:'), start + 1];
266
+ return [firstToken.toNode(), start + 1];
272
267
  case '_':
273
268
  case '^':
274
269
  return [ EMPTY_NODE, start];
275
270
  case '&':
276
- return [new TexNode('control', '&'), start + 1];
271
+ return [firstToken.toNode(), start + 1];
277
272
  default:
278
273
  throw new LatexParserError('Unknown control sequence');
279
274
  }
@@ -285,7 +280,8 @@ export class LatexParser {
285
280
  parseCommandExpr(tokens: TexToken[], start: number): ParseResult {
286
281
  assert(tokens[start].type === TexTokenType.COMMAND);
287
282
 
288
- const command = tokens[start].value; // command name starts with a \
283
+ const command_token = tokens[start];
284
+ const command = command_token.value; // command name starts with a \
289
285
 
290
286
  let pos = start + 1;
291
287
 
@@ -297,10 +293,7 @@ export class LatexParser {
297
293
  const paramNum = get_command_param_num(command.slice(1));
298
294
  switch (paramNum) {
299
295
  case 0:
300
- if (!symbolMap.has(command.slice(1))) {
301
- return [new TexNode('unknownMacro', command), pos];
302
- }
303
- return [new TexNode('symbol', command), pos];
296
+ return [command_token.toNode(), pos];
304
297
  case 1: {
305
298
  // TODO: JavaScript gives undefined instead of throwing an error when accessing an index out of bounds,
306
299
  // so index checking like this should be everywhere. This is rough.
@@ -315,7 +308,7 @@ export class LatexParser {
315
308
  }
316
309
  const [exponent, _] = this.parseGroup(tokens, posLeftSquareBracket + 1, posRightSquareBracket);
317
310
  const [arg1, newPos] = this.parseNextArg(tokens, posRightSquareBracket + 1);
318
- return [new TexNode('unaryFunc', command, [arg1], exponent), newPos];
311
+ return [new TexFuncCall(command_token, [arg1], exponent), newPos];
319
312
  } else if (command === '\\text') {
320
313
  if (pos + 2 >= tokens.length) {
321
314
  throw new LatexParserError('Expecting content for \\text command');
@@ -323,16 +316,16 @@ export class LatexParser {
323
316
  assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
324
317
  assert(tokens[pos + 1].type === TexTokenType.LITERAL);
325
318
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
326
- const text = tokens[pos + 1].value;
327
- return [new TexNode('text', text), pos + 3];
319
+ const literal = tokens[pos + 1];
320
+ return [new TexText(literal), pos + 3];
328
321
  }
329
322
  let [arg1, newPos] = this.parseNextArg(tokens, pos);
330
- return [new TexNode('unaryFunc', command, [arg1]), newPos];
323
+ return [new TexFuncCall(command_token, [arg1]), newPos];
331
324
  }
332
325
  case 2: {
333
326
  const [arg1, pos1] = this.parseNextArg(tokens, pos);
334
327
  const [arg2, pos2] = this.parseNextArg(tokens, pos1);
335
- return [new TexNode('binaryFunc', command, [arg1, arg2]), pos2];
328
+ return [new TexFuncCall(command_token, [arg1, arg2]), pos2];
336
329
  }
337
330
  default:
338
331
  throw new Error( 'Invalid number of parameters');
@@ -342,9 +335,9 @@ export class LatexParser {
342
335
  /*
343
336
  Extract a non-space argument from the token stream.
344
337
  So that `\frac{12} 3` is parsed as
345
- TexCommand{ content: '\frac', args: ['12', '3'] }
338
+ TypstFuncCall{ head: '\frac', args: [ELEMENT_12, ELEMENT_3] }
346
339
  rather than
347
- TexCommand{ content: '\frac', args: ['12', ' '] }, TexElement{ content: '3' }
340
+ TypstFuncCall{ head: '\frac', args: [ELEMENT_12, SPACE] }, ELEMENT_3
348
341
  */
349
342
  parseNextArg(tokens: TexToken[], start: number): ParseResult {
350
343
  let pos = start;
@@ -352,7 +345,7 @@ export class LatexParser {
352
345
  while (pos < tokens.length) {
353
346
  let node: TexNode;
354
347
  [node, pos] = this.parseNextExprWithoutSupSub(tokens, pos);
355
- if (node.type !== 'whitespace') {
348
+ if (!(node.head.type === TexTokenType.SPACE || node.head.type === TexTokenType.NEWLINE)) {
356
349
  arg = node;
357
350
  break;
358
351
  }
@@ -398,12 +391,9 @@ export class LatexParser {
398
391
  pos++;
399
392
 
400
393
  const [body, _] = this.parseGroup(tokens, exprInsideStart, exprInsideEnd);
401
- const args: TexNode[] = [
402
- new TexNode('element', leftDelimiter.value),
403
- body,
404
- new TexNode('element', rightDelimiter.value)
405
- ]
406
- const res = new TexNode('leftright', '', args);
394
+ const left = leftDelimiter.value === '.'? null: leftDelimiter;
395
+ const right = rightDelimiter.value === '.'? null: rightDelimiter;
396
+ const res = new TexLeftRight({body: body, left: left, right: right});
407
397
  return [res, pos];
408
398
  }
409
399
 
@@ -418,14 +408,10 @@ export class LatexParser {
418
408
  pos += 3;
419
409
 
420
410
 
421
- const args: TexNode[] = [];
411
+ let data: TexNode | null = null;
422
412
  if(['array', 'subarray'].includes(envName)) {
423
- if (pos >= tokens.length || !tokens[pos].eq(LEFT_CURLY_BRACKET)) {
424
- throw new LatexParserError(`Missing arg for \\begin{${envName}}`);
425
- }
426
- const [arg, newPos] = this.parseNextArg(tokens, pos);
427
- args.push(arg);
428
- pos = newPos;
413
+ pos += eat_whitespaces(tokens, pos).length;
414
+ [data, pos] = this.parseNextArg(tokens, pos);
429
415
  }
430
416
 
431
417
  pos += eat_whitespaces(tokens, pos).length; // ignore whitespaces and '\n' after \begin{envName}
@@ -454,7 +440,7 @@ export class LatexParser {
454
440
  exprInside.pop();
455
441
  }
456
442
  const body = this.parseAligned(exprInside);
457
- const res = new TexNode('beginend', envName, args, body);
443
+ const res = new TexBeginEnd(new TexToken(TexTokenType.LITERAL, envName), body, data);
458
444
  return [res, pos];
459
445
  }
460
446
 
@@ -463,32 +449,32 @@ export class LatexParser {
463
449
  const allRows: TexNode[][] = [];
464
450
  let row: TexNode[] = [];
465
451
  allRows.push(row);
466
- let group = new TexNode('ordgroup', '', []);
452
+ let group = new TexGroup([]);
467
453
  row.push(group);
468
454
 
469
455
  while (pos < tokens.length) {
470
456
  const [res, newPos] = this.parseNextExpr(tokens, pos);
471
457
  pos = newPos;
472
458
 
473
- if (res.type === 'whitespace') {
474
- if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
459
+ if (res.head.type === TexTokenType.SPACE || res.head.type === TexTokenType.NEWLINE) {
460
+ if (!this.space_sensitive && res.head.value.replace(/ /g, '').length === 0) {
475
461
  continue;
476
462
  }
477
- if (!this.newline_sensitive && res.content === '\n') {
463
+ if (!this.newline_sensitive && res.head.value === '\n') {
478
464
  continue;
479
465
  }
480
466
  }
481
467
 
482
- if (res.type === 'control' && res.content === '\\\\') {
468
+ if (res.head.eq(new TexToken(TexTokenType.CONTROL, '\\\\'))) {
483
469
  row = [];
484
- group = new TexNode('ordgroup', '', []);
470
+ group = new TexGroup([]);
485
471
  row.push(group);
486
472
  allRows.push(row);
487
- } else if (res.type === 'control' && res.content === '&') {
488
- group = new TexNode('ordgroup', '', []);
473
+ } else if (res.head.eq(new TexToken(TexTokenType.CONTROL, '&'))) {
474
+ group = new TexGroup([]);
489
475
  row.push(group);
490
476
  } else {
491
- group.args!.push(res);
477
+ group.items.push(res);
492
478
  }
493
479
  }
494
480
  return allRows;
@@ -1,4 +1,4 @@
1
- import { TexToken, TexTokenType } from "./types";
1
+ import { TexToken, TexTokenType } from "./tex-types";
2
2
  import { JSLex, Scanner } from "./jslex";
3
3
 
4
4
  export const TEX_UNARY_COMMANDS = [
@@ -46,6 +46,7 @@ export const TEX_BINARY_COMMANDS = [
46
46
  'tbinom',
47
47
  'overset',
48
48
  'underset',
49
+ 'textcolor',
49
50
  ]
50
51
 
51
52
 
@@ -58,7 +59,7 @@ function unescape(str: string): string {
58
59
  }
59
60
 
60
61
  const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[]>([
61
- // math `\begin{array}{cc}`
62
+ // match `\begin{array}{cc}`
62
63
  [
63
64
  String.raw`\\begin{(array|subarry)}{(.+?)}`, (s) => {
64
65
  const match = s.reMatchArray()!;
@@ -74,7 +75,7 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
74
75
  }
75
76
  ],
76
77
  [
77
- String.raw`\\(text|operatorname|begin|end|hspace|array){(.+?)}`, (s) => {
78
+ String.raw`\\(text|operatorname|textcolor|begin|end|hspace|array){(.+?)}`, (s) => {
78
79
  const match = s.reMatchArray()!;
79
80
  return [
80
81
  new TexToken(TexTokenType.COMMAND, '\\' + match[1]),