tex2typst 0.0.18 → 0.1.20

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/types.d.ts CHANGED
@@ -19,7 +19,7 @@ export interface TexNode {
19
19
  irregularData?: TexSqrtData | TexSupsubData | TexArrayData;
20
20
  }
21
21
  export interface TypstNode {
22
- type: 'atom' | 'symbol' | 'text' | 'softSpace';
22
+ type: 'atom' | 'symbol' | 'text' | 'softSpace' | 'comment' | 'newline';
23
23
  content: string;
24
24
  args?: TypstNode[];
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tex2typst",
3
- "version": "0.0.18",
3
+ "version": "0.1.20",
4
4
  "description": "JavaScript library for converting TeX code to Typst",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "prebuild": "rimraf dist/",
18
18
  "build:node": "bun build --entrypoints src/index.ts --outdir ./dist --target node --external katex",
19
- "build:browser": "bun build --entrypoints src/index.ts --outdir ./dist --target browser --entry-naming [dir]/tex2typst.min.[ext] --minify",
19
+ "build:browser": "bun build --entrypoints src/tex2typst.ts --outdir ./dist --target browser --entry-naming [dir]/[name].min.[ext] --minify",
20
20
  "build:types": "tsc --project ./tsconfig.json",
21
21
  "build": "npm run build:node && npm run build:browser && npm run build:types",
22
22
  "test": "vitest run"
package/src/index.ts CHANGED
@@ -26,9 +26,4 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
26
26
  return writer.finalize();
27
27
  }
28
28
 
29
-
30
- if(typeof window !== 'undefined') {
31
- (window as any).tex2typst = tex2typst;
32
- }
33
-
34
29
  export { Tex2TypstOptions };
