tex2typst 0.3.24 → 0.3.26

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.
@@ -46,6 +46,7 @@ export const TEX_BINARY_COMMANDS = [
46
46
  'tbinom',
47
47
  'overset',
48
48
  'underset',
49
+ 'textcolor',
49
50
  ]
50
51
 
51
52
 
@@ -58,7 +59,7 @@ function unescape(str: string): string {
58
59
  }
59
60
 
60
61
  const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[]>([
61
- // math `\begin{array}{cc}`
62
+ // match `\begin{array}{cc}`
62
63
  [
63
64
  String.raw`\\begin{(array|subarry)}{(.+?)}`, (s) => {
64
65
  const match = s.reMatchArray()!;
@@ -74,7 +75,7 @@ const rules_map = new Map<string, (a: Scanner<TexToken>) => TexToken | TexToken[
74
75
  }
75
76
  ],
76
77
  [
77
- String.raw`\\(text|operatorname|begin|end|hspace|array){(.+?)}`, (s) => {
78
+ String.raw`\\(text|operatorname|textcolor|begin|end|hspace|array){(.+?)}`, (s) => {
78
79
  const match = s.reMatchArray()!;
79
80
  return [
80
81
  new TexToken(TexTokenType.COMMAND, '\\' + match[1]),
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) {
@@ -46,6 +46,7 @@ export class TypstToken {
46
46
  }
47
47
 
48
48
  public static readonly NONE = new TypstToken(TypstTokenType.NONE, '#none');
49
+ public static readonly EMPTY = new TypstToken(TypstTokenType.ELEMENT, '');
49
50
  }
50
51
 
51
52
  export interface TypstSupsubData {
@@ -56,6 +57,7 @@ export interface TypstSupsubData {
56
57
 
57
58
 
58
59
  export interface TypstLeftRightData {
60
+ body: TypstNode;
59
61
  left: TypstToken | null;
60
62
  right: TypstToken | null;
61
63
  }
@@ -64,22 +66,21 @@ export interface TypstLeftRightData {
64
66
  * fraction: `1/2`, `(x + y)/2`, `(1+x)/(1-x)`
65
67
  * group: `a + 1/3`
66
68
  * leftright: `(a + 1/3)`, `[a + 1/3)`, `lr(]sum_(x=1)^n])`
69
+ * markupFunc: `#heading(level: 2)[something]`, `#text(fill: red)[some text and math $x + y$]`
67
70
  */
68
- export type TypstNodeType = 'terminal' | 'group' | 'supsub' | 'funcCall' | 'fraction'| 'leftright' | 'align' | 'matrix' | 'cases';
71
+ export type TypstNodeType = 'terminal' | 'group' | 'supsub' | 'funcCall' | 'fraction'| 'leftright' | 'matrixLike'| 'markupFunc';
69
72
 
70
73
  export type TypstNamedParams = { [key: string]: TypstNode; };
71
74
 
72
75
  export abstract class TypstNode {
73
76
  readonly type: TypstNodeType;
74
77
  head: TypstToken;
75
- args?: TypstNode[];
76
78
  // Some Typst functions accept additional options. e.g. mat() has option "delim", op() has option "limits"
77
79
  options?: TypstNamedParams;
78
80
 
79
- constructor(type: TypstNodeType, head: TypstToken | null, args?: TypstNode[]) {
81
+ constructor(type: TypstNodeType, head: TypstToken | null) {
80
82
  this.type = type;
81
83
  this.head = head ? head : TypstToken.NONE;
82
- this.args = args;
83
84
  }
84
85
 
85
86
  // 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 +116,14 @@ export class TypstTerminal extends TypstNode {
115
116
  }
116
117
 
117
118
  export class TypstGroup extends TypstNode {
118
- constructor(args: TypstNode[]) {
119
- super('group', TypstToken.NONE, args);
119
+ public items: TypstNode[];
120
+ constructor(items: TypstNode[]) {
121
+ super('group', TypstToken.NONE);
122
+ this.items = items;
120
123
  }
121
124
 
122
125
  public isOverHigh(): boolean {
123
- return this.args!.some((n) => n.isOverHigh());
126
+ return this.items.some((n) => n.isOverHigh());
124
127
  }
125
128
  }
126
129
 
@@ -130,7 +133,7 @@ export class TypstSupsub extends TypstNode {
130
133
  public sub: TypstNode | null;
131
134
 
132
135
  constructor(data: TypstSupsubData) {
133
- super('supsub', TypstToken.NONE, []);
136
+ super('supsub', TypstToken.NONE);
134
137
  this.base = data.base;
135
138
  this.sup = data.sup;
136
139
  this.sub = data.sub;
@@ -142,21 +145,26 @@ export class TypstSupsub extends TypstNode {
142
145
  }
143
146
 
144
147
  export class TypstFuncCall extends TypstNode {
148
+ public args: TypstNode[];
145
149
  constructor(head: TypstToken, args: TypstNode[]) {
146
- super('funcCall', head, args);
150
+ super('funcCall', head);
151
+ this.args = args;
147
152
  }
148
153
 
149
154
  public isOverHigh(): boolean {
150
155
  if (this.head.value === 'frac') {
151
156
  return true;
152
157
  }
153
- return this.args!.some((n) => n.isOverHigh());
158
+ return this.args.some((n) => n.isOverHigh());
154
159
  }
155
160
  }
156
161
 
157
162
  export class TypstFraction extends TypstNode {
163
+ public args: TypstNode[];
164
+
158
165
  constructor(args: TypstNode[]) {
159
- super('fraction', TypstToken.NONE, args);
166
+ super('fraction', TypstToken.NONE);
167
+ this.args = args;
160
168
  }
161
169
 
162
170
  public isOverHigh(): boolean {
@@ -166,56 +174,57 @@ export class TypstFraction extends TypstNode {
166
174
 
167
175
 
168
176
  export class TypstLeftright extends TypstNode {
177
+ public body: TypstNode;
169
178
  public left: TypstToken | null;
170
179
  public right: TypstToken | null;
171
180
 
172
- constructor(head: TypstToken | null, args: TypstNode[], data: TypstLeftRightData) {
173
- super('leftright', head, args);
181
+ // head is either null or 'lr'
182
+ constructor(head: TypstToken | null, data: TypstLeftRightData) {
183
+ super('leftright', head);
184
+ this.body = data.body;
174
185
  this.left = data.left;
175
186
  this.right = data.right;
176
187
  }
177
188
 
178
189
  public isOverHigh(): boolean {
179
- return this.args!.some((n) => n.isOverHigh());
190
+ return this.body.isOverHigh();
180
191
  }
181
192
  }
182
193
 
183
- export class TypstAlign extends TypstNode {
184
- public matrix: TypstNode[][];
185
194
 
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 {
195
+ export class TypstMatrixLike extends TypstNode {
197
196
  public matrix: TypstNode[][];
198
197
 
199
- constructor(data: TypstNode[][]) {
200
- super('matrix', TypstToken.NONE, []);
198
+ // head is 'mat', 'cases' or null
199
+ constructor(head: TypstToken | null, data: TypstNode[][]) {
200
+ super('matrixLike', head);
201
201
  this.matrix = data;
202
202
  }
203
203
 
204
204
  public isOverHigh(): boolean {
205
205
  return true;
206
206
  }
207
+
208
+ static readonly MAT = new TypstToken(TypstTokenType.SYMBOL, 'mat');
209
+ static readonly CASES = new TypstToken(TypstTokenType.SYMBOL, 'cases');
207
210
  }
208
211
 
209
- export class TypstCases extends TypstNode {
210
- public matrix: TypstNode[][];
212
+ export class TypstMarkupFunc extends TypstNode {
213
+ /*
214
+ In idealized situations, for `#heading([some text and math $x + y$ example])`,
215
+ fragments would be [TypstMarkup{"some text and math "}, TypstNode{"x + y"}, TypstMarkup{" example"}]
216
+ At present, we haven't implemented anything about TypstMarkup.
217
+ So only pattens like `#heading(level: 2)[$x+y$]`, `#text(fill: red)[$x + y$]` are supported.
218
+ Therefore, fragments is always a list containing exactly 1 TypstNode in well-working situations.
219
+ */
220
+ public fragments: TypstNode[];
211
221
 
212
- constructor(data: TypstNode[][]) {
213
- super('cases', TypstToken.NONE, []);
214
- this.matrix = data;
222
+ constructor(head: TypstToken, fragments: TypstNode[]) {
223
+ super('markupFunc', head);
224
+ this.fragments = fragments;
215
225
  }
216
226
 
217
227
  public isOverHigh(): boolean {
218
- return true;
228
+ return this.fragments.some((n) => n.isOverHigh());
219
229
  }
220
230
  }
221
-