tex2typst 0.3.0 → 0.3.1

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 } from "./types";
1
+ import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue } from "./types";
2
2
  import { TypstWriterError, TYPST_INTRINSIC_SYMBOLS } from "./typst-writer";
3
3
  import { symbolMap, reverseSymbolMap } from "./map";
4
4
 
@@ -60,7 +60,7 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
60
60
  'op',
61
61
  [convert_tex_node_to_typst(base, options)]
62
62
  );
63
- op_call.setOptions({ limits: '#true' });
63
+ op_call.setOptions({ limits: TYPST_TRUE });
64
64
  return new TypstNode(
65
65
  'supsub',
66
66
  '',
@@ -240,11 +240,51 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
240
240
  if (node.content!.startsWith('align')) {
241
241
  // align, align*, alignat, alignat*, aligned, etc.
242
242
  return new TypstNode('align', '', [], data);
243
- } else {
243
+ }
244
+ if (node.content!.endsWith('matrix')) {
245
+ let delim: TypstPrimitiveValue = null;
246
+ switch (node.content) {
247
+ case 'matrix':
248
+ delim = TYPST_NONE;
249
+ break;
250
+ case 'pmatrix':
251
+ delim = '(';
252
+ break;
253
+ case 'bmatrix':
254
+ delim = '[';
255
+ break;
256
+ case 'Bmatrix':
257
+ delim = '{';
258
+ break;
259
+ case 'vmatrix':
260
+ delim = '|';
261
+ break;
262
+ case 'Vmatrix': {
263
+ // mat(delim: "||") does not compile in Typst.
264
+ // For a workaround, translate
265
+ // \begin{Vmatrix}
266
+ // a & b \\
267
+ // c & d
268
+ // \end{Vmatrix}
269
+ // to
270
+ // lr(||mat(delim: #none, a, b; c, d)||)
271
+ const matrix = new TypstNode('matrix', '', [], data);
272
+ matrix.setOptions({ 'delim': TYPST_NONE });
273
+ const group = new TypstNode('group', '', [
274
+ new TypstNode('symbol', '||'),
275
+ matrix,
276
+ new TypstNode('symbol', '||'),
277
+ ]);
278
+ return new TypstNode('funcCall', 'lr', [ group ]);
279
+ }
280
+ default:
281
+ throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
282
+ }
244
283
  const res = new TypstNode('matrix', '', [], data);
245
- res.setOptions({ 'delim': '#none' });
284
+ res.setOptions({ 'delim': delim });
246
285
  return res;
247
286
  }
287
+ throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
248
288
  }
249
289
  case 'unknownMacro':
250
290
  return new TypstNode('unknown', tex_token_to_typst(node.content));
