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.
- package/README.md +1 -1
- package/dist/index.d.ts +22 -24
- package/dist/index.js +509 -417
- package/dist/parser.js +23 -0
- package/dist/tex2typst.min.js +13 -13
- package/package.json +1 -1
- package/src/convert.ts +112 -8
- package/src/exposed-types.ts +22 -24
- package/src/generic.ts +16 -0
- package/src/index.ts +7 -4
- package/src/jslex.ts +1 -1
- package/src/map.ts +1 -1
- package/src/tex-parser.ts +128 -107
- package/src/tex-tokenizer.ts +6 -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 +188 -0
- package/tests/struct-tex2typst.yaml +463 -0
- package/tests/struct-typst2tex.yaml +412 -0
- package/tests/symbol.yml +126 -0
- package/tests/test-common.ts +26 -0
- package/tests/tex-parser.test.ts +97 -0
- package/tests/tex-to-typst.test.ts +136 -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/tex-parser.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TexBeginEnd, TexFuncCall, TexLeftRight, TexNode, TexGroup, TexSupSub, TexSupsubData, TexText, TexToken, TexTokenType } from "./tex-types";
|
|
2
2
|
import { assert } from "./util";
|
|
3
|
-
import { array_find } from "./generic";
|
|
3
|
+
import { array_find, array_join } from "./generic";
|
|
4
4
|
import { TEX_BINARY_COMMANDS, TEX_UNARY_COMMANDS, tokenize_tex } from "./tex-tokenizer";
|
|
5
5
|
|
|
6
6
|
const IGNORED_COMMANDS = [
|
|
@@ -42,7 +42,7 @@ function eat_parenthesis(tokens: TexToken[], start: number): TexToken | null {
|
|
|
42
42
|
const firstToken = tokens[start];
|
|
43
43
|
if (firstToken.type === TexTokenType.ELEMENT && ['(', ')', '[', ']', '|', '\\{', '\\}', '.', '\\|'].includes(firstToken.value)) {
|
|
44
44
|
return firstToken;
|
|
45
|
-
} else if (firstToken.type === TexTokenType.COMMAND && ['lfloor', 'rfloor', 'lceil', 'rceil', 'langle', 'rangle'].includes(firstToken.value.slice(1))) {
|
|
45
|
+
} else if (firstToken.type === TexTokenType.COMMAND && ['lfloor', 'rfloor', 'lceil', 'rceil', 'langle', 'rangle', 'lparen', 'rparen', 'lbrace', 'rbrace'].includes(firstToken.value.slice(1))) {
|
|
46
46
|
return firstToken;
|
|
47
47
|
} else {
|
|
48
48
|
return null;
|
|
@@ -58,49 +58,29 @@ function eat_primes(tokens: TexToken[], start: number): number {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
function find_closing_match(tokens: TexToken[], start: number, leftToken: TexToken, rightToken: TexToken): number {
|
|
62
|
-
assert(tokens[start].eq(leftToken));
|
|
63
|
-
let count = 1;
|
|
64
|
-
let pos = start + 1;
|
|
65
|
-
|
|
66
|
-
while (count > 0) {
|
|
67
|
-
if (pos >= tokens.length) {
|
|
68
|
-
return -1;
|
|
69
|
-
}
|
|
70
|
-
if (tokens[pos].eq(leftToken)) {
|
|
71
|
-
count += 1;
|
|
72
|
-
} else if (tokens[pos].eq(rightToken)) {
|
|
73
|
-
count -= 1;
|
|
74
|
-
}
|
|
75
|
-
pos += 1;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return pos - 1;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
61
|
|
|
82
62
|
const LEFT_COMMAND: TexToken = new TexToken(TexTokenType.COMMAND, '\\left');
|
|
83
63
|
const RIGHT_COMMAND: TexToken = new TexToken(TexTokenType.COMMAND, '\\right');
|
|
84
64
|
|
|
85
|
-
function find_closing_right_command(tokens: TexToken[], start: number): number {
|
|
86
|
-
return find_closing_match(tokens, start, LEFT_COMMAND, RIGHT_COMMAND);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
65
|
const BEGIN_COMMAND: TexToken = new TexToken(TexTokenType.COMMAND, '\\begin');
|
|
91
66
|
const END_COMMAND: TexToken = new TexToken(TexTokenType.COMMAND, '\\end');
|
|
92
67
|
|
|
93
|
-
|
|
94
|
-
function find_closing_end_command(tokens: TexToken[], start: number): number {
|
|
95
|
-
return find_closing_match(tokens, start, BEGIN_COMMAND, END_COMMAND);
|
|
96
|
-
}
|
|
97
|
-
|
|
68
|
+
const CONTROL_LINEBREAK = new TexToken(TexTokenType.CONTROL, '\\\\');
|
|
98
69
|
|
|
99
70
|
export class LatexParserError extends Error {
|
|
100
71
|
constructor(message: string) {
|
|
101
72
|
super(message);
|
|
102
73
|
this.name = 'LatexParserError';
|
|
103
74
|
}
|
|
75
|
+
|
|
76
|
+
static readonly UNMATCHED_LEFT_BRACE = new LatexParserError("Unmatched '\\{'");
|
|
77
|
+
static readonly UNMATCHED_RIGHT_BRACE = new LatexParserError("Unmatched '\\}'");
|
|
78
|
+
static readonly UNMATCHED_LEFT_BRACKET = new LatexParserError("Unmatched '\\['");
|
|
79
|
+
static readonly UNMATCHED_RIGHT_BRACKET = new LatexParserError("Unmatched '\\]'");
|
|
80
|
+
static readonly UNMATCHED_COMMAND_BEGIN = new LatexParserError("Unmatched '\\begin'");
|
|
81
|
+
static readonly UNMATCHED_COMMAND_END = new LatexParserError("Unmatched '\\end'");
|
|
82
|
+
static readonly UNMATCHED_COMMAND_LEFT = new LatexParserError("Unmatched '\\left'");
|
|
83
|
+
static readonly UNMATCHED_COMMAND_RIGHT = new LatexParserError("Unmatched '\\right'");
|
|
104
84
|
}
|
|
105
85
|
|
|
106
86
|
|
|
@@ -110,8 +90,12 @@ const SUB_SYMBOL:TexToken = new TexToken(TexTokenType.CONTROL, '_');
|
|
|
110
90
|
const SUP_SYMBOL:TexToken = new TexToken(TexTokenType.CONTROL, '^');
|
|
111
91
|
|
|
112
92
|
export class LatexParser {
|
|
113
|
-
space_sensitive: boolean;
|
|
114
|
-
newline_sensitive: boolean;
|
|
93
|
+
public space_sensitive: boolean;
|
|
94
|
+
public newline_sensitive: boolean;
|
|
95
|
+
|
|
96
|
+
// how many levels of \begin{...} \end{...} are we currently in
|
|
97
|
+
public alignmentDepth: number = 0;
|
|
98
|
+
|
|
115
99
|
|
|
116
100
|
constructor(space_sensitive: boolean = false, newline_sensitive: boolean = true) {
|
|
117
101
|
this.space_sensitive = space_sensitive;
|
|
@@ -123,25 +107,35 @@ export class LatexParser {
|
|
|
123
107
|
const idx = array_find(tokens, token_displaystyle);
|
|
124
108
|
if (idx === -1) {
|
|
125
109
|
// no \displaystyle, normal execution path
|
|
126
|
-
|
|
127
|
-
return tree;
|
|
110
|
+
return this.parseGroup(tokens.slice(0));
|
|
128
111
|
} else if (idx === 0) {
|
|
129
112
|
// \displaystyle at the beginning. Wrap the whole thing in \displaystyle
|
|
130
|
-
const
|
|
113
|
+
const tree = this.parseGroup(tokens.slice(1));
|
|
131
114
|
return new TexFuncCall(token_displaystyle, [tree]);
|
|
132
115
|
} else {
|
|
133
116
|
// \displaystyle somewhere in the middle. Split the expression to two parts
|
|
134
|
-
const
|
|
135
|
-
const
|
|
117
|
+
const tree1 = this.parseGroup(tokens.slice(0, idx));
|
|
118
|
+
const tree2 = this.parseGroup(tokens.slice(idx + 1, tokens.length));
|
|
136
119
|
const display = new TexFuncCall(token_displaystyle, [tree2]);
|
|
137
120
|
return new TexGroup([tree1, display]);
|
|
138
121
|
}
|
|
139
122
|
}
|
|
140
123
|
|
|
141
|
-
parseGroup(tokens: TexToken[]
|
|
124
|
+
parseGroup(tokens: TexToken[]): TexNode {
|
|
125
|
+
const [tree, _] = this.parseClosure(tokens, 0, null);
|
|
126
|
+
return tree;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// return pos: (position of closingToken) + 1
|
|
130
|
+
// pos will be -1 if closingToken is not found
|
|
131
|
+
parseClosure(tokens: TexToken[], start: number, closingToken: TexToken | null): ParseResult {
|
|
142
132
|
const results: TexNode[] = [];
|
|
143
133
|
let pos = start;
|
|
144
|
-
while (pos <
|
|
134
|
+
while (pos < tokens.length) {
|
|
135
|
+
if (closingToken !== null && tokens[pos].eq(closingToken)) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
145
139
|
const [res, newPos] = this.parseNextExpr(tokens, pos);
|
|
146
140
|
pos = newPos;
|
|
147
141
|
if(res.head.type === TexTokenType.SPACE || res.head.type === TexTokenType.NEWLINE) {
|
|
@@ -152,11 +146,11 @@ export class LatexParser {
|
|
|
152
146
|
continue;
|
|
153
147
|
}
|
|
154
148
|
}
|
|
155
|
-
if (res.head.eq(new TexToken(TexTokenType.CONTROL, '&'))) {
|
|
156
|
-
throw new LatexParserError('Unexpected & outside of an alignment');
|
|
157
|
-
}
|
|
158
149
|
results.push(res);
|
|
159
150
|
}
|
|
151
|
+
if (pos >= tokens.length && closingToken !== null) {
|
|
152
|
+
return [EMPTY_NODE, -1];
|
|
153
|
+
}
|
|
160
154
|
|
|
161
155
|
let node: TexNode;
|
|
162
156
|
if (results.length === 1) {
|
|
@@ -164,7 +158,7 @@ export class LatexParser {
|
|
|
164
158
|
} else {
|
|
165
159
|
node = new TexGroup(results);
|
|
166
160
|
}
|
|
167
|
-
return [node,
|
|
161
|
+
return [node, pos + 1];
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
parseNextExpr(tokens: TexToken[], start: number): ParseResult {
|
|
@@ -175,25 +169,28 @@ export class LatexParser {
|
|
|
175
169
|
|
|
176
170
|
num_prime += eat_primes(tokens, pos);
|
|
177
171
|
pos += num_prime;
|
|
178
|
-
if (pos < tokens.length
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
172
|
+
if (pos < tokens.length) {
|
|
173
|
+
const next_token = tokens[pos];
|
|
174
|
+
if (next_token.eq(SUB_SYMBOL)) {
|
|
175
|
+
[sub, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
|
|
176
|
+
num_prime += eat_primes(tokens, pos);
|
|
177
|
+
pos += num_prime;
|
|
178
|
+
if (pos < tokens.length && tokens[pos].eq(SUP_SYMBOL)) {
|
|
179
|
+
[sup, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
|
|
180
|
+
if (eat_primes(tokens, pos) > 0) {
|
|
181
|
+
throw new LatexParserError('Double superscript');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} else if (next_token.eq(SUP_SYMBOL)) {
|
|
183
185
|
[sup, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
|
|
184
186
|
if (eat_primes(tokens, pos) > 0) {
|
|
185
187
|
throw new LatexParserError('Double superscript');
|
|
186
188
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
if (pos < tokens.length && tokens[pos].eq(SUB_SYMBOL)) {
|
|
194
|
-
[sub, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
|
|
195
|
-
if (eat_primes(tokens, pos) > 0) {
|
|
196
|
-
throw new LatexParserError('Double superscript');
|
|
189
|
+
if (pos < tokens.length && tokens[pos].eq(SUB_SYMBOL)) {
|
|
190
|
+
[sub, pos] = this.parseNextExprWithoutSupSub(tokens, pos + 1);
|
|
191
|
+
if (eat_primes(tokens, pos) > 0) {
|
|
192
|
+
throw new LatexParserError('Double superscript');
|
|
193
|
+
}
|
|
197
194
|
}
|
|
198
195
|
}
|
|
199
196
|
}
|
|
@@ -223,7 +220,7 @@ export class LatexParser {
|
|
|
223
220
|
|
|
224
221
|
parseNextExprWithoutSupSub(tokens: TexToken[], start: number): ParseResult {
|
|
225
222
|
if (start >= tokens.length) {
|
|
226
|
-
|
|
223
|
+
throw new LatexParserError("Unexpected end of input");
|
|
227
224
|
}
|
|
228
225
|
const firstToken = tokens[start];
|
|
229
226
|
switch (firstToken.type) {
|
|
@@ -240,8 +237,12 @@ export class LatexParser {
|
|
|
240
237
|
}
|
|
241
238
|
if (firstToken.eq(BEGIN_COMMAND)) {
|
|
242
239
|
return this.parseBeginEndExpr(tokens, start);
|
|
240
|
+
} else if(firstToken.eq(END_COMMAND)) {
|
|
241
|
+
throw LatexParserError.UNMATCHED_COMMAND_END;
|
|
243
242
|
} else if (firstToken.eq(LEFT_COMMAND)) {
|
|
244
243
|
return this.parseLeftRightExpr(tokens, start);
|
|
244
|
+
} else if (firstToken.eq(RIGHT_COMMAND)) {
|
|
245
|
+
throw LatexParserError.UNMATCHED_COMMAND_RIGHT;
|
|
245
246
|
} else {
|
|
246
247
|
return this.parseCommandExpr(tokens, start);
|
|
247
248
|
}
|
|
@@ -249,13 +250,13 @@ export class LatexParser {
|
|
|
249
250
|
const controlChar = firstToken.value;
|
|
250
251
|
switch (controlChar) {
|
|
251
252
|
case '{':
|
|
252
|
-
const
|
|
253
|
-
if(
|
|
254
|
-
throw
|
|
253
|
+
const [group, newPos] = this.parseClosure(tokens, start + 1, RIGHT_CURLY_BRACKET);
|
|
254
|
+
if (newPos === -1) {
|
|
255
|
+
throw LatexParserError.UNMATCHED_LEFT_BRACE;
|
|
255
256
|
}
|
|
256
|
-
return
|
|
257
|
+
return [group, newPos];
|
|
257
258
|
case '}':
|
|
258
|
-
throw
|
|
259
|
+
throw LatexParserError.UNMATCHED_RIGHT_BRACE;
|
|
259
260
|
case '\\\\':
|
|
260
261
|
case '\\!':
|
|
261
262
|
case '\\,':
|
|
@@ -266,8 +267,12 @@ export class LatexParser {
|
|
|
266
267
|
return [firstToken.toNode(), start + 1];
|
|
267
268
|
case '_':
|
|
268
269
|
case '^':
|
|
270
|
+
// e.g. "_1" or "^2" are valid LaTeX math expressions
|
|
269
271
|
return [ EMPTY_NODE, start];
|
|
270
272
|
case '&':
|
|
273
|
+
if (this.alignmentDepth <= 0) {
|
|
274
|
+
throw new LatexParserError('Unexpected & outside of an alignment');
|
|
275
|
+
}
|
|
271
276
|
return [firstToken.toNode(), start + 1];
|
|
272
277
|
default:
|
|
273
278
|
throw new LatexParserError('Unknown control sequence');
|
|
@@ -285,11 +290,6 @@ export class LatexParser {
|
|
|
285
290
|
|
|
286
291
|
let pos = start + 1;
|
|
287
292
|
|
|
288
|
-
if (['left', 'right', 'begin', 'end'].includes(command.slice(1))) {
|
|
289
|
-
throw new LatexParserError('Unexpected command: ' + command);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
293
|
const paramNum = get_command_param_num(command.slice(1));
|
|
294
294
|
switch (paramNum) {
|
|
295
295
|
case 0:
|
|
@@ -301,14 +301,12 @@ export class LatexParser {
|
|
|
301
301
|
throw new LatexParserError('Expecting argument for ' + command);
|
|
302
302
|
}
|
|
303
303
|
if (command === '\\sqrt' && pos < tokens.length && tokens[pos].eq(LEFT_SQUARE_BRACKET)) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
throw new LatexParserError('No matching right square bracket for [');
|
|
304
|
+
const [exponent, newPos1] = this.parseClosure(tokens, pos + 1, RIGHT_SQUARE_BRACKET);
|
|
305
|
+
if (newPos1 === -1) {
|
|
306
|
+
throw LatexParserError.UNMATCHED_LEFT_BRACKET;
|
|
308
307
|
}
|
|
309
|
-
const [
|
|
310
|
-
|
|
311
|
-
return [new TexFuncCall(command_token, [arg1], exponent), newPos];
|
|
308
|
+
const [arg1, newPos2] = this.parseNextArg(tokens, newPos1);
|
|
309
|
+
return [new TexFuncCall(command_token, [arg1], exponent), newPos2];
|
|
312
310
|
} else if (command === '\\text') {
|
|
313
311
|
if (pos + 2 >= tokens.length) {
|
|
314
312
|
throw new LatexParserError('Expecting content for \\text command');
|
|
@@ -318,6 +316,14 @@ export class LatexParser {
|
|
|
318
316
|
assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
|
|
319
317
|
const literal = tokens[pos + 1];
|
|
320
318
|
return [new TexText(literal), pos + 3];
|
|
319
|
+
} else if (command === '\\displaylines') {
|
|
320
|
+
assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
|
|
321
|
+
const [matrix, newPos] = this.parseAligned(tokens, pos + 1, RIGHT_CURLY_BRACKET);
|
|
322
|
+
if (newPos === -1) {
|
|
323
|
+
throw LatexParserError.UNMATCHED_LEFT_BRACE;
|
|
324
|
+
}
|
|
325
|
+
const group = new TexGroup(array_join(matrix, CONTROL_LINEBREAK.toNode()));
|
|
326
|
+
return [new TexFuncCall(command_token, [group]), newPos];
|
|
321
327
|
}
|
|
322
328
|
let [arg1, newPos] = this.parseNextArg(tokens, pos);
|
|
323
329
|
return [new TexFuncCall(command_token, [arg1]), newPos];
|
|
@@ -328,7 +334,7 @@ export class LatexParser {
|
|
|
328
334
|
return [new TexFuncCall(command_token, [arg1, arg2]), pos2];
|
|
329
335
|
}
|
|
330
336
|
default:
|
|
331
|
-
throw new Error(
|
|
337
|
+
throw new Error('Invalid number of parameters');
|
|
332
338
|
}
|
|
333
339
|
}
|
|
334
340
|
|
|
@@ -363,7 +369,7 @@ export class LatexParser {
|
|
|
363
369
|
pos += eat_whitespaces(tokens, pos).length;
|
|
364
370
|
|
|
365
371
|
if (pos >= tokens.length) {
|
|
366
|
-
throw new LatexParserError('Expecting delimiter after \\left');
|
|
372
|
+
throw new LatexParserError('Expecting a delimiter after \\left');
|
|
367
373
|
}
|
|
368
374
|
|
|
369
375
|
const leftDelimiter = eat_parenthesis(tokens, pos);
|
|
@@ -371,17 +377,16 @@ export class LatexParser {
|
|
|
371
377
|
throw new LatexParserError('Invalid delimiter after \\left');
|
|
372
378
|
}
|
|
373
379
|
pos++;
|
|
374
|
-
|
|
375
|
-
const idx =
|
|
380
|
+
|
|
381
|
+
const [body, idx] = this.parseClosure(tokens, pos, RIGHT_COMMAND);
|
|
376
382
|
if (idx === -1) {
|
|
377
|
-
throw
|
|
383
|
+
throw LatexParserError.UNMATCHED_COMMAND_LEFT;
|
|
378
384
|
}
|
|
379
|
-
|
|
380
|
-
pos = idx + 1;
|
|
385
|
+
pos = idx;
|
|
381
386
|
|
|
382
387
|
pos += eat_whitespaces(tokens, pos).length;
|
|
383
388
|
if (pos >= tokens.length) {
|
|
384
|
-
throw new LatexParserError('Expecting
|
|
389
|
+
throw new LatexParserError('Expecting a delimiter after \\right');
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
const rightDelimiter = eat_parenthesis(tokens, pos);
|
|
@@ -390,7 +395,6 @@ export class LatexParser {
|
|
|
390
395
|
}
|
|
391
396
|
pos++;
|
|
392
397
|
|
|
393
|
-
const [body, _] = this.parseGroup(tokens, exprInsideStart, exprInsideEnd);
|
|
394
398
|
const left = leftDelimiter.value === '.'? null: leftDelimiter;
|
|
395
399
|
const right = rightDelimiter.value === '.'? null: rightDelimiter;
|
|
396
400
|
const res = new TexLeftRight({body: body, left: left, right: right});
|
|
@@ -414,38 +418,34 @@ export class LatexParser {
|
|
|
414
418
|
[data, pos] = this.parseNextArg(tokens, pos);
|
|
415
419
|
}
|
|
416
420
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const exprInsideStart = pos;
|
|
421
|
-
|
|
422
|
-
const endIdx = find_closing_end_command(tokens, start);
|
|
421
|
+
const [body, endIdx] = this.parseAligned(tokens, pos, END_COMMAND);
|
|
423
422
|
if (endIdx === -1) {
|
|
424
|
-
throw
|
|
423
|
+
throw LatexParserError.UNMATCHED_COMMAND_BEGIN;
|
|
425
424
|
}
|
|
426
|
-
|
|
427
|
-
pos = endIdx
|
|
425
|
+
|
|
426
|
+
pos = endIdx;
|
|
428
427
|
|
|
429
428
|
assert(tokens[pos].eq(LEFT_CURLY_BRACKET));
|
|
430
429
|
assert(tokens[pos + 1].type === TexTokenType.LITERAL);
|
|
431
430
|
assert(tokens[pos + 2].eq(RIGHT_CURLY_BRACKET));
|
|
432
431
|
if (tokens[pos + 1].value !== envName) {
|
|
433
|
-
throw new LatexParserError('
|
|
432
|
+
throw new LatexParserError('\\begin and \\end environments mismatch');
|
|
434
433
|
}
|
|
435
434
|
pos += 3;
|
|
436
435
|
|
|
437
|
-
const exprInside = tokens.slice(exprInsideStart, exprInsideEnd);
|
|
438
|
-
// ignore spaces and '\n' before \end{envName}
|
|
439
|
-
while(exprInside.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(exprInside[exprInside.length - 1].type)) {
|
|
440
|
-
exprInside.pop();
|
|
441
|
-
}
|
|
442
|
-
const body = this.parseAligned(exprInside);
|
|
443
436
|
const res = new TexBeginEnd(new TexToken(TexTokenType.LITERAL, envName), body, data);
|
|
444
437
|
return [res, pos];
|
|
445
438
|
}
|
|
446
439
|
|
|
447
|
-
|
|
448
|
-
|
|
440
|
+
// return pos: (position of closingToken) + 1
|
|
441
|
+
// pos will be -1 if closingToken is not found
|
|
442
|
+
parseAligned(tokens: TexToken[], start: number, closingToken: TexToken): [TexNode[][], number] {
|
|
443
|
+
this.alignmentDepth++;
|
|
444
|
+
|
|
445
|
+
let pos = start;
|
|
446
|
+
// ignore whitespaces and '\n' after \begin{envName}
|
|
447
|
+
pos += eat_whitespaces(tokens, pos).length;
|
|
448
|
+
|
|
449
449
|
const allRows: TexNode[][] = [];
|
|
450
450
|
let row: TexNode[] = [];
|
|
451
451
|
allRows.push(row);
|
|
@@ -453,6 +453,10 @@ export class LatexParser {
|
|
|
453
453
|
row.push(group);
|
|
454
454
|
|
|
455
455
|
while (pos < tokens.length) {
|
|
456
|
+
if (tokens[pos].eq(closingToken)) {
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
|
|
456
460
|
const [res, newPos] = this.parseNextExpr(tokens, pos);
|
|
457
461
|
pos = newPos;
|
|
458
462
|
|
|
@@ -477,7 +481,24 @@ export class LatexParser {
|
|
|
477
481
|
group.items.push(res);
|
|
478
482
|
}
|
|
479
483
|
}
|
|
480
|
-
|
|
484
|
+
|
|
485
|
+
if (pos >= tokens.length) {
|
|
486
|
+
return [[], -1];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ignore spaces and '\n' before \end{envName}
|
|
490
|
+
if (allRows.length > 0 && allRows[allRows.length - 1].length > 0) {
|
|
491
|
+
const last_cell = allRows[allRows.length - 1][allRows[allRows.length - 1].length - 1];
|
|
492
|
+
if (last_cell.type === 'ordgroup') {
|
|
493
|
+
const last_cell_items = (last_cell as TexGroup).items;
|
|
494
|
+
while(last_cell_items.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(last_cell_items[last_cell_items.length - 1].head.type)) {
|
|
495
|
+
last_cell_items.pop();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
this.alignmentDepth--;
|
|
501
|
+
return [allRows, pos + 1];
|
|
481
502
|
}
|
|
482
503
|
}
|
|
483
504
|
|
|
@@ -511,7 +532,7 @@ function passExpandCustomTexMacros(tokens: TexToken[], customTexMacros: {[key: s
|
|
|
511
532
|
return out_tokens;
|
|
512
533
|
}
|
|
513
534
|
|
|
514
|
-
export function parseTex(tex: string, customTexMacros: {[key: string]: string}): TexNode {
|
|
535
|
+
export function parseTex(tex: string, customTexMacros: {[key: string]: string} = {}): TexNode {
|
|
515
536
|
const parser = new LatexParser();
|
|
516
537
|
let tokens = tokenize_tex(tex);
|
|
517
538
|
tokens = passIgnoreWhitespaceBeforeScriptMark(tokens);
|
package/src/tex-tokenizer.ts
CHANGED
package/src/tex2typst.ts
CHANGED
package/src/typst-parser.ts
CHANGED
|
@@ -53,8 +53,8 @@ function find_closing_delim(tokens: TypstToken[], start: number): number {
|
|
|
53
53
|
return _find_closing_match(
|
|
54
54
|
tokens,
|
|
55
55
|
start,
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
TypstToken.LEFT_DELIMITERS,
|
|
57
|
+
TypstToken.RIGHT_DELIMITERS
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -218,7 +218,6 @@ const LEFT_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '[');
|
|
|
218
218
|
const RIGHT_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, ']');
|
|
219
219
|
const LEFT_CURLY_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '{');
|
|
220
220
|
const RIGHT_CURLY_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '}');
|
|
221
|
-
const VERTICAL_BAR = new TypstToken(TypstTokenType.ELEMENT, '|');
|
|
222
221
|
const COMMA = new TypstToken(TypstTokenType.ELEMENT, ',');
|
|
223
222
|
const SEMICOLON = new TypstToken(TypstTokenType.ELEMENT, ';');
|
|
224
223
|
const SINGLE_SPACE = new TypstToken(TypstTokenType.SPACE, ' ');
|
|
@@ -373,20 +372,20 @@ export class TypstParser {
|
|
|
373
372
|
// start: the position of the left parentheses
|
|
374
373
|
parseLrArguments(tokens: TypstToken[], start: number): [TypstNode, number] {
|
|
375
374
|
const lr_token = tokens[start];
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
const end = find_closing_match(tokens, start);
|
|
376
|
+
if (tokens[start + 1].isOneOf(TypstToken.LEFT_DELIMITERS)) {
|
|
378
377
|
const inner_start = start + 1;
|
|
379
378
|
const inner_end = find_closing_delim(tokens, inner_start);
|
|
380
|
-
const inner_args= this.
|
|
379
|
+
const [inner_args, _]= this.parseGroup(tokens, inner_start + 1, inner_end);
|
|
381
380
|
return [
|
|
382
|
-
new TypstLeftright(lr_token, { body:
|
|
381
|
+
new TypstLeftright(lr_token, { body: inner_args, left: tokens[inner_start], right: tokens[inner_end]}),
|
|
383
382
|
end + 1,
|
|
384
383
|
];
|
|
385
384
|
} else {
|
|
386
|
-
const [
|
|
385
|
+
const [inner_args, _] = this.parseGroup(tokens, start + 1, end - 1);
|
|
387
386
|
return [
|
|
388
|
-
new TypstLeftright(lr_token, { body:
|
|
389
|
-
end,
|
|
387
|
+
new TypstLeftright(lr_token, { body: inner_args, left: null, right: null }),
|
|
388
|
+
end + 1,
|
|
390
389
|
];
|
|
391
390
|
}
|
|
392
391
|
}
|