tex2typst 0.3.27-beta.1 → 0.3.28

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.
@@ -1,12 +1,8 @@
1
1
  import { TexNode } from "./tex-types";
2
- import { TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstMarkupFunc, TypstMatrixLike, TypstNode, TypstSupsub, TypstTerminal } from "./typst-types";
2
+ import { TypstNode, TypstWriterOptions } from "./typst-types";
3
3
  import { TypstToken } from "./typst-types";
4
4
  import { TypstTokenType } from "./typst-types";
5
- import { shorthandMap } from "./typst-shorthands";
6
5
 
7
- function is_delimiter(c: TypstNode): boolean {
8
- return c.head.type === TypstTokenType.ELEMENT && ['(', ')', '[', ']', '{', '}', '|', '⌊', '⌋', '⌈', '⌉'].includes(c.head.value);
9
- }
10
6
 
11
7
  const TYPST_LEFT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '(');
12
8
  const TYPST_RIGHT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ')');
@@ -25,32 +21,16 @@ export class TypstWriterError extends Error {
25
21
  }
26
22
  }
27
23
 
28
- export interface TypstWriterOptions {
29
- nonStrict: boolean;
30
- preferShorthands: boolean;
31
- keepSpaces: boolean;
32
- inftyToOo: boolean;
33
- optimize: boolean;
34
- }
35
24
 
