tex2typst 0.3.24 → 0.3.25

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/src/tex-types.ts CHANGED
@@ -18,7 +18,7 @@ export enum TexTokenType {
18
18
  }
19
19
 
20
20
  export class TexToken {
21
- type: TexTokenType;
21
+ readonly type: TexTokenType;
22
22
  value: string;
23
23
 
24
24
  constructor(type: TexTokenType, value: string) {
@@ -55,6 +55,7 @@ export interface TexSupsubData {
55
55
 
56
56
  // \left. or \right. will be represented as null.
57
57
  export interface TexLeftRightData {
58
+ body: TexNode;
58
59
  left: TexToken | null;
59
60
  right: TexToken | null;
60
61
  }
@@ -68,14 +69,12 @@ type TexNodeType = 'terminal' | 'text' | 'ordgroup' | 'supsub'
68
69
 
69
70
 
70
71
  export abstract class TexNode {
71
- type: TexNodeType;
72
+ readonly type: TexNodeType;
72
73
  head: TexToken;
73
- args?: TexNode[];
74
74
 
75
- constructor(type: TexNodeType, head: TexToken | null, args?: TexNode[]) {
75
+ constructor(type: TexNodeType, head: TexToken | null) {
76
76
  this.type = type;
77
77
  this.head = head ? head : TexToken.EMPTY;
78
- this.args = args;
79
78
  }
80
79
 
81
80
  // Note that this is only shallow equality.
@@ -148,12 +147,14 @@ export class TexText extends TexNode {
148
147
  }
149
148
 
150
149
  export class TexGroup extends TexNode {
151
- constructor(args: TexNode[]) {
152
- super('ordgroup', TexToken.EMPTY, args);
150
+ public items: TexNode[];
151
+ constructor(items: TexNode[]) {
152
+ super('ordgroup', TexToken.EMPTY);
153
+ this.items = items;
153
154
  }
154
155
 
155
156
  public serialize(): TexToken[] {
156
- return this.args!.map((n) => n.serialize()).flat();
157
+ return this.items.map((n) => n.serialize()).flat();
157
158
  }
158
159
  }
159
160
 
@@ -163,7 +164,7 @@ export class TexSupSub extends TexNode {
163
164
  public sub: TexNode | null;
164
165
 
165
166
  constructor(data: TexSupsubData) {
166
- super('supsub', TexToken.EMPTY, []);
167
+ super('supsub', TexToken.EMPTY);
167
168
  this.base = data.base;
168
169
  this.sup = data.sup;
169
170
  this.sub = data.sub;
@@ -211,11 +212,14 @@ export class TexSupSub extends TexNode {
211
212
  }
212
213
 
213
214
  export class TexFuncCall extends TexNode {
215
+ public args: TexNode[];
216
+
214
217
  // For type="sqrt", it's additional argument wrapped square bracket. e.g. 3 in \sqrt[3]{x}
215
218
  public data: TexNode | null;
216
219
 
217
220
  constructor(head: TexToken, args: TexNode[], data: TexNode | null = null) {
218
- super('funcCall', head, args);
221
+ super('funcCall', head);
222
+ this.args = args;
219
223
  this.data = data;
220
224
  }
221
225
 
@@ -230,7 +234,7 @@ export class TexFuncCall extends TexNode {
230
234
  tokens.push(new TexToken(TexTokenType.ELEMENT, ']'));
231
235
  }
232
236
 
233
- for (const arg of this.args!) {
237
+ for (const arg of this.args) {
234
238
  tokens.push(new TexToken(TexTokenType.ELEMENT, '{'));
235
239
  tokens = tokens.concat(arg.serialize());
236
240
  tokens.push(new TexToken(TexTokenType.ELEMENT, '}'));
@@ -241,11 +245,13 @@ export class TexFuncCall extends TexNode {
241
245
  }
242
246
 
243
247
  export class TexLeftRight extends TexNode {
248
+ public body: TexNode;
244
249
  public left: TexToken | null;
245
250
  public right: TexToken | null;
246
251
 
247
- constructor(args: TexNode[], data: TexLeftRightData) {
248
- super('leftright', TexToken.EMPTY, args);
252
+ constructor(data: TexLeftRightData) {
253
+ super('leftright', TexToken.EMPTY);
254
+ this.body = data.body;
249
255
  this.left = data.left;
250
256
  this.right = data.right;
251
257
  }
@@ -254,7 +260,7 @@ export class TexLeftRight extends TexNode {
254
260
  let tokens: TexToken[] = [];
255
261
  tokens.push(new TexToken(TexTokenType.COMMAND, '\\left'));
256
262
  tokens.push(new TexToken(TexTokenType.ELEMENT, this.left? this.left.value: '.'));
257
- tokens = tokens.concat(this.args!.map((n) => n.serialize()).flat());
263
+ tokens = tokens.concat(this.body.serialize());
258
264
  tokens.push(new TexToken(TexTokenType.COMMAND, '\\right'));
259
265
  tokens.push(new TexToken(TexTokenType.ELEMENT, this.right? this.right.value: '.'));
260
266
  return tokens;
@@ -263,11 +269,14 @@ export class TexLeftRight extends TexNode {
263
269
 
264
270
  export class TexBeginEnd extends TexNode {
265
271
  public matrix: TexNode[][];
272
+ // for environment="array" or "subarray", there's additional data like {c|c} right after \begin{env}
273
+ public data: TexNode | null;
266
274
 
267
- constructor(head: TexToken, args: TexNode[], data: TexNode[][]) {
275
+ constructor(head: TexToken, matrix: TexNode[][], data: TexNode | null = null) {
268
276
  assert(head.type === TexTokenType.LITERAL);
269
- super('beginend', head, args);
270
- this.matrix = data;
277
+ super('beginend', head);
278
+ this.matrix = matrix;
279
+ this.data = data;
271
280
  }
272
281
 
273
282
  public serialize(): TexToken[] {
@@ -330,6 +339,8 @@ export function writeTexTokenBuffer(buffer: string, token: TexToken): string {
330
339
  no_need_space ||= /[\(\[{]\s*(-|\+)$/.test(buffer) || buffer === '-' || buffer === '+';
331
340
  // "&=" instead of "& ="
332
341
  no_need_space ||= buffer.endsWith('&') && str === '=';
342
+ // "2y" instead of "2 y"
343
+ no_need_space ||= /\d$/.test(buffer) && /^[a-zA-Z]$/.test(str);
333
344
  }
334
345
 
335
346
  if (!no_need_space) {
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { array_find } from "./generic";
3
- import { TypstCases, TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstLeftRightData, TypstMatrix, TypstNode, TypstSupsub } from "./typst-types";
3
+ import { TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstLeftRightData, TypstMarkupFunc, TypstMatrixLike, TypstNode, TypstSupsub, TypstTerminal } from "./typst-types";
4
4
  import { TypstNamedParams } from "./typst-types";
5
5
  import { TypstSupsubData } from "./typst-types";
6
6
  import { TypstToken } from "./typst-types";
@@ -19,7 +19,6 @@ function eat_primes(tokens: TypstToken[], start: number): number {
19
19
  return pos - start;
20
20
  }
21
21
 
22
-
23
22
  function _find_closing_match(tokens: TypstToken[], start: number,
24
23
  leftBrackets: TypstToken[], rightBrackets: TypstToken[]): number {
25
24
  assert(tokens[start].isOneOf(leftBrackets));
@@ -168,10 +167,10 @@ function process_operators(nodes: TypstNode[], parenthesis = false): TypstNode {
168
167
  let numerator = args.pop()!;
169
168
 
170
169
  if(denominator.type === 'leftright') {
171
- denominator = new TypstGroup(denominator.args!);
170
+ denominator = (denominator as TypstLeftright).body;
172
171
  }
173
172
  if(numerator.type === 'leftright') {
174
- numerator = new TypstGroup(numerator.args!);
173
+ numerator = (numerator as TypstLeftright).body;
175
174
  }
176
175
 
177
176
  args.push(new TypstFraction([numerator, denominator]));
@@ -181,15 +180,24 @@ function process_operators(nodes: TypstNode[], parenthesis = false): TypstNode {
181
180
  }
182
181
  }
183
182
  }
183
+ const body = args.length === 1? args[0]: new TypstGroup(args);
184
184
  if(parenthesis) {
185
- return new TypstLeftright(null, args, { left: LEFT_PARENTHESES, right: RIGHT_PARENTHESES } as TypstLeftRightData);
185
+ return new TypstLeftright(null, { body: body, left: LEFT_PARENTHESES, right: RIGHT_PARENTHESES } as TypstLeftRightData);
186
186
  } else {
187
- if(args.length === 1) {
188
- return args[0];
189
- } else {
190
- return new TypstGroup(args);
191
- }
187
+ return body;
188
+ }
189
+ }
190
+
191
+ function parse_named_params(groups: TypstGroup[]): TypstNamedParams {
192
+ const COLON = new TypstToken(TypstTokenType.ELEMENT, ':').toNode();
193
+
194
+ const np: TypstNamedParams = {};
195
+ for (const group of groups) {
196
+ assert(group.items.length == 3);
197
+ assert(group.items[1].eq(COLON));
198
+ np[group.items[0].toString()] = new TypstTerminal(new TypstToken(TypstTokenType.LITERAL, group.items[2].toString()));
192
199
  }
200
+ return np;
193
201
  }
194
202
 
195
203
  export class TypstParserError extends Error {
@@ -322,19 +330,31 @@ export class TypstParser {
322
330
  if (start + 1 < tokens.length && tokens[start + 1].eq(LEFT_PARENTHESES)) {
323
331
  if(firstToken.value === 'mat') {
324
332
  const [matrix, named_params, newPos] = this.parseMatrix(tokens, start + 1, SEMICOLON, COMMA);
325
- const mat = new TypstMatrix(matrix);
333
+ const mat = new TypstMatrixLike(firstToken, matrix);
326
334
  mat.setOptions(named_params);
327
335
  return [mat, newPos];
328
336
  }
329
337
  if(firstToken.value === 'cases') {
330
338
  const [cases, named_params, newPos] = this.parseMatrix(tokens, start + 1, COMMA, CONTROL_AND);
331
- const casesNode = new TypstCases(cases);
339
+ const casesNode = new TypstMatrixLike(firstToken, cases);
332
340
  casesNode.setOptions(named_params);
333
341
  return [casesNode, newPos];
334
342
  }
335
343
  if (firstToken.value === 'lr') {
336
344
  return this.parseLrArguments(tokens, start + 1);
337
345
  }
346
+ if (['#heading', '#text'].includes(firstToken.value)) {
347
+ const [args, newPos] = this.parseArguments(tokens, start + 1);
348
+ const named_params = parse_named_params(args as TypstGroup[]);
349
+ assert(tokens[newPos].eq(LEFT_BRACKET));
350
+ const DOLLAR = new TypstToken(TypstTokenType.ELEMENT, '$');
351
+ const end = _find_closing_match(tokens, newPos + 1, [DOLLAR], [DOLLAR]);
352
+ const [group, _] = this.parseGroup(tokens, newPos + 2, end);
353
+ assert(tokens[end + 1].eq(RIGHT_BRACKET));
354
+ const markup_func = new TypstMarkupFunc(firstToken, [group]);
355
+ markup_func.setOptions(named_params);
356
+ return [markup_func, end + 2];
357
+ }
338
358
  const [args, newPos] = this.parseArguments(tokens, start + 1);
339
359
  const func_call = new TypstFuncCall(firstToken, args);
340
360
  return [func_call, newPos];
@@ -359,13 +379,13 @@ export class TypstParser {
359
379
  const inner_end = find_closing_delim(tokens, inner_start);
360
380
  const inner_args= this.parseArgumentsWithSeparator(tokens, inner_start + 1, inner_end, COMMA);
361
381
  return [
362
- new TypstLeftright(lr_token, inner_args, {left: tokens[inner_start], right: tokens[inner_end]}),
382
+ new TypstLeftright(lr_token, { body: new TypstGroup(inner_args), left: tokens[inner_start], right: tokens[inner_end]}),
363
383
  end + 1,
364
384
  ];
365
385
  } else {
366
386
  const [args, end] = this.parseArguments(tokens, start);
367
387
  return [
368
- new TypstLeftright(lr_token, args, { left: null, right: null }),
388
+ new TypstLeftright(lr_token, { body: new TypstGroup(args), left: null, right: null }),
369
389
  end,
370
390
  ];
371
391
  }
@@ -400,18 +420,18 @@ export class TypstParser {
400
420
  continue;
401
421
  }
402
422
 
403
- const g = arr[i];
404
- const pos_colon = array_find(g.args!, COLON);
423
+ const g = arr[i] as TypstGroup;
424
+ const pos_colon = array_find(g.items, COLON);
405
425
  if(pos_colon === -1 || pos_colon === 0) {
406
426
  continue;
407
427
  }
408
428
  to_delete.push(i);
409
- const param_name = g.args![pos_colon - 1];
429
+ const param_name = g.items[pos_colon - 1];
410
430
  if(param_name.eq(new TypstToken(TypstTokenType.SYMBOL, 'delim').toNode())) {
411
- if(g.args!.length !== 3) {
431
+ if(g.items.length !== 3) {
412
432
  throw new TypstParserError('Invalid number of arguments for delim');
413
433
  }
414
- np['delim'] = g.args![pos_colon + 1];
434
+ np['delim'] = g.items[pos_colon + 1];
415
435
  } else {
416
436
  throw new TypstParserError('Not implemented for other named parameters');
417
437
  }
@@ -69,10 +69,10 @@ const rules_map = new Map<string, (a: Scanner<TypstToken>) => TypstToken | Typst
69
69
  new TypstToken(TypstTokenType.ELEMENT, ")"),
70
70
  ];
71
71
  }],
72
- [String.raw`[a-zA-Z\.]+`, (s) => {
72
+ [String.raw`#none`, (s) => new TypstToken(TypstTokenType.NONE, s.text()!)],
73
+ [String.raw`#?[a-zA-Z\.]+`, (s) => {
73
74
  return new TypstToken(s.text()!.length === 1? TypstTokenType.ELEMENT: TypstTokenType.SYMBOL, s.text()!);
74
75
  }],
75
- [String.raw`#none`, (s) => new TypstToken(TypstTokenType.NONE, s.text()!)],
76
76
  [String.raw`.`, (s) => new TypstToken(TypstTokenType.ELEMENT, s.text()!)],
77
77
  ]);
78
78
 
@@ -14,7 +14,7 @@ export enum TypstTokenType {
14
14
 
15
15
 
16
16
  export class TypstToken {
17
- type: TypstTokenType;
17
+ readonly type: TypstTokenType;
18
18
  value: string;
19
19
 
20
20
  constructor(type: TypstTokenType, content: string) {
@@ -56,6 +56,7 @@ export interface TypstSupsubData {
56
56
 
57
57
 
58
58
  export interface TypstLeftRightData {
59
+ body: TypstNode;
59
60
  left: TypstToken | null;
60
61
  right: TypstToken | null;
61
62
  }
@@ -64,22 +65,21 @@ export interface TypstLeftRightData {
64
65
  * fraction: `1/2`, `(x + y)/2`, `(1+x)/(1-x)`
65
66
  * group: `a + 1/3`
66
67
  * leftright: `(a + 1/3)`, `[a + 1/3)`, `lr(]sum_(x=1)^n])`
68
+ * markupFunc: `#heading(level: 2)[something]`, `#text(fill: red)[some text and math $x + y$]`
67
69
  */
68
- export type TypstNodeType = 'terminal' | 'group' | 'supsub' | 'funcCall' | 'fraction'| 'leftright' | 'align' | 'matrix' | 'cases';
70
+ export type TypstNodeType = 'terminal' | 'group' | 'supsub' | 'funcCall' | 'fraction'| 'leftright' | 'matrixLike'| 'markupFunc';
69
71
 
70
72
  export type TypstNamedParams = { [key: string]: TypstNode; };
71
73
 
72
74
  export abstract class TypstNode {
73
75
  readonly type: TypstNodeType;
74
76
  head: TypstToken;
75
- args?: TypstNode[];
76
77
  // Some Typst functions accept additional options. e.g. mat() has option "delim", op() has option "limits"
77
78
  options?: TypstNamedParams;
78
79
 
79
- constructor(type: TypstNodeType, head: TypstToken | null, args?: TypstNode[]) {
80
+ constructor(type: TypstNodeType, head: TypstToken | null) {
80
81
  this.type = type;
81
82
  this.head = head ? head : TypstToken.NONE;
82
- this.args = args;
83
83
  }
84
84
 
85
85
  // whether the node is over high so that if it's wrapped in braces, \left and \right should be used in its TeX form
@@ -115,12 +115,14 @@ export class TypstTerminal extends TypstNode {
115
115
  }
116
116
 
117
117
  export class TypstGroup extends TypstNode {
118
- constructor(args: TypstNode[]) {
119
- super('group', TypstToken.NONE, args);
118
+ public items: TypstNode[];
119
+ constructor(items: TypstNode[]) {
120
+ super('group', TypstToken.NONE);
121
+ this.items = items;
120
122
  }
121
123
 
122
124
  public isOverHigh(): boolean {
123
- return this.args!.some((n) => n.isOverHigh());
125
+ return this.items.some((n) => n.isOverHigh());
124
126
  }
125
127
  }
126
128
 
@@ -130,7 +132,7 @@ export class TypstSupsub extends TypstNode {
130
132
  public sub: TypstNode | null;
131
133
 
132
134
  constructor(data: TypstSupsubData) {
133
- super('supsub', TypstToken.NONE, []);
135
+ super('supsub', TypstToken.NONE);
134
136
  this.base = data.base;
135
137
  this.sup = data.sup;
136
138
  this.sub = data.sub;
@@ -142,21 +144,26 @@ export class TypstSupsub extends TypstNode {
142
144
  }
143
145
 
144
146
  export class TypstFuncCall extends TypstNode {
147
+ public args: TypstNode[];
145
148
  constructor(head: TypstToken, args: TypstNode[]) {
146
- super('funcCall', head, args);
149
+ super('funcCall', head);
150
+ this.args = args;
147
151
  }
148
152
 
149
153
  public isOverHigh(): boolean {
150
154
  if (this.head.value === 'frac') {
151
155
  return true;
152
156
  }
153
- return this.args!.some((n) => n.isOverHigh());
157
+ return this.args.some((n) => n.isOverHigh());
154
158
  }
155
159
  }
156
160
 
157
161
  export class TypstFraction extends TypstNode {
162
+ public args: TypstNode[];
163
+
158
164
  constructor(args: TypstNode[]) {
159
- super('fraction', TypstToken.NONE, args);
165
+ super('fraction', TypstToken.NONE);
166
+ this.args = args;
160
167
  }
161
168
 
162
169
  public isOverHigh(): boolean {
@@ -166,56 +173,57 @@ export class TypstFraction extends TypstNode {
166
173
 
167
174
 
168
175
  export class TypstLeftright extends TypstNode {
176
+ public body: TypstNode;
169
177
  public left: TypstToken | null;
170
178
  public right: TypstToken | null;
171
179
 
172
- constructor(head: TypstToken | null, args: TypstNode[], data: TypstLeftRightData) {
173
- super('leftright', head, args);
180
+ // head is either null or 'lr'
181
+ constructor(head: TypstToken | null, data: TypstLeftRightData) {
182
+ super('leftright', head);
183
+ this.body = data.body;
174
184
  this.left = data.left;
175
185
  this.right = data.right;
176
186
  }
177
187
 
178
188
  public isOverHigh(): boolean {
179
- return this.args!.some((n) => n.isOverHigh());
189
+ return this.body.isOverHigh();
180
190
  }
181
191
  }
182
192
 
183
- export class TypstAlign extends TypstNode {
184
- public matrix: TypstNode[][];
185
193
 
186
- constructor(data: TypstNode[][]) {
187
- super('align', TypstToken.NONE, []);
188
- this.matrix = data;
189
- }
190
-
191
- public isOverHigh(): boolean {
192
- return true;
193
- }
194
- }
195
-
196
- export class TypstMatrix extends TypstNode {
194
+ export class TypstMatrixLike extends TypstNode {
197
195
  public matrix: TypstNode[][];
198
196
 
199
- constructor(data: TypstNode[][]) {
200
- super('matrix', TypstToken.NONE, []);
197
+ // head is 'mat', 'cases' or null
198
+ constructor(head: TypstToken | null, data: TypstNode[][]) {
199
+ super('matrixLike', head);
201
200
  this.matrix = data;
202
201
  }
203
202
 
204
203
  public isOverHigh(): boolean {
205
204
  return true;
206
205
  }
206
+
207
+ static readonly MAT = new TypstToken(TypstTokenType.SYMBOL, 'mat');
208
+ static readonly CASES = new TypstToken(TypstTokenType.SYMBOL, 'cases');
207
209
  }
208
210
 
209
- export class TypstCases extends TypstNode {
210
- public matrix: TypstNode[][];
211
+ export class TypstMarkupFunc extends TypstNode {
212
+ /*
213
+ In idealized situations, for `#heading([some text and math $x + y$ example])`,
214
+ fragments would be [TypstMarkup{"some text and math "}, TypstNode{"x + y"}, TypstMarkup{" example"}]
215
+ At present, we haven't implemented anything about TypstMarkup.
216
+ So only pattens like `#heading(level: 2)[$x+y$]`, `#text(fill: red)[$x + y$]` are supported.
217
+ Therefore, fragments is always a list containing exactly 1 TypstNode in well-working situations.
218
+ */
219
+ public fragments: TypstNode[];
211
220
 
212
- constructor(data: TypstNode[][]) {
213
- super('cases', TypstToken.NONE, []);
214
- this.matrix = data;
221
+ constructor(head: TypstToken, fragments: TypstNode[]) {
222
+ super('markupFunc', head);
223
+ this.fragments = fragments;
215
224
  }
216
225
 
217
226
  public isOverHigh(): boolean {
218
- return true;
227
+ return this.fragments.some((n) => n.isOverHigh());
219
228
  }
220
229
  }
221
-