tex2typst 0.3.27 → 0.3.29

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/index.ts CHANGED
@@ -17,7 +17,6 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
17
17
  fracToSlash: true,
18
18
  inftyToOo: false,
19
19
  optimize: true,
20
- nonAsciiWrapper: "",
21
20
  customTexMacros: {}
22
21
  };
23
22
 
package/src/map.ts CHANGED
@@ -56,6 +56,7 @@ const symbolMap = new Map<string, string>([
56
56
  ['mathbb', 'bb'],
57
57
  ['mathbf', 'bold'],
58
58
  ['mathcal', 'cal'],
59
+ ['mathscr', 'scr'],
59
60
  ['mathit', 'italic'],
60
61
  ['mathfrak', 'frak'],
61
62
  ['mathrm', 'upright'],
@@ -260,7 +261,6 @@ const symbolMap = new Map<string, string>([
260
261
  ['intop', 'limits(integral)'],
261
262
 
262
263
  // extended
263
- ['mathscr', 'scr'],
264
264
  ['LaTeX', '#LaTeX'],
265
265
  ['TeX', '#TeX'],
266
266
  ]);
package/src/tex-parser.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { TexBeginEnd, TexFuncCall, TexLeftRight, TexNode, TexGroup, TexSupSub, TexSupsubData, TexText, TexToken, TexTokenType } from "./tex-types";
2
2
  import { assert } from "./util";
3
- import { array_find } from "./generic";
3
+ import { array_find, array_join } from "./generic";
4
4
  import { TEX_BINARY_COMMANDS, TEX_UNARY_COMMANDS, tokenize_tex } from "./tex-tokenizer";
5
5
 
6
6
  const IGNORED_COMMANDS = [
@@ -42,7 +42,7 @@ function eat_parenthesis(tokens: TexToken[], start: number): TexToken | null {
42
42
  const firstToken = tokens[start];
43
43
  if (firstToken.type === TexTokenType.ELEMENT && ['(', ')', '[', ']', '|', '\\{', '\\}', '.', '\\|'].includes(firstToken.value)) {
44
44
  return firstToken;
45
- } else if (firstToken.type === TexTokenType.COMMAND && ['lfloor', 'rfloor', 'lceil', 'rceil', 'langle', 'rangle'].includes(firstToken.value.slice(1))) {
45
+ } else if (firstToken.type === TexTokenType.COMMAND && ['lfloor', 'rfloor', 'lceil', 'rceil', 'langle', 'rangle', 'lparen', 'rparen', 'lbrace', 'rbrace'].includes(firstToken.value.slice(1))) {
46
46
  return firstToken;
47
47
  } else {
48
48
  return null;
@@ -58,49 +58,29 @@ function eat_primes(tokens: TexToken[], start: number): number {
58
58
  }
59
59
 
60
60
 
61
- function find_closing_match(tokens: TexToken[], start: number, leftToken: TexToken, rightToken: TexToken): number {
62
- assert(tokens[start].eq(leftToken));
63
- let count = 1;
64
- let pos = start + 1;
65
-
66
- while (count > 0) {
67
- if (pos >= tokens.length) {
68
- return -1;
69
- }
70
- if (tokens[pos].eq(leftToken)) {
71
- count += 1;
72
- } else if (tokens[pos].eq(rightToken)) {
73
- count -= 1;
74
- }
75
- pos += 1;
76
- }
77
-
78
- return pos - 1;
79
- }
80
-
81
61
 
82
62
  const LEFT_COMMAND: TexToken = new TexToken(TexTokenType.COMMAND, '\\left');
83
63
  const RIGHT_COMMAND: TexToken = new TexToken(TexTokenType.COMMAND, '\\right');
84
64
 
85
- function find_closing_right_command(tokens: TexToken[], start: number): number {
86
- return find_closing_match(tokens, start, LEFT_COMMAND, RIGHT_COMMAND);
87
- }
88
-
89
-
90
65
  const BEGIN_COMMAND: TexToken = new TexToken(TexTokenType.COMMAND, '\\begin');
91
66
  const END_COMMAND: TexToken = new TexToken(TexTokenType.COMMAND, '\\end');
92
67
 
93
-
94
- function find_closing_end_command(tokens: TexToken[], start: number): number {
95
- return find_closing_match(tokens, start, BEGIN_COMMAND, END_COMMAND);
96
- }
97
-
68
+ const CONTROL_LINEBREAK = new TexToken(TexTokenType.CONTROL, '\\\\');
98
69
 
99
70
  export class LatexParserError extends Error {
100
71
  constructor(message: string) {
101
72
  super(message);
102
73
  this.name = 'LatexParserError';
103
74
  }
75
+
76
+ static readonly UNMATCHED_LEFT_BRACE = new LatexParserError("Unmatched '\\{'");
77
+ static readonly UNMATCHED_RIGHT_BRACE = new LatexParserError("Unmatched '\\}'");
78
+ static readonly UNMATCHED_LEFT_BRACKET = new LatexParserError("Unmatched '\\['");
79
+ static readonly UNMATCHED_RIGHT_BRACKET = new LatexParserError("Unmatched '\\]'");
80
+ static readonly UNMATCHED_COMMAND_BEGIN = new LatexParserError("Unmatched '\\begin'");
81
+ static readonly UNMATCHED_COMMAND_END = new LatexParserError("Unmatched '\\end'");
82
+ static readonly UNMATCHED_COMMAND_LEFT = new LatexParserError("Unmatched '\\left'");
83
+ static readonly UNMATCHED_COMMAND_RIGHT = new LatexParserError("Unmatched '\\right'");
104
84
  }
105
85
 
106
86
 
@@ -110,8 +90,12 @@ const SUB_SYMBOL:TexToken = new TexToken(TexTokenType.CONTROL, '_');
110
90
  const SUP_SYMBOL:TexToken = new TexToken(TexTokenType.CONTROL, '^');
111
91
 
112
92
  export class LatexParser {
113
- space_sensitive: boolean;
114
- newline_sensitive: boolean;
93
+ public space_sensitive: boolean;
94
+ public newline_sensitive: boolean;
95
+
96
+ // how many levels of \begin{...} \end{...} are we currently in
97
+ public alignmentDepth: number = 0;
98
+
115
99
 
116
100
  constructor(space_sensitive: boolean = false, newline_sensitive: boolean = true) {
117
101
  this.space_sensitive = space_sensitive;
@@ -123,25 +107,35 @@ export class LatexParser {
123
107
  const idx = array_find(tokens, token_displaystyle);
124
108
  if (idx === -1) {
125
109
  // no \displaystyle, normal execution path
126
- const [tree, _] = this.parseGroup(tokens, 0, tokens.length);
127
- return tree;
110
+ return this.parseGroup(tokens.slice(0));
128
111
  } else if (idx === 0) {
129
112
  // \displaystyle at the beginning. Wrap the whole thing in \displaystyle
130
- const [tree, _] = this.parseGroup(tokens, 1, tokens.length);
113
+ const tree = this.parseGroup(tokens.slice(1));
131
114
  return new TexFuncCall(token_displaystyle, [tree]);
132
115
  } else {
133
116
  // \displaystyle somewhere in the middle. Split the expression to two parts
134
- const [tree1, _1] = this.parseGroup(tokens, 0, idx);
135
- const [tree2, _2] = this.parseGroup(tokens, idx + 1, tokens.length);
117
+ const tree1 = this.parseGroup(tokens.slice(0, idx));
118
+ const tree2 = this.parseGroup(tokens.slice(idx + 1, tokens.length));
136
119
  const display = new TexFuncCall(token_displaystyle, [tree2]);
137
120
  return new TexGroup([tree1, display]);
138
121
  }
139
122
  }
140
123
 
141
- parseGroup(tokens: TexToken[], start: number, end: number): ParseResult {
124
+ parseGroup(tokens: TexToken[]): TexNode {
125
+ const [tree, _] = this.parseClosure(tokens, 0, null);
126
+ return tree;
127
+ }
128
+
129
+ // return pos: (position of closingToken) + 1
130
+ // pos will be -1 if closingToken is not found
131
+ parseClosure(tokens: TexToken[], start: number, closingToken: TexToken | null): ParseResult {
142
132
  const results: TexNode[] = [];
143
133
  let pos = start;
144
- while (pos < end) {
134
+ while (pos < tokens.length) {
135
+ if (closingToken !== null && tokens[pos].eq(closingToken)) {
136
+ break;
137
+ }
138
+
145
139
  const [res, newPos] = this.parseNextExpr(tokens, pos);
146
140
  pos = newPos;
147
141
  if(res.head.type === TexTokenType.SPACE || res.head.type === TexTokenType.NEWLINE) {
@@ -152,11 +146,11 @@ export class LatexParser {
152
146
  continue;
153
147
  }
154
148
  }
155
- if (res.head.eq(new TexToken(TexTokenType.CONTROL, '&'))) {
156
- throw new LatexParserError('Unexpected & outside of an alignment');
157
- }
158
149
  results.push(res);
159
150
  }
151
+ if (pos >= tokens.length && closingToken !== null) {
152
+ return [EMPTY_NODE, -1];
153
+ }
160
154
 
161
155
  let node: TexNode;
162
156
  if (results.length === 1) {
@@ -164,7 +158,7 @@ export class LatexParser {
164
158
  } else {
165
159
  node = new TexGroup(results);
166
160
  }
167
- return [node, end + 1];
161
+ return [node, pos + 1];
168
162
  }
169
163
 
170
164
  parseNextExpr(tokens: TexToken[], start: number): ParseResult {
@@ -175,25 +169,28 @@ export class LatexParser {
175
169
 
176
170
  num_prime += eat_primes(tokens, pos);
177
171
  pos += num_prime;
178
- if (pos < tokens.length && tokens[pos].eq(SUB_SYMBOL)) {
179
- [sub, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
180
- num_prime += eat_primes(tokens, pos);
181
- pos += num_prime;
182
- if (pos < tokens.length && tokens[pos].eq(SUP_SYMBOL)) {
172
+ if (pos < tokens.length) {
173
+ const next_token = tokens[pos];
174
+ if (next_token.eq(SUB_SYMBOL)) {
175
+ [sub, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
176
+ num_prime += eat_primes(tokens, pos);
177
+ pos += num_prime;
178
+ if (pos < tokens.length && tokens[pos].eq(SUP_SYMBOL)) {
179
+ [sup, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
180
+ if (eat_primes(tokens, pos) > 0) {
181
+ throw new LatexParserError('Double superscript');
182
+ }
183
+ }
184
+ } else if (next_token.eq(SUP_SYMBOL)) {
183
185
  [sup, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
184
186
  if (eat_primes(tokens, pos) > 0) {
185
187
  throw new LatexParserError('Double superscript');
186
188
  }
187
- }
188
- } else if (pos < tokens.length && tokens[pos].eq(SUP_SYMBOL)) {
189
- [sup, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
190
- if (eat_primes(tokens, pos) > 0) {
191
- throw new LatexParserError('Double superscript');
192
- }
193
- if (pos < tokens.length && tokens[pos].eq(SUB_SYMBOL)) {
194
- [sub, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
195
- if (eat_primes(tokens, pos) > 0) {
196
- throw new LatexParserError('Double superscript');
189
+ if (pos < tokens.length && tokens[pos].eq(SUB_SYMBOL)) {
190
+ [sub, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
191
+ if (eat_primes(tokens, pos) > 0) {
192
+ throw new LatexParserError('Double superscript');
193
+ }
197
194
  }
198
195
  }
199
196
  }
@@ -223,7 +220,7 @@ export class LatexParser {
223
220
 
224
221
  parseNextExprWithoutSupSub(tokens: TexToken[], start: number): ParseResult {
225
222
  if (start >= tokens.length) {
226
- return [EMPTY_NODE, start];
223
+ throw new LatexParserError("Unexpected end of input");
227
224
  }
228
225
  const firstToken = tokens[start];
229
226
  switch (firstToken.type) {
@@ -240,8 +237,12 @@ export class LatexParser {
240
237
  }
241
238
  if (firstToken.eq(BEGIN_COMMAND)) {
242
239
  return this.parseBeginEndExpr(tokens, start);
240
+ } else if(firstToken.eq(END_COMMAND)) {
241
+ throw LatexParserError.UNMATCHED_COMMAND_END;
243
242
  } else if (firstToken.eq(LEFT_COMMAND)) {
244
243
  return this.parseLeftRightExpr(tokens, start);
244
+ } else if (firstToken.eq(RIGHT_COMMAND)) {
245
+ throw LatexParserError.UNMATCHED_COMMAND_RIGHT;
245
246
  } else {
246
247
  return this.parseCommandExpr(tokens, start);
247
248
  }
@@ -249,13 +250,13 @@ export class LatexParser {
249
250
  const controlChar = firstToken.value;
250
251
  switch (controlChar) {
251
252
  case '{':
252
- const posClosingBracket = find_closing_match(tokens, start, LEFT_CURLY_BRACKET, RIGHT_CURLY_BRACKET);
253
- if(posClosingBracket === -1) {
254
- throw new LatexParserError("Unmatched '{'");
253
+ const [group, newPos] = this.parseClosure(tokens, start + 1, RIGHT_CURLY_BRACKET);
254
+ if (newPos === -1) {
255
+ throw LatexParserError.UNMATCHED_LEFT_BRACE;
255
256
  }
256
- return this.parseGroup(tokens, start + 1, posClosingBracket);
257
+ return [group, newPos];
257
258
  case '}':
258
- throw new LatexParserError("Unmatched '}'");
259
+ throw LatexParserError.UNMATCHED_RIGHT_BRACE;
259
260
  case '\\\\':
260
261
  case '\\!':
261
262
  case '\\,':
@@ -266,8 +267,12 @@ export class LatexParser {
266
267
  return [firstToken.toNode(), start + 1];
267
268
  case '_':
268
269
  case '^':
270
+ // e.g. "_1" or "^2" are valid LaTeX math expressions
269
271
  return [ EMPTY_NODE, start];
270
272
  case '&':
273
+ if (this.alignmentDepth <= 0) {
274
+ throw new LatexParserError('Unexpected & outside of an alignment');
275
+ }
271
276
  return [firstToken.toNode(), start + 1];
272
277
  default:
273
278
  throw new LatexParserError('Unknown control sequence');
@@ -285,11 +290,6 @@ export class LatexParser {
285
290
 
286
291
  let pos = start + 1;
287
292
 
288
- if (['left', 'right', 'begin', 'end'].includes(command.slice(1))) {
289
- throw new LatexParserError('Unexpected command: ' + command);
290
- }
291
-
292
-
293
293
  const paramNum = get_command_param_num(command.slice(1));
294
294
  switch (paramNum) {
295
295
  case 0:
@@ -301,14 +301,12 @@ export class LatexParser {
301
301
  throw new LatexParserError('Expecting argument for ' + command);
302
302
  }
303
303
  if (command === '\\sqrt' && pos < tokens.length && tokens[pos].eq(LEFT_SQUARE_BRACKET)) {
304
- const posLeftSquareBracket = pos;
305
- const posRightSquareBracket = find_closing_match(tokens, pos, LEFT_SQUARE_BRACKET, RIGHT_SQUARE_BRACKET);
306
- if (posRightSquareBracket === -1) {
307
- throw new LatexParserError('No matching right square bracket for [');
304
+ const [exponent, newPos1] = this.parseClosure(tokens, pos + 1, RIGHT_SQUARE_BRACKET);
305
+ if (newPos1 === -1) {
306
+ throw LatexParserError.UNMATCHED_LEFT_BRACKET;
308
307
  }
309
- const [exponent, _] = this.parseGroup(tokens, posLeftSquareBracket + 1, posRightSquareBracket);
310
- const [arg1, newPos] = this.parseNextArg(tokens, posRightSquareBracket + 1);
311
- return [new TexFuncCall(command_token, [arg1], exponent), newPos];
308
+ const [arg1, newPos2] = this.parseNextArg(tokens, newPos1);
309
+ return [new TexFuncCall(command_token, [arg1], exponent), newPos2];
312
310
  } else if (command === '\\text') {
313
311
  if (pos + 2 >= tokens.length) {
314
312
  throw new LatexParserError('Expecting content for \\text command');
@@ -318,6 +316,14 @@ export class LatexParser {
318
316
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
319
317
  const literal = tokens[pos + 1];
320
318
  return [new TexText(literal), pos + 3];
319
+ } else if (command === '\\displaylines') {
320
+ assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
321
+ const [matrix, newPos] = this.parseAligned(tokens, pos + 1, RIGHT_CURLY_BRACKET);
322
+ if (newPos === -1) {
323
+ throw LatexParserError.UNMATCHED_LEFT_BRACE;
324
+ }
325
+ const group = new TexGroup(array_join(matrix, CONTROL_LINEBREAK.toNode()));
326
+ return [new TexFuncCall(command_token, [group]), newPos];
321
327
  }
322
328
  let [arg1, newPos] = this.parseNextArg(tokens, pos);
323
329
  return [new TexFuncCall(command_token, [arg1]), newPos];
@@ -328,7 +334,7 @@ export class LatexParser {
328
334
  return [new TexFuncCall(command_token, [arg1, arg2]), pos2];
329
335
  }
330
336
  default:
331
- throw new Error( 'Invalid number of parameters');
337
+ throw new Error('Invalid number of parameters');
332
338
  }
333
339
  }
334
340
 
@@ -363,7 +369,7 @@ export class LatexParser {
363
369
  pos += eat_whitespaces(tokens, pos).length;
364
370
 
365
371
  if (pos >= tokens.length) {
366
- throw new LatexParserError('Expecting delimiter after \\left');
372
+ throw new LatexParserError('Expecting a delimiter after \\left');
367
373
  }
368
374
 
369
375
  const leftDelimiter = eat_parenthesis(tokens, pos);
@@ -371,17 +377,16 @@ export class LatexParser {
371
377
  throw new LatexParserError('Invalid delimiter after \\left');
372
378
  }
373
379
  pos++;
374
- const exprInsideStart = pos;
375
- const idx = find_closing_right_command(tokens, start);
380
+
381
+ const [body, idx] = this.parseClosure(tokens, pos, RIGHT_COMMAND);
376
382
  if (idx === -1) {
377
- throw new LatexParserError('No matching \\right');
383
+ throw LatexParserError.UNMATCHED_COMMAND_LEFT;
378
384
  }
379
- const exprInsideEnd = idx;
380
- pos = idx + 1;
385
+ pos = idx;
381
386
 
382
387
  pos += eat_whitespaces(tokens, pos).length;
383
388
  if (pos >= tokens.length) {
384
- throw new LatexParserError('Expecting \\right after \\left');
389
+ throw new LatexParserError('Expecting a delimiter after \\right');
385
390
  }
386
391
 
387
392
  const rightDelimiter = eat_parenthesis(tokens, pos);
@@ -390,7 +395,6 @@ export class LatexParser {
390
395
  }
391
396
  pos++;
392
397
 
393
- const [body, _] = this.parseGroup(tokens, exprInsideStart, exprInsideEnd);
394
398
  const left = leftDelimiter.value === '.'? null: leftDelimiter;
395
399
  const right = rightDelimiter.value === '.'? null: rightDelimiter;
396
400
  const res = new TexLeftRight({body: body, left: left, right: right});
@@ -414,38 +418,34 @@ export class LatexParser {
414
418
  [data, pos] = this.parseNextArg(tokens, pos);
415
419
  }
416
420
 
417
- pos += eat_whitespaces(tokens, pos).length; // ignore whitespaces and '\n' after \begin{envName}
418
-
419
-
420
- const exprInsideStart = pos;
421
-
422
- const endIdx = find_closing_end_command(tokens, start);
421
+ const [body, endIdx] = this.parseAligned(tokens, pos, END_COMMAND);
423
422
  if (endIdx === -1) {
424
- throw new LatexParserError('No matching \\end');
423
+ throw LatexParserError.UNMATCHED_COMMAND_BEGIN;
425
424
  }
426
- const exprInsideEnd = endIdx;
427
- pos = endIdx + 1;
425
+
426
+ pos = endIdx;
428
427
 
429
428
  assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
430
429
  assert(tokens[pos + 1].type === TexTokenType.LITERAL);
431
430
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
432
431
  if (tokens[pos + 1].value !== envName) {
433
- throw new LatexParserError('Mismatched \\begin and \\end environments');
432
+ throw new LatexParserError('\\begin and \\end environments mismatch');
434
433
  }
435
434
  pos += 3;
436
435
 
437
- const exprInside = tokens.slice(exprInsideStart, exprInsideEnd);
438
- // ignore spaces and '\n' before \end{envName}
439
- while(exprInside.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(exprInside[exprInside.length - 1].type)) {
440
- exprInside.pop();
441
- }
442
- const body = this.parseAligned(exprInside);
443
436
  const res = new TexBeginEnd(new TexToken(TexTokenType.LITERAL, envName), body, data);
444
437
  return [res, pos];
445
438
  }
446
439
 
447
- parseAligned(tokens: TexToken[]): TexNode[][] {
448
- let pos = 0;
440
+ // return pos: (position of closingToken) + 1
441
+ // pos will be -1 if closingToken is not found
442
+ parseAligned(tokens: TexToken[], start: number, closingToken: TexToken): [TexNode[][], number] {
443
+ this.alignmentDepth++;
444
+
445
+ let pos = start;
446
+ // ignore whitespaces and '\n' after \begin{envName}
447
+ pos += eat_whitespaces(tokens, pos).length;
448
+
449
449
  const allRows: TexNode[][] = [];
450
450
  let row: TexNode[] = [];
451
451
  allRows.push(row);
@@ -453,6 +453,10 @@ export class LatexParser {
453
453
  row.push(group);
454
454
 
455
455
  while (pos < tokens.length) {
456
+ if (tokens[pos].eq(closingToken)) {
457
+ break;
458
+ }
459
+
456
460
  const [res, newPos] = this.parseNextExpr(tokens, pos);
457
461
  pos = newPos;
458
462
 
@@ -477,7 +481,24 @@ export class LatexParser {
477
481
  group.items.push(res);
478
482
  }
479
483
  }
480
- return allRows;
484
+
485
+ if (pos >= tokens.length) {
486
+ return [[], -1];
487
+ }
488
+
489
+ // ignore spaces and '\n' before \end{envName}
490
+ if (allRows.length > 0 && allRows[allRows.length - 1].length > 0) {
491
+ const last_cell = allRows[allRows.length - 1][allRows[allRows.length - 1].length - 1];
492
+ if (last_cell.type === 'ordgroup') {
493
+ const last_cell_items = (last_cell as TexGroup).items;
494
+ while(last_cell_items.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(last_cell_items[last_cell_items.length - 1].head.type)) {
495
+ last_cell_items.pop();
496
+ }
497
+ }
498
+ }
499
+
500
+ this.alignmentDepth--;
501
+ return [allRows, pos + 1];
481
502
  }
482
503
  }
483
504
 
@@ -511,7 +532,7 @@ function passExpandCustomTexMacros(tokens: TexToken[], customTexMacros: {[key: s
511
532
  return out_tokens;
512
533
  }
513
534
 
514
- export function parseTex(tex: string, customTexMacros: {[key: string]: string}): TexNode {
535
+ export function parseTex(tex: string, customTexMacros: {[key: string]: string} = {}): TexNode {
515
536
  const parser = new LatexParser();
516
537
  let tokens = tokenize_tex(tex);
517
538
  tokens = passIgnoreWhitespaceBeforeScriptMark(tokens);
@@ -36,6 +36,11 @@ export const TEX_UNARY_COMMANDS = [
36
36
  'hspace',
37
37
  'substack',
38
38
  'set',
39
+ 'displaylines',
40
+ 'mathinner',
41
+ 'mathrel',
42
+ 'mathbin',
43
+ 'mathop',
39
44
  ]
40
45
 
41
46
  export const TEX_BINARY_COMMANDS = [
@@ -58,6 +58,8 @@ export class TypstToken {
58
58
  new TypstToken(TypstTokenType.ELEMENT, '{'),
59
59
  new TypstToken(TypstTokenType.ELEMENT, '|'),
60
60
  new TypstToken(TypstTokenType.SYMBOL, 'angle.l'),
61
+ new TypstToken(TypstTokenType.SYMBOL, 'paren.l'),
62
+ new TypstToken(TypstTokenType.SYMBOL, 'brace.l'),
61
63
  ];
62
64
 
63
65
  public static readonly RIGHT_DELIMITERS = [
@@ -66,6 +68,8 @@ export class TypstToken {
66
68
  new TypstToken(TypstTokenType.ELEMENT, '}'),
67
69
  new TypstToken(TypstTokenType.ELEMENT, '|'),
68
70
  new TypstToken(TypstTokenType.SYMBOL, 'angle.r'),
71
+ new TypstToken(TypstTokenType.SYMBOL, 'paren.r'),
72
+ new TypstToken(TypstTokenType.SYMBOL, 'brace.r'),
69
73
  ];
70
74
  }
71
75
 
@@ -156,6 +156,12 @@ cases:
156
156
  - title: left right angles
157
157
  tex: \left\langle \frac{1}{2} \right\rangle
158
158
  typst: lr(angle.l 1/2 angle.r)
159
+ - title: lparen rparen
160
+ tex: \left\lparen \frac{1}{3} \right\rparen
161
+ typst: lr(paren.l 1/3 paren.r)
162
+ - title: lbrace rbrace
163
+ tex: \left\lbrace \frac{1}{3} \right\rbrace
164
+ typst: lr(brace.l 1/3 brace.r)
159
165
  - title: fractions as function argument
160
166
  tex: g(\frac{1}{2})
161
167
  typst: g(1/2)
@@ -177,3 +183,12 @@ cases:
177
183
  - title: textcolor
178
184
  tex: x = \textcolor{red}{a + y}
179
185
  typst: "x = #text(fill: red)[$a + y$]"
186
+ - title: mathrel
187
+ tex: a \mathrel{X} b
188
+ typst: a class("relation", X) b
189
+ - title: mathbin
190
+ tex: a \mathbin{X} b
191
+ typst: a class("binary", X) b
192
+ - title: mathop
193
+ tex: a \mathop{X} b
194
+ typst: a class("large", X) b
@@ -440,4 +440,18 @@ cases:
440
440
  typst: "{a, b, c}"
441
441
  - title: command set, empty
442
442
  tex: \set{}
443
- typst: "{}"
443
+ typst: "{}"
444
+ - title: command displaylines is ignored
445
+ tex: |-
446
+ \displaylines{
447
+ E = mc^2 \\
448
+ F = ma \\
449
+ P = IV
450
+ }
451
+ typst: |-
452
+ E = m c^2 \
453
+ F = m a \
454
+ P = I V
455
+ - title: command mathinner is ignored
456
+ tex: ab\mathinner{\text{inside}}cd
457
+ typst: a b "inside" c d
package/tests/symbol.yml CHANGED
@@ -21,6 +21,9 @@ cases:
21
21
  - title: mathcal
22
22
  tex: \mathcal{A} \mathcal{B} \mathcal{C} \mathcal{D} \mathcal{E} \mathcal{F} \mathcal{G} \mathcal{H} \mathcal{I} \mathcal{J} \mathcal{K} \mathcal{L} \mathcal{M} \mathcal{N} \mathcal{O} \mathcal{P} \mathcal{Q} \mathcal{R} \mathcal{S} \mathcal{T} \mathcal{U} \mathcal{V} \mathcal{W} \mathcal{X} \mathcal{Y} \mathcal{Z}
23
23
  typst: cal(A) cal(B) cal(C) cal(D) cal(E) cal(F) cal(G) cal(H) cal(I) cal(J) cal(K) cal(L) cal(M) cal(N) cal(O) cal(P) cal(Q) cal(R) cal(S) cal(T) cal(U) cal(V) cal(W) cal(X) cal(Y) cal(Z)
24
+ - title: mathscr
25
+ tex: \mathscr{A}
26
+ typst: scr(A)
24
27
  - title: mathrm
25
28
  tex: \mathrm{a} \rm{a}
26
29
  typst: upright(a) upright(a)
@@ -103,8 +106,8 @@ cases:
103
106
  typst: myop
104
107
  nonStrict: true
105
108
  - title: extended
106
- tex: \mathscr{A} \LaTeX \TeX
107
- typst: "scr(A) #LaTeX #TeX"
109
+ tex: \LaTeX \TeX
110
+ typst: "#LaTeX #TeX"
108
111
  - title: mathbf
109
112
  tex: \mathbf{A}
110
113
  typst: upright(bold(A))
@@ -1,10 +1,11 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, test, expect } from 'vitest';
2
2
  import { tokenize_tex } from '../src/tex-tokenizer';
3
+ import { LatexParserError, parseTex } from '../src/tex-parser';
3
4
  import { TexToken, TexTokenType } from '../src/tex-types';
4
5
 
5
6
 
6
7
  describe('typst-tokenizer', () => {
7
- it('a + b', function () {
8
+ test('a + b', function () {
8
9
  const res = tokenize_tex('a + b');
9
10
  expect(res).toEqual([
10
11
  new TexToken(TexTokenType.ELEMENT, 'a'),
@@ -15,7 +16,7 @@ describe('typst-tokenizer', () => {
15
16
  ]);
16
17
  });
17
18
 
18
- it('a (x)', function () {
19
+ test('a (x)', function () {
19
20
  const res = tokenize_tex('a (x)');
20
21
  expect(res).toEqual([
21
22
  new TexToken(TexTokenType.ELEMENT, 'a'),
@@ -26,7 +27,7 @@ describe('typst-tokenizer', () => {
26
27
  ]);
27
28
  });
28
29
 
29
- it('f(x)', function () {
30
+ test('f(x)', function () {
30
31
  const res = tokenize_tex('f(x)');
31
32
  expect(res).toEqual([
32
33
  new TexToken(TexTokenType.ELEMENT, 'f'),
@@ -36,7 +37,7 @@ describe('typst-tokenizer', () => {
36
37
  ]);
37
38
  });
38
39
 
39
- it('comment', function() {
40
+ test('comment', function() {
40
41
  const res = tokenize_tex('a % comment');
41
42
  expect(res).toEqual([
42
43
  new TexToken(TexTokenType.ELEMENT, 'a'),
@@ -45,7 +46,7 @@ describe('typst-tokenizer', () => {
45
46
  ]);
46
47
  });
47
48
 
48
- it('macro', function() {
49
+ test('macro', function() {
49
50
  const res = tokenize_tex('\\sqrt{a}');
50
51
  expect(res).toEqual([
51
52
  new TexToken(TexTokenType.COMMAND, '\\sqrt'),
@@ -53,5 +54,44 @@ describe('typst-tokenizer', () => {
53
54
  new TexToken(TexTokenType.ELEMENT, 'a'),
54
55
  new TexToken(TexTokenType.CONTROL, '}'),
55
56
  ]);
56
- })
57
+ });
58
+
59
+ test('throw error on & outside of an alignment', function() {
60
+ expect(() => parseTex('a & b')).toThrow();
61
+ });
62
+
63
+ test('throw on missing ] for sqrt', function() {
64
+ const input = '\\sqrt[3{x}';
65
+ expect(() => parseTex(input)).toThrowError(LatexParserError.UNMATCHED_LEFT_BRACKET);
66
+ });
67
+
68
+ test('throw on extra {', function() {
69
+ const input = 'a { {b}';
70
+ expect(() => parseTex(input)).toThrowError(LatexParserError.UNMATCHED_LEFT_BRACE);
71
+ });
72
+
73
+ test('throw on extra }', function() {
74
+ const input = 'a { b } }';
75
+ expect(() => parseTex(input)).toThrowError(LatexParserError.UNMATCHED_RIGHT_BRACE);
76
+ });
77
+
78
+ test('throw on extra \\left', function() {
79
+ const input = 'a \\left( \\left( b \\right)';
80
+ expect(() => parseTex(input)).toThrowError(LatexParserError.UNMATCHED_COMMAND_LEFT);
81
+ });
82
+
83
+ test('throw on extra \\right', function() {
84
+ const input = 'a \\left( b \\right) \\right)';
85
+ expect(() => parseTex(input)).toThrowError(LatexParserError.UNMATCHED_COMMAND_RIGHT);
86
+ });
87
+
88
+ test('throw on extra \\begin', function() {
89
+ const input = 'a \\begin{aligned} \\begin{aligned} b \\end{aligned}';
90
+ expect(() => parseTex(input)).toThrowError(LatexParserError.UNMATCHED_COMMAND_BEGIN);
91
+ });
92
+
93
+ test('throw on extra \\end', function() {
94
+ const input = 'a \\begin{aligned} b \\end{aligned} \\end{aligned}';
95
+ expect(() => parseTex(input)).toThrowError(LatexParserError.UNMATCHED_COMMAND_END);
96
+ });
57
97
  });