tex2typst 0.3.22 → 0.3.23

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/tex-parser.ts CHANGED
@@ -231,8 +231,8 @@ export class LatexParser {
231
231
  switch (firstToken.type) {
232
232
  case TexTokenType.ELEMENT:
233
233
  return [new TexNode('element', firstToken.value), start + 1];
234
- case TexTokenType.TEXT:
235
- return [new TexNode('text', firstToken.value), start + 1];
234
+ case TexTokenType.LITERAL:
235
+ return [new TexNode('literal', firstToken.value), start + 1];
236
236
  case TexTokenType.COMMENT:
237
237
  return [new TexNode('comment', firstToken.value), start + 1];
238
238
  case TexTokenType.SPACE:
@@ -321,7 +321,7 @@ export class LatexParser {
321
321
  throw new LatexParserError('Expecting content for \\text command');
322
322
  }
323
323
  assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
324
- assert(tokens[pos + 1].type === TexTokenType.TEXT);
324
+ assert(tokens[pos + 1].type === TexTokenType.LITERAL);
325
325
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
326
326
  const text = tokens[pos + 1].value;
327
327
  return [new TexNode('text', text), pos + 3];
@@ -412,24 +412,25 @@ export class LatexParser {
412
412
 
413
413
  let pos = start + 1;
414
414
  assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
415
- assert(tokens[pos + 1].type === TexTokenType.TEXT);
415
+ assert(tokens[pos + 1].type === TexTokenType.LITERAL);
416
416
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
417
417
  const envName = tokens[pos + 1].value;
418
418
  pos += 3;
419
419
 
420
- const args: TexNode[] = [];
421
- while (pos < tokens.length) {
422
- const whitespaceCount = eat_whitespaces(tokens, pos).length;
423
- pos += whitespaceCount;
424
420
 
421
+ const args: TexNode[] = [];
422
+ if(['array', 'subarray'].includes(envName)) {
425
423
  if (pos >= tokens.length || !tokens[pos].eq(LEFT_CURLY_BRACKET)) {
426
- break;
424
+ throw new LatexParserError(`Missing arg for \\begin{${envName}}`);
427
425
  }
428
426
  const [arg, newPos] = this.parseNextArg(tokens, pos);
429
427
  args.push(arg);
430
428
  pos = newPos;
431
429
  }
432
430
 
431
+ pos += eat_whitespaces(tokens, pos).length; // ignore whitespaces and '\n' after \begin{envName}
432
+
433
+
433
434
  const exprInsideStart = pos;
434
435
 
435
436
  const endIdx = find_closing_end_command(tokens, start);
@@ -440,7 +441,7 @@ export class LatexParser {
440
441
  pos = endIdx + 1;
441
442
 
442
443
  assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
443
- assert(tokens[pos + 1].type === TexTokenType.TEXT);
444
+ assert(tokens[pos + 1].type === TexTokenType.LITERAL);
444
445
  assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
445
446
  if (tokens[pos + 1].value !== envName) {
446
447
  throw new LatexParserError('Mismatched \\begin and \\end environments');
@@ -1,5 +1,4 @@
1
1
  import { TexToken, TexTokenType } from "./types";
2
- import { assert } from "./util";
3
2
  import { JSLex, Scanner } from "./jslex";
4
3
 
5
4
  export const TEX_UNARY_COMMANDS = [
@@ -59,15 +58,28 @@ function unescape(str: string): string {
59
58
  }
60
59
 
61
60
  const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[]>([
61
+ // math `\begin{array}{cc}`
62
62
  [
63
- String.raw`\\(text|operatorname|begin|end|hspace){.+?}`, (s) => {
64
- const text = s.text()!;
65
- const command = text.substring(0, text.indexOf('{'));
66
- const text_inside = text.substring(text.indexOf('{') + 1, text.lastIndexOf('}'));
63
+ String.raw`\\begin{(array|subarry)}{(.+?)}`, (s) => {
64
+ const match = s.reMatchArray()!;
67
65
  return [
68
- new TexToken(TexTokenType.COMMAND, command),
66
+ new TexToken(TexTokenType.COMMAND, '\\begin'),
67
+ new TexToken(TexTokenType.CONTROL, '{'),
68
+ new TexToken(TexTokenType.LITERAL, match[1]),
69
+ new TexToken(TexTokenType.CONTROL, '}'),
70
+ new TexToken(TexTokenType.CONTROL, '{'),
71
+ new TexToken(TexTokenType.LITERAL, match[2]),
72
+ new TexToken(TexTokenType.CONTROL, '}'),
73
+ ]
74
+ }
75
+ ],
76
+ [
77
+ String.raw`\\(text|operatorname|begin|end|hspace|array){(.+?)}`, (s) => {
78
+ const match = s.reMatchArray()!;
79
+ return [
80
+ new TexToken(TexTokenType.COMMAND, '\\' + match[1]),
69
81
  new TexToken(TexTokenType.CONTROL, '{'),
70
- new TexToken(TexTokenType.TEXT, unescape(text_inside)),
82
+ new TexToken(TexTokenType.LITERAL, unescape(match[2])),
71
83
  new TexToken(TexTokenType.CONTROL, '}')
72
84
  ]
73
85
  }
@@ -80,14 +92,11 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
80
92
  [String.raw`\\[{}%$&#_|]`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
81
93
  // e.g. match `\frac13`, `\frac1 b`, `\frac a b`
82
94
  [String.raw`(\\[a-zA-Z]+)(\s*\d|\s+[a-zA-Z])\s*([0-9a-zA-Z])`, (s) => {
83
- const text = s.text()!;
84
- const regex = RegExp(String.raw`(\\[a-zA-Z]+)(\s*\d|\s+[a-zA-Z])\s*([0-9a-zA-Z])`);
85
- const match = text.match(regex);
86
- assert(match !== null);
95
+ const match = s.reMatchArray()!;
87
96
  const command = match![1];
88
97
  if (TEX_BINARY_COMMANDS.includes(command.substring(1))) {
89
- const arg1 = match![2].trimStart();
90
- const arg2 = match![3];
98
+ const arg1 = match[2].trimStart();
99
+ const arg2 = match[3];
91
100
  return [
92
101
  new TexToken(TexTokenType.COMMAND, command),
93
102
  new TexToken(TexTokenType.ELEMENT, arg1),
@@ -100,13 +109,10 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
100
109
  }],
101
110
  // e.g. match `\sqrt3`, `\sqrt a`
102
111
  [String.raw`(\\[a-zA-Z]+)(\s*\d|\s+[a-zA-Z])`, (s) => {
103
- const text = s.text()!;
104
- const regex = RegExp(String.raw`(\\[a-zA-Z]+)(\s*\d|\s+[a-zA-Z])`);
105
- const match = text.match(regex);
106
- assert(match !== null);
107
- const command = match![1];
112
+ const match = s.reMatchArray()!;
113
+ const command = match[1];
108
114
  if (TEX_UNARY_COMMANDS.includes(command.substring(1))) {
109
- const arg1 = match![2].trimStart();
115
+ const arg1 = match[2].trimStart();
110
116
  return [
111
117
  new TexToken(TexTokenType.COMMAND, command),
112
118
  new TexToken(TexTokenType.ELEMENT, arg1),
@@ -116,10 +122,7 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
116
122
  return [];
117
123
  }
118
124
  }],
119
- [String.raw`\\[a-zA-Z]+`, (s) => {
120
- const command = s.text()!;
121
- return [ new TexToken(TexTokenType.COMMAND, command), ];
122
- }],
125
+ [String.raw`\\[a-zA-Z]+`, (s) => new TexToken(TexTokenType.COMMAND, s.text()!)],
123
126
  // Numbers like "123", "3.14"
124
127
  [String.raw`[0-9]+(\.[0-9]+)?`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
125
128
  [String.raw`[a-zA-Z]`, (s) => new TexToken(TexTokenType.ELEMENT, s.text()!)],
package/src/tex-writer.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { array_includes, array_split } from "./generic";
2
- import { TexNode, TexToken, TexSupsubData, TexTokenType } from "./types";
2
+ import { TexNode, TexToken, TexTokenType } from "./types";
3
3
 
4
4
 
5
5
  export class TexWriter {
package/src/types.ts CHANGED
@@ -3,7 +3,7 @@ import { array_includes } from "./generic";
3
3
  export enum TexTokenType {
4
4
  ELEMENT,
5
5
  COMMAND,
6
- TEXT,
6
+ LITERAL,
7
7
  COMMENT,
8
8
  SPACE,
9
9
  NEWLINE,
@@ -26,10 +26,8 @@ export class TexToken {
26
26
 
27
27
  public toString(): string {
28
28
  switch (this.type) {
29
- case TexTokenType.TEXT:
30
- return `\\text{${this.value}}`;
31
29
  case TexTokenType.COMMENT:
32
- return `%${this.value}`;
30
+ return "%" + this.value;
33
31
  default:
34
32
  return this.value;
35
33
  }
@@ -56,7 +54,7 @@ export type TexArrayData = TexNode[][];
56
54
  * empty: special type when something is empty. e.g. the base of _{a} or ^{a}
57
55
  * whitespace: space, tab, newline
58
56
  */
59
- type TexNodeType = 'element' | 'text' | 'comment' | 'whitespace' | 'control' | 'ordgroup' | 'supsub'
57
+ type TexNodeType = 'element' | 'text' | 'literal' | 'comment' | 'whitespace' | 'control' | 'ordgroup' | 'supsub'
60
58
  | 'unaryFunc' | 'binaryFunc' | 'leftright' | 'beginend' | 'symbol' | 'empty' | 'unknownMacro';
61
59
 
62
60
 
@@ -89,16 +87,6 @@ export class TexNode {
89
87
  return this.type === other.type && this.content === other.content;
90
88
  }
91
89
 
92
- public toString(): string {
93
- switch (this.type) {
94
- case 'text':
95
- return `\\text{${this.content}}`;
96
- default:
97
- throw new Error(`toString() is not implemented for type ${this.type}`);
98
- }
99
- }
100
-
101
-
102
90
  public serialize(): TexToken[] {
103
91
  switch (this.type) {
104
92
  case 'empty':
@@ -110,8 +98,15 @@ export class TexNode {
110
98
  }
111
99
  case 'symbol':
112
100
  return [new TexToken(TexTokenType.COMMAND, this.content)];
101
+ case 'literal':
102
+ return [new TexToken(TexTokenType.LITERAL, this.content)];
113
103
  case 'text':
114
- return [new TexToken(TexTokenType.TEXT, this.content)];
104
+ return [
105
+ new TexToken(TexTokenType.COMMAND, '\\text'),
106
+ new TexToken(TexTokenType.ELEMENT, '{'),
107
+ new TexToken(TexTokenType.LITERAL, this.content),
108
+ new TexToken(TexTokenType.ELEMENT, '}'),
109
+ ];
115
110
  case 'comment':
116
111
  return [new TexToken(TexTokenType.COMMENT, this.content)];
117
112
  case 'whitespace': {
@@ -243,6 +238,7 @@ export enum TypstTokenType {
243
238
  NONE,
244
239
  SYMBOL,
245
240
  ELEMENT,
241
+ LITERAL,
246
242
  TEXT,
247
243
  COMMENT,
248
244
  SPACE,
@@ -271,6 +267,8 @@ export class TypstToken {
271
267
  switch(this.type) {
272
268
  case TypstTokenType.NONE:
273
269
  return new TypstNode('none', '#none');
270
+ case TypstTokenType.LITERAL:
271
+ return new TypstNode('literal', this.value);
274
272
  case TypstTokenType.TEXT:
275
273
  return new TypstNode('text', this.value);
276
274
  case TypstTokenType.COMMENT:
@@ -326,11 +324,10 @@ export interface TypstLrData {
326
324
  rightDelim: string | null;
327
325
  }
328
326
 
329
- type TypstNodeType = 'atom' | 'symbol' | 'text' | 'control' | 'comment' | 'whitespace'
327
+ type TypstNodeType = 'atom' | 'symbol' | 'text' | 'literal' | 'control' | 'comment' | 'whitespace'
330
328
  | 'none' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'align' | 'matrix' | 'cases' | 'unknown';
331
329
 
332
- export type TypstPrimitiveValue = string | boolean | TypstNode;
333
- export type TypstNamedParams = { [key: string]: TypstPrimitiveValue };
330
+ export type TypstNamedParams = { [key: string]: TypstNode };
334
331
 
335
332
 
336
333
  export class TypstNode {
@@ -382,12 +379,21 @@ export class TypstNode {
382
379
  return false;
383
380
  }
384
381
  }
382
+
383
+ public toString(): string {
384
+ switch (this.type) {
385
+ case 'text':
386
+ return `"${this.content}"`;
387
+ case 'comment':
388
+ return `//${this.content}`;
389
+ default:
390
+ return this.content;
391
+ }
392
+ }
385
393
  }
386
394
 
387
395
  // #none
388
396
  export const TYPST_NONE = new TypstNode('none', '#none');
389
- export const TYPST_TRUE: TypstPrimitiveValue = true;
390
- export const TYPST_FALSE: TypstPrimitiveValue = false;
391
397
 
392
398
  /**
393
399
  * ATTENTION:
@@ -413,22 +413,7 @@ export class TypstParser {
413
413
  if(g.args!.length !== 3) {
414
414
  throw new TypstParserError('Invalid number of arguments for delim');
415
415
  }
416
- switch (g.args![pos_colon + 1].type) {
417
- case 'text': {
418
- np['delim'] = g.args![pos_colon + 1].content;
419
- break;
420
- }
421
- case 'none': {
422
- np['delim'] = TYPST_NONE;
423
- break;
424
- }
425
- case 'symbol': {
426
- np['delim'] = g.args![pos_colon + 1];
427
- break;
428
- }
429
- default:
430
- throw new TypstParserError('Not implemented for other types of delim');
431
- }
416
+ np['delim'] = g.args![pos_colon + 1];
432
417
  } else {
433
418
  throw new TypstParserError('Not implemented for other named parameters');
434
419
  }
@@ -59,6 +59,15 @@ const rules_map = new Map<string, (a: Scanner<TypstToken>) => TypstToken | Typst
59
59
  ],
60
60
  [String.raw`[0-9]+(\.[0-9]+)?`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
61
61
  [String.raw`[+\-*/=\'<>!.,;?()\[\]|]`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
62
+ [String.raw`#h\((.+?)\)`, (s) => {
63
+ const match = s.reMatchArray()!;
64
+ return [
65
+ new TypstToken(TypstTokenType.SYMBOL, "#h"),
66
+ new TypstToken(TypstTokenType.ELEMENT, "("),
67
+ new TypstToken(TypstTokenType.LITERAL, match[1]),
68
+ new TypstToken(TypstTokenType.ELEMENT, ")"),
69
+ ];
70
+ }],
62
71
  [String.raw`[a-zA-Z\.]+`, (s) => {
63
72
  return new TypstToken(s.text()!.length === 1? TypstTokenType.ELEMENT: TypstTokenType.SYMBOL, s.text()!);
64
73
  }],
@@ -1,6 +1,5 @@
1
- import { TexNode, TypstNode, TypstPrimitiveValue, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
1
+ import { TexNode, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
2
2
  import { shorthandMap } from "./typst-shorthands";
3
- import { assert } from "./util";
4
3
 
5
4
  function is_delimiter(c: TypstNode): boolean {
6
5
  return c.type === 'atom' && ['(', ')', '[', ']', '{', '}', '|', '⌊', '⌋', '⌈', '⌉'].includes(c.content);
@@ -13,20 +12,6 @@ const TYPST_NEWLINE: TypstToken = new TypstToken(TypstTokenType.SYMBOL, '\n');
13
12
 
14
13
  const SOFT_SPACE = new TypstToken(TypstTokenType.CONTROL, ' ');
15
14
 
16
- function typst_primitive_to_string(value: TypstPrimitiveValue) {
17
- switch (typeof value) {
18
- case 'string':
19
- return `"${value}"`;
20
- case 'number':
21
- return (value as number).toString();
22
- case 'boolean':
23
- return (value as boolean) ? '#true' : '#false';
24
- default:
25
- assert(value instanceof TypstNode, 'Not a valid primitive value');
26
- return (value as TypstNode).content;
27
- }
28
- }
29
-
30
15
  export class TypstWriterError extends Error {
31
16
  node: TexNode | TypstNode | TypstToken;
32
17
 
@@ -132,6 +117,9 @@ export class TypstWriter {
132
117
  this.queue.push(new TypstToken(TypstTokenType.SYMBOL, content));
133
118
  break;
134
119
  }
120
+ case 'literal':
121
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, node.content));
122
+ break;
135
123
  case 'text':
136
124
  this.queue.push(new TypstToken(TypstTokenType.TEXT, node.content));
137
125
  break;
@@ -198,8 +186,7 @@ export class TypstWriter {
198
186
  }
199
187
  if (node.options) {
200
188
  for (const [key, value] of Object.entries(node.options)) {
201
- const value_str = typst_primitive_to_string(value);
202
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `, ${key}: ${value_str}`));
189
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, `, ${key}: ${value.toString()}`));
203
190
  }
204
191
  }
205
192
  this.queue.push(TYPST_RIGHT_PARENTHESIS);
@@ -246,8 +233,7 @@ export class TypstWriter {
246
233
  this.queue.push(TYPST_LEFT_PARENTHESIS);
247
234
  if (node.options) {
248
235
  for (const [key, value] of Object.entries(node.options)) {
249
- const value_str = typst_primitive_to_string(value);
250
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `${key}: ${value_str}, `));
236
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, `${key}: ${value.toString()}, `));
251
237
  }
252
238
  }
253
239
  matrix.forEach((row, i) => {
@@ -282,8 +268,7 @@ export class TypstWriter {
282
268
  this.queue.push(TYPST_LEFT_PARENTHESIS);
283
269
  if (node.options) {
284
270
  for (const [key, value] of Object.entries(node.options)) {
285
- const value_str = typst_primitive_to_string(value);
286
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `${key}: ${value_str}, `));
271
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, `${key}: ${value.toString()}, `));
287
272
  }
288
273
  }
289
274
  cases.forEach((row, i) => {
@@ -316,7 +301,7 @@ export class TypstWriter {
316
301
  }
317
302
 
318
303
  private appendWithBracketsIfNeeded(node: TypstNode): boolean {
319
- let need_to_wrap = ['group', 'supsub', 'fraction','empty'].includes(node.type);
304
+ let need_to_wrap = ['group', 'supsub', 'align', 'fraction','empty'].includes(node.type);
320
305
 
321
306
  if (node.type === 'group') {
322
307
  if (node.args!.length === 0) {