@sciexpr/parser 0.1.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 ADDED
@@ -0,0 +1,540 @@
1
+ // src/types.ts
2
+ var TokenType = /* @__PURE__ */ ((TokenType2) => {
3
+ TokenType2["Command"] = "command";
4
+ TokenType2["OpenBrace"] = "openBrace";
5
+ TokenType2["CloseBrace"] = "closeBrace";
6
+ TokenType2["OpenBracket"] = "openBracket";
7
+ TokenType2["CloseBracket"] = "closeBracket";
8
+ TokenType2["OpenParen"] = "openParen";
9
+ TokenType2["CloseParen"] = "closeParen";
10
+ TokenType2["Superscript"] = "superscript";
11
+ TokenType2["Subscript"] = "subscript";
12
+ TokenType2["Whitespace"] = "whitespace";
13
+ TokenType2["Text"] = "text";
14
+ TokenType2["Number"] = "number";
15
+ TokenType2["Ampersand"] = "ampersand";
16
+ TokenType2["Newline"] = "newline";
17
+ TokenType2["Dollar"] = "dollar";
18
+ TokenType2["DoubleDollar"] = "doubleDollar";
19
+ TokenType2["EOF"] = "eof";
20
+ return TokenType2;
21
+ })(TokenType || {});
22
+
23
+ // src/tokenizer.ts
24
+ var LatexTokenizer = class {
25
+ input;
26
+ pos = 0;
27
+ constructor(input) {
28
+ this.input = input;
29
+ }
30
+ /** 获取所有 token */
31
+ tokenize() {
32
+ const tokens = [];
33
+ while (this.pos < this.input.length) {
34
+ const token = this.nextToken();
35
+ if (token) tokens.push(token);
36
+ }
37
+ tokens.push({
38
+ type: "eof" /* EOF */,
39
+ value: "",
40
+ position: this.pos,
41
+ length: 0
42
+ });
43
+ return tokens;
44
+ }
45
+ /** 获取下一个 token */
46
+ nextToken() {
47
+ const ch = this.input[this.pos];
48
+ if (ch === void 0) return null;
49
+ const start = this.pos;
50
+ if (ch === " " || ch === " ") {
51
+ this.pos++;
52
+ while (this.pos < this.input.length && (this.input[this.pos] === " " || this.input[this.pos] === " ")) {
53
+ this.pos++;
54
+ }
55
+ return { type: "whitespace" /* Whitespace */, value: this.input.slice(start, this.pos), position: start, length: this.pos - start };
56
+ }
57
+ if (ch === "$" && this.input[this.pos + 1] === "$") {
58
+ this.pos += 2;
59
+ return { type: "doubleDollar" /* DoubleDollar */, value: "$$", position: start, length: 2 };
60
+ }
61
+ if (ch === "$") {
62
+ this.pos++;
63
+ return { type: "dollar" /* Dollar */, value: "$", position: start, length: 1 };
64
+ }
65
+ if (ch === "\\") {
66
+ this.pos++;
67
+ if (this.pos < this.input.length && /[a-zA-Z]/.test(this.input[this.pos])) {
68
+ while (this.pos < this.input.length && /[a-zA-Z*]/.test(this.input[this.pos])) {
69
+ this.pos++;
70
+ }
71
+ return { type: "command" /* Command */, value: this.input.slice(start, this.pos), position: start, length: this.pos - start };
72
+ }
73
+ this.pos++;
74
+ return { type: "command" /* Command */, value: this.input.slice(start, this.pos), position: start, length: this.pos - start };
75
+ }
76
+ if (ch === "{") {
77
+ this.pos++;
78
+ return { type: "openBrace" /* OpenBrace */, value: "{", position: start, length: 1 };
79
+ }
80
+ if (ch === "}") {
81
+ this.pos++;
82
+ return { type: "closeBrace" /* CloseBrace */, value: "}", position: start, length: 1 };
83
+ }
84
+ if (ch === "[") {
85
+ this.pos++;
86
+ return { type: "openBracket" /* OpenBracket */, value: "[", position: start, length: 1 };
87
+ }
88
+ if (ch === "]") {
89
+ this.pos++;
90
+ return { type: "closeBracket" /* CloseBracket */, value: "]", position: start, length: 1 };
91
+ }
92
+ if (ch === "(") {
93
+ this.pos++;
94
+ return { type: "openParen" /* OpenParen */, value: "(", position: start, length: 1 };
95
+ }
96
+ if (ch === ")") {
97
+ this.pos++;
98
+ return { type: "closeParen" /* CloseParen */, value: ")", position: start, length: 1 };
99
+ }
100
+ if (ch === "^") {
101
+ this.pos++;
102
+ return { type: "superscript" /* Superscript */, value: "^", position: start, length: 1 };
103
+ }
104
+ if (ch === "_") {
105
+ this.pos++;
106
+ return { type: "subscript" /* Subscript */, value: "_", position: start, length: 1 };
107
+ }
108
+ if (ch === "&") {
109
+ this.pos++;
110
+ return { type: "ampersand" /* Ampersand */, value: "&", position: start, length: 1 };
111
+ }
112
+ if (ch === "\\" && this.input[this.pos + 1] === "\\") {
113
+ }
114
+ if (/[0-9]/.test(ch)) {
115
+ this.pos++;
116
+ while (this.pos < this.input.length && /[0-9.]/.test(this.input[this.pos])) {
117
+ this.pos++;
118
+ }
119
+ return { type: "number" /* Number */, value: this.input.slice(start, this.pos), position: start, length: this.pos - start };
120
+ }
121
+ this.pos++;
122
+ while (this.pos < this.input.length && /[a-zA-Z0-9]/.test(this.input[this.pos])) {
123
+ this.pos++;
124
+ }
125
+ return { type: "text" /* Text */, value: this.input.slice(start, this.pos), position: start, length: this.pos - start };
126
+ }
127
+ };
128
+
129
+ // src/latex-parser.ts
130
+ import { ExpressionKind, SymbolType } from "@sciexpr/core";
131
+ import {
132
+ createRoot,
133
+ createSymbol,
134
+ createFraction,
135
+ createScript,
136
+ createRadical,
137
+ createDelimiter,
138
+ createText,
139
+ createGroup
140
+ } from "@sciexpr/core";
141
+ var LatexParser = class {
142
+ id = "latex";
143
+ formats = ["latex", "tex"];
144
+ tokens = [];
145
+ pos = 0;
146
+ displayMode = false;
147
+ canParse(input) {
148
+ return /\\[a-zA-Z]+/.test(input) || /[\^_]/.test(input) || /[{}]/.test(input);
149
+ }
150
+ parse(input, options) {
151
+ this.displayMode = options?.displayMode ?? false;
152
+ const tokenizer = new LatexTokenizer(input);
153
+ this.tokens = tokenizer.tokenize();
154
+ this.pos = 0;
155
+ const children = this.parseExpressions();
156
+ return createRoot(ExpressionKind.Math, children, {
157
+ displayMode: this.displayMode,
158
+ originalSource: input
159
+ });
160
+ }
161
+ // ==========================================================================
162
+ // Parse Expressions
163
+ // ==========================================================================
164
+ parseExpressions() {
165
+ const exprs = [];
166
+ while (!this.isEOF()) {
167
+ const expr = this.parseExpression();
168
+ if (expr) exprs.push(expr);
169
+ }
170
+ return exprs;
171
+ }
172
+ parseExpression() {
173
+ const token = this.peek();
174
+ if (!token) return null;
175
+ let expr = null;
176
+ switch (token.type) {
177
+ case "command" /* Command */:
178
+ expr = this.parseCommand();
179
+ break;
180
+ case "openBrace" /* OpenBrace */:
181
+ expr = this.parseGroup();
182
+ break;
183
+ case "closeBrace" /* CloseBrace */:
184
+ this.advance();
185
+ return null;
186
+ case "superscript" /* Superscript */:
187
+ case "subscript" /* Subscript */: {
188
+ this.advance();
189
+ const next = this.parseExpression();
190
+ if (next) {
191
+ expr = createScript(createText(""), {
192
+ super: token.type === "superscript" /* Superscript */ ? next : void 0,
193
+ sub: token.type === "subscript" /* Subscript */ ? next : void 0
194
+ });
195
+ }
196
+ break;
197
+ }
198
+ case "text" /* Text */:
199
+ this.advance();
200
+ expr = createSymbol(token.value, this.classifySymbol(token.value));
201
+ break;
202
+ case "number" /* Number */:
203
+ this.advance();
204
+ expr = createSymbol(token.value, SymbolType.Number);
205
+ break;
206
+ case "whitespace" /* Whitespace */:
207
+ this.advance();
208
+ return null;
209
+ // Skip whitespace
210
+ case "openParen" /* OpenParen */:
211
+ this.advance();
212
+ const body = this.parseExpressions();
213
+ this.skipType("closeParen" /* CloseParen */);
214
+ expr = createDelimiter("(", ")", body.length === 1 ? body[0] : createGroup(body));
215
+ break;
216
+ case "openBracket" /* OpenBracket */:
217
+ this.advance();
218
+ const bracketBody = this.parseExpressions();
219
+ this.skipType("closeBracket" /* CloseBracket */);
220
+ expr = createDelimiter("[", "]", bracketBody.length === 1 ? bracketBody[0] : createGroup(bracketBody));
221
+ break;
222
+ case "eof" /* EOF */:
223
+ return null;
224
+ default:
225
+ this.advance();
226
+ expr = createText(token.value);
227
+ break;
228
+ }
229
+ if (expr) {
230
+ expr = this.handleScripts(expr);
231
+ }
232
+ return expr;
233
+ }
234
+ // ==========================================================================
235
+ // Script Handling (shared)
236
+ // ==========================================================================
237
+ /** 处理上/下标:检查 ^ 和 _ 后附加到 base 节点 */
238
+ handleScripts(base) {
239
+ let result = base;
240
+ while (this.peek()?.type === "subscript" /* Subscript */ || this.peek()?.type === "superscript" /* Superscript */) {
241
+ const scriptType = this.advance().type;
242
+ const scriptContent = this.parseScriptContent();
243
+ if (scriptType === "subscript" /* Subscript */) {
244
+ result = createScript(result, { sub: scriptContent });
245
+ } else {
246
+ result = createScript(result, { super: scriptContent });
247
+ }
248
+ }
249
+ return result;
250
+ }
251
+ // ==========================================================================
252
+ // Parse Commands
253
+ // ==========================================================================
254
+ parseCommand() {
255
+ const token = this.advance();
256
+ const command = token.value;
257
+ if (command === "\\frac") {
258
+ const num = this.parseBraceGroup();
259
+ const den = this.parseBraceGroup();
260
+ return this.handleScripts(createFraction(num, den));
261
+ }
262
+ if (command === "\\sqrt") {
263
+ let index;
264
+ if (this.peek()?.type === "openBracket" /* OpenBracket */) {
265
+ this.advance();
266
+ index = this.parseSingleExpression();
267
+ this.skipType("closeBracket" /* CloseBracket */);
268
+ }
269
+ const radicand = this.parseBraceGroup();
270
+ return this.handleScripts(createRadical(radicand, index));
271
+ }
272
+ const greekLower = {
273
+ "\\alpha": "\u03B1",
274
+ "\\beta": "\u03B2",
275
+ "\\gamma": "\u03B3",
276
+ "\\delta": "\u03B4",
277
+ "\\epsilon": "\u03B5",
278
+ "\\zeta": "\u03B6",
279
+ "\\eta": "\u03B7",
280
+ "\\theta": "\u03B8",
281
+ "\\iota": "\u03B9",
282
+ "\\kappa": "\u03BA",
283
+ "\\lambda": "\u03BB",
284
+ "\\mu": "\u03BC",
285
+ "\\pi": "\u03C0",
286
+ "\\sigma": "\u03C3",
287
+ "\\tau": "\u03C4",
288
+ "\\phi": "\u03C6",
289
+ "\\omega": "\u03C9",
290
+ "\\xi": "\u03BE",
291
+ "\\rho": "\u03C1",
292
+ "\\psi": "\u03C8"
293
+ };
294
+ if (command in greekLower) {
295
+ return this.handleScripts(createSymbol(command, SymbolType.Greek));
296
+ }
297
+ if (command === "\\text") {
298
+ const content = this.parseBraceGroup();
299
+ if (content.kind === "group") {
300
+ const group = content;
301
+ const textContent = group.children.map((c) => {
302
+ if (c.kind === "symbol") return c.value;
303
+ return "";
304
+ }).join("");
305
+ return this.handleScripts(createText(textContent));
306
+ }
307
+ return this.handleScripts(content);
308
+ }
309
+ const specialSymbols = ["\\infty", "\\partial", "\\nabla", "\\forall", "\\exists", "\\emptyset", "\\neg"];
310
+ if (specialSymbols.includes(command)) {
311
+ return this.handleScripts(createSymbol(command, SymbolType.Operator));
312
+ }
313
+ return this.handleScripts(createSymbol(command, SymbolType.Ordinary));
314
+ }
315
+ // ==========================================================================
316
+ // Helpers
317
+ // ==========================================================================
318
+ /** 解析 {} 包裹的分组 */
319
+ parseBraceGroup() {
320
+ if (this.peek()?.type === "openBrace" /* OpenBrace */) {
321
+ return this.parseGroup();
322
+ }
323
+ return this.parseSingleExpression();
324
+ }
325
+ /** 解析 { expressions } 分组 */
326
+ parseGroup() {
327
+ this.skipType("openBrace" /* OpenBrace */);
328
+ const children = this.parseExpressions();
329
+ this.skipType("closeBrace" /* CloseBrace */);
330
+ return createGroup(children);
331
+ }
332
+ /** 解析单个表达式(用于上/下标参数) */
333
+ parseSingleExpression() {
334
+ const children = this.parseExpressions();
335
+ return children.length === 1 ? children[0] : createGroup(children);
336
+ }
337
+ /** 解析上/下标内容 */
338
+ parseScriptContent() {
339
+ if (this.peek()?.type === "openBrace" /* OpenBrace */) {
340
+ return this.parseGroup();
341
+ }
342
+ const expr = this.parseExpression();
343
+ return expr ?? createText("");
344
+ }
345
+ // ==========================================================================
346
+ // Token Stream Helpers
347
+ // ==========================================================================
348
+ peek() {
349
+ return this.tokens[this.pos];
350
+ }
351
+ advance() {
352
+ const token = this.tokens[this.pos];
353
+ this.pos++;
354
+ return token;
355
+ }
356
+ skipType(type) {
357
+ if (this.peek()?.type === type) {
358
+ this.advance();
359
+ }
360
+ }
361
+ isEOF() {
362
+ return this.pos >= this.tokens.length || this.peek()?.type === "eof" /* EOF */;
363
+ }
364
+ /** 根据文本内容判断符号类型 */
365
+ classifySymbol(text) {
366
+ if (/^[a-zA-Z]$/.test(text)) return SymbolType.Variable;
367
+ if (/^[0-9.]+$/.test(text)) return SymbolType.Number;
368
+ if (/^[+\-*/=<>|]$/.test(text)) return SymbolType.Operator;
369
+ return SymbolType.Ordinary;
370
+ }
371
+ };
372
+
373
+ // src/chem-parser.ts
374
+ import { ExpressionKind as ExpressionKind2 } from "@sciexpr/core";
375
+ import { createRoot as createRoot2, createChemElement, createChemGroup, createSymbol as createSymbol2, SymbolType as SymbolType2 } from "@sciexpr/core";
376
+ var ChemicalParser = class {
377
+ id = "chemical";
378
+ formats = ["chemical", "chem"];
379
+ canParse(input) {
380
+ return /^[A-Z][a-z]?\d*/.test(input) || /[A-Z][a-z]?[A-Z]/.test(input);
381
+ }
382
+ parse(input, _options) {
383
+ const elements = this.parseFormula(input);
384
+ return createRoot2(ExpressionKind2.Molecule, elements, {
385
+ originalSource: input
386
+ });
387
+ }
388
+ /**
389
+ * 解析化学式,如 "H2SO4" → [H, 2, S, O, 4]
390
+ */
391
+ parseFormula(formula) {
392
+ const nodes = [];
393
+ let i = 0;
394
+ while (i < formula.length) {
395
+ const ch = formula[i];
396
+ if (/[A-Z]/.test(ch)) {
397
+ let symbol = ch;
398
+ i++;
399
+ while (i < formula.length && /[a-z]/.test(formula[i])) {
400
+ symbol += formula[i];
401
+ i++;
402
+ }
403
+ let count;
404
+ if (i < formula.length && /[0-9]/.test(formula[i])) {
405
+ let numStr = "";
406
+ while (i < formula.length && /[0-9]/.test(formula[i])) {
407
+ numStr += formula[i];
408
+ i++;
409
+ }
410
+ count = parseInt(numStr, 10);
411
+ }
412
+ nodes.push(createChemElement(symbol, count ? { count } : void 0));
413
+ continue;
414
+ }
415
+ if (ch === "(") {
416
+ i++;
417
+ const groupStart = i;
418
+ let depth = 1;
419
+ while (i < formula.length && depth > 0) {
420
+ if (formula[i] === "(") depth++;
421
+ else if (formula[i] === ")") depth--;
422
+ i++;
423
+ }
424
+ const groupContent = formula.slice(groupStart, i - 1);
425
+ const groupAtoms = this.parseFormula(groupContent);
426
+ let groupCount;
427
+ if (i < formula.length && /[0-9]/.test(formula[i])) {
428
+ let numStr = "";
429
+ while (i < formula.length && /[0-9]/.test(formula[i])) {
430
+ numStr += formula[i];
431
+ i++;
432
+ }
433
+ groupCount = parseInt(numStr, 10);
434
+ if (groupCount && groupCount > 1) {
435
+ for (const atom of groupAtoms) {
436
+ atom.count = (atom.count ?? 1) * groupCount;
437
+ }
438
+ }
439
+ }
440
+ nodes.push(createChemGroup(groupAtoms));
441
+ continue;
442
+ }
443
+ if (ch === "^") {
444
+ i++;
445
+ let chargeStr = "";
446
+ while (i < formula.length && /[0-9+\-]/.test(formula[i])) {
447
+ chargeStr += formula[i];
448
+ i++;
449
+ }
450
+ const lastNode = nodes[nodes.length - 1];
451
+ if (lastNode && lastNode.kind === "chem-element") {
452
+ const chargeMatch = chargeStr.match(/^(\d*)([+\-])$/);
453
+ if (chargeMatch) {
454
+ const el = lastNode;
455
+ el.charge = parseInt(chargeMatch[1] || "1", 10);
456
+ el.chargeSign = chargeMatch[2];
457
+ }
458
+ }
459
+ continue;
460
+ }
461
+ if (ch === "=" || ch === "-" || ch === "<") {
462
+ i++;
463
+ continue;
464
+ }
465
+ if (ch === " " || ch === "+" || ch === ",") {
466
+ i++;
467
+ continue;
468
+ }
469
+ nodes.push(createSymbol2(ch, SymbolType2.Ordinary));
470
+ i++;
471
+ }
472
+ return nodes;
473
+ }
474
+ };
475
+
476
+ // src/registry.ts
477
+ var ParserRegistry = class {
478
+ parsers = /* @__PURE__ */ new Map();
479
+ defaultParsers;
480
+ constructor() {
481
+ const latex = new LatexParser();
482
+ const chemical = new ChemicalParser();
483
+ this.defaultParsers = [latex, chemical];
484
+ this.register(latex);
485
+ this.register(chemical);
486
+ }
487
+ /** 注册自定义解析器 */
488
+ register(parser) {
489
+ this.parsers.set(parser.id, parser);
490
+ }
491
+ /** 移除解析器 */
492
+ unregister(id) {
493
+ return this.parsers.delete(id);
494
+ }
495
+ /** 根据 ID 获取解析器 */
496
+ get(id) {
497
+ return this.parsers.get(id);
498
+ }
499
+ /**
500
+ * 自动检测并选择合适的解析器
501
+ */
502
+ find(input) {
503
+ for (const parser of this.parsers.values()) {
504
+ if (parser.canParse(input)) {
505
+ return parser;
506
+ }
507
+ }
508
+ return null;
509
+ }
510
+ /**
511
+ * 解析输入
512
+ * @param input 输入文本
513
+ * @param options 解析选项
514
+ * @returns 解析后的 AST
515
+ */
516
+ parse(input, options) {
517
+ if (options?.hint && options.hint !== "auto") {
518
+ const parser2 = this.parsers.get(options.hint);
519
+ if (parser2) {
520
+ return parser2.parse(input, options);
521
+ }
522
+ }
523
+ const parser = this.find(input);
524
+ if (parser) {
525
+ return parser.parse(input, options);
526
+ }
527
+ return this.defaultParsers[0].parse(input, options);
528
+ }
529
+ /** 列出所有已注册的解析器 ID */
530
+ list() {
531
+ return Array.from(this.parsers.keys());
532
+ }
533
+ };
534
+ export {
535
+ ChemicalParser,
536
+ LatexParser,
537
+ LatexTokenizer,
538
+ ParserRegistry,
539
+ TokenType
540
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@sciexpr/parser",
3
+ "version": "0.1.0",
4
+ "description": "Scientific Expression Engine - LaTeX & Chemical formula parser",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "sideEffects": false,
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "keywords": [
22
+ "scientific",
23
+ "expression",
24
+ "parser",
25
+ "latex",
26
+ "chemistry"
27
+ ],
28
+ "license": "MIT",
29
+ "author": {
30
+ "name": "haoguodong09-svg",
31
+ "email": "haoguodong09@gmail.com"
32
+ },
33
+ "homepage": "https://github.com/haoguodong09-svg/scientific-expression-engine#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/haoguodong09-svg/scientific-expression-engine/issues"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/haoguodong09-svg/scientific-expression-engine",
40
+ "directory": "packages/parser"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@sciexpr/core": "0.1.0"
47
+ },
48
+ "devDependencies": {
49
+ "tsup": "^8.0.0",
50
+ "typescript": "^5.5.0",
51
+ "vitest": "^2.0.0",
52
+ "rimraf": "^5.0.0"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "dev": "tsup --watch",
57
+ "lint": "tsc --noEmit",
58
+ "clean": "rimraf dist",
59
+ "test": "vitest run"
60
+ }
61
+ }