tex2typst 0.4.0 → 0.5.0
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/dist/index.js +324 -306
- package/dist/tex2typst.min.js +10 -11
- package/package.json +2 -2
- package/src/convert.ts +37 -32
- package/src/generic.ts +6 -6
- package/src/map.ts +136 -79
- package/src/tex-parser.ts +53 -64
- package/src/tex-tokenizer.ts +1 -0
- package/src/tex-types.ts +1 -1
- package/src/typst-parser.ts +114 -160
- package/src/typst-shorthands.ts +6 -3
- package/src/typst-types.ts +2 -2
- package/tests/cheat-sheet.test.ts +0 -42
- package/tests/cheat-sheet.toml +0 -304
- package/tests/example.ts +0 -15
- package/tests/general-symbols.test.ts +0 -22
- package/tests/general-symbols.toml +0 -755
- package/tests/integration-tex2typst.yaml +0 -89
- package/tests/struct-bidirection.yaml +0 -203
- package/tests/struct-tex2typst.yaml +0 -451
- package/tests/struct-typst2tex.yaml +0 -412
- package/tests/symbol.yml +0 -126
- package/tests/test-common.ts +0 -26
- package/tests/tex-parser.test.ts +0 -97
- package/tests/tex-to-typst.test.ts +0 -136
- package/tests/typst-parser.test.ts +0 -134
- package/tests/typst-to-tex.test.ts +0 -100
- /package/src/{util.ts → utils.ts} +0 -0
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
|
-
import { assert } from "./
|
|
3
|
-
import {
|
|
2
|
+
import { assert } from "./utils";
|
|
3
|
+
import { array_join, array_split } from "./generic";
|
|
4
4
|
import { TEX_BINARY_COMMANDS, TEX_UNARY_COMMANDS, tokenize_tex } from "./tex-tokenizer";
|
|
5
5
|
|
|
6
6
|
const IGNORED_COMMANDS = [
|
|
@@ -103,22 +103,7 @@ export class LatexParser {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
parse(tokens: TexToken[]): TexNode {
|
|
106
|
-
|
|
107
|
-
const idx = array_find(tokens, token_displaystyle);
|
|
108
|
-
if (idx === -1) {
|
|
109
|
-
// no \displaystyle, normal execution path
|
|
110
|
-
return this.parseGroup(tokens.slice(0));
|
|
111
|
-
} else if (idx === 0) {
|
|
112
|
-
// \displaystyle at the beginning. Wrap the whole thing in \displaystyle
|
|
113
|
-
const tree = this.parseGroup(tokens.slice(1));
|
|
114
|
-
return new TexFuncCall(token_displaystyle, [tree]);
|
|
115
|
-
} else {
|
|
116
|
-
// \displaystyle somewhere in the middle. Split the expression to two parts
|
|
117
|
-
const tree1 = this.parseGroup(tokens.slice(0, idx));
|
|
118
|
-
const tree2 = this.parseGroup(tokens.slice(idx + 1, tokens.length));
|
|
119
|
-
const display = new TexFuncCall(token_displaystyle, [tree2]);
|
|
120
|
-
return new TexGroup([tree1, display]);
|
|
121
|
-
}
|
|
106
|
+
return this.parseGroup(tokens.slice(0));
|
|
122
107
|
}
|
|
123
108
|
|
|
124
109
|
parseGroup(tokens: TexToken[]): TexNode {
|
|
@@ -152,11 +137,13 @@ export class LatexParser {
|
|
|
152
137
|
return [EMPTY_NODE, -1];
|
|
153
138
|
}
|
|
154
139
|
|
|
140
|
+
const styledResults = this.applyStyleCommands(results);
|
|
141
|
+
|
|
155
142
|
let node: TexNode;
|
|
156
|
-
if (
|
|
157
|
-
node =
|
|
143
|
+
if (styledResults.length === 1) {
|
|
144
|
+
node = styledResults[0];
|
|
158
145
|
} else {
|
|
159
|
-
node = new TexGroup(
|
|
146
|
+
node = new TexGroup(styledResults);
|
|
160
147
|
}
|
|
161
148
|
return [node, pos + 1];
|
|
162
149
|
}
|
|
@@ -446,59 +433,61 @@ export class LatexParser {
|
|
|
446
433
|
// ignore whitespaces and '\n' after \begin{envName}
|
|
447
434
|
pos += eat_whitespaces(tokens, pos).length;
|
|
448
435
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
allRows.push(row);
|
|
452
|
-
let group = new TexGroup([]);
|
|
453
|
-
row.push(group);
|
|
454
|
-
|
|
455
|
-
while (pos < tokens.length) {
|
|
456
|
-
if (tokens[pos].eq(closingToken)) {
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const [res, newPos] = this.parseNextExpr(tokens, pos);
|
|
461
|
-
pos = newPos;
|
|
436
|
+
let closure: TexNode;
|
|
437
|
+
[closure, pos] = this.parseClosure(tokens, pos, closingToken);
|
|
462
438
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
if (!this.newline_sensitive && res.head.value === '\n') {
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
439
|
+
if (pos === -1) {
|
|
440
|
+
return [[], -1];
|
|
441
|
+
}
|
|
471
442
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
group = new TexGroup([]);
|
|
479
|
-
row.push(group);
|
|
480
|
-
} else {
|
|
481
|
-
group.items.push(res);
|
|
443
|
+
let allRows: TexNode[][];
|
|
444
|
+
if (closure.type === 'ordgroup') {
|
|
445
|
+
const elements = (closure as TexGroup).items;
|
|
446
|
+
// ignore spaces and '\n' before \end{envName}
|
|
447
|
+
while(elements.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(elements[elements.length - 1].head.type)) {
|
|
448
|
+
elements.pop();
|
|
482
449
|
}
|
|
450
|
+
allRows = array_split(elements, new TexToken(TexTokenType.CONTROL, '\\\\').toNode())
|
|
451
|
+
.map(row => {
|
|
452
|
+
return array_split(row, new TexToken(TexTokenType.CONTROL, '&').toNode())
|
|
453
|
+
.map(arr => new TexGroup(arr));
|
|
454
|
+
});
|
|
455
|
+
} else {
|
|
456
|
+
allRows = [[closure]];
|
|
483
457
|
}
|
|
484
458
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
459
|
+
this.alignmentDepth--;
|
|
460
|
+
return [allRows, pos];
|
|
461
|
+
}
|
|
488
462
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
if (
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
463
|
+
private applyStyleCommands(nodes: TexNode[]): TexNode[] {
|
|
464
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
465
|
+
const styleToken = this.getStyleToken(nodes[i]);
|
|
466
|
+
if (styleToken) {
|
|
467
|
+
const before = this.applyStyleCommands(nodes.slice(0, i));
|
|
468
|
+
const after = this.applyStyleCommands(nodes.slice(i + 1));
|
|
469
|
+
let body: TexNode;
|
|
470
|
+
if (after.length === 0) {
|
|
471
|
+
body = EMPTY_NODE;
|
|
472
|
+
} else if (after.length === 1) {
|
|
473
|
+
body = after[0];
|
|
474
|
+
} else {
|
|
475
|
+
body = new TexGroup(after);
|
|
496
476
|
}
|
|
477
|
+
const funcCall = new TexFuncCall(styleToken, [body]);
|
|
478
|
+
return before.concat(funcCall);
|
|
497
479
|
}
|
|
498
480
|
}
|
|
481
|
+
return nodes;
|
|
482
|
+
}
|
|
499
483
|
|
|
500
|
-
|
|
501
|
-
|
|
484
|
+
private getStyleToken(node: TexNode): TexToken | null {
|
|
485
|
+
if (node.type === 'terminal') {
|
|
486
|
+
if (node.head.eq(TexToken.COMMAND_DISPLAYSTYLE) || node.head.eq(TexToken.COMMAND_TEXTSTYLE)) {
|
|
487
|
+
return node.head;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return null;
|
|
502
491
|
}
|
|
503
492
|
}
|
|
504
493
|
|
package/src/tex-tokenizer.ts
CHANGED
package/src/tex-types.ts
CHANGED
package/src/typst-parser.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { TypstSupsubData } from "./typst-types";
|
|
|
6
6
|
import { TypstToken } from "./typst-types";
|
|
7
7
|
import { TypstTokenType } from "./typst-types";
|
|
8
8
|
import { tokenize_typst } from "./typst-tokenizer";
|
|
9
|
-
import { assert, isalpha } from "./
|
|
9
|
+
import { assert, isalpha } from "./utils";
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
// TODO: In Typst, y' ' is not the same as y''.
|
|
@@ -49,41 +49,37 @@ function find_closing_match(tokens: TypstToken[], start: number): number {
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function find_closing_delim(tokens: TypstToken[], start: number): number {
|
|
53
|
-
return _find_closing_match(
|
|
54
|
-
tokens,
|
|
55
|
-
start,
|
|
56
|
-
TypstToken.LEFT_DELIMITERS,
|
|
57
|
-
TypstToken.RIGHT_DELIMITERS
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
52
|
|
|
53
|
+
function extract_named_params(arr: TypstNode[]): [TypstNode[], TypstNamedParams] {
|
|
54
|
+
const COLON = new TypstToken(TypstTokenType.ELEMENT, ':').toNode();
|
|
55
|
+
const np: TypstNamedParams = {};
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
assert(nodes[start].eq(left_parenthesis));
|
|
70
|
-
|
|
71
|
-
let count = 1;
|
|
72
|
-
let pos = start + 1;
|
|
57
|
+
const to_delete: number[] = [];
|
|
58
|
+
for(let i = 0; i < arr.length; i++) {
|
|
59
|
+
if(arr[i].type !== 'group') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
const g = arr[i] as TypstGroup;
|
|
64
|
+
const pos_colon = array_find(g.items, COLON);
|
|
65
|
+
if(pos_colon === -1 || pos_colon === 0) {
|
|
66
|
+
continue;
|
|
77
67
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
68
|
+
to_delete.push(i);
|
|
69
|
+
const param_name = g.items[pos_colon - 1];
|
|
70
|
+
if(param_name.eq(new TypstToken(TypstTokenType.SYMBOL, 'delim').toNode())) {
|
|
71
|
+
if(g.items.length !== 3) {
|
|
72
|
+
throw new TypstParserError('Invalid number of arguments for delim');
|
|
73
|
+
}
|
|
74
|
+
np['delim'] = g.items[pos_colon + 1];
|
|
75
|
+
} else {
|
|
76
|
+
throw new TypstParserError('Not implemented for other named parameters');
|
|
82
77
|
}
|
|
83
|
-
pos += 1;
|
|
84
78
|
}
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
for(let i = to_delete.length - 1; i >= 0; i--) {
|
|
80
|
+
arr.splice(to_delete[i], 1);
|
|
81
|
+
}
|
|
82
|
+
return [arr, np];
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
function primes(num: number): TypstNode[] {
|
|
@@ -129,36 +125,18 @@ function trim_whitespace_around_operators(nodes: TypstNode[]): TypstNode[] {
|
|
|
129
125
|
return res;
|
|
130
126
|
}
|
|
131
127
|
|
|
132
|
-
function process_operators(nodes: TypstNode[]
|
|
128
|
+
function process_operators(nodes: TypstNode[]): TypstNode {
|
|
133
129
|
nodes = trim_whitespace_around_operators(nodes);
|
|
134
130
|
|
|
135
|
-
const opening_bracket = LEFT_PARENTHESES.toNode();
|
|
136
|
-
const closing_bracket = RIGHT_PARENTHESES.toNode();
|
|
137
|
-
|
|
138
131
|
const stack: TypstNode[] = [];
|
|
139
132
|
|
|
140
133
|
const args: TypstNode[] = [];
|
|
141
134
|
let pos = 0;
|
|
142
135
|
while (pos < nodes.length) {
|
|
143
|
-
const
|
|
144
|
-
if
|
|
145
|
-
|
|
146
|
-
} else if(current.eq(DIV)) {
|
|
147
|
-
stack.push(current);
|
|
148
|
-
pos++;
|
|
136
|
+
const current_tree = nodes[pos];
|
|
137
|
+
if(current_tree.eq(DIV)) {
|
|
138
|
+
stack.push(current_tree);
|
|
149
139
|
} else {
|
|
150
|
-
let current_tree: TypstNode;
|
|
151
|
-
if(current.eq(opening_bracket)) {
|
|
152
|
-
// the expression is a group wrapped in parenthesis
|
|
153
|
-
const pos_closing = find_closing_parenthesis(nodes, pos);
|
|
154
|
-
current_tree = process_operators(nodes.slice(pos + 1, pos_closing), true);
|
|
155
|
-
pos = pos_closing + 1;
|
|
156
|
-
} else {
|
|
157
|
-
// the expression is just a single item
|
|
158
|
-
current_tree = current;
|
|
159
|
-
pos++;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
140
|
if(stack.length > 0 && stack[stack.length-1].eq(DIV)) {
|
|
163
141
|
let denominator = current_tree;
|
|
164
142
|
if(args.length === 0) {
|
|
@@ -179,13 +157,9 @@ function process_operators(nodes: TypstNode[], parenthesis = false): TypstNode {
|
|
|
179
157
|
args.push(current_tree);
|
|
180
158
|
}
|
|
181
159
|
}
|
|
160
|
+
pos++;
|
|
182
161
|
}
|
|
183
|
-
|
|
184
|
-
if(parenthesis) {
|
|
185
|
-
return new TypstLeftright(null, { body: body, left: LEFT_PARENTHESES, right: RIGHT_PARENTHESES } as TypstLeftRightData);
|
|
186
|
-
} else {
|
|
187
|
-
return body;
|
|
188
|
-
}
|
|
162
|
+
return args.length === 1? args[0]: new TypstGroup(args);
|
|
189
163
|
}
|
|
190
164
|
|
|
191
165
|
function parse_named_params(groups: TypstGroup[]): TypstNamedParams {
|
|
@@ -220,9 +194,14 @@ const LEFT_CURLY_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '{
|
|
|
220
194
|
const RIGHT_CURLY_BRACKET: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '}');
|
|
221
195
|
const COMMA = new TypstToken(TypstTokenType.ELEMENT, ',');
|
|
222
196
|
const SEMICOLON = new TypstToken(TypstTokenType.ELEMENT, ';');
|
|
223
|
-
const SINGLE_SPACE = new TypstToken(TypstTokenType.SPACE, ' ');
|
|
224
197
|
const CONTROL_AND = new TypstToken(TypstTokenType.CONTROL, '&');
|
|
225
198
|
|
|
199
|
+
|
|
200
|
+
interface TexParseEnv {
|
|
201
|
+
spaceSensitive: boolean;
|
|
202
|
+
newlineSensitive: boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
226
205
|
export class TypstParser {
|
|
227
206
|
space_sensitive: boolean;
|
|
228
207
|
newline_sensitive: boolean;
|
|
@@ -237,35 +216,8 @@ export class TypstParser {
|
|
|
237
216
|
return tree;
|
|
238
217
|
}
|
|
239
218
|
|
|
240
|
-
parseGroup(tokens: TypstToken[], start: number, end: number
|
|
241
|
-
|
|
242
|
-
let pos = start;
|
|
243
|
-
|
|
244
|
-
while (pos < end) {
|
|
245
|
-
const [res, newPos] = this.parseNextExpr(tokens, pos);
|
|
246
|
-
pos = newPos;
|
|
247
|
-
if (res.head.type === TypstTokenType.SPACE || res.head.type === TypstTokenType.NEWLINE) {
|
|
248
|
-
if (!this.space_sensitive && res.head.value.replace(/ /g, '').length === 0) {
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
if (!this.newline_sensitive && res.head.value === '\n') {
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
results.push(res);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
let node: TypstNode;
|
|
259
|
-
if(parentheses) {
|
|
260
|
-
node = process_operators(results, true);
|
|
261
|
-
} else {
|
|
262
|
-
if (results.length === 1) {
|
|
263
|
-
node = results[0];
|
|
264
|
-
} else {
|
|
265
|
-
node = process_operators(results);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return [node, end + 1];
|
|
219
|
+
parseGroup(tokens: TypstToken[], start: number, end: number): TypstParseResult {
|
|
220
|
+
return this.parseUntil(tokens.slice(start, end), 0, null);
|
|
269
221
|
}
|
|
270
222
|
|
|
271
223
|
parseNextExpr(tokens: TypstToken[], start: number): TypstParseResult {
|
|
@@ -298,12 +250,51 @@ export class TypstParser {
|
|
|
298
250
|
}
|
|
299
251
|
}
|
|
300
252
|
|
|
253
|
+
// return pos: (position of stopToken) + 1
|
|
254
|
+
// pos will be -1 if stopToken is not found
|
|
255
|
+
parseUntil(tokens: TypstToken[], start: number, stopToken: TypstToken | null, env: Partial<TexParseEnv> = {}): TypstParseResult {
|
|
256
|
+
if (env.spaceSensitive === undefined) {
|
|
257
|
+
env.spaceSensitive = this.space_sensitive;
|
|
258
|
+
}
|
|
259
|
+
if (env.newlineSensitive === undefined) {
|
|
260
|
+
env.newlineSensitive = this.newline_sensitive;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const results: TypstNode[] = [];
|
|
264
|
+
let pos = start;
|
|
265
|
+
|
|
266
|
+
while (pos < tokens.length) {
|
|
267
|
+
if (stopToken !== null && tokens[pos].eq(stopToken)) {
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
const [res, newPos] = this.parseNextExpr(tokens, pos);
|
|
271
|
+
pos = newPos;
|
|
272
|
+
if (res.head.type === TypstTokenType.SPACE || res.head.type === TypstTokenType.NEWLINE) {
|
|
273
|
+
if (!env.spaceSensitive && res.head.value.replace(/ /g, '').length === 0) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (!env.newlineSensitive && res.head.value === '\n') {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
results.push(res);
|
|
281
|
+
}
|
|
282
|
+
if (pos >= tokens.length && stopToken !== null) {
|
|
283
|
+
return [TypstToken.NONE.toNode(), -1];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const node = process_operators(results);
|
|
287
|
+
return [node, pos + 1];
|
|
288
|
+
}
|
|
289
|
+
|
|
301
290
|
parseSupOrSub(tokens: TypstToken[], start: number): TypstParseResult {
|
|
302
291
|
let node: TypstNode;
|
|
303
292
|
let end: number;
|
|
304
293
|
if(tokens[start].eq(LEFT_PARENTHESES)) {
|
|
305
|
-
|
|
306
|
-
|
|
294
|
+
[node, end] = this.parseUntil(tokens, start + 1, RIGHT_PARENTHESES);
|
|
295
|
+
if (end === -1) {
|
|
296
|
+
throw new Error("Unmatched '('");
|
|
297
|
+
}
|
|
307
298
|
} else {
|
|
308
299
|
[node, end] = this.parseNextExprWithoutSupSub(tokens, start);
|
|
309
300
|
}
|
|
@@ -319,8 +310,12 @@ export class TypstParser {
|
|
|
319
310
|
const firstToken = tokens[start];
|
|
320
311
|
const node = firstToken.toNode();
|
|
321
312
|
if(firstToken.eq(LEFT_PARENTHESES)) {
|
|
322
|
-
const
|
|
323
|
-
|
|
313
|
+
const [body, end] = this.parseUntil(tokens, start + 1, RIGHT_PARENTHESES);
|
|
314
|
+
if (end === -1) {
|
|
315
|
+
throw new Error("Unmatched '('");
|
|
316
|
+
}
|
|
317
|
+
const res = new TypstLeftright(null, { body: body, left: LEFT_PARENTHESES, right: RIGHT_PARENTHESES } as TypstLeftRightData);
|
|
318
|
+
return [res, end];
|
|
324
319
|
}
|
|
325
320
|
if(firstToken.type === TypstTokenType.ELEMENT && !isalpha(firstToken.value[0])) {
|
|
326
321
|
return [node, start + 1];
|
|
@@ -371,29 +366,32 @@ export class TypstParser {
|
|
|
371
366
|
|
|
372
367
|
// start: the position of the left parentheses
|
|
373
368
|
parseLrArguments(tokens: TypstToken[], start: number): [TypstNode, number] {
|
|
374
|
-
const lr_token =
|
|
369
|
+
const lr_token = new TypstToken(TypstTokenType.SYMBOL, 'lr');
|
|
375
370
|
const end = find_closing_match(tokens, start);
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
} else {
|
|
385
|
-
const [inner_args, _] = this.parseGroup(tokens, start + 1, end - 1);
|
|
386
|
-
return [
|
|
387
|
-
new TypstLeftright(lr_token, { body: inner_args, left: null, right: null }),
|
|
388
|
-
end + 1,
|
|
389
|
-
];
|
|
371
|
+
|
|
372
|
+
let left: TypstToken | null = null;
|
|
373
|
+
let right: TypstToken | null = null;
|
|
374
|
+
let inner_start = start + 1;
|
|
375
|
+
let inner_end = end;
|
|
376
|
+
if (inner_end > inner_start && tokens[inner_start].isOneOf(TypstToken.LEFT_DELIMITERS)) {
|
|
377
|
+
left = tokens[inner_start];
|
|
378
|
+
inner_start += 1;
|
|
390
379
|
}
|
|
380
|
+
if (inner_end - 1 > inner_start && tokens[inner_end - 1].isOneOf(TypstToken.RIGHT_DELIMITERS)) {
|
|
381
|
+
right = tokens[inner_end - 1];
|
|
382
|
+
inner_end -= 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const [inner_args, _] = this.parseGroup(tokens, inner_start, inner_end);
|
|
386
|
+
return [
|
|
387
|
+
new TypstLeftright(lr_token, { body: inner_args, left: left, right: right }),
|
|
388
|
+
end + 1,
|
|
389
|
+
];
|
|
391
390
|
}
|
|
392
391
|
|
|
393
392
|
// start: the position of the left parentheses
|
|
394
393
|
parseMatrix(tokens: TypstToken[], start: number, rowSepToken: TypstToken, cellSepToken: TypstToken): [TypstNode[][], TypstNamedParams, number] {
|
|
395
394
|
const end = find_closing_match(tokens, start);
|
|
396
|
-
tokens = tokens.slice(0, end);
|
|
397
395
|
|
|
398
396
|
const matrix: TypstNode[][] = [];
|
|
399
397
|
let named_params: TypstNamedParams = {};
|
|
@@ -402,45 +400,13 @@ export class TypstParser {
|
|
|
402
400
|
while (pos < end) {
|
|
403
401
|
while(pos < end) {
|
|
404
402
|
let next_stop = array_find(tokens, rowSepToken, pos);
|
|
405
|
-
if (next_stop === -1) {
|
|
403
|
+
if (next_stop === -1 || next_stop > end) {
|
|
406
404
|
next_stop = end;
|
|
407
405
|
}
|
|
408
406
|
|
|
409
407
|
let row = this.parseArgumentsWithSeparator(tokens, pos, next_stop, cellSepToken);
|
|
410
408
|
let np: TypstNamedParams = {};
|
|
411
409
|
|
|
412
|
-
function extract_named_params(arr: TypstNode[]): [TypstNode[], TypstNamedParams] {
|
|
413
|
-
const COLON = new TypstToken(TypstTokenType.ELEMENT, ':').toNode();
|
|
414
|
-
const np: TypstNamedParams = {};
|
|
415
|
-
|
|
416
|
-
const to_delete: number[] = [];
|
|
417
|
-
for(let i = 0; i < arr.length; i++) {
|
|
418
|
-
if(arr[i].type !== 'group') {
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const g = arr[i] as TypstGroup;
|
|
423
|
-
const pos_colon = array_find(g.items, COLON);
|
|
424
|
-
if(pos_colon === -1 || pos_colon === 0) {
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
to_delete.push(i);
|
|
428
|
-
const param_name = g.items[pos_colon - 1];
|
|
429
|
-
if(param_name.eq(new TypstToken(TypstTokenType.SYMBOL, 'delim').toNode())) {
|
|
430
|
-
if(g.items.length !== 3) {
|
|
431
|
-
throw new TypstParserError('Invalid number of arguments for delim');
|
|
432
|
-
}
|
|
433
|
-
np['delim'] = g.items[pos_colon + 1];
|
|
434
|
-
} else {
|
|
435
|
-
throw new TypstParserError('Not implemented for other named parameters');
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
for(let i = to_delete.length - 1; i >= 0; i--) {
|
|
439
|
-
arr.splice(to_delete[i], 1);
|
|
440
|
-
}
|
|
441
|
-
return [arr, np];
|
|
442
|
-
}
|
|
443
|
-
|
|
444
410
|
[row, np] = extract_named_params(row);
|
|
445
411
|
matrix.push(row);
|
|
446
412
|
Object.assign(named_params, np);
|
|
@@ -455,29 +421,17 @@ export class TypstParser {
|
|
|
455
421
|
parseArgumentsWithSeparator(tokens: TypstToken[], start: number, end: number, sepToken: TypstToken): TypstNode[] {
|
|
456
422
|
const args: TypstNode[] = [];
|
|
457
423
|
let pos = start;
|
|
458
|
-
while (pos < end) {
|
|
459
|
-
let nodes: TypstNode[] = [];
|
|
460
|
-
while(pos < end) {
|
|
461
|
-
if(tokens[pos].eq(sepToken)) {
|
|
462
|
-
pos += 1;
|
|
463
|
-
break;
|
|
464
|
-
} else if(tokens[pos].eq(SINGLE_SPACE)) {
|
|
465
|
-
pos += 1;
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
const [argItem, newPos] = this.parseNextExpr(tokens, pos);
|
|
469
|
-
pos = newPos;
|
|
470
|
-
nodes.push(argItem);
|
|
471
|
-
}
|
|
472
424
|
|
|
425
|
+
while (pos < end) {
|
|
473
426
|
let arg: TypstNode;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
427
|
+
let newPos: number;
|
|
428
|
+
const env = { spaceSensitive: false, newlineSensitive: true };
|
|
429
|
+
[arg, newPos] = this.parseUntil(tokens.slice(0, end), pos, sepToken, env);
|
|
430
|
+
if (newPos == -1) {
|
|
431
|
+
[arg, newPos] = this.parseUntil(tokens.slice(0, end), pos, null, env);
|
|
478
432
|
}
|
|
479
|
-
|
|
480
433
|
args.push(arg);
|
|
434
|
+
pos = newPos;
|
|
481
435
|
}
|
|
482
436
|
return args;
|
|
483
437
|
}
|
package/src/typst-shorthands.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const shorthandMap = new Map<string, string>([
|
|
2
|
+
// The following snippet is generated with tools/make-short-hand-map.py
|
|
2
3
|
['arrow.l.r.double.long', '<==>'],
|
|
3
4
|
['arrow.l.r.long', '<-->'],
|
|
4
5
|
['arrow.r.bar', '|->'],
|
|
@@ -13,7 +14,6 @@ const shorthandMap = new Map<string, string>([
|
|
|
13
14
|
['arrow.l.long.squiggly', '<~~'],
|
|
14
15
|
['arrow.l.tail', '<-<'],
|
|
15
16
|
['arrow.l.twohead', '<<-'],
|
|
16
|
-
['arrow.l.r', '<->'],
|
|
17
17
|
['arrow.l.r.double', '<=>'],
|
|
18
18
|
['colon.double.eq', '::='],
|
|
19
19
|
['dots.h', '...'],
|
|
@@ -25,8 +25,8 @@ const shorthandMap = new Map<string, string>([
|
|
|
25
25
|
['arrow.l', '<-'],
|
|
26
26
|
['arrow.l.squiggly', '<~'],
|
|
27
27
|
['bar.v.double', '||'],
|
|
28
|
-
['bracket.l.
|
|
29
|
-
['bracket.r.
|
|
28
|
+
['bracket.l.stroked', '[|'],
|
|
29
|
+
['bracket.r.stroked', '|]'],
|
|
30
30
|
['colon.eq', ':='],
|
|
31
31
|
['eq.colon', '=:'],
|
|
32
32
|
['eq.not', '!='],
|
|
@@ -37,6 +37,9 @@ const shorthandMap = new Map<string, string>([
|
|
|
37
37
|
['ast.op', '*'],
|
|
38
38
|
['minus', '-'],
|
|
39
39
|
['tilde.op', '~'],
|
|
40
|
+
|
|
41
|
+
// Typst's documentation doesn't include this. Wondering why
|
|
42
|
+
['arrow.l.r', '<->'],
|
|
40
43
|
]);
|
|
41
44
|
|
|
42
45
|
|
package/src/typst-types.ts
CHANGED
|
@@ -57,7 +57,7 @@ export class TypstToken {
|
|
|
57
57
|
new TypstToken(TypstTokenType.ELEMENT, '['),
|
|
58
58
|
new TypstToken(TypstTokenType.ELEMENT, '{'),
|
|
59
59
|
new TypstToken(TypstTokenType.ELEMENT, '|'),
|
|
60
|
-
new TypstToken(TypstTokenType.SYMBOL, '
|
|
60
|
+
new TypstToken(TypstTokenType.SYMBOL, 'chevron.l'),
|
|
61
61
|
new TypstToken(TypstTokenType.SYMBOL, 'paren.l'),
|
|
62
62
|
new TypstToken(TypstTokenType.SYMBOL, 'brace.l'),
|
|
63
63
|
];
|
|
@@ -67,7 +67,7 @@ export class TypstToken {
|
|
|
67
67
|
new TypstToken(TypstTokenType.ELEMENT, ']'),
|
|
68
68
|
new TypstToken(TypstTokenType.ELEMENT, '}'),
|
|
69
69
|
new TypstToken(TypstTokenType.ELEMENT, '|'),
|
|
70
|
-
new TypstToken(TypstTokenType.SYMBOL, '
|
|
70
|
+
new TypstToken(TypstTokenType.SYMBOL, 'chevron.r'),
|
|
71
71
|
new TypstToken(TypstTokenType.SYMBOL, 'paren.r'),
|
|
72
72
|
new TypstToken(TypstTokenType.SYMBOL, 'brace.r'),
|
|
73
73
|
];
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
});
|