@@ -416,7 +456,7 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
416
456
  if (node.options) {
417
457
  if ('delim' in node.options) {
418
458
  switch (node.options.delim) {
419
- case '#none':
459
+ case TYPST_NONE:
420
460
  return matrix;
421
461
  case '[':
422
462
  left_delim = "\\left[";
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseTex } from "./tex-parser";
2
- import { Tex2TypstOptions } from "./types";
2
+ import type { Tex2TypstOptions } from "./types";
3
3
  import { TypstWriter } 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/tex-parser.ts CHANGED
@@ -296,46 +296,39 @@ export class LatexParser {
296
296
  }
297
297
 
298
298
  parse(tokens: TexToken[]): TexNode {
299
+ const [tree, _] = this.parseGroup(tokens, 0, tokens.length);
300
+ return tree;
301
+ }
302
+
303
+ parseGroup(tokens: TexToken[], start: number, end: number): ParseResult {
299
304
  const results: TexNode[] = [];
300
- let pos = 0;
301
- while (pos < tokens.length) {
302
- const results: TexNode[] = [];
303
- let pos = 0;
304
-
305
- while (pos < tokens.length) {
306
- const [res, newPos] = this.parseNextExpr(tokens, pos);
307
- pos = newPos;
308
- if(res.type === 'whitespace') {
309
- if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
310
- continue;
311
- }
312
- if (!this.newline_sensitive && res.content === '\n') {
313
- continue;
314
- }
305
+ let pos = start;
306
+ while (pos < end) {
307
+ const [res, newPos] = this.parseNextExpr(tokens, pos);
308
+ pos = newPos;
309
+ if(res.type === 'whitespace') {
310
+ if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
311
+ continue;
315
312
  }
316
- if (res.type === 'control' && res.content === '&') {
317
- throw new LatexParserError('Unexpected & outside of an alignment');
313
+ if (!this.newline_sensitive && res.content === '\n') {
314
+ continue;
318
315
  }
319
- results.push(res);
320
316
  }
321
-
322
- if (results.length === 0) {
323
- return EMPTY_NODE;
324
- } else if (results.length === 1) {
325
- return results[0];
326
- } else {
327
- return new TexNode('ordgroup', '', results);
317
+ if (res.type === 'control' && res.content === '&') {
318
+ throw new LatexParserError('Unexpected & outside of an alignment');
328
319
  }
320
+ results.push(res);
329
321
  }
330
322
 
331
-
323
+ let node: TexNode;
332
324
  if (results.length === 0) {
333
- return EMPTY_NODE;
325
+ node = EMPTY_NODE;
334
326
  } else if (results.length === 1) {
335
- return results[0];
327
+ node = results[0];
336
328
  } else {
337
- return new TexNode('ordgroup', '', results);
329
+ node = new TexNode('ordgroup', '', results);
338
330
  }
331
+ return [node, end + 1];
339
332
  }
340
333
 
341
334
  parseNextExpr(tokens: TexToken[], start: number): ParseResult {
@@ -396,8 +389,7 @@ export class LatexParser {
396
389
 
397
390
  parseNextExprWithoutSupSub(tokens: TexToken[], start: number): ParseResult {
398
391
  const firstToken = tokens[start];
399
- const tokenType = firstToken.type;
400
- switch (tokenType) {
392
+ switch (firstToken.type) {
401
393
  case TexTokenType.ELEMENT:
402
394
  return [new TexNode('element', firstToken.value), start + 1];
403
395
  case TexTokenType.TEXT:
@@ -423,8 +415,7 @@ export class LatexParser {
423
415
  if(posClosingBracket === -1) {
424
416
  throw new LatexParserError("Unmatched '{'");
425
417
  }
426
- const exprInside = tokens.slice(start + 1, posClosingBracket);
427
- return [this.parse(exprInside), posClosingBracket + 1];
418
+ return this.parseGroup(tokens, start + 1, posClosingBracket);
428
419
  case '}':
429
420
  throw new LatexParserError("Unmatched '}'");
430
421
  case '\\\\':
@@ -453,7 +444,7 @@ export class LatexParser {
453
444
 
454
445
  if (['left', 'right', 'begin', 'end'].includes(command.slice(1))) {
455
446
  throw new LatexParserError('Unexpected command: ' + command);
456
- }
447
+ }
457
448
 
458
449
 
459
450
  const paramNum = get_command_param_num(command.slice(1));
@@ -475,8 +466,7 @@ export class LatexParser {
475
466
  if (posRightSquareBracket === -1) {
476
467
  throw new LatexParserError('No matching right square bracket for [');
477
468
  }
478
- const exprInside = tokens.slice(posLeftSquareBracket + 1, posRightSquareBracket);
479
- const exponent = this.parse(exprInside);
469
+ const [exponent, _] = this.parseGroup(tokens, posLeftSquareBracket + 1, posRightSquareBracket);
480
470
  const [arg1, newPos] = this.parseNextExprWithoutSupSub(tokens, posRightSquareBracket + 1);
481
471
  return [new TexNode('unaryFunc', command, [arg1], exponent), newPos];
482
472
  } else if (command === '\\text') {
@@ -529,15 +519,14 @@ export class LatexParser {
529
519
  if (pos >= tokens.length) {
530
520
  throw new LatexParserError('Expecting \\right after \\left');
531
521
  }
532
-
522
+
533
523
  const rightDelimiter = eat_parenthesis(tokens, pos);
534
524
  if (rightDelimiter === null) {
535
525
  throw new LatexParserError('Invalid delimiter after \\right');
536
526
  }
537
527
  pos++;
538
528
 
539
- const exprInside = tokens.slice(exprInsideStart, exprInsideEnd);
540
- const body = this.parse(exprInside);
529
+ const [body, _] = this.parseGroup(tokens, exprInsideStart, exprInsideEnd);
541
530
  const args: TexNode[] = [
542
531
  new TexNode('element', leftDelimiter.value),
543
532
  body,
@@ -556,9 +545,9 @@ export class LatexParser {
556
545
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
557
546
  const envName = tokens[pos + 1].value;
558
547
  pos += 3;
559
-
548
+
560
549
  pos += eat_whitespaces(tokens, pos).length; // ignore whitespaces and '\n' after \begin{envName}
561
-
550
+
562
551
  const exprInsideStart = pos;
563
552
 
564
553
  const endIdx = find_closing_end_command(tokens, start);
@@ -567,7 +556,7 @@ export class LatexParser {
567
556
  }
568
557
  const exprInsideEnd = endIdx;
569
558
  pos = endIdx + 1;
570
-
559
+
571
560
  assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
572
561
  assert(tokens[pos + 1].type === TexTokenType.TEXT);
573
562
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
@@ -575,7 +564,7 @@ export class LatexParser {
575
564
  throw new LatexParserError('Mismatched \\begin and \\end environments');
576
565
  }
577
566
  pos += 3;
578
-
567
+
579
568
  const exprInside = tokens.slice(exprInsideStart, exprInsideEnd);
580
569
  // ignore spaces and '\n' before \end{envName}
581
570
  while(exprInside.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(exprInside[exprInside.length - 1].type)) {
@@ -606,7 +595,7 @@ export class LatexParser {
606
595
  continue;
607
596
  }
608
597
  }
609
-
598
+
610
599
  if (res.type === 'control' && res.content === '\\\\') {
611
600
  row = [];
612
601
  group = new TexNode('ordgroup', '', []);
package/src/tex-writer.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { array_includes, array_split } from "./generic";
2
- import { reverseSymbolMap } from "./map";
3
2
  import { TexNode, TexToken, TexSupsubData, TexTokenType } from "./types";
4
3
 
5
4
 
package/src/types.ts CHANGED
@@ -352,7 +352,13 @@ export type TypstArrayData = TypstNode[][];
352
352
  type TypstNodeType = 'atom' | 'symbol' | 'text' | 'control' | 'comment' | 'whitespace'
353
353
  | 'empty' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'align' | 'matrix' | 'unknown';
354
354
 
355
- export type TypstNamedParams = { [key: string]: string };
355
+ export type TypstPrimitiveValue = string | boolean | null;
356
+ export type TypstNamedParams = { [key: string]: TypstPrimitiveValue };
357
+
358
+ // #none
359
+ export const TYPST_NONE: TypstPrimitiveValue = null;
360
+ export const TYPST_TRUE: TypstPrimitiveValue = true;
361
+ export const TYPST_FALSE: TypstPrimitiveValue = false;
356
362
 
357
363
  export class TypstNode {
358
364
  type: TypstNodeType;
@@ -370,7 +376,7 @@ export class TypstNode {
370
376
  this.data = data;
371
377
  }
372
378
 
373
- public setOptions(options: { [key: string]: string }) {
379
+ public setOptions(options: TypstNamedParams) {
374
380
  this.options = options;
375
381
  }
376
382
 
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { array_find } from "./generic";
3
- import { TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
3
+ import { TYPST_NONE, TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
4
4
  import { assert, isalpha, isdigit } from "./util";
5
5
 
6
6
  // TODO: In Typst, y' ' is not the same as y''.
@@ -450,7 +450,6 @@ export class TypstParser {
450
450
  // start: the position of the left parentheses
451
451
  parseArguments(tokens: TypstToken[], start: number): [TypstNode[], number] {
452
452
  const end = find_closing_match(tokens, start);
453
-
454
453
  return [this.parseCommaSeparatedArguments(tokens, start + 1, end), end + 1];
455
454
  }
456
455
 
@@ -501,7 +500,7 @@ export class TypstParser {
501
500
  if(g.args!.length !== 4 || !g.args![pos_colon + 2].eq(new TypstNode('symbol', 'none'))) {
502
501
  throw new TypstParserError('Invalid number of arguments for delim');
503
502
  }
504
- np['delim'] = "#none";
503
+ np['delim'] = TYPST_NONE;
505
504
  } else {
506
505
  throw new TypstParserError('Not implemented for other types of delim');
507
506
  }
@@ -1,4 +1,4 @@
1
- import { TexNode, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
1
+ import { TexNode, TypstNode, TypstPrimitiveValue, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
2
2
 
3
3
 
4
4
  // symbols that are supported by Typst but not by KaTeX
@@ -24,6 +24,22 @@ const TYPST_RIGHT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMEN
24
24
  const TYPST_COMMA: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ',');
25
25
  const TYPST_NEWLINE: TypstToken = new TypstToken(TypstTokenType.SYMBOL, '\n');
26
26
 
27
+ function typst_primitive_to_string(value: TypstPrimitiveValue) {
28
+ switch (typeof value) {
29
+ case 'string':
30
+ return `"${value}"`;
31
+ case 'number':
32
+ return (value as number).toString();
33
+ case 'boolean':
34
+ return (value as boolean) ? '#true' : '#false';
35
+ default:
36
+ if (value === null) {
37
+ return '#none';
38
+ }
39
+ throw new TypstWriterError(`Invalid primitive value: ${value}`, value);
40
+ }
41
+ }
42
+
27
43
  export class TypstWriterError extends Error {
28
44
  node: TexNode | TypstNode | TypstToken;
29
45
 
@@ -58,6 +74,8 @@ export class TypstWriter {
58
74
  return;
59
75
  }
60
76
 
77
+ // TODO: "C \frac{xy}{z}" should translate to "C (x y)/z" instead of "C(x y)/z"
78
+
61
79
  let no_need_space = false;
62
80
  // putting the first token in clause
63
81
  no_need_space ||= /[\(\[\|]$/.test(this.buffer) && /^\w/.test(str);
@@ -172,7 +190,8 @@ export class TypstWriter {
172
190
  }
173
191
  if (node.options) {
174
192
  for (const [key, value] of Object.entries(node.options)) {
175
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `, ${key}: ${value}`));
193
+ const value_str = typst_primitive_to_string(value);
194
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `, ${key}: ${value_str}`));
176
195
  }
177
196
  }
178
197
  this.queue.push(TYPST_RIGHT_PARENTHESIS);
@@ -220,7 +239,8 @@ export class TypstWriter {
220
239
  this.queue.push(TYPST_LEFT_PARENTHESIS);
221
240
  if (node.options) {
222
241
  for (const [key, value] of Object.entries(node.options)) {
223
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `${key}: ${value}, `));
242
+ const value_str = typst_primitive_to_string(value);
243
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `${key}: ${value_str}, `));
224
244
  }
225
245
  }
226
246
  matrix.forEach((row, i) => {