tex2typst 0.3.23 → 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.
@@ -0,0 +1,229 @@
1
+ import { array_includes } from "./generic";
2
+
3
+ export enum TypstTokenType {
4
+ NONE,
5
+ SYMBOL,
6
+ ELEMENT,
7
+ LITERAL,
8
+ TEXT,
9
+ COMMENT,
10
+ SPACE,
11
+ CONTROL,
12
+ NEWLINE
13
+ }
14
+
15
+
16
+ export class TypstToken {
17
+ readonly type: TypstTokenType;
18
+ value: string;
19
+
20
+ constructor(type: TypstTokenType, content: string) {
21
+ this.type = type;
22
+ this.value = content;
23
+ }
24
+
25
+ eq(other: TypstToken): boolean {
26
+ return this.type === other.type && this.value === other.value;
27
+ }
28
+
29
+ isOneOf(tokens: TypstToken[]): boolean {
30
+ return array_includes(tokens, this);
31
+ }
32
+
33
+ public toNode(): TypstNode {
34
+ return new TypstTerminal(this);
35
+ }
36
+
37
+ public toString(): string {
38
+ switch (this.type) {
39
+ case TypstTokenType.TEXT:
40
+ return `"${this.value}"`;
41
+ case TypstTokenType.COMMENT:
42
+ return `//${this.value}`;
43
+ default:
44
+ return this.value;
45
+ }
46
+ }
47
+
48
+ public static readonly NONE = new TypstToken(TypstTokenType.NONE, '#none');
49
+ }
50
+
51
+ export interface TypstSupsubData {
52
+ base: TypstNode;
53
+ sup: TypstNode | null;
54
+ sub: TypstNode | null;
55
+ }
56
+
57
+
58
+ export interface TypstLeftRightData {
59
+ body: TypstNode;
60
+ left: TypstToken | null;
61
+ right: TypstToken | null;
62
+ }
63
+
64
+ /**
65
+ * fraction: `1/2`, `(x + y)/2`, `(1+x)/(1-x)`
66
+ * group: `a + 1/3`
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$]`
69
+ */
70
+ export type TypstNodeType = 'terminal' | 'group' | 'supsub' | 'funcCall' | 'fraction'| 'leftright' | 'matrixLike'| 'markupFunc';
71
+
72
+ export type TypstNamedParams = { [key: string]: TypstNode; };
73
+
74
+ export abstract class TypstNode {
75
+ readonly type: TypstNodeType;
76
+ head: TypstToken;
77
+ // Some Typst functions accept additional options. e.g. mat() has option "delim", op() has option "limits"
78
+ options?: TypstNamedParams;
79
+
80
+ constructor(type: TypstNodeType, head: TypstToken | null) {
81
+ this.type = type;
82
+ this.head = head ? head : TypstToken.NONE;
83
+ }
84
+
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
86
+ // e.g. 1/2 is over high, "2" is not.
87
+ abstract isOverHigh(): boolean;
88
+
89
+ public setOptions(options: TypstNamedParams) {
90
+ this.options = options;
91
+ }
92
+
93
+ // Note that this is only shallow equality.
94
+ public eq(other: TypstNode): boolean {
95
+ return this.type === other.type && this.head.eq(other.head);
96
+ }
97
+
98
+ public toString(): string {
99
+ throw new Error(`Unimplemented toString() in base class TypstNode`);
100
+ }
101
+ }
102
+
103
+ export class TypstTerminal extends TypstNode {
104
+ constructor(head: TypstToken) {
105
+ super('terminal', head);
106
+ }
107
+
108
+ public isOverHigh(): boolean {
109
+ return false;
110
+ }
111
+
112
+ public toString(): string {
113
+ return this.head.toString();
114
+ }
115
+ }
116
+
117
+ export class TypstGroup extends TypstNode {
118
+ public items: TypstNode[];
119
+ constructor(items: TypstNode[]) {
120
+ super('group', TypstToken.NONE);
121
+ this.items = items;
122
+ }
123
+
124
+ public isOverHigh(): boolean {
125
+ return this.items.some((n) => n.isOverHigh());
126
+ }
127
+ }
128
+
129
+ export class TypstSupsub extends TypstNode {
130
+ public base: TypstNode;
131
+ public sup: TypstNode | null;
132
+ public sub: TypstNode | null;
133
+
134
+ constructor(data: TypstSupsubData) {
135
+ super('supsub', TypstToken.NONE);
136
+ this.base = data.base;
137
+ this.sup = data.sup;
138
+ this.sub = data.sub;
139
+ }
140
+
141
+ public isOverHigh(): boolean {
142
+ return this.base.isOverHigh();
143
+ }
144
+ }
145
+
146
+ export class TypstFuncCall extends TypstNode {
147
+ public args: TypstNode[];
148
+ constructor(head: TypstToken, args: TypstNode[]) {
149
+ super('funcCall', head);
150
+ this.args = args;
151
+ }
152
+
153
+ public isOverHigh(): boolean {
154
+ if (this.head.value === 'frac') {
155
+ return true;
156
+ }
157
+ return this.args.some((n) => n.isOverHigh());
158
+ }
159
+ }
160
+
161
+ export class TypstFraction extends TypstNode {
162
+ public args: TypstNode[];
163
+
164
+ constructor(args: TypstNode[]) {
165
+ super('fraction', TypstToken.NONE);
166
+ this.args = args;
167
+ }
168
+
169
+ public isOverHigh(): boolean {
170
+ return true;
171
+ }
172
+ }
173
+
174
+
175
+ export class TypstLeftright extends TypstNode {
176
+ public body: TypstNode;
177
+ public left: TypstToken | null;
178
+ public right: TypstToken | null;
179
+
180
+ // head is either null or 'lr'
181
+ constructor(head: TypstToken | null, data: TypstLeftRightData) {
182
+ super('leftright', head);
183
+ this.body = data.body;
184
+ this.left = data.left;
185
+ this.right = data.right;
186
+ }
187
+
188
+ public isOverHigh(): boolean {
189
+ return this.body.isOverHigh();
190
+ }
191
+ }
192
+
193
+
194
+ export class TypstMatrixLike extends TypstNode {
195
+ public matrix: TypstNode[][];
196
+
197
+ // head is 'mat', 'cases' or null
198
+ constructor(head: TypstToken | null, data: TypstNode[][]) {
199
+ super('matrixLike', head);
200
+ this.matrix = data;
201
+ }
202
+
203
+ public isOverHigh(): boolean {
204
+ return true;
205
+ }
206
+
207
+ static readonly MAT = new TypstToken(TypstTokenType.SYMBOL, 'mat');
208
+ static readonly CASES = new TypstToken(TypstTokenType.SYMBOL, 'cases');
209
+ }
210
+
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[];
220
+
221
+ constructor(head: TypstToken, fragments: TypstNode[]) {
222
+ super('markupFunc', head);
223
+ this.fragments = fragments;
224
+ }
225
+
226
+ public isOverHigh(): boolean {
227
+ return this.fragments.some((n) => n.isOverHigh());
228
+ }
229
+ }
@@ -1,8 +1,11 @@
1
- import { TexNode, TypstNode, TypstSupsubData, TypstToken, TypstTokenType } from "./types";
1
+ import { TexNode } from "./tex-types";
2
+ import { TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstMarkupFunc, TypstMatrixLike, TypstNode, TypstSupsub, TypstTerminal } from "./typst-types";
3
+ import { TypstToken } from "./typst-types";
4
+ import { TypstTokenType } from "./typst-types";
2
5
  import { shorthandMap } from "./typst-shorthands";