package/src/map.ts CHANGED
@@ -2,9 +2,6 @@ export const symbolMap = new Map<string, string>([
2
2
  ['gets', 'arrow.l'],
3
3
  ['nonumber', ''],
4
4
  ['vec', 'arrow'],
5
- ['mathbf', 'bold'],
6
- ['boldsymbol', 'bold'],
7
- ['mathfrak', 'frak'],
8
5
  ['neq', 'eq.not'],
9
6
  ['dot', 'dot'],
10
7
  ['ddot', 'dot.double'],
@@ -23,8 +20,13 @@ export const symbolMap = new Map<string, string>([
23
20
  ['underline', 'underline'], // same
24
21
  ['bar', 'macron'],
25
22
 
23
+ ['boldsymbol', 'bold'],
24
+ ['mathbf', 'bold'],
26
25
  ['mathbb', 'bb'],
27
26
  ['mathcal', 'cal'],
27
+ ['mathfrak', 'frak'],
28
+ ['mathsf', 'sans'],
29
+ ['mathtt', 'mono'],
28
30
 
29
31
  ['mathrm', 'upright'],
30
32
  ['rm', 'upright'],
package/src/parser.ts CHANGED
@@ -228,6 +228,10 @@ export function katexNodeToTexNode(node: KatexParseNode): TexNode {
228
228
  }
229
229
  }
230
230
  throw new KatexNodeToTexNodeError(`Unknown error type in parsed result:`, node);
231
+ case 'comment':
232
+ res.type = 'comment';
233
+ res.content = node.text!;
234
+ break;
231
235
  default:
232
236
  throw new KatexNodeToTexNodeError(`Unknown node type: ${node.type}`, node);
233
237
  break;
@@ -238,6 +242,61 @@ export function katexNodeToTexNode(node: KatexParseNode): TexNode {
238
242
  }
239
243
  }
240
244
 
245
+ // Split tex into a list of tex strings and comments.
246
+ // Each item in the returned list is either a tex snippet or a comment.
247
+ // Each comment item is a string starting with '%'.
248
+ function splitTex(tex: string): string[] {
249
+ const lines = tex.split("\n");
250
+ const out_tex_list: string[] = [];
251
+ let current_tex = "";
252
+ // let inside_begin_depth = 0;
253
+ for (let i = 0; i < lines.length; i++) {
254
+ const line = lines[i];
255
+ // if (line.includes('\\begin{')) {
256
+ // inside_begin_depth += line.split('\\begin{').length - 1;
257
+ // }
258
+
259
+ let index = -1;
260
+ while (index + 1 < line.length) {
261
+ index = line.indexOf('%', index + 1);
262
+ if (index === -1) {
263
+ // No comment in this line
264
+ break;
265
+ }
266
+ if (index === 0 || line[index - 1] !== '\\') {
267
+ // Found a comment
268
+ break;
269
+ }
270
+ }
271
+ if (index !== -1) {
272
+ current_tex += line.substring(0, index);
273
+ const comment = line.substring(index);
274
+ out_tex_list.push(current_tex);
275
+ current_tex = "";
276
+ out_tex_list.push(comment);
277
+ } else {
278
+ current_tex += line;
279
+ }
280
+ if (i < lines.length - 1) {
281
+ const has_begin_command = line.includes('\\begin{');
282
+ const followed_by_end_command = lines[i + 1].includes('\\end{');
283
+ if(!has_begin_command && !followed_by_end_command) {
284
+ current_tex += "\\SyMbOlNeWlInE ";
285
+ }
286
+ }
287
+
288
+ // if (line.includes('\\end{')) {
289
+ // inside_begin_depth -= line.split('\\end{').length - 1;
290
+ // }
291
+ }
292
+
293
+ if (current_tex.length > 0) {
294
+ out_tex_list.push(current_tex);
295
+ }
296
+
297
+ return out_tex_list;
298
+ }
299
+
241
300
  export function parseTex(tex: string, customTexMacros: {[key: string]: string}): TexNode {
242
301
  // displayMode=true. Otherwise, "KaTeX parse error: {align*} can be used only in display mode."
243
302
  const macros = {
@@ -257,6 +316,7 @@ export function parseTex(tex: string, customTexMacros: {[key: string]: string}):
257
316
  '\\slash': '\\operatorname{SyMb01-slash}',
258
317
  '\\LaTeX': '\\operatorname{SyMb01-LaTeX}',
259
318
  '\\TeX': '\\operatorname{SyMb01-TeX}',
319
+ '\\SyMbOlNeWlInE': '\\operatorname{SyMb01-newline}',
260
320
  ...customTexMacros
261
321
  };
262
322
  const options = {
@@ -265,7 +325,25 @@ export function parseTex(tex: string, customTexMacros: {[key: string]: string}):
265
325
  strict: "ignore",
266
326
  throwOnError: false
267
327
  };
268
- let treeArray = generateParseTree(tex, options);
328
+
329
+ const tex_list = splitTex(tex);
330
+
331
+ let treeArray: KatexParseNode[] = [];
332
+
333
+ for (const tex_item of tex_list) {
334
+ if (tex_item.startsWith('%')) {
335
+ const tex_node: KatexParseNode = {
336
+ type: 'comment',
337
+ mode: 'math',
338
+ text: tex_item.substring(1),
339
+ };
340
+ treeArray.push(tex_node);
341
+ continue;
342
+ }
343
+ const trees = generateParseTree(tex_item, options);
344
+ treeArray = treeArray.concat(trees);
345
+ }
346
+
269
347
  let t = {
270
348
  type: 'ordgroup',
271
349
  mode: 'math',
@@ -0,0 +1,9 @@
1
+ /**
2
+ * This file is the entry point for bundling the .js file for the browser.
3
+ */
4
+
5
+ import { tex2typst } from './index';
6
+
7
+ if(typeof window !== 'undefined') {
8
+ (window as any).tex2typst = tex2typst;
9
+ }
package/src/types.ts CHANGED
@@ -28,7 +28,7 @@ export interface TexNode {
28
28
  }
29
29
 
30
30
  export interface TypstNode {
31
- type: 'atom' | 'symbol' | 'text' | 'softSpace';
31
+ type: 'atom' | 'symbol' | 'text' | 'softSpace' | 'comment' | 'newline',
32
32
  content: string;
33
33
  args?: TypstNode[];
34
34
  }
package/src/writer.ts CHANGED
@@ -55,6 +55,8 @@ export class TypstWriter {
55
55
  no_need_space ||= /[0-9]$/.test(this.buffer) && /^[0-9]/.test(str);
56
56
  // leading sign
57
57
  no_need_space ||= /[\(\[{]\s*(-|\+)$/.test(this.buffer) || this.buffer === "-" || this.buffer === "+";
58
+ // new line
59
+ no_need_space ||= str.startsWith('\n');
58
60
  // buffer is empty
59
61
  no_need_space ||= this.buffer === "";
60
62
  // other cases
@@ -172,6 +174,19 @@ export class TypstWriter {
172
174
  this.queue.push({ type: 'atom', content: ')'});
173
175
  this.insideFunctionDepth --;
174
176
  return;
177
+ } else if (node.content === '\\mathbf') {
178
+ this.append({ type: 'symbol', content: 'upright' });
179
+ this.insideFunctionDepth ++;
180
+ this.queue.push({ type: 'atom', content: '('});
181
+ this.queue.push(func_symbol);
182
+ this.insideFunctionDepth ++;
183
+ this.queue.push({ type: 'atom', content: '('});
184
+ this.append(arg0);
185
+ this.queue.push({ type: 'atom', content: ')'});
186
+ this.insideFunctionDepth --;
187
+ this.queue.push({ type: 'atom', content: ')'});
188
+ this.insideFunctionDepth --;
189
+ return;
175
190
  } else if (node.content === '\\mathbb') {
176
191
  const body = node.args![0];
177
192
  if (body.type === 'symbol' && /^[A-Z]$/.test(body.content)) {
@@ -197,7 +212,12 @@ export class TypstWriter {
197
212
  this.queue.push({ type: 'symbol', content: text});
198
213
  } else if (text.startsWith('SyMb01-')) {
199
214
  // special hacks made in parseTex()
200
- this.queue.push({ type: 'symbol', content: '\\' + text.substring(7)});
215
+ const special_symbol = text.substring(7);
216
+ if (special_symbol === 'newline') {
217
+ this.queue.push({ type: 'newline', content: '\n'});
218
+ return;
219
+ }
220
+ this.queue.push({ type: 'symbol', content: '\\' + special_symbol});
201
221
  } else {
202
222
  this.queue.push({ type: 'symbol', content: 'op' });
203
223
  this.queue.push({ type: 'atom', content: '('});
@@ -236,8 +256,12 @@ export class TypstWriter {
236
256
  row.forEach((cell, j) => {
237
257
  // There is a leading & in row
238
258
  if (cell.type === 'ordgroup' && cell.args!.length === 0) {
259
+ this.queue.push({ type: 'atom', content: ',' });
239
260
  return;
240
261
  }
262
+ // if (j == 0 && cell.type === 'newline' && cell.content === '\n') {
263
+ // return;
264
+ // }
241
265
  this.append(cell);
242
266
  // cell.args!.forEach((n) => this.append(n));
243
267
  if (j < row.length - 1) {
@@ -257,6 +281,8 @@ export class TypstWriter {
257
281
  } else {
258
282
  throw new TypstWriterError(`Unknown macro: ${node.content}`, node);
259
283
  }
284
+ } else if (node.type === 'comment') {
285
+ this.queue.push({ type: 'comment', content: node.content });
260
286
  } else {
261
287
  throw new TypstWriterError(`Unimplemented node type to append: ${node.type}`, node);
262
288
  }
@@ -279,6 +305,12 @@ export class TypstWriter {
279
305
  this.needSpaceAfterSingleItemScript = true;
280
306
  str = '';
281
307
  break;
308
+ case 'comment':
309
+ str = `//${node.content}`;
310
+ break;
311
+ case 'newline':
312
+ str = '\n';
313
+ break;
282
314
  default:
283
315
  throw new TypstWriterError(`Unexpected node type to stringify: ${node.type}`, node)
284
316
  }
@@ -337,7 +369,9 @@ function convertToken(token: string): string {
337
369
  if (/^[a-zA-Z0-9]$/.test(token)) {
338
370
  return token;
339
371
  } else if (token === '\\\\') {
340
- return '\\\n';
372
+ return '\\';
373
+ } else if (token == '/') {
374
+ return '\\/';
341
375
  } else if (['\\$', '\\#', '\\&', '\\_'].includes(token)) {
342
376
  return token;
343
377
  } else if (token.startsWith('\\')) {