tex2typst 0.2.6 → 0.2.8

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