tex2typst 0.2.7 → 0.2.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/writer.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { symbolMap } from "./map";
2
- import { TexNode, TexSqrtData, TexSupsubData, TypstNode } from "./types";
2
+ import { TexNode, TexSqrtData, TexSupsubData, TypstNode, TypstSupsubData } from "./types";
3
3
 
4
4
 
5
5
  // symbols that are supported by Typst but not by KaTeX
@@ -14,8 +14,13 @@ const TYPST_INTRINSIC_SYMBOLS = [
14
14
  // 'sgn
15
15
  ];
16
16
 
17
+
18
+ function is_delimiter(c: TypstNode): boolean {
19
+ return c.type === 'atom' && ['(', ')', '[', ']', '{', '}', '|', '⌊', '⌋', '⌈', '⌉'].includes(c.content);
20
+ }
21
+
17
22
  export class TypstWriterError extends Error {
18
- node: TexNode;
23
+ node: TexNode | TypstNode;
19
24
 
20
25
  constructor(message: string, node: TexNode | TypstNode) {
21
26
  super(message);
@@ -60,7 +65,7 @@ export class TypstWriter {
60
65
  // buffer is empty
61
66
  no_need_space ||= this.buffer === "";
62
67
  // other cases
63
- no_need_space ||= /[\s"_^{\(]$/.test(this.buffer);
68
+ no_need_space ||= /[\s_^{\(]$/.test(this.buffer);
64
69
  if(!no_need_space) {
65
70
  this.buffer += ' ';
66
71
  }
@@ -73,165 +78,82 @@ export class TypstWriter {
73
78
  this.buffer += str;
74
79
  }
75
80
 
76
- public append(node: TexNode) {
77
- if (node.type === 'empty' || node.type === 'whitespace') {
78
- return;
79
- } else if (node.type === 'ordgroup') {
80
- // const index = this.startBlock();
81
- node.args!.forEach((arg) => this.append(arg));
82
- // this.endBlock(index);
83
- } else if (node.type === 'element') {
84
- let content = node.content!;
85
- if (node.content === ',' && this.insideFunctionDepth > 0) {
86
- content = 'comma';
87
- }
88
- this.queue.push({ type: 'symbol', content: content });
89
- } else if (node.type === 'symbol') {
90
- this.queue.push({ type: 'symbol', content: node.content });
91
- } else if (node.type === 'text') {
92
- this.queue.push(node as TypstNode)
93
- } else if (node.type === 'supsub') {
94
- let { base, sup, sub } = node.data as TexSupsubData;
95
-
96
- // Special logic for overbrace
97
- if (base && base.type === 'unaryFunc' && base.content === '\\overbrace' && sup) {
98
- this.append({ type: 'binaryFunc', content: '\\overbrace', args: [base.args![0], sup] });
99
- return;
100
- } else if (base && base.type === 'unaryFunc' && base.content === '\\underbrace' && sub) {
101
- this.append({ type: 'binaryFunc', content: '\\underbrace', args: [base.args![0], sub] });
102
- return;
103
-
81
+ public append(node: TypstNode) {
82
+ switch (node.type) {
83
+ case 'empty':
84
+ break;
85
+ case 'atom': {
86
+ if (node.content === ',' && this.insideFunctionDepth > 0) {
87
+ this.queue.push({ type: 'symbol', content: 'comma' });
88
+ } else {
89
+ this.queue.push({ type: 'atom', content: node.content });
90
+ }
91
+ break;
104
92
  }
105
-
106
- if (base.type === 'empty') {
107
- this.queue.push({ type: 'text', content: '' });
108
- } else {
93
+ case 'symbol':
94
+ case 'text':
95
+ case 'comment':
96
+ case 'newline':
97
+ this.queue.push(node);
98
+ break;
99
+ case 'group':
100
+ for (const item of node.args!) {
101
+ this.append(item);
102
+ }
103
+ break;
104
+ case 'supsub': {
105
+ let { base, sup, sub } = node.data as TypstSupsubData;
109
106
  this.appendWithBracketsIfNeeded(base);
110
- }
111
107
 
112
-
113
- let trailing_space_needed = false;
114
- const has_prime = (sup && sup.type === 'symbol' && sup.content === '\\prime');
115
- if (has_prime) {
116
- // Put prime symbol before '_'. Because $y_1'$ is not displayed properly in Typst (so far)
117
- // e.g.
118
- // y_1' -> y'_1
119
- // y_{a_1}' -> y'_{a_1}
120
- this.queue.push({ type: 'atom', content: '\''});
121
- trailing_space_needed = false;
122
- }
123
- if (sub) {
124
- this.queue.push({ type: 'atom', content: '_'});
125
- trailing_space_needed = this.appendWithBracketsIfNeeded(sub);
126
- }
127
- if (sup && !has_prime) {
128
- this.queue.push({ type: 'atom', content: '^'});
129
- trailing_space_needed = this.appendWithBracketsIfNeeded(sup);
130
- }
131
- if (trailing_space_needed) {
132
- this.queue.push({ type: 'softSpace', content: ''});
133
- }
134
- } else if (node.type === 'leftright') {
135
- const [left, body, right] = node.args!;
136
- // These pairs will be handled by Typst compiler by default. No need to add lr()
137
- if (["[]", "()", "\\{\\}", "\\lfloor\\rfloor", "\\lceil\\rceil"].includes(left.content + right.content)) {
138
- this.append(left);
139
- this.append(body);
140
- this.append(right);
141
- return;
108
+ let trailing_space_needed = false;
109
+ const has_prime = (sup && sup.type === 'atom' && sup.content === '\'');
110
+ if (has_prime) {
111
+ // Put prime symbol before '_'. Because $y_1'$ is not displayed properly in Typst (so far)
112
+ // e.g.
113
+ // y_1' -> y'_1
114
+ // y_{a_1}' -> y'_{a_1}
115
+ this.queue.push({ type: 'atom', content: '\''});
116
+ trailing_space_needed = false;
117
+ }
118
+ if (sub) {
119
+ this.queue.push({ type: 'atom', content: '_'});
120
+ trailing_space_needed = this.appendWithBracketsIfNeeded(sub);
121
+ }
122
+ if (sup && !has_prime) {
123
+ this.queue.push({ type: 'atom', content: '^'});
124
+ trailing_space_needed = this.appendWithBracketsIfNeeded(sup);
125
+ }
126
+ if (trailing_space_needed) {
127
+ this.queue.push({ type: 'softSpace', content: ''});
128
+ }
129
+ break;
142
130
  }
143
- const func_symbol: TypstNode = { type: 'symbol', content: 'lr' };
144
- this.queue.push(func_symbol);
145
- this.insideFunctionDepth ++;
146
- this.queue.push({ type: 'atom', content: '('});
147
- this.append(left);
148
- this.append(body);
149
- this.append(right);
150
- this.queue.push({ type: 'atom', content: ')'});
151
- this.insideFunctionDepth --;
152
- } else if (node.type === 'binaryFunc') {
153
- const func_symbol: TypstNode = { type: 'symbol', content: node.content };
154
- const [arg0, arg1] = node.args!;
155
- this.queue.push(func_symbol);
156
- this.insideFunctionDepth ++;
157
- this.queue.push({ type: 'atom', content: '('});
158
- this.append(arg0);
159
- this.queue.push({ type: 'atom', content: ','});
160
- this.append(arg1);
161
- this.queue.push({ type: 'atom', content: ')'});
162
- this.insideFunctionDepth --;
163
- } else if (node.type === 'unaryFunc') {
164
- const func_symbol: TypstNode = { type: 'symbol', content: node.content };
165
- const arg0 = node.args![0];
166
- if (node.content === '\\sqrt' && node.data) {
167
- func_symbol.content = 'root';
131
+ case 'binaryFunc': {
132
+ const func_symbol: TypstNode = { type: 'symbol', content: node.content };
133
+ const [arg0, arg1] = node.args!;
168
134
  this.queue.push(func_symbol);
169
135
  this.insideFunctionDepth ++;
170
136
  this.queue.push({ type: 'atom', content: '('});
171
- this.append(node.data as TexSqrtData); // the number of times to take the root
172
- this.queue.push({ type: 'atom', content: ','});
173
137
  this.append(arg0);
138
+ this.queue.push({ type: 'atom', content: ','});
139
+ this.append(arg1);
174
140
  this.queue.push({ type: 'atom', content: ')'});
175
141
  this.insideFunctionDepth --;
176
- return;
177
- } else if (node.content === '\\mathbf') {
178
- this.append({ type: 'symbol', content: 'upright' });
179
- this.insideFunctionDepth ++;
180
- this.queue.push({ type: 'atom', content: '('});
142
+ break;
143
+ }
144
+ case 'unaryFunc': {
145
+ const func_symbol: TypstNode = { type: 'symbol', content: node.content };
146
+ const arg0 = node.args![0];
181
147
  this.queue.push(func_symbol);
182
148
  this.insideFunctionDepth ++;
183
149
  this.queue.push({ type: 'atom', content: '('});
184
150
  this.append(arg0);
185
151
  this.queue.push({ type: 'atom', content: ')'});
186
152
  this.insideFunctionDepth --;
187
- this.queue.push({ type: 'atom', content: ')'});
188
- this.insideFunctionDepth --;
189
- return;
190
- } else if (node.content === '\\mathbb') {
191
- const body = node.args![0];
192
- if (body.type === 'element' && /^[A-Z]$/.test(body.content)) {
193
- // \mathbb{R} -> RR
194
- this.queue.push({ type: 'symbol', content: body.content + body.content});
195
- return;
196
- }
197
- // Fall through
198
- } else if (node.content === '\\operatorname') {
199
- let body = node.args!;
200
- if (body.length === 1 && body[0].type == 'ordgroup') {
201
- body = body[0].args!;
202
- }
203
- const text = body.reduce((buff, n) => {
204
- // Hope convertToken() will not throw an error
205
- // If it does, the input is bad.
206
- buff += convertToken(n.content);
207
- return buff;
208
- }, "" as string);
209
-
210
- if (this.preferTypstIntrinsic && TYPST_INTRINSIC_SYMBOLS.includes(text)) {
211
- // e.g. we prefer just sech over op("sech")
212
- this.queue.push({ type: 'symbol', content: text});
213
- } else {
214
- this.queue.push({ type: 'symbol', content: 'op' });
215
- this.queue.push({ type: 'atom', content: '('});
216
- this.queue.push({ type: 'text', content: text});
217
- this.queue.push({ type: 'atom', content: ')'});
218
- }
219
-
220
- return;
153
+ break;
221
154
  }
222
- this.queue.push(func_symbol);
223
- this.insideFunctionDepth ++;
224
- this.queue.push({ type: 'atom', content: '('});
225
- this.append(arg0);
226
- this.queue.push({ type: 'atom', content: ')'});
227
- this.insideFunctionDepth --;
228
- } else if (node.type === 'newline') {
229
- this.queue.push({ type: 'newline', content: '\n'});
230
- return;
231
- } else if (node.type === 'beginend') {
232
- if (node.content!.startsWith('align')) {
233
- // align, align*, alignat, alignat*, aligned, etc.
234
- const matrix = node.data as TexNode[][];
155
+ case 'align': {
156
+ const matrix = node.data as TypstNode[][];
235
157
  matrix.forEach((row, i) => {
236
158
  row.forEach((cell, j) => {
237
159
  if (j > 0) {
@@ -240,11 +162,13 @@ export class TypstWriter {
240
162
  this.append(cell);
241
163
  });
242
164
  if (i < matrix.length - 1) {
243
- this.queue.push({ type: 'symbol', content: '\\\\' });
165
+ this.queue.push({ type: 'symbol', content: '\\' });
244
166
  }
245
167
  });
246
- } else {
247
- const matrix = node.data as TexNode[][];
168
+ break;
169
+ }
170
+ case 'matrix': {
171
+ const matrix = node.data as TypstNode[][];
248
172
  this.queue.push({ type: 'symbol', content: 'mat' });
249
173
  this.insideFunctionDepth ++;
250
174
  this.queue.push({ type: 'atom', content: '('});
@@ -252,10 +176,10 @@ export class TypstWriter {
252
176
  matrix.forEach((row, i) => {
253
177
  row.forEach((cell, j) => {
254
178
  // There is a leading & in row
255
- if (cell.type === 'ordgroup' && cell.args!.length === 0) {
256
- this.queue.push({ type: 'atom', content: ',' });
257
- return;
258
- }
179
+ // if (cell.type === 'ordgroup' && cell.args!.length === 0) {
180
+ // this.queue.push({ type: 'atom', content: ',' });
181
+ // return;
182
+ // }
259
183
  // if (j == 0 && cell.type === 'newline' && cell.content === '\n') {
260
184
  // return;
261
185
  // }
@@ -272,27 +196,41 @@ export class TypstWriter {
272
196
  });
273
197
  this.queue.push({ type: 'atom', content: ')'});
274
198
  this.insideFunctionDepth --;
199
+ break;
275
200
  }
276
- } else if (node.type === 'matrix') {
277
- } else if (node.type === 'unknownMacro') {
278
- if (this.nonStrict) {
279
- this.queue.push({ type: 'symbol', content: node.content });
280
- } else {
281
- throw new TypstWriterError(`Unknown macro: ${node.content}`, node);
201
+ case 'unknown': {
202
+ if (this.nonStrict) {
203
+ this.queue.push({ type: 'symbol', content: node.content });
204
+ } else {
205
+ throw new TypstWriterError(`Unknown macro: ${node.content}`, node);
206
+ }
207
+ break;
282
208
  }
283
- } else if (node.type === 'control') {
284
- if (node.content === '\\\\') {
285
- this.queue.push({ type: 'symbol', content: node.content });
286
- } else if (node.content === '\\,') {
287
- this.queue.push({ type: 'symbol', content: 'thin' });
288
- } else {
289
- throw new TypstWriterError(`Unknown control sequence: ${node.content}`, node);
209
+ default:
210
+ throw new TypstWriterError(`Unimplemented node type to append: ${node.type}`, node);
211
+ }
212
+ }
213
+
214
+ private appendWithBracketsIfNeeded(node: TypstNode): boolean {
215
+ let need_to_wrap = ['group', 'supsub', 'empty'].includes(node.type);
216
+
217
+ if (node.type === 'group') {
218
+ const first = node.args![0];
219
+ const last = node.args![node.args!.length - 1];
220
+ if (is_delimiter(first) && is_delimiter(last)) {
221
+ need_to_wrap = false;
290
222
  }
291
- } else if (node.type === 'comment') {
292
- this.queue.push({ type: 'comment', content: node.content });
223
+ }
224
+
225
+ if (need_to_wrap) {
226
+ this.queue.push({ type: 'atom', content: '(' });
227
+ this.append(node);
228
+ this.queue.push({ type: 'atom', content: ')' });
293
229
  } else {
294
- throw new TypstWriterError(`Unimplemented node type to append: ${node.type}`, node);
230
+ this.append(node);
295
231
  }
232
+
233
+ return !need_to_wrap;
296
234
  }
297
235
 
298
236
  protected flushQueue() {
@@ -300,10 +238,8 @@ export class TypstWriter {
300
238
  let str = "";
301
239
  switch (node.type) {
302
240
  case 'atom':
303
- str = node.content;
304
- break;
305
241
  case 'symbol':
306
- str = convertToken(node.content);
242
+ str = node.content;
307
243
  break;
308
244
  case 'text':
309
245
  str = `"${node.content}"`;
@@ -328,24 +264,6 @@ export class TypstWriter {
328
264
  this.queue = [];
329
265
  }
330
266
 
331
- private appendWithBracketsIfNeeded(node: TexNode): boolean {
332
- const is_single = ['symbol', 'element', 'unaryFunc', 'binaryFunc', 'leftright'].includes(node.type);
333
- if (is_single) {
334
- this.append(node);
335
- } else {
336
- this.queue.push({
337
- type: 'atom',
338
- content: '('
339
- });
340
- this.append(node);
341
- this.queue.push({
342
- type: 'atom',
343
- content: ')'
344
- });
345
- }
346
- return is_single;
347
- }
348
-
349
267
  public finalize(): string {
350
268
  this.flushQueue();
351
269
  const smartFloorPass = function (input: string): string {
@@ -368,6 +286,184 @@ export class TypstWriter {
368
286
  }
369
287
  }
370
288
 
289
+ export function convertTree(node: TexNode): TypstNode {
290
+ switch (node.type) {
291
+ case 'empty':
292
+ case 'whitespace':
293
+ return { type: 'empty', content: '' };
294
+ case 'ordgroup':
295
+ return {
296
+ type: 'group',
297
+ content: '',
298
+ args: node.args!.map(convertTree),
299
+ };
300
+ case 'element':
301
+ return { type: 'atom', content: convertToken(node.content) };
302
+ case 'symbol':
303
+ return { type: 'symbol', content: convertToken(node.content) };
304
+ case 'text':
305
+ return { type: 'text', content: node.content };
306
+ case 'comment':
307
+ return { type: 'comment', content: node.content };
308
+ case 'supsub': {
309
+ let { base, sup, sub } = node.data as TexSupsubData;
310
+
311
+ // Special logic for overbrace
312
+ if (base && base.type === 'unaryFunc' && base.content === '\\overbrace' && sup) {
313
+ return {
314
+ type: 'binaryFunc',
315
+ content: 'overbrace',
316
+ args: [convertTree(base.args![0]), convertTree(sup)],
317
+ };
318
+ } else if (base && base.type === 'unaryFunc' && base.content === '\\underbrace' && sub) {
319
+ return {
320
+ type: 'binaryFunc',
321
+ content: 'underbrace',
322
+ args: [convertTree(base.args![0]), convertTree(sub)],
323
+ };
324
+ }
325
+
326
+ const data: TypstSupsubData = {
327
+ base: convertTree(base),
328
+ };
329
+ if (data.base.type === 'empty') {
330
+ data.base = { type: 'text', content: '' };
331
+ }
332
+
333
+ if (sup) {
334
+ data.sup = convertTree(sup);
335
+ }
336
+
337
+ if (sub) {
338
+ data.sub = convertTree(sub);
339
+ }
340
+
341
+ return {
342
+ type: 'supsub',
343
+ content: '',
344
+ data: data,
345
+ };
346
+ }
347
+ case 'leftright': {
348
+ const [left, body, right] = node.args!;
349
+ // These pairs will be handled by Typst compiler by default. No need to add lr()
350
+ const group: TypstNode = {
351
+ type: 'group',
352
+ content: '',
353
+ args: node.args!.map(convertTree),
354
+ };
355
+ if (["[]", "()", "\\{\\}", "\\lfloor\\rfloor", "\\lceil\\rceil"].includes(left.content + right.content)) {
356
+ return group;
357
+ }
358
+ return {
359
+ type: 'unaryFunc',
360
+ content: 'lr',
361
+ args: [group],
362
+ };
363
+ }
364
+ case 'binaryFunc': {
365
+ return {
366
+ type: 'binaryFunc',
367
+ content: convertToken(node.content),
368
+ args: node.args!.map(convertTree),
369
+ };
370
+ }
371
+ case 'unaryFunc': {
372
+ const arg0 = convertTree(node.args![0]);
373
+ // \sqrt{3}{x} -> root(3, x)
374
+ if (node.content === '\\sqrt' && node.data) {
375
+ const data = convertTree(node.data as TexSqrtData); // the number of times to take the root
376
+ return {
377
+ type: 'binaryFunc',
378
+ content: 'root',
379
+ args: [data, arg0],
380
+ };
381
+ }
382
+ // \mathbf{a} -> upright(mathbf(a))
383
+ if (node.content === '\\mathbf') {
384
+ const inner: TypstNode = {
385
+ type: 'unaryFunc',
386
+ content: 'bold',
387
+ args: [arg0],
388
+ };
389
+ return {
390
+ type: 'unaryFunc',
391
+ content: 'upright',
392
+ args: [inner],
393
+ };
394
+ }
395
+ // \mathbb{R} -> RR
396
+ if (node.content === '\\mathbb' && arg0.type === 'atom' && /^[A-Z]$/.test(arg0.content)) {
397
+ return {
398
+ type: 'symbol',
399
+ content: arg0.content + arg0.content,
400
+ };
401
+ }
402
+ // \operatorname{opname} -> op("opname")
403
+ if (node.content === '\\operatorname') {
404
+ const body = node.args!;
405
+ if (body.length !== 1 || body[0].type !== 'text') {
406
+ throw new TypstWriterError(`Expecting body of \\operatorname to be text but got`, node);
407
+ }
408
+ const text = body[0].content;
409
+
410
+ if (TYPST_INTRINSIC_SYMBOLS.includes(text)) {
411
+ return {
412
+ type: 'symbol',
413
+ content: text,
414
+ };
415
+ } else {
416
+ return {
417
+ type: 'unaryFunc',
418
+ content: 'op',
419
+ args: [{ type: 'text', content: text }],
420
+ };
421
+ }
422
+ }
423
+
424
+ // generic case
425
+ return {
426
+ type: 'unaryFunc',
427
+ content: convertToken(node.content),
428
+ args: node.args!.map(convertTree),
429
+ };
430
+ }
431
+ case 'newline':
432
+ return { type: 'newline', content: '\n' };
433
+ case 'beginend': {
434
+ const matrix = node.data as TexNode[][];
435
+ const data = matrix.map((row) => row.map(convertTree));
436
+
437
+ if (node.content!.startsWith('align')) {
438
+ // align, align*, alignat, alignat*, aligned, etc.
439
+ return {
440
+ type: 'align',
441
+ content: '',
442
+ data: data,
443
+ };
444
+ } else {
445
+ return {
446
+ type: 'matrix',
447
+ content: 'mat',
448
+ data: data,
449
+ };
450
+ }
451
+ }
452
+ case 'unknownMacro':
453
+ return { type: 'unknown', content: convertToken(node.content) };
454
+ case 'control':
455
+ if (node.content === '\\\\') {
456
+ return { type: 'symbol', content: '\\' };
457
+ } else if (node.content === '\\,') {
458
+ return { type: 'symbol', content: 'thin' };
459
+ } else {
460
+ throw new TypstWriterError(`Unknown control sequence: ${node.content}`, node);
461
+ }
462
+ default:
463
+ throw new TypstWriterError(`Unimplemented node type: ${node.type}`, node);
464
+ }
465
+ }
466
+
371
467
 
372
468
  function convertToken(token: string): string {
373
469
  if (/^[a-zA-Z0-9]$/.test(token)) {