3
6
 
4
7
  function is_delimiter(c: TypstNode): boolean {
5
- return c.type === 'atom' && ['(', ')', '[', ']', '{', '}', '|', '⌊', '⌋', '⌈', '⌉'].includes(c.content);
8
+ return c.head.type === TypstTokenType.ELEMENT && ['(', ')', '[', ']', '{', '}', '|', '⌊', '⌋', '⌈', '⌉'].includes(c.head.value);
6
9
  }
7
10
 
8
11
  const TYPST_LEFT_PARENTHESIS: TypstToken = new TypstToken(TypstTokenType.ELEMENT, '(');
@@ -51,7 +54,7 @@ export class TypstWriter {
51
54
  }
52
55
 
53
56
 
54
- private writeBuffer(token: TypstToken) {
57
+ private writeBuffer(previousToken: TypstToken | null, token: TypstToken) {
55
58
  const str = token.toString();
56
59
 
57
60
  if (str === '') {
@@ -81,8 +84,13 @@ export class TypstWriter {
81
84
  no_need_space ||= this.buffer.endsWith('&') && str === '=';
82
85
  // before or after a slash e.g. "a/b" instead of "a / b"
83
86
  no_need_space ||= this.buffer.endsWith('/') || str === '/';
87
+ // "[$x + y$]" instead of "[ $ x + y $ ]"
88
+ no_need_space ||= token.type === TypstTokenType.LITERAL;
84
89
  // other cases
85
90
  no_need_space ||= /[\s_^{\(]$/.test(this.buffer);
91
+ if (previousToken !== null) {
92
+ no_need_space ||= previousToken.type === TypstTokenType.LITERAL;
93
+ }
86
94
  if (!no_need_space) {
87
95
  this.buffer += ' ';
88
96
  }
@@ -91,65 +99,81 @@ export class TypstWriter {
91
99
  }
92
100
 
93
101
  // Serialize a tree of TypstNode into a list of TypstToken
94
- public serialize(node: TypstNode) {
95
- switch (node.type) {
96
- case 'none':
97
- this.queue.push(new TypstToken(TypstTokenType.NONE, '#none'));
98
- break;
99
- case 'atom': {
100
- if (node.content === ',' && this.insideFunctionDepth > 0) {
101
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, 'comma'));
102
+ public serialize(abstractNode: TypstNode) {
103
+ switch (abstractNode.type) {
104
+ case 'terminal': {
105
+ const node = abstractNode as TypstTerminal;
106
+ if (node.head.type === TypstTokenType.ELEMENT) {
107
+ if (node.head.value === ',' && this.insideFunctionDepth > 0) {
108
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, 'comma'));
109
+ } else {
110
+ this.queue.push(node.head);
111
+ }
112
+ break;
113
+ } else if (node.head.type === TypstTokenType.SYMBOL) {
114
+ let symbol_name = node.head.value;
115
+ if(this.preferShorthands) {
116
+ if (shorthandMap.has(symbol_name)) {
117
+ symbol_name = shorthandMap.get(symbol_name)!;
118
+ }
119
+ }
120
+ if (this.inftyToOo && symbol_name === 'infinity') {
121
+ symbol_name = 'oo';
122
+ }
123
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, symbol_name));
124
+ break;
125
+ } else if (node.head.type === TypstTokenType.SPACE || node.head.type === TypstTokenType.NEWLINE) {
126
+ for (const c of node.head.value) {
127
+ if (c === ' ') {
128
+ if (this.keepSpaces) {
129
+ this.queue.push(new TypstToken(TypstTokenType.SPACE, c));
130
+ }
131
+ } else if (c === '\n') {
132
+ this.queue.push(new TypstToken(TypstTokenType.SYMBOL, c));
133
+ } else {
134
+ throw new TypstWriterError(`Unexpected whitespace character: ${c}`, node);
135
+ }
136
+ }
137
+ break;
102
138
  } else {
103
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, node.content));
139
+ this.queue.push(node.head);
140
+ break;
104
141
  }
105
- break;
106
142
  }
107
- case 'symbol': {
108
- let content = node.content;
109
- if(this.preferShorthands) {
110
- if (shorthandMap.has(content)) {
111
- content = shorthandMap.get(content)!;
112
- }
113
- }
114
- if (this.inftyToOo && content === 'infinity') {
115
- content = 'oo';
143
+ case 'group': {
144
+ const node = abstractNode as TypstGroup;
145
+ for (const item of node.items) {
146
+ this.serialize(item);
116
147
  }
117
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, content));
118
148
  break;
119
149
  }
120
- case 'literal':
121
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, node.content));
122
- break;
123
- case 'text':
124
- this.queue.push(new TypstToken(TypstTokenType.TEXT, node.content));
125
- break;
126
- case 'comment':
127
- this.queue.push(new TypstToken(TypstTokenType.COMMENT, node.content));
128
- break;
129
- case 'whitespace':
130
- for (const c of node.content) {
131
- if (c === ' ') {
132
- if (this.keepSpaces) {
133
- this.queue.push(new TypstToken(TypstTokenType.SPACE, c));
134
- }
135
- } else if (c === '\n') {
136
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, c));
137
- } else {
138
- throw new TypstWriterError(`Unexpected whitespace character: ${c}`, node);
139
- }
150
+ case 'leftright': {
151
+ const node = abstractNode as TypstLeftright;
152
+ const LR = new TypstToken(TypstTokenType.SYMBOL, 'lr');
153
+ const {left, right} = node;
154
+ if (node.head.eq(LR)) {
155
+ this.queue.push(LR);
156
+ this.queue.push(TYPST_LEFT_PARENTHESIS);
140
157
  }
141
- break;
142
- case 'group':
143
- for (const item of node.args!) {
144
- this.serialize(item);
158
+ if (left) {
159
+ this.queue.push(left);
160
+ }
161
+ this.serialize(node.body);
162
+ if (right) {
163
+ this.queue.push(right);
164
+ }
165
+ if (node.head.eq(LR)) {
166
+ this.queue.push(TYPST_RIGHT_PARENTHESIS);
145
167
  }
146
168
  break;
169
+ }
147
170
  case 'supsub': {
148
- let { base, sup, sub } = node.data as TypstSupsubData;
171
+ const node = abstractNode as TypstSupsub;
172
+ let { base, sup, sub } = node;
149
173
  this.appendWithBracketsIfNeeded(base);
150
174
 
151
175
  let trailing_space_needed = false;
152
- const has_prime = (sup && sup.type === 'atom' && sup.content === '\'');
176
+ const has_prime = (sup && sup.head.eq(new TypstToken(TypstTokenType.ELEMENT, "'")));
153
177
  if (has_prime) {
154
178
  // Put prime symbol before '_'. Because $y_1'$ is not displayed properly in Typst (so far)
155
179
  // e.g.
@@ -172,15 +196,14 @@ export class TypstWriter {
172
196
  break;
173
197
  }
174
198
  case 'funcCall': {
175
- const func_symbol: TypstToken = new TypstToken(TypstTokenType.SYMBOL, node.content);
199
+ const node = abstractNode as TypstFuncCall;
200
+ const func_symbol: TypstToken = node.head;
176
201
  this.queue.push(func_symbol);
177
- if (node.content !== 'lr') {
178
- this.insideFunctionDepth++;
179
- }
202
+ this.insideFunctionDepth++;
180
203
  this.queue.push(TYPST_LEFT_PARENTHESIS);
181
- for (let i = 0; i < node.args!.length; i++) {
182
- this.serialize(node.args![i]);
183
- if (i < node.args!.length - 1) {
204
+ for (let i = 0; i < node.args.length; i++) {
205
+ this.serialize(node.args[i]);
206
+ if (i < node.args.length - 1) {
184
207
  this.queue.push(new TypstToken(TypstTokenType.ELEMENT, ','));
185
208
  }
186
209
  }
@@ -190,13 +213,12 @@ export class TypstWriter {
190
213
  }
191
214
  }
192
215
  this.queue.push(TYPST_RIGHT_PARENTHESIS);
193
- if (node.content !== 'lr') {
194
- this.insideFunctionDepth--;
195
- }
216
+ this.insideFunctionDepth--;
196
217
  break;
197
218
  }
198
219
  case 'fraction': {
199
- const [numerator, denominator] = node.args!;
220
+ const node = abstractNode as TypstFraction;
221
+ const [numerator, denominator] = node.args;
200
222
  const pos = this.queue.length;
201
223
  const no_wrap = this.appendWithBracketsIfNeeded(numerator);
202
224
 
@@ -211,105 +233,95 @@ export class TypstWriter {
211
233
  this.appendWithBracketsIfNeeded(denominator);
212
234
  break;
213
235
  }
214
- case 'align': {
215
- const matrix = node.data as TypstNode[][];
216
- matrix.forEach((row, i) => {
217
- row.forEach((cell, j) => {
218
- if (j > 0) {
219
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '&'));
236
+ case 'matrixLike': {
237
+ const node = abstractNode as TypstMatrixLike;
238
+ const matrix = node.matrix;
239
+
240
+ let cell_sep: TypstToken;
241
+ let row_sep: TypstToken;
242
+ if (node.head.eq(TypstMatrixLike.MAT)) {
243
+ cell_sep = new TypstToken(TypstTokenType.ELEMENT, ',');
244
+ row_sep = new TypstToken(TypstTokenType.ELEMENT, ';');
245
+ } else if (node.head.eq(TypstMatrixLike.CASES)) {
246
+ cell_sep = new TypstToken(TypstTokenType.ELEMENT, '&');
247
+ row_sep = new TypstToken(TypstTokenType.ELEMENT, ',');
248
+ } else if (node.head.eq(TypstToken.NONE)){ // head is null
249
+ cell_sep = new TypstToken(TypstTokenType.ELEMENT, '&');
250
+ row_sep = new TypstToken(TypstTokenType.SYMBOL, '\\');
251
+ }
252
+
253
+ if (!node.head.eq(TypstToken.NONE)) {
254
+ this.queue.push(node.head);
255
+ this.insideFunctionDepth++;
256
+ this.queue.push(TYPST_LEFT_PARENTHESIS);
257
+ if (node.options) {
258
+ for (const [key, value] of Object.entries(node.options)) {
259
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, `${key}: ${value.toString()}, `));
220
260
  }
221
- this.serialize(cell);
222
- });
223
- if (i < matrix.length - 1) {
224
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, '\\'));
225
- }
226
- });
227
- break;
228
- }
229
- case 'matrix': {
230
- const matrix = node.data as TypstNode[][];
231
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, 'mat'));
232
- this.insideFunctionDepth++;
233
- this.queue.push(TYPST_LEFT_PARENTHESIS);
234
- if (node.options) {
235
- for (const [key, value] of Object.entries(node.options)) {
236
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, `${key}: ${value.toString()}, `));
237
261
  }
238
262
  }
263
+
239
264
  matrix.forEach((row, i) => {
240
265
  row.forEach((cell, j) => {
241
- // There is a leading & in row
242
- // if (cell.type === 'ordgroup' && cell.args!.length === 0) {
243
- // this.queue.push(new TypstNode('atom', ','));
244
- // return;
245
- // }
246
- // if (j == 0 && cell.type === 'newline' && cell.content === '\n') {
247
- // return;
248
- // }
249
266
  this.serialize(cell);
250
- // cell.args!.forEach((n) => this.append(n));
251
267
  if (j < row.length - 1) {
252
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, ','));
268
+ this.queue.push(cell_sep);
253
269
  } else {
254
270
  if (i < matrix.length - 1) {
255
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, ';'));
271
+ this.queue.push(row_sep);
256
272
  }
257
273
  }
258
274
  });
