tex2typst 0.3.0 → 0.3.1
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 +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +346 -178
- package/dist/tex-parser.d.ts +1 -0
- package/dist/tex2typst.min.js +20 -19
- package/dist/types.d.ts +6 -4
- package/package.json +5 -5
- package/src/convert.ts +45 -5
- package/src/index.ts +1 -1
- package/src/tex-parser.ts +33 -44
- package/src/tex-writer.ts +0 -1
- package/src/types.ts +8 -2
- package/src/typst-parser.ts +2 -3
- package/src/typst-writer.ts +23 -3
package/src/convert.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions } from "./types";
|
|
1
|
+
import { TexNode, TypstNode, TexSupsubData, TypstSupsubData, TexSqrtData, Tex2TypstOptions, TYPST_NONE, TYPST_TRUE, TypstPrimitiveValue } from "./types";
|
|
2
2
|
import { TypstWriterError, TYPST_INTRINSIC_SYMBOLS } from "./typst-writer";
|
|
3
3
|
import { symbolMap, reverseSymbolMap } from "./map";
|
|
4
4
|
|
|
@@ -60,7 +60,7 @@ function convert_overset(node: TexNode, options: Tex2TypstOptions): TypstNode {
|
|
|
60
60
|
'op',
|
|
61
61
|
[convert_tex_node_to_typst(base, options)]
|
|
62
62
|
);
|
|
63
|
-
op_call.setOptions({ limits:
|
|
63
|
+
op_call.setOptions({ limits: TYPST_TRUE });
|
|
64
64
|
return new TypstNode(
|
|
65
65
|
'supsub',
|
|
66
66
|
'',
|
|
@@ -240,11 +240,51 @@ export function convert_tex_node_to_typst(node: TexNode, options: Tex2TypstOptio
|
|
|
240
240
|
if (node.content!.startsWith('align')) {
|
|
241
241
|
// align, align*, alignat, alignat*, aligned, etc.
|
|
242
242
|
return new TypstNode('align', '', [], data);
|
|
243
|
-
}
|
|
243
|
+
}
|
|
244
|
+
if (node.content!.endsWith('matrix')) {
|
|
245
|
+
let delim: TypstPrimitiveValue = null;
|
|
246
|
+
switch (node.content) {
|
|
247
|
+
case 'matrix':
|
|
248
|
+
delim = TYPST_NONE;
|
|
249
|
+
break;
|
|
250
|
+
case 'pmatrix':
|
|
251
|
+
delim = '(';
|
|
252
|
+
break;
|
|
253
|
+
case 'bmatrix':
|
|
254
|
+
delim = '[';
|
|
255
|
+
break;
|
|
256
|
+
case 'Bmatrix':
|
|
257
|
+
delim = '{';
|
|
258
|
+
break;
|
|
259
|
+
case 'vmatrix':
|
|
260
|
+
delim = '|';
|
|
261
|
+
break;
|
|
262
|
+
case 'Vmatrix': {
|
|
263
|
+
// mat(delim: "||") does not compile in Typst.
|
|
264
|
+
// For a workaround, translate
|
|
265
|
+
// \begin{Vmatrix}
|
|
266
|
+
// a & b \\
|
|
267
|
+
// c & d
|
|
268
|
+
// \end{Vmatrix}
|
|
269
|
+
// to
|
|
270
|
+
// lr(||mat(delim: #none, a, b; c, d)||)
|
|
271
|
+
const matrix = new TypstNode('matrix', '', [], data);
|
|
272
|
+
matrix.setOptions({ 'delim': TYPST_NONE });
|
|
273
|
+
const group = new TypstNode('group', '', [
|
|
274
|
+
new TypstNode('symbol', '||'),
|
|
275
|
+
matrix,
|
|
276
|
+
new TypstNode('symbol', '||'),
|
|
277
|
+
]);
|
|
278
|
+
return new TypstNode('funcCall', 'lr', [ group ]);
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
|
|
282
|
+
}
|
|
244
283
|
const res = new TypstNode('matrix', '', [], data);
|
|
245
|
-
res.setOptions({ 'delim':
|
|
284
|
+
res.setOptions({ 'delim': delim });
|
|
246
285
|
return res;
|
|
247
286
|
}
|
|
287
|
+
throw new TypstWriterError(`Unimplemented beginend: ${node.content}`, node);
|
|
248
288
|
}
|
|
249
289
|
case 'unknownMacro':
|
|
250
290
|
return new TypstNode('unknown', tex_token_to_typst(node.content));
|
|
@@ -416,7 +456,7 @@ export function convert_typst_node_to_tex(node: TypstNode): TexNode {
|
|
|
416
456
|
if (node.options) {
|
|
417
457
|
if ('delim' in node.options) {
|
|
418
458
|
switch (node.options.delim) {
|
|
419
|
-
case
|
|
459
|
+
case TYPST_NONE:
|
|
420
460
|
return matrix;
|
|
421
461
|
case '[':
|
|
422
462
|
left_delim = "\\left[";
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseTex } from "./tex-parser";
|
|
2
|
-
import { Tex2TypstOptions } from "./types";
|
|
2
|
+
import type { Tex2TypstOptions } from "./types";
|
|
3
3
|
import { TypstWriter } from "./typst-writer";
|
|
4
4
|
import { convert_tex_node_to_typst, convert_typst_node_to_tex } from "./convert";
|
|
5
5
|
import { symbolMap } from "./map";
|
package/src/tex-parser.ts
CHANGED
|
@@ -296,46 +296,39 @@ export class LatexParser {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
parse(tokens: TexToken[]): TexNode {
|
|
299
|
+
const [tree, _] = this.parseGroup(tokens, 0, tokens.length);
|
|
300
|
+
return tree;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
parseGroup(tokens: TexToken[], start: number, end: number): ParseResult {
|
|
299
304
|
const results: TexNode[] = [];
|
|
300
|
-
let pos =
|
|
301
|
-
while (pos <
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
pos = newPos;
|
|
308
|
-
if(res.type === 'whitespace') {
|
|
309
|
-
if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
if (!this.newline_sensitive && res.content === '\n') {
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
305
|
+
let pos = start;
|
|
306
|
+
while (pos < end) {
|
|
307
|
+
const [res, newPos] = this.parseNextExpr(tokens, pos);
|
|
308
|
+
pos = newPos;
|
|
309
|
+
if(res.type === 'whitespace') {
|
|
310
|
+
if (!this.space_sensitive && res.content.replace(/ /g, '').length === 0) {
|
|
311
|
+
continue;
|
|
315
312
|
}
|
|
316
|
-
if (
|
|
317
|
-
|
|
313
|
+
if (!this.newline_sensitive && res.content === '\n') {
|
|
314
|
+
continue;
|
|
318
315
|
}
|
|
319
|
-
results.push(res);
|
|
320
316
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return EMPTY_NODE;
|
|
324
|
-
} else if (results.length === 1) {
|
|
325
|
-
return results[0];
|
|
326
|
-
} else {
|
|
327
|
-
return new TexNode('ordgroup', '', results);
|
|
317
|
+
if (res.type === 'control' && res.content === '&') {
|
|
318
|
+
throw new LatexParserError('Unexpected & outside of an alignment');
|
|
328
319
|
}
|
|
320
|
+
results.push(res);
|
|
329
321
|
}
|
|
330
322
|
|
|
331
|
-
|
|
323
|
+
let node: TexNode;
|
|
332
324
|
if (results.length === 0) {
|
|
333
|
-
|
|
325
|
+
node = EMPTY_NODE;
|
|
334
326
|
} else if (results.length === 1) {
|
|
335
|
-
|
|
327
|
+
node = results[0];
|
|
336
328
|
} else {
|
|
337
|
-
|
|
329
|
+
node = new TexNode('ordgroup', '', results);
|
|
338
330
|
}
|
|
331
|
+
return [node, end + 1];
|
|
339
332
|
}
|
|
340
333
|
|
|
341
334
|
parseNextExpr(tokens: TexToken[], start: number): ParseResult {
|
|
@@ -396,8 +389,7 @@ export class LatexParser {
|
|
|
396
389
|
|
|
397
390
|
parseNextExprWithoutSupSub(tokens: TexToken[], start: number): ParseResult {
|
|
398
391
|
const firstToken = tokens[start];
|
|
399
|
-
|
|
400
|
-
switch (tokenType) {
|
|
392
|
+
switch (firstToken.type) {
|
|
401
393
|
case TexTokenType.ELEMENT:
|
|
402
394
|
return [new TexNode('element', firstToken.value), start + 1];
|
|
403
395
|
case TexTokenType.TEXT:
|
|
@@ -423,8 +415,7 @@ export class LatexParser {
|
|
|
423
415
|
if(posClosingBracket === -1) {
|
|
424
416
|
throw new LatexParserError("Unmatched '{'");
|
|
425
417
|
}
|
|
426
|
-
|
|
427
|
-
return [this.parse(exprInside), posClosingBracket + 1];
|
|
418
|
+
return this.parseGroup(tokens, start + 1, posClosingBracket);
|
|
428
419
|
case '}':
|
|
429
420
|
throw new LatexParserError("Unmatched '}'");
|
|
430
421
|
case '\\\\':
|
|
@@ -453,7 +444,7 @@ export class LatexParser {
|
|
|
453
444
|
|
|
454
445
|
if (['left', 'right', 'begin', 'end'].includes(command.slice(1))) {
|
|
455
446
|
throw new LatexParserError('Unexpected command: ' + command);
|
|
456
|
-
}
|
|
447
|
+
}
|
|
457
448
|
|
|
458
449
|
|
|
459
450
|
const paramNum = get_command_param_num(command.slice(1));
|
|
@@ -475,8 +466,7 @@ export class LatexParser {
|
|
|
475
466
|
if (posRightSquareBracket === -1) {
|
|
476
467
|
throw new LatexParserError('No matching right square bracket for [');
|
|
477
468
|
}
|
|
478
|
-
const
|
|
479
|
-
const exponent = this.parse(exprInside);
|
|
469
|
+
const [exponent, _] = this.parseGroup(tokens, posLeftSquareBracket + 1, posRightSquareBracket);
|
|
480
470
|
const [arg1, newPos] = this.parseNextExprWithoutSupSub(tokens, posRightSquareBracket + 1);
|
|
481
471
|
return [new TexNode('unaryFunc', command, [arg1], exponent), newPos];
|
|
482
472
|
} else if (command === '\\text') {
|
|
@@ -529,15 +519,14 @@ export class LatexParser {
|
|
|
529
519
|
if (pos >= tokens.length) {
|
|
530
520
|
throw new LatexParserError('Expecting \\right after \\left');
|
|
531
521
|
}
|
|
532
|
-
|
|
522
|
+
|
|
533
523
|
const rightDelimiter = eat_parenthesis(tokens, pos);
|
|
534
524
|
if (rightDelimiter === null) {
|
|
535
525
|
throw new LatexParserError('Invalid delimiter after \\right');
|
|
536
526
|
}
|
|
537
527
|
pos++;
|
|
538
528
|
|
|
539
|
-
const
|
|
540
|
-
const body = this.parse(exprInside);
|
|
529
|
+
const [body, _] = this.parseGroup(tokens, exprInsideStart, exprInsideEnd);
|
|
541
530
|
const args: TexNode[] = [
|
|
542
531
|
new TexNode('element', leftDelimiter.value),
|
|
543
532
|
body,
|
|
@@ -556,9 +545,9 @@ export class LatexParser {
|
|
|
556
545
|
assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
|
|
557
546
|
const envName = tokens[pos + 1].value;
|
|
558
547
|
pos += 3;
|
|
559
|
-
|
|
548
|
+
|
|
560
549
|
pos += eat_whitespaces(tokens, pos).length; // ignore whitespaces and '\n' after \begin{envName}
|
|
561
|
-
|
|
550
|
+
|
|
562
551
|
const exprInsideStart = pos;
|
|
563
552
|
|
|
564
553
|
const endIdx = find_closing_end_command(tokens, start);
|
|
@@ -567,7 +556,7 @@ export class LatexParser {
|
|
|
567
556
|
}
|
|
568
557
|
const exprInsideEnd = endIdx;
|
|
569
558
|
pos = endIdx + 1;
|
|
570
|
-
|
|
559
|
+
|
|
571
560
|
assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
|
|
572
561
|
assert(tokens[pos + 1].type === TexTokenType.TEXT);
|
|
573
562
|
assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
|
|
@@ -575,7 +564,7 @@ export class LatexParser {
|
|
|
575
564
|
throw new LatexParserError('Mismatched \\begin and \\end environments');
|
|
576
565
|
}
|
|
577
566
|
pos += 3;
|
|
578
|
-
|
|
567
|
+
|
|
579
568
|
const exprInside = tokens.slice(exprInsideStart, exprInsideEnd);
|
|
580
569
|
// ignore spaces and '\n' before \end{envName}
|
|
581
570
|
while(exprInside.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(exprInside[exprInside.length - 1].type)) {
|
|
@@ -606,7 +595,7 @@ export class LatexParser {
|
|
|
606
595
|
continue;
|
|
607
596
|
}
|
|
608
597
|
}
|
|
609
|
-
|
|
598
|
+
|
|
610
599
|
if (res.type === 'control' && res.content === '\\\\') {
|
|
611
600
|
row = [];
|
|
612
601
|
group = new TexNode('ordgroup', '', []);
|
package/src/tex-writer.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -352,7 +352,13 @@ export type TypstArrayData = TypstNode[][];
|
|
|
352
352
|
type TypstNodeType = 'atom' | 'symbol' | 'text' | 'control' | 'comment' | 'whitespace'
|
|
353
353
|
| 'empty' | 'group' | 'supsub' | 'funcCall' | 'fraction' | 'align' | 'matrix' | 'unknown';
|
|
354
354
|
|
|
355
|
-
export type
|
|
355
|
+
export type TypstPrimitiveValue = string | boolean | null;
|
|
356
|
+
export type TypstNamedParams = { [key: string]: TypstPrimitiveValue };
|
|
357
|
+
|
|
358
|
+
// #none
|
|
359
|
+
export const TYPST_NONE: TypstPrimitiveValue = null;
|
|
360
|
+
export const TYPST_TRUE: TypstPrimitiveValue = true;
|
|
361
|
+
export const TYPST_FALSE: TypstPrimitiveValue = false;
|
|
356
362
|
|
|
357
363
|
export class TypstNode {
|
|
358
364
|
type: TypstNodeType;
|
|
@@ -370,7 +376,7 @@ export class TypstNode {
|
|
|
370
376
|
this.data = data;
|
|
371
377
|
}
|
|
372
378
|
|
|
373
|
-
public setOptions(options:
|
|
379
|
+
public setOptions(options: TypstNamedParams) {
|
|
374
380
|
this.options = options;
|
|
375
381
|
}
|
|
376
382
|
|
package/src/typst-parser.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { array_find } from "./generic";
|
|
3
|
-
import { TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
|
|
3
|
+
import { TYPST_NONE, TypstNamedParams, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
|
|
4
4
|
import { assert, isalpha, isdigit } from "./util";
|
|
5
5
|
|
|
6
6
|
// TODO: In Typst, y' ' is not the same as y''.
|
|
@@ -450,7 +450,6 @@ export class TypstParser {
|
|
|
450
450
|
// start: the position of the left parentheses
|
|
451
451
|
parseArguments(tokens: TypstToken[], start: number): [TypstNode[], number] {
|
|
452
452
|
const end = find_closing_match(tokens, start);
|
|
453
|
-
|
|
454
453
|
return [this.parseCommaSeparatedArguments(tokens, start + 1, end), end + 1];
|
|
455
454
|
}
|
|
456
455
|
|
|
@@ -501,7 +500,7 @@ export class TypstParser {
|
|
|
501
500
|
if(g.args!.length !== 4 || !g.args![pos_colon + 2].eq(new TypstNode('symbol', 'none'))) {
|
|
502
501
|
throw new TypstParserError('Invalid number of arguments for delim');
|
|
503
502
|
}
|
|
504
|
-
np['delim'] =
|
|
503
|
+
np['delim'] = TYPST_NONE;
|
|
505
504
|
} else {
|
|
506
505
|
throw new TypstParserError('Not implemented for other types of delim');
|
|
507
506
|
}
|
package/src/typst-writer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TexNode, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
|
|
1
|
+
import { TexNode, TypstNode, TypstPrimitiveValue, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
// symbols that are supported by Typst but not by KaTeX
|
|
@@ -24,6 +24,22 @@ const TYPST_RIGHT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMEN
|
|
|
24
24
|
const TYPST_COMMA: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ',');
|
|
25
25
|
const TYPST_NEWLINE: TypstToken = new TypstToken(TypstTokenType.SYMBOL, '\n');
|
|
26
26
|
|
|
27
|
+
function typst_primitive_to_string(value: TypstPrimitiveValue) {
|
|
28
|
+
switch (typeof value) {
|
|
29
|
+
case 'string':
|
|
30
|
+
return `"${value}"`;
|
|
31
|
+
case 'number':
|
|
32
|
+
return (value as number).toString();
|
|
33
|
+
case 'boolean':
|
|
34
|
+
return (value as boolean) ? '#true' : '#false';
|
|
35
|
+
default:
|
|
36
|
+
if (value === null) {
|
|
37
|
+
return '#none';
|
|
38
|
+
}
|
|
39
|
+
throw new TypstWriterError(`Invalid primitive value: ${value}`, value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
export class TypstWriterError extends Error {
|
|
28
44
|
node: TexNode | TypstNode | TypstToken;
|
|
29
45
|
|
|
@@ -58,6 +74,8 @@ export class TypstWriter {
|
|
|
58
74
|
return;
|
|
59
75
|
}
|
|
60
76
|
|
|
77
|
+
// TODO: "C \frac{xy}{z}" should translate to "C (x y)/z" instead of "C(x y)/z"
|
|
78
|
+
|
|
61
79
|
let no_need_space = false;
|
|
62
80
|
// putting the first token in clause
|
|
63
81
|
no_need_space ||= /[\(\[\|]$/.test(this.buffer) && /^\w/.test(str);
|
|
@@ -172,7 +190,8 @@ export class TypstWriter {
|
|
|
172
190
|
}
|
|
173
191
|
if (node.options) {
|
|
174
192
|
for (const [key, value] of Object.entries(node.options)) {
|
|
175
|
-
|
|
193
|
+
const value_str = typst_primitive_to_string(value);
|
|
194
|
+
this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `, ${key}: ${value_str}`));
|
|
176
195
|
}
|
|
177
196
|
}
|
|
178
197
|
this.queue.push(TYPST_RIGHT_PARENTHESIS);
|
|
@@ -220,7 +239,8 @@ export class TypstWriter {
|
|
|
220
239
|
this.queue.push(TYPST_LEFT_PARENTHESIS);
|
|
221
240
|
if (node.options) {
|
|
222
241
|
for (const [key, value] of Object.entries(node.options)) {
|
|
223
|
-
|
|
242
|
+
const value_str = typst_primitive_to_string(value);
|
|
243
|
+
this.queue.push(new TypstToken(TypstTokenType.SYMBOL, `${key}: ${value_str}, `));
|
|
224
244
|
}
|
|
225
245
|
}
|
|
226
246
|
matrix.forEach((row, i) => {
|