tex2typst 0.3.27-beta.1 → 0.3.27
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/README.md +1 -1
- package/dist/index.d.ts +23 -24
- package/dist/index.js +338 -314
- package/dist/parser.js +23 -0
- package/dist/tex2typst.min.js +12 -12
- package/package.json +1 -1
- package/src/convert.ts +53 -2
- package/src/exposed-types.ts +23 -24
- package/src/index.ts +7 -3
- package/src/jslex.ts +1 -1
- package/src/tex-tokenizer.ts +1 -0
- package/src/tex2typst.ts +2 -4
- package/src/typst-parser.ts +9 -10
- package/src/typst-types.ts +484 -230
- package/src/typst-writer.ts +28 -274
- package/tests/cheat-sheet.test.ts +42 -0
- package/tests/cheat-sheet.toml +304 -0
- package/tests/example.ts +15 -0
- package/tests/general-symbols.test.ts +22 -0
- package/tests/general-symbols.toml +755 -0
- package/tests/integration-tex2typst.yaml +89 -0
- package/tests/struct-bidirection.yaml +179 -0
- package/tests/struct-tex2typst.yaml +443 -0
- package/tests/struct-typst2tex.yaml +412 -0
- package/tests/symbol.yml +123 -0
- package/tests/test-common.ts +26 -0
- package/tests/tex-parser.test.ts +57 -0
- package/tests/tex-to-typst.test.ts +143 -0
- package/tests/typst-parser.test.ts +134 -0
- package/tests/typst-to-tex.test.ts +76 -0
- package/tsconfig.json +4 -4
package/src/typst-writer.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { TexNode } from "./tex-types";
|
|
2
|
-
import {
|
|
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
|
|
30
|
+
private options: TypstWriterOptions;
|
|
47
31
|
|
|
48
32
|
constructor(options: TypstWriterOptions) {
|
|
49
|
-
this.
|
|
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
|
-
//
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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 ===
|
|
351
|
-
|| (
|
|
352
|
-
||
|
|
353
|
-
||
|
|
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
|
-
|
|
109
|
+
qu[i] = dummy_token;
|
|
356
110
|
}
|
|
357
111
|
}
|
|
358
112
|
}
|
|
359
113
|
|
|
360
|
-
|
|
114
|
+
qu = qu.filter((token) => !token.eq(dummy_token));
|
|
361
115
|
|
|
362
|
-
for(let i = 0; i <
|
|
363
|
-
let token =
|
|
364
|
-
let previous_token = i === 0 ? null :
|
|
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
|
+
});
|