259
275
  });
260
- this.queue.push(TYPST_RIGHT_PARENTHESIS);
261
- this.insideFunctionDepth--;
276
+
277
+ if (!node.head.eq(TypstToken.NONE)) {
278
+ this.queue.push(TYPST_RIGHT_PARENTHESIS);
279
+ this.insideFunctionDepth--;
280
+ }
262
281
  break;
263
282
  }
264
- case 'cases': {
265
- const cases = node.data as TypstNode[][];
266
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, 'cases'));
267
- this.insideFunctionDepth++;
283
+ case 'markupFunc': {
284
+ const node = abstractNode as TypstMarkupFunc;
285
+ this.queue.push(node.head);
268
286
  this.queue.push(TYPST_LEFT_PARENTHESIS);
269
287
  if (node.options) {
270
- for (const [key, value] of Object.entries(node.options)) {
271
- this.queue.push(new TypstToken(TypstTokenType.LITERAL, `${key}: ${value.toString()}, `));
288
+ const entries = Object.entries(node.options);
289
+ for (let i = 0; i < entries.length; i++) {
290
+ const [key, value] = entries[i];
291
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, `${key}: ${value.toString()}`));
292
+ if (i < entries.length - 1) {
293
+ this.queue.push(new TypstToken(TypstTokenType.ELEMENT, ','));
294
+ }
272
295
  }
273
296
  }
274
- cases.forEach((row, i) => {
275
- row.forEach((cell, j) => {
276
- this.serialize(cell);
277
- if (j < row.length - 1) {
278
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, '&'));
279
- } else {
280
- if (i < cases.length - 1) {
281
- this.queue.push(new TypstToken(TypstTokenType.ELEMENT, ','));
282
- }
283
- }
284
- });
285
- });
286
297
  this.queue.push(TYPST_RIGHT_PARENTHESIS);
287
- this.insideFunctionDepth--;
288
- break;
289
- }
290
- case 'unknown': {
291
- if (this.nonStrict) {
292
- this.queue.push(new TypstToken(TypstTokenType.SYMBOL, node.content));
293
- } else {
294
- throw new TypstWriterError(`Unknown macro: ${node.content}`, node);
298
+
299
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, '['));
300
+ for (const frag of node.fragments) {
301
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, '$'));
302
+ this.serialize(frag);
303
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, '$'));
295
304
  }
305
+ this.queue.push(new TypstToken(TypstTokenType.LITERAL, ']'));
306
+
296
307
  break;
297
308
  }
298
309
  default:
299
- throw new TypstWriterError(`Unimplemented node type to append: ${node.type}`, node);
310
+ throw new TypstWriterError(`Unimplemented node type to append: ${abstractNode.type}`, abstractNode);
300
311
  }
301
312
  }
302
313
 
303
314
  private appendWithBracketsIfNeeded(node: TypstNode): boolean {
304
- let need_to_wrap = ['group', 'supsub', 'align', 'fraction','empty'].includes(node.type);
315
+ let need_to_wrap = ['group', 'supsub', 'matrixLike', 'fraction','empty'].includes(node.type);
305
316
 
306
317
  if (node.type === 'group') {
307
- if (node.args!.length === 0) {
318
+ const group = node as TypstGroup;
319
+ if (group.items.length === 0) {
308
320
  // e.g. TeX `P_{}` converts to Typst `P_()`
309
321
  need_to_wrap = true;
310
322
  } else {
311
- const first = node.args![0];
312
- const last = node.args![node.args!.length - 1];
323
+ const first = group.items[0];
324
+ const last = group.items[group.items.length - 1];
313
325
  if (is_delimiter(first) && is_delimiter(last)) {
314
326
  need_to_wrap = false;
315
327
  }
@@ -347,9 +359,11 @@ export class TypstWriter {
347
359
 
348
360
  this.queue = this.queue.filter((token) => !token.eq(dummy_token));
349
361
 
350
- this.queue.forEach((token) => {
351
- this.writeBuffer(token)
352
- });
362
+ for(let i = 0; i < this.queue.length; i++) {
363
+ let token = this.queue[i];
364
+ let previous_token = i === 0 ? null : this.queue[i - 1];
365
+ this.writeBuffer(previous_token, token);
366
+ }
353
367
 
354
368
  this.queue = [];
355
369
  }
package/src/util.ts CHANGED
@@ -7,7 +7,7 @@ export function isdigit(char: string): boolean {
7
7
  return '0123456789'.includes(char);
8
8
  }
9
9
 
10
- export function assert(condition: boolean, message: string = ''): void {
10
+ export function assert(condition: boolean, message: string = 'Assertion failed.'): void {
11
11
  if (!condition) {
12
12
  throw new Error(message);
13
13
  }