tex2typst 0.2.8 → 0.2.11
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/LICENSE +674 -21
- package/README.md +4 -3
- package/dist/index.js +106 -77
- package/dist/parser.d.ts +7 -1
- package/dist/tex2typst.min.js +1 -1
- package/dist/types.d.ts +0 -4
- package/package.json +2 -2
- package/src/parser.ts +69 -60
- package/src/types.ts +0 -4
- package/src/writer.ts +46 -21
- package/tool/dist/dist/ka.js +0 -13654
- package/tool/dist/ka.js +0 -13634
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tex2typst",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "JavaScript library for converting TeX code to Typst",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"repository": "https://github.com/qwinsi/tex2typst",
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "GPL-3.0",
|
|
10
10
|
"keywords": [
|
|
11
11
|
"LaTeX",
|
|
12
12
|
"Typst",
|
package/src/parser.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { symbolMap } from "./map";
|
|
2
|
-
import { TexNode, TexSupsubData,
|
|
2
|
+
import { TexNode, TexSupsubData, TokenType } from "./types";
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
const UNARY_COMMANDS = [
|
|
@@ -43,6 +43,21 @@ const BINARY_COMMANDS = [
|
|
|
43
43
|
'tbinom',
|
|
44
44
|
]
|
|
45
45
|
|
|
46
|
+
|
|
47
|
+
export class Token {
|
|
48
|
+
type: TokenType;
|
|
49
|
+
value: string;
|
|
50
|
+
|
|
51
|
+
constructor(type: TokenType, value: string) {
|
|
52
|
+
this.type = type;
|
|
53
|
+
this.value = value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public eq(token: Token): boolean {
|
|
57
|
+
return this.type === token.type && this.value === token.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
const EMPTY_NODE: TexNode = { type: 'empty', content: '' };
|
|
47
62
|
|
|
48
63
|
function assert(condition: boolean, message: string = ''): void {
|
|
@@ -61,11 +76,11 @@ function get_command_param_num(command: string): number {
|
|
|
61
76
|
}
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
const LEFT_CURLY_BRACKET: Token =
|
|
65
|
-
const RIGHT_CURLY_BRACKET: Token =
|
|
79
|
+
const LEFT_CURLY_BRACKET: Token = new Token(TokenType.CONTROL, '{');
|
|
80
|
+
const RIGHT_CURLY_BRACKET: Token = new Token(TokenType.CONTROL, '}');
|
|
66
81
|
|
|
67
82
|
function find_closing_curly_bracket(tokens: Token[], start: number): number {
|
|
68
|
-
assert(
|
|
83
|
+
assert(tokens[start].eq(LEFT_CURLY_BRACKET));
|
|
69
84
|
let count = 1;
|
|
70
85
|
let pos = start + 1;
|
|
71
86
|
|
|
@@ -73,9 +88,9 @@ function find_closing_curly_bracket(tokens: Token[], start: number): number {
|
|
|
73
88
|
if (pos >= tokens.length) {
|
|
74
89
|
throw new LatexParserError('Unmatched curly brackets');
|
|
75
90
|
}
|
|
76
|
-
if (
|
|
91
|
+
if (tokens[pos].eq(LEFT_CURLY_BRACKET)) {
|
|
77
92
|
count += 1;
|
|
78
|
-
} else if (
|
|
93
|
+
} else if (tokens[pos].eq(RIGHT_CURLY_BRACKET)) {
|
|
79
94
|
count -= 1;
|
|
80
95
|
}
|
|
81
96
|
pos += 1;
|
|
@@ -84,11 +99,11 @@ function find_closing_curly_bracket(tokens: Token[], start: number): number {
|
|
|
84
99
|
return pos - 1;
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
const LEFT_SQUARE_BRACKET: Token =
|
|
88
|
-
const RIGHT_SQUARE_BRACKET: Token =
|
|
102
|
+
const LEFT_SQUARE_BRACKET: Token = new Token(TokenType.ELEMENT, '[');
|
|
103
|
+
const RIGHT_SQUARE_BRACKET: Token = new Token(TokenType.ELEMENT, ']');
|
|
89
104
|
|
|
90
105
|
function find_closing_square_bracket(tokens: Token[], start: number): number {
|
|
91
|
-
assert(
|
|
106
|
+
assert(tokens[start].eq(LEFT_SQUARE_BRACKET));
|
|
92
107
|
let count = 1;
|
|
93
108
|
let pos = start + 1;
|
|
94
109
|
|
|
@@ -96,9 +111,9 @@ function find_closing_square_bracket(tokens: Token[], start: number): number {
|
|
|
96
111
|
if (pos >= tokens.length) {
|
|
97
112
|
throw new LatexParserError('Unmatched square brackets');
|
|
98
113
|
}
|
|
99
|
-
if (
|
|
114
|
+
if (tokens[pos].eq(LEFT_SQUARE_BRACKET)) {
|
|
100
115
|
count += 1;
|
|
101
|
-
} else if (
|
|
116
|
+
} else if (tokens[pos].eq(RIGHT_SQUARE_BRACKET)) {
|
|
102
117
|
count -= 1;
|
|
103
118
|
}
|
|
104
119
|
pos += 1;
|
|
@@ -138,7 +153,7 @@ function eat_parenthesis(tokens: Token[], start: number): Token | null {
|
|
|
138
153
|
|
|
139
154
|
function eat_primes(tokens: Token[], start: number): number {
|
|
140
155
|
let pos = start;
|
|
141
|
-
while (pos < tokens.length &&
|
|
156
|
+
while (pos < tokens.length && tokens[pos].eq(new Token(TokenType.ELEMENT, "'"))) {
|
|
142
157
|
pos += 1;
|
|
143
158
|
}
|
|
144
159
|
return pos - start;
|
|
@@ -154,10 +169,8 @@ function eat_command_name(latex: string, start: number): string {
|
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const LEFT_COMMAND: Token = { type: TokenType.COMMAND, value: '\\left' };
|
|
160
|
-
const RIGHT_COMMAND: Token = { type: TokenType.COMMAND, value: '\\right' };
|
|
172
|
+
const LEFT_COMMAND: Token = new Token(TokenType.COMMAND, '\\left');
|
|
173
|
+
const RIGHT_COMMAND: Token = new Token(TokenType.COMMAND, '\\right');
|
|
161
174
|
|
|
162
175
|
function find_closing_right_command(tokens: Token[], start: number): number {
|
|
163
176
|
let count = 1;
|
|
@@ -167,9 +180,9 @@ function find_closing_right_command(tokens: Token[], start: number): number {
|
|
|
167
180
|
if (pos >= tokens.length) {
|
|
168
181
|
return -1;
|
|
169
182
|
}
|
|
170
|
-
if (
|
|
183
|
+
if (tokens[pos].eq(LEFT_COMMAND)) {
|
|
171
184
|
count += 1;
|
|
172
|
-
} else if (
|
|
185
|
+
} else if (tokens[pos].eq(RIGHT_COMMAND)) {
|
|
173
186
|
count -= 1;
|
|
174
187
|
}
|
|
175
188
|
pos += 1;
|
|
@@ -179,8 +192,8 @@ function find_closing_right_command(tokens: Token[], start: number): number {
|
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
|
|
182
|
-
const BEGIN_COMMAND: Token =
|
|
183
|
-
const END_COMMAND: Token =
|
|
195
|
+
const BEGIN_COMMAND: Token = new Token(TokenType.COMMAND, '\\begin');
|
|
196
|
+
const END_COMMAND: Token = new Token(TokenType.COMMAND, '\\end');
|
|
184
197
|
|
|
185
198
|
|
|
186
199
|
function find_closing_end_command(tokens: Token[], start: number): number {
|
|
@@ -191,9 +204,9 @@ function find_closing_end_command(tokens: Token[], start: number): number {
|
|
|
191
204
|
if (pos >= tokens.length) {
|
|
192
205
|
return -1;
|
|
193
206
|
}
|
|
194
|
-
if (
|
|
207
|
+
if (tokens[pos].eq(BEGIN_COMMAND)) {
|
|
195
208
|
count += 1;
|
|
196
|
-
} else if (
|
|
209
|
+
} else if (tokens[pos].eq(END_COMMAND)) {
|
|
197
210
|
count -= 1;
|
|
198
211
|
}
|
|
199
212
|
pos += 1;
|
|
@@ -240,7 +253,7 @@ export function tokenize(latex: string): Token[] {
|
|
|
240
253
|
while (newPos < latex.length && latex[newPos] !== '\n') {
|
|
241
254
|
newPos += 1;
|
|
242
255
|
}
|
|
243
|
-
token =
|
|
256
|
+
token = new Token(TokenType.COMMENT, latex.slice(pos + 1, newPos));
|
|
244
257
|
pos = newPos;
|
|
245
258
|
break;
|
|
246
259
|
}
|
|
@@ -249,19 +262,19 @@ export function tokenize(latex: string): Token[] {
|
|
|
249
262
|
case '_':
|
|
250
263
|
case '^':
|
|
251
264
|
case '&':
|
|
252
|
-
token =
|
|
265
|
+
token = new Token(TokenType.CONTROL, firstChar);
|
|
253
266
|
pos++;
|
|
254
267
|
break;
|
|
255
268
|
case '\n':
|
|
256
|
-
token =
|
|
269
|
+
token = new Token(TokenType.NEWLINE, firstChar);
|
|
257
270
|
pos++;
|
|
258
271
|
break;
|
|
259
272
|
case '\r': {
|
|
260
273
|
if (pos + 1 < latex.length && latex[pos + 1] === '\n') {
|
|
261
|
-
token =
|
|
274
|
+
token = new Token(TokenType.NEWLINE, '\n');
|
|
262
275
|
pos += 2;
|
|
263
276
|
} else {
|
|
264
|
-
token =
|
|
277
|
+
token = new Token(TokenType.NEWLINE, '\n');
|
|
265
278
|
pos ++;
|
|
266
279
|
}
|
|
267
280
|
break;
|
|
@@ -271,7 +284,7 @@ export function tokenize(latex: string): Token[] {
|
|
|
271
284
|
while (newPos < latex.length && latex[newPos] === ' ') {
|
|
272
285
|
newPos += 1;
|
|
273
286
|
}
|
|
274
|
-
token =
|
|
287
|
+
token = new Token(TokenType.WHITESPACE, latex.slice(pos, newPos));
|
|
275
288
|
pos = newPos;
|
|
276
289
|
break;
|
|
277
290
|
}
|
|
@@ -281,12 +294,12 @@ export function tokenize(latex: string): Token[] {
|
|
|
281
294
|
}
|
|
282
295
|
const firstTwoChars = latex.slice(pos, pos + 2);
|
|
283
296
|
if (['\\\\', '\\,'].includes(firstTwoChars)) {
|
|
284
|
-
token =
|
|
297
|
+
token = new Token(TokenType.CONTROL, firstTwoChars);
|
|
285
298
|
} else if (['\\{','\\}', '\\%', '\\$', '\\&', '\\#', '\\_'].includes(firstTwoChars)) {
|
|
286
|
-
token =
|
|
299
|
+
token = new Token(TokenType.ELEMENT, firstTwoChars);
|
|
287
300
|
} else {
|
|
288
301
|
const command = eat_command_name(latex, pos + 1);
|
|
289
|
-
token =
|
|
302
|
+
token = new Token(TokenType.COMMAND, '\\' + command);
|
|
290
303
|
}
|
|
291
304
|
pos += token.value.length;
|
|
292
305
|
break;
|
|
@@ -297,13 +310,13 @@ export function tokenize(latex: string): Token[] {
|
|
|
297
310
|
while (newPos < latex.length && isdigit(latex[newPos])) {
|
|
298
311
|
newPos += 1;
|
|
299
312
|
}
|
|
300
|
-
token =
|
|
313
|
+
token = new Token(TokenType.ELEMENT, latex.slice(pos, newPos));
|
|
301
314
|
} else if (isalpha(firstChar)) {
|
|
302
|
-
token =
|
|
315
|
+
token = new Token(TokenType.ELEMENT, firstChar);
|
|
303
316
|
} else if ('+-*/=\'<>!.,;?()[]|'.includes(firstChar)) {
|
|
304
|
-
token =
|
|
317
|
+
token = new Token(TokenType.ELEMENT, firstChar)
|
|
305
318
|
} else {
|
|
306
|
-
token =
|
|
319
|
+
token = new Token(TokenType.UNKNOWN, firstChar);
|
|
307
320
|
}
|
|
308
321
|
pos += token.value.length;
|
|
309
322
|
}
|
|
@@ -315,7 +328,7 @@ export function tokenize(latex: string): Token[] {
|
|
|
315
328
|
if (pos >= latex.length || latex[pos] !== '{') {
|
|
316
329
|
throw new LatexParserError(`No content for ${token.value} command`);
|
|
317
330
|
}
|
|
318
|
-
tokens.push(
|
|
331
|
+
tokens.push(new Token(TokenType.CONTROL, '{'));
|
|
319
332
|
const posClosingBracket = find_closing_curly_bracket_char(latex, pos);
|
|
320
333
|
pos++;
|
|
321
334
|
let textInside = latex.slice(pos, posClosingBracket);
|
|
@@ -324,18 +337,14 @@ export function tokenize(latex: string): Token[] {
|
|
|
324
337
|
for (const char of chars) {
|
|
325
338
|
textInside = textInside.replaceAll('\\' + char, char);
|
|
326
339
|
}
|
|
327
|
-
tokens.push(
|
|
328
|
-
tokens.push(
|
|
340
|
+
tokens.push(new Token(TokenType.TEXT, textInside));
|
|
341
|
+
tokens.push(new Token(TokenType.CONTROL, '}'));
|
|
329
342
|
pos = posClosingBracket + 1;
|
|
330
343
|
}
|
|
331
344
|
}
|
|
332
345
|
return tokens;
|
|
333
346
|
}
|
|
334
347
|
|
|
335
|
-
function token_eq(token1: Token, token2: Token) {
|
|
336
|
-
return token1.type == token2.type && token1.value == token2.value;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
348
|
|
|
340
349
|
export class LatexParserError extends Error {
|
|
341
350
|
constructor(message: string) {
|
|
@@ -347,8 +356,8 @@ export class LatexParserError extends Error {
|
|
|
347
356
|
|
|
348
357
|
type ParseResult = [TexNode, number];
|
|
349
358
|
|
|
350
|
-
const SUB_SYMBOL:Token =
|
|
351
|
-
const SUP_SYMBOL:Token =
|
|
359
|
+
const SUB_SYMBOL:Token = new Token(TokenType.CONTROL, '_');
|
|
360
|
+
const SUP_SYMBOL:Token = new Token(TokenType.CONTROL, '^');
|
|
352
361
|
|
|
353
362
|
export class LatexParser {
|
|
354
363
|
space_sensitive: boolean;
|
|
@@ -408,22 +417,22 @@ export class LatexParser {
|
|
|
408
417
|
|
|
409
418
|
num_prime += eat_primes(tokens, pos);
|
|
410
419
|
pos += num_prime;
|
|
411
|
-
if (pos < tokens.length &&
|
|
420
|
+
if (pos < tokens.length && tokens[pos].eq(SUB_SYMBOL)) {
|
|
412
421
|
[sub, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
|
|
413
422
|
num_prime += eat_primes(tokens, pos);
|
|
414
423
|
pos += num_prime;
|
|
415
|
-
if (pos < tokens.length &&
|
|
424
|
+
if (pos < tokens.length && tokens[pos].eq(SUP_SYMBOL)) {
|
|
416
425
|
[sup, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
|
|
417
426
|
if (eat_primes(tokens, pos) > 0) {
|
|
418
427
|
throw new LatexParserError('Double superscript');
|
|
419
428
|
}
|
|
420
429
|
}
|
|
421
|
-
} else if (pos < tokens.length &&
|
|
430
|
+
} else if (pos < tokens.length && tokens[pos].eq(SUP_SYMBOL)) {
|
|
422
431
|
[sup, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
|
|
423
432
|
if (eat_primes(tokens, pos) > 0) {
|
|
424
433
|
throw new LatexParserError('Double superscript');
|
|
425
434
|
}
|
|
426
|
-
if (pos < tokens.length &&
|
|
435
|
+
if (pos < tokens.length && tokens[pos].eq(SUB_SYMBOL)) {
|
|
427
436
|
[sub, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
|
|
428
437
|
if (eat_primes(tokens, pos) > 0) {
|
|
429
438
|
throw new LatexParserError('Double superscript');
|
|
@@ -471,9 +480,9 @@ export class LatexParser {
|
|
|
471
480
|
case TokenType.NEWLINE:
|
|
472
481
|
return [{ type: 'newline', content: firstToken.value }, start + 1];
|
|
473
482
|
case TokenType.COMMAND:
|
|
474
|
-
if (
|
|
483
|
+
if (firstToken.eq(BEGIN_COMMAND)) {
|
|
475
484
|
return this.parseBeginEndExpr(tokens, start);
|
|
476
|
-
} else if (
|
|
485
|
+
} else if (firstToken.eq(LEFT_COMMAND)) {
|
|
477
486
|
return this.parseLeftRightExpr(tokens, start);
|
|
478
487
|
} else {
|
|
479
488
|
return this.parseCommandExpr(tokens, start);
|
|
@@ -527,7 +536,7 @@ export class LatexParser {
|
|
|
527
536
|
}
|
|
528
537
|
return [{ type: 'symbol', content: command }, pos];
|
|
529
538
|
case 1: {
|
|
530
|
-
if (command === '\\sqrt' && pos < tokens.length &&
|
|
539
|
+
if (command === '\\sqrt' && pos < tokens.length && tokens[pos].eq(LEFT_SQUARE_BRACKET)) {
|
|
531
540
|
const posLeftSquareBracket = pos;
|
|
532
541
|
const posRightSquareBracket = find_closing_square_bracket(tokens, pos);
|
|
533
542
|
const exprInside = tokens.slice(posLeftSquareBracket + 1, posRightSquareBracket);
|
|
@@ -538,9 +547,9 @@ export class LatexParser {
|
|
|
538
547
|
if (pos + 2 >= tokens.length) {
|
|
539
548
|
throw new LatexParserError('Expecting content for \\text command');
|
|
540
549
|
}
|
|
541
|
-
assert(
|
|
550
|
+
assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
|
|
542
551
|
assert(tokens[pos + 1].type === TokenType.TEXT);
|
|
543
|
-
assert(
|
|
552
|
+
assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
|
|
544
553
|
const text = tokens[pos + 1].value;
|
|
545
554
|
return [{ type: 'text', content: text }, pos + 3];
|
|
546
555
|
}
|
|
@@ -558,7 +567,7 @@ export class LatexParser {
|
|
|
558
567
|
}
|
|
559
568
|
|
|
560
569
|
parseLeftRightExpr(tokens: Token[], start: number): ParseResult {
|
|
561
|
-
assert(
|
|
570
|
+
assert(tokens[start].eq(LEFT_COMMAND));
|
|
562
571
|
|
|
563
572
|
let pos = start + 1;
|
|
564
573
|
pos += eat_whitespaces(tokens, pos).length;
|
|
@@ -603,12 +612,12 @@ export class LatexParser {
|
|
|
603
612
|
}
|
|
604
613
|
|
|
605
614
|
parseBeginEndExpr(tokens: Token[], start: number): ParseResult {
|
|
606
|
-
assert(
|
|
615
|
+
assert(tokens[start].eq(BEGIN_COMMAND));
|
|
607
616
|
|
|
608
617
|
let pos = start + 1;
|
|
609
|
-
assert(
|
|
618
|
+
assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
|
|
610
619
|
assert(tokens[pos + 1].type === TokenType.TEXT);
|
|
611
|
-
assert(
|
|
620
|
+
assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
|
|
612
621
|
const envName = tokens[pos + 1].value;
|
|
613
622
|
pos += 3;
|
|
614
623
|
|
|
@@ -623,9 +632,9 @@ export class LatexParser {
|
|
|
623
632
|
const exprInsideEnd = endIdx;
|
|
624
633
|
pos = endIdx + 1;
|
|
625
634
|
|
|
626
|
-
assert(
|
|
635
|
+
assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
|
|
627
636
|
assert(tokens[pos + 1].type === TokenType.TEXT);
|
|
628
|
-
assert(
|
|
637
|
+
assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
|
|
629
638
|
if (tokens[pos + 1].value !== envName) {
|
|
630
639
|
throw new LatexParserError('Mismatched \\begin and \\end environments');
|
|
631
640
|
}
|
|
@@ -674,7 +683,7 @@ export class LatexParser {
|
|
|
674
683
|
|
|
675
684
|
// Remove all whitespace before or after _ or ^
|
|
676
685
|
function passIgnoreWhitespaceBeforeScriptMark(tokens: Token[]): Token[] {
|
|
677
|
-
const is_script_mark = (token: Token) =>
|
|
686
|
+
const is_script_mark = (token: Token) => token.eq(SUB_SYMBOL) || token.eq(SUP_SYMBOL);
|
|
678
687
|
let out_tokens: Token[] = [];
|
|
679
688
|
for (let i = 0; i < tokens.length; i++) {
|
|
680
689
|
if (tokens[i].type === TokenType.WHITESPACE && i + 1 < tokens.length && is_script_mark(tokens[i + 1])) {
|
package/src/types.ts
CHANGED
package/src/writer.ts
CHANGED
|
@@ -14,6 +14,11 @@ 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
23
|
node: TexNode | TypstNode;
|
|
19
24
|
|
|
@@ -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 ||= /[\
|
|
68
|
+
no_need_space ||= /[\s_^{\(]$/.test(this.buffer);
|
|
64
69
|
if(!no_need_space) {
|
|
65
70
|
this.buffer += ' ';
|
|
66
71
|
}
|
|
@@ -77,14 +82,15 @@ export class TypstWriter {
|
|
|
77
82
|
switch (node.type) {
|
|
78
83
|
case 'empty':
|
|
79
84
|
break;
|
|
80
|
-
case '
|
|
81
|
-
let content = node.content!;
|
|
85
|
+
case 'atom': {
|
|
82
86
|
if (node.content === ',' && this.insideFunctionDepth > 0) {
|
|
83
|
-
|
|
87
|
+
this.queue.push({ type: 'symbol', content: 'comma' });
|
|
88
|
+
} else {
|
|
89
|
+
this.queue.push({ type: 'atom', content: node.content });
|
|
84
90
|
}
|
|
85
|
-
this.queue.push({ type: 'symbol', content: content });
|
|
86
91
|
break;
|
|
87
92
|
}
|
|
93
|
+
case 'symbol':
|
|
88
94
|
case 'text':
|
|
89
95
|
case 'comment':
|
|
90
96
|
case 'newline':
|
|
@@ -100,7 +106,7 @@ export class TypstWriter {
|
|
|
100
106
|
this.appendWithBracketsIfNeeded(base);
|
|
101
107
|
|
|
102
108
|
let trailing_space_needed = false;
|
|
103
|
-
const has_prime = (sup && sup.type === '
|
|
109
|
+
const has_prime = (sup && sup.type === 'atom' && sup.content === '\'');
|
|
104
110
|
if (has_prime) {
|
|
105
111
|
// Put prime symbol before '_'. Because $y_1'$ is not displayed properly in Typst (so far)
|
|
106
112
|
// e.g.
|
|
@@ -206,21 +212,25 @@ export class TypstWriter {
|
|
|
206
212
|
}
|
|
207
213
|
|
|
208
214
|
private appendWithBracketsIfNeeded(node: TypstNode): boolean {
|
|
209
|
-
|
|
210
|
-
|
|
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;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (need_to_wrap) {
|
|
226
|
+
this.queue.push({ type: 'atom', content: '(' });
|
|
211
227
|
this.append(node);
|
|
228
|
+
this.queue.push({ type: 'atom', content: ')' });
|
|
212
229
|
} else {
|
|
213
|
-
this.queue.push({
|
|
214
|
-
type: 'atom',
|
|
215
|
-
content: '('
|
|
216
|
-
});
|
|
217
230
|
this.append(node);
|
|
218
|
-
this.queue.push({
|
|
219
|
-
type: 'atom',
|
|
220
|
-
content: ')'
|
|
221
|
-
});
|
|
222
231
|
}
|
|
223
|
-
|
|
232
|
+
|
|
233
|
+
return !need_to_wrap;
|
|
224
234
|
}
|
|
225
235
|
|
|
226
236
|
protected flushQueue() {
|
|
@@ -270,8 +280,17 @@ export class TypstWriter {
|
|
|
270
280
|
res = res.replace(/ceil\(\)/g, 'ceil("")');
|
|
271
281
|
return res;
|
|
272
282
|
}
|
|
273
|
-
|
|
274
|
-
|
|
283
|
+
const smartRoundPass = function (input: string): string {
|
|
284
|
+
// Use regex to replace all "⌊ xxx ⌉" with "round(xxx)"
|
|
285
|
+
let res = input.replace(/⌊\s*(.*?)\s*⌉/g, "round($1)");
|
|
286
|
+
// Typst disallow "round()" with empty argument, so add an empty string inside if it's empty.
|
|
287
|
+
res = res.replace(/round\(\)/g, 'round("")');
|
|
288
|
+
return res;
|
|
289
|
+
}
|
|
290
|
+
const all_passes = [smartFloorPass, smartCeilPass, smartRoundPass];
|
|
291
|
+
for (const pass of all_passes) {
|
|
292
|
+
this.buffer = pass(this.buffer);
|
|
293
|
+
}
|
|
275
294
|
return this.buffer;
|
|
276
295
|
}
|
|
277
296
|
}
|
|
@@ -288,6 +307,7 @@ export function convertTree(node: TexNode): TypstNode {
|
|
|
288
307
|
args: node.args!.map(convertTree),
|
|
289
308
|
};
|
|
290
309
|
case 'element':
|
|
310
|
+
return { type: 'atom', content: convertToken(node.content) };
|
|
291
311
|
case 'symbol':
|
|
292
312
|
return { type: 'symbol', content: convertToken(node.content) };
|
|
293
313
|
case 'text':
|
|
@@ -341,7 +361,12 @@ export function convertTree(node: TexNode): TypstNode {
|
|
|
341
361
|
content: '',
|
|
342
362
|
args: node.args!.map(convertTree),
|
|
343
363
|
};
|
|
344
|
-
if ([
|
|
364
|
+
if ([
|
|
365
|
+
"[]", "()", "\\{\\}",
|
|
366
|
+
"\\lfloor\\rfloor",
|
|
367
|
+
"\\lceil\\rceil",
|
|
368
|
+
"\\lfloor\\rceil",
|
|
369
|
+
].includes(left.content + right.content)) {
|
|
345
370
|
return group;
|
|
346
371
|
}
|
|
347
372
|
return {
|
|
@@ -382,7 +407,7 @@ export function convertTree(node: TexNode): TypstNode {
|
|
|
382
407
|
};
|
|
383
408
|
}
|
|
384
409
|
// \mathbb{R} -> RR
|
|
385
|
-
if (node.content === '\\mathbb' && arg0.type === '
|
|
410
|
+
if (node.content === '\\mathbb' && arg0.type === 'atom' && /^[A-Z]$/.test(arg0.content)) {
|
|
386
411
|
return {
|
|
387
412
|
type: 'symbol',
|
|
388
413
|
content: arg0.content + arg0.content,
|