36
- export class TypstWriter {
37
- private nonStrict: boolean;
38
- private preferShorthands: boolean;
39
- private keepSpaces: boolean;
40
- private inftyToOo: boolean;
41
- private optimize: boolean;
42
25
 
26
+ export class TypstWriter {
43
27
  protected buffer: string = "";
44
28
  protected queue: TypstToken[] = [];
45
29
 
46
- private insideFunctionDepth = 0;
30
+ private options: TypstWriterOptions;
47
31
 
48
32
  constructor(options: TypstWriterOptions) {
49
- this.nonStrict = options.nonStrict;
50
- this.preferShorthands = options.preferShorthands;
51
- this.keepSpaces = options.keepSpaces;
52
- this.inftyToOo = options.inftyToOo;
53
- this.optimize = options.optimize;
33
+ this.options = options;
54
34
  }
55
35
 
56
36
 
@@ -78,8 +58,8 @@ export class TypstWriter {
78
58
  no_need_space ||= str.startsWith('\n');
79
59
  // buffer is empty
80
60
  no_need_space ||= this.buffer === "";
81
- // str is starting with a space itself
82
- no_need_space ||= /^\s/.test(str);
61
+ // don't put space multiple times
62
+ no_need_space ||= (/\s$/.test(this.buffer) || /^\s/.test(str));
83
63
  // "&=" instead of "& ="
84
64
  no_need_space ||= this.buffer.endsWith('&') && str === '=';
85
65
  // before or after a slash e.g. "a/b" instead of "a / b"
@@ -100,268 +80,42 @@ export class TypstWriter {
100
80
 
101
81
  // Serialize a tree of TypstNode into a list of TypstToken
102
82
  public serialize(abstractNode: TypstNode) {
103
- switch (abstractNode.type) {
104
- case 'terminal': {
105
- const node = abstractNode as TypstTerminal;
106
- if (node.head.type === TypstTokenType.ELEMENT) {
107
- if (node.head.value === ',' && this.insideFunctionDepth > 0) {
108
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, 'comma'));
109
- } else {
110
- this.queue.push(node.head);
111
- }
112
- break;
113
- } else if (node.head.type === TypstTokenType.SYMBOL) {
114
- let symbol_name = node.head.value;
115
- if(this.preferShorthands) {
116
- if (shorthandMap.has(symbol_name)) {
117
- symbol_name = shorthandMap.get(symbol_name)!;
118
- }
119
- }
120
- if (this.inftyToOo && symbol_name === 'infinity') {
121
- symbol_name = 'oo';
122
- }
123
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, symbol_name));
124
- break;
125
- } else if (node.head.type === TypstTokenType.SPACE || node.head.type === TypstTokenType.NEWLINE) {
126
- for (const c of node.head.value) {
127
- if (c === ' ') {
128
- if (this.keepSpaces) {
129
- this.queue.push(new TypstToken(TypstTokenType.SPACE, c));
130
- }
131
- } else if (c === '\n') {
132
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, c));
133
- } else {
134
- throw new TypstWriterError(`Unexpected whitespace character: ${c}`, node);
135
- }
136
- }
137
- break;
138
- } else {
139
- this.queue.push(node.head);
140
- break;
141
- }
142
- }
143
- case 'group': {
144
- const node = abstractNode as TypstGroup;
145
- for (const item of node.items) {
146
- this.serialize(item);
147
- }
148
- break;
149
- }
150
- case 'leftright': {
151
- const node = abstractNode as TypstLeftright;
152
- const LR = new TypstToken(TypstTokenType.SYMBOL, 'lr');
153
- const {left, right} = node;
154
- if (node.head.eq(LR)) {
155
- this.queue.push(LR);
156
- this.queue.push(TYPST_LEFT_PARENTHESIS);
157
- }
158
- if (left) {
159
- this.queue.push(left);
160
- }
161
- this.serialize(node.body);
162
- if (right) {
163
- this.queue.push(right);
164
- }
165
- if (node.head.eq(LR)) {
166
- this.queue.push(TYPST_RIGHT_PARENTHESIS);
167
- }
168
- break;
169
- }
170
- case 'supsub': {
171
- const node = abstractNode as TypstSupsub;
172
- let { base, sup, sub } = node;
173
- this.appendWithBracketsIfNeeded(base);
174
-
175
- let trailing_space_needed = false;
176
- const has_prime = (sup && sup.head.eq(new TypstToken(TypstTokenType.ELEMENT, "'")));
177
- if (has_prime) {
178
- // Put prime symbol before '_'. Because $y_1'$ is not displayed properly in Typst (so far)
179
- // e.g.
180
- // y_1' -> y'_1
181
- // y_{a_1}' -> y'_(a_1)
182
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '\''));
183
- trailing_space_needed = false;
184
- }
185
- if (sub) {
186
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '_'));
187
- trailing_space_needed = this.appendWithBracketsIfNeeded(sub);
188
- }
189
- if (sup && !has_prime) {
190
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '^'));
191
- trailing_space_needed = this.appendWithBracketsIfNeeded(sup);
192
- }
193
- if (trailing_space_needed) {
194
- this.queue.push(SOFT_SPACE);
195
- }
196
- break;
197
- }
198
- case 'funcCall': {
199
- const node = abstractNode as TypstFuncCall;
200
- const func_symbol: TypstToken = node.head;
201
- this.queue.push(func_symbol);
202
- this.insideFunctionDepth++;
203
- this.queue.push(TYPST_LEFT_PARENTHESIS);
204
- for (let i = 0; i < node.args.length; i++) {
205
- this.serialize(node.args[i]);
206
- if (i < node.args.length - 1) {
207
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, ','));
208
- }
209
- }
210
- if (node.options) {
211
- for (const [key, value] of Object.entries(node.options)) {
212
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, `, ${key}: ${value.toString()}`));
213
- }
214
- }
215
- this.queue.push(TYPST_RIGHT_PARENTHESIS);
216
- this.insideFunctionDepth--;
217
- break;
218
- }
219
- case 'fraction': {
220
- const node = abstractNode as TypstFraction;
221
- const [numerator, denominator] = node.args;
222
- const pos = this.queue.length;
223
- const no_wrap = this.appendWithBracketsIfNeeded(numerator);
224
-
225
- // This is a dirty hack to force `C \frac{xy}{z}`to translate to `C (x y)/z` instead of `C(x y)/z`
226
- // To solve this properly, we should implement a Typst formatter
227
- const wrapped = !no_wrap;
228
- if (wrapped) {
229
- this.queue.splice(pos, 0, SOFT_SPACE);
230
- }
231
-
232
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '/'));
233
- this.appendWithBracketsIfNeeded(denominator);
234
- break;
235
- }
236
- case 'matrixLike': {
237
- const node = abstractNode as TypstMatrixLike;
238
- const matrix = node.matrix;
239
-
240
- let cell_sep: TypstToken;
241
- let row_sep: TypstToken;
242
- if (node.head.eq(TypstMatrixLike.MAT)) {
243
- cell_sep = new TypstToken(TypstTokenType.ELEMENT, ',');
244
- row_sep = new TypstToken(TypstTokenType.ELEMENT, ';');
245
- } else if (node.head.eq(TypstMatrixLike.CASES)) {
246
- cell_sep = new TypstToken(TypstTokenType.ELEMENT, '&');
247
- row_sep = new TypstToken(TypstTokenType.ELEMENT, ',');
248
- } else if (node.head.eq(TypstToken.NONE)){ // head is null
249
- cell_sep = new TypstToken(TypstTokenType.ELEMENT, '&');
250
- row_sep = new TypstToken(TypstTokenType.SYMBOL, '\\');
251
- }
252
-
253
- if (!node.head.eq(TypstToken.NONE)) {
254
- this.queue.push(node.head);
255
- this.insideFunctionDepth++;
256
- this.queue.push(TYPST_LEFT_PARENTHESIS);
257
- if (node.options) {
258
- for (const [key, value] of Object.entries(node.options)) {
259
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, `${key}: ${value.toString()}, `));
260
- }
261
- }
262
- }
263
-
264
- matrix.forEach((row, i) => {
265
- row.forEach((cell, j) => {
266
- this.serialize(cell);
267
- if (j < row.length - 1) {
268
- this.queue.push(cell_sep);
269
- } else {
270
- if (i < matrix.length - 1) {
271
- this.queue.push(row_sep);
272
- }
273
- }
274
- });
275
- });
276
-
277
- if (!node.head.eq(TypstToken.NONE)) {
278
- this.queue.push(TYPST_RIGHT_PARENTHESIS);
279
- this.insideFunctionDepth--;
280
- }
281
- break;
282
- }
283
- case 'markupFunc': {
284
- const node = abstractNode as TypstMarkupFunc;
285
- this.queue.push(node.head);
286
- this.queue.push(TYPST_LEFT_PARENTHESIS);
287
- if (node.options) {
288
- const entries = Object.entries(node.options);
289
- for (let i = 0; i < entries.length; i++) {
290
- const [key, value] = entries[i];
291
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, `${key}: ${value.toString()}`));
292
- if (i < entries.length - 1) {
293
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, ','));
294
- }
295
- }
296
- }
297
- this.queue.push(TYPST_RIGHT_PARENTHESIS);
298
-
299
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, '['));
300
- for (const frag of node.fragments) {
301
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, '$'));
302
- this.serialize(frag);
303
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, '$'));
304
- }
305
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, ']'));
306
-
307
- break;
308
- }
309
- default:
310
- throw new TypstWriterError(`Unimplemented node type to append: ${abstractNode.type}`, abstractNode);
311
- }
83
+ const env = {insideFunctionDepth: 0};
84
+ this.queue.push(...abstractNode.serialize(env, this.options));
312
85
  }
313
86
 
314
- private appendWithBracketsIfNeeded(node: TypstNode): boolean {
315
- let need_to_wrap = ['group', 'supsub', 'matrixLike', 'fraction','empty'].includes(node.type);
316
87
 
317
- if (node.type === 'group') {
318
- const group = node as TypstGroup;
319
- if (group.items.length === 0) {
320
- // e.g. TeX `P_{}` converts to Typst `P_()`
321
- need_to_wrap = true;
322
- } else {
323
- const first = group.items[0];
324
- const last = group.items[group.items.length - 1];
325
- if (is_delimiter(first) && is_delimiter(last)) {
326
- need_to_wrap = false;
327
- }
88
+ protected flushQueue() {
89
+ // merge consecutive soft spaces
90
+ let qu: TypstToken[] = [];
91
+ for(const token of this.queue) {
92
+ if (token.eq(SOFT_SPACE) && qu.length > 0 && qu[qu.length - 1].eq(SOFT_SPACE)) {
93
+ continue;
328
94
  }
95
+ qu.push(token);
329
96
  }
330
97
 
331
- if (need_to_wrap) {
332
- this.queue.push(TYPST_LEFT_PARENTHESIS);
333
- this.serialize(node);
334
- this.queue.push(TYPST_RIGHT_PARENTHESIS);
335
- } else {
336
- this.serialize(node);
337
- }
338
-
339
- return !need_to_wrap;
340
- }
341
-
342
- protected flushQueue() {
343
- const dummy_token = new TypstToken(TypstTokenType.SYMBOL, '');
344
-
345
98
  // delete soft spaces if they are not needed
346
- for(let i = 0; i < this.queue.length; i++) {
347
- let token = this.queue[i];
99
+ const dummy_token = new TypstToken(TypstTokenType.SYMBOL, '');
100
+ for(let i = 0; i < qu.length; i++) {
101
+ let token = qu[i];
348
102
  if (token.eq(SOFT_SPACE)) {
349
103
  const to_delete = (i === 0)
350
- || (i === this.queue.length - 1)
351
- || (this.queue[i - 1].type === TypstTokenType.SPACE)
352
- || this.queue[i - 1].isOneOf([TYPST_LEFT_PARENTHESIS, TYPST_NEWLINE])
353
- || this.queue[i + 1].isOneOf([TYPST_RIGHT_PARENTHESIS, TYPST_COMMA, TYPST_NEWLINE]);
104
+ || (i === qu.length - 1)
105
+ || (qu[i - 1].type === TypstTokenType.SPACE)
106
+ || qu[i - 1].isOneOf([TYPST_LEFT_PARENTHESIS, TYPST_NEWLINE])
107
+ || qu[i + 1].isOneOf([TYPST_RIGHT_PARENTHESIS, TYPST_COMMA, TYPST_NEWLINE]);
354
108
  if (to_delete) {
355
- this.queue[i] = dummy_token;
109
+ qu[i] = dummy_token;
356
110
  }
357
111
  }
358
112
  }
359
113
 
360
- this.queue = this.queue.filter((token) => !token.eq(dummy_token));
114
+ qu = qu.filter((token) => !token.eq(dummy_token));
361
115
 
362
- for(let i = 0; i < this.queue.length; i++) {
363
- let token = this.queue[i];
364
- let previous_token = i === 0 ? null : this.queue[i - 1];
116
+ for(let i = 0; i < qu.length; i++) {
117
+ let token = qu[i];
118
+ let previous_token = i === 0 ? null : qu[i - 1];
365
119
  this.writeBuffer(previous_token, token);
366
120
  }
367
121
 
@@ -391,7 +145,7 @@ export class TypstWriter {
391
145
  res = res.replace(/round\(\)/g, 'round("")');
392
146
  return res;
393
147
  }
394
- if (this.optimize) {
148
+ if (this.options.optimize) {
395
149
  const all_passes = [smartFloorPass, smartCeilPass, smartRoundPass];
396
150
  for (const pass of all_passes) {
397
151
  this.buffer = pass(this.buffer);
@@ -0,0 +1,42 @@
1
+ import path from 'path';
2
+ import toml from 'toml';
3
+ import fs from 'node:fs';
4
+ import { describe, it, test, expect } from 'vitest';
5
+ import { tex2typst, symbolMap } from '../src';
6
+
7
+ interface CheatSheet {
8
+ math_commands: { [key: string]: string };
9
+ math_symbols: { [key: string]: string };
10
+ }
11
+
12
+ describe('cheat sheet', () => {
13
+ const cheatSheetFile = path.join(__dirname, 'cheat-sheet.toml');
14
+ const text_content = fs.readFileSync(cheatSheetFile, { encoding: 'utf-8' });
15
+ const data = toml.parse(text_content) as CheatSheet;
16
+
17
+ test('math_commands', () => {
18
+ expect(data.math_commands).toBeDefined();
19
+
20
+
21
+ for (const [key, value] of Object.entries(data.math_commands)) {
22
+ const input = `\\${key}{x}{y}`;
23
+ const expected1 = `${value} x y`;
24
+ const expected2 = `${value}(x) y`;
25
+ const expected3 = `${value}(x, y)`;
26
+ const result = tex2typst(input, {preferShorthands: false});
27
+ expect([expected1, expected2, expected3]).toContain(result);
28
+ }
29
+ });
30
+
31
+ test('math_symbols', () => {
32
+ expect(data.math_symbols).toBeDefined();
33
+
34
+ for (const [key, value] of Object.entries(data.math_symbols)) {
35
+ const input = `\\${key}`;
36
+ const expected = value;
37
+ const result = tex2typst(input, {preferShorthands: false});
38
+ expect(result).toBe(expected);
39
+ expect(symbolMap.get(key)).toBe(expected);
40
+ }
41
+ });
42
+ });