@sciexpr/renderer 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.cjs ADDED
@@ -0,0 +1,489 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ KatexRenderer: () => KatexRenderer,
34
+ LatexReverseRenderer: () => LatexReverseRenderer,
35
+ MathMLRenderer: () => MathMLRenderer,
36
+ RendererRegistry: () => RendererRegistry,
37
+ UnicodeRenderer: () => UnicodeRenderer
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/types.ts
42
+ var RendererRegistry = class {
43
+ renderers = /* @__PURE__ */ new Map();
44
+ /** 注册渲染器 */
45
+ register(renderer) {
46
+ this.renderers.set(renderer.id, renderer);
47
+ }
48
+ /** 移除渲染器 */
49
+ unregister(id) {
50
+ return this.renderers.delete(id);
51
+ }
52
+ /** 按 ID 获取 */
53
+ get(id) {
54
+ return this.renderers.get(id);
55
+ }
56
+ /** 按输出格式查找 */
57
+ findByFormat(format) {
58
+ for (const r of this.renderers.values()) {
59
+ if (r.outputFormat === format) return r;
60
+ }
61
+ return void 0;
62
+ }
63
+ /**
64
+ * 渲染 AST
65
+ * @param root AST 根节点
66
+ * @param format 目标输出格式
67
+ * @param options 渲染选项
68
+ */
69
+ render(root, format, options) {
70
+ const renderer = this.findByFormat(format);
71
+ if (!renderer) {
72
+ throw new Error(`No renderer registered for format: ${format}`);
73
+ }
74
+ return renderer.render(root, options);
75
+ }
76
+ /** 列出所有已注册格式 */
77
+ listFormats() {
78
+ return Array.from(this.renderers.values()).map((r) => r.outputFormat);
79
+ }
80
+ };
81
+
82
+ // src/latex-renderer.ts
83
+ var LatexReverseRenderer = class {
84
+ id = "latex-reverse";
85
+ outputFormat = "latex";
86
+ render(root, _options) {
87
+ const latex = this.renderNode(root);
88
+ return { value: latex, format: "latex" };
89
+ }
90
+ renderNode(node) {
91
+ const n = node;
92
+ switch (n.kind) {
93
+ case "root":
94
+ return n.children.map((c) => this.renderNode(c)).join("");
95
+ case "symbol":
96
+ return n.value;
97
+ case "frac":
98
+ return `\\frac{${this.renderNode(n.numerator)}}{${this.renderNode(n.denominator)}}`;
99
+ case "script": {
100
+ let result = this.renderNode(n.base);
101
+ if (n.sub) {
102
+ result += `_{${this.renderNode(n.sub)}}`;
103
+ }
104
+ if (n.super) {
105
+ result += `^{${this.renderNode(n.super)}}`;
106
+ }
107
+ return result;
108
+ }
109
+ case "radical": {
110
+ if (n.index) {
111
+ return `\\sqrt[${this.renderNode(n.index)}]{${this.renderNode(n.radicand)}}`;
112
+ }
113
+ return `\\sqrt{${this.renderNode(n.radicand)}}`;
114
+ }
115
+ case "delimiter":
116
+ return `${n.left}${this.renderNode(n.body)}${n.right}`;
117
+ case "matrix": {
118
+ const cols = n.columnAlign ? `{${n.columnAlign.join("")}}` : "";
119
+ const type = n.matrixType ?? "matrix";
120
+ const rows = n.rows.map((row) => row.map((cell) => this.renderNode(cell)).join(" & ")).join(" \\\\ ");
121
+ return `\\begin{${type}}${cols}${rows}\\end{${type}}`;
122
+ }
123
+ case "text":
124
+ return `\\text{${n.content}}`;
125
+ case "space":
126
+ return n.width === 1 ? "\\," : "\\quad";
127
+ case "accent":
128
+ return `\\${n.accentType}{${this.renderNode(n.base)}}`;
129
+ case "largeop": {
130
+ let result = `\\${n.opType}`;
131
+ if (n.lower) {
132
+ result += `_{${this.renderNode(n.lower)}}`;
133
+ }
134
+ if (n.upper) {
135
+ result += `^{${this.renderNode(n.upper)}}`;
136
+ }
137
+ if (n.body) {
138
+ result += `{${this.renderNode(n.body)}}`;
139
+ }
140
+ return result;
141
+ }
142
+ case "color":
143
+ return `\\color{${n.color}}{${this.renderNode(n.body)}}`;
144
+ case "group":
145
+ return `{${n.children.map((c) => this.renderNode(c)).join("")}}`;
146
+ case "chem-element": {
147
+ let result = n.symbol;
148
+ if (n.count && n.count > 1) {
149
+ result += n.count;
150
+ }
151
+ if (n.charge) {
152
+ result += `^{${n.charge}${n.chargeSign ?? "+"}}`;
153
+ }
154
+ return result;
155
+ }
156
+ case "chem-bond": {
157
+ const bondMap = {
158
+ single: "-",
159
+ double: "=",
160
+ triple: "\\equiv ",
161
+ aromatic: "~",
162
+ coordinate: "->"
163
+ };
164
+ return `${this.renderNode(n.left)}${bondMap[n.bondType] ?? "-"}${this.renderNode(n.right)}`;
165
+ }
166
+ case "chem-reaction": {
167
+ const arrowMap = {
168
+ "->": "->",
169
+ "<-": "<-",
170
+ "<=>": "<=>",
171
+ "<->": "<->",
172
+ "->[": "->[",
173
+ "<-[": "<-[",
174
+ "<=>>": "<=>>",
175
+ "<<=>": "<<=>"
176
+ };
177
+ const react = n.reactants.map((r) => this.renderNode(r)).join(" + ");
178
+ const prod = n.products.map((p) => this.renderNode(p)).join(" + ");
179
+ return `\\ce{${react} ${arrowMap[n.arrowType] ?? "->"} ${prod}}`;
180
+ }
181
+ case "chem-group":
182
+ return n.atoms.map((a) => this.renderNode(a)).join("");
183
+ default:
184
+ return "";
185
+ }
186
+ }
187
+ };
188
+
189
+ // src/katex-renderer.ts
190
+ var KatexRenderer = class {
191
+ id = "katex";
192
+ outputFormat = "html";
193
+ latexRenderer = new LatexReverseRenderer();
194
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
195
+ katexInstance = null;
196
+ constructor() {
197
+ this.loadKatex();
198
+ }
199
+ async loadKatex() {
200
+ try {
201
+ this.katexInstance = await import("katex");
202
+ } catch {
203
+ this.katexInstance = null;
204
+ }
205
+ }
206
+ /** 手动设置 KaTeX 实例 */
207
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
208
+ setKatex(katex) {
209
+ this.katexInstance = katex;
210
+ }
211
+ /** 检查 KaTeX 是否可用 */
212
+ isAvailable() {
213
+ return this.katexInstance !== null;
214
+ }
215
+ render(root, options) {
216
+ if (!this.katexInstance) {
217
+ const latex = this.latexRenderer.render(root).value;
218
+ return {
219
+ value: `<code class="sciexpr-latex-fallback">${this.escapeHtml(latex)}</code>`,
220
+ format: "html",
221
+ warnings: [{ message: "KaTeX not available, showing LaTeX source", code: "KATEX_UNAVAILABLE" }]
222
+ };
223
+ }
224
+ const latexSource = this.latexRenderer.render(root).value;
225
+ try {
226
+ const html = this.katexInstance.renderToString(latexSource, {
227
+ displayMode: options?.displayMode ?? root.metadata?.displayMode ?? false,
228
+ throwOnError: options?.throwOnError ?? false,
229
+ macros: options?.macros,
230
+ trust: true
231
+ });
232
+ return { value: html, format: "html" };
233
+ } catch (err) {
234
+ const error = {
235
+ message: err instanceof Error ? err.message : String(err)
236
+ };
237
+ options?.hooks?.onError?.(error);
238
+ if (options?.throwOnError) {
239
+ throw error;
240
+ }
241
+ return {
242
+ value: `<span class="sciexpr-error">${this.escapeHtml(latexSource)}</span>`,
243
+ format: "html",
244
+ warnings: [{ message: error.message, code: "KATEX_RENDER_ERROR" }]
245
+ };
246
+ }
247
+ }
248
+ escapeHtml(text) {
249
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
250
+ }
251
+ };
252
+
253
+ // src/unicode-renderer.ts
254
+ var UnicodeRenderer = class _UnicodeRenderer {
255
+ id = "unicode";
256
+ outputFormat = "unicode";
257
+ static GREEK_MAP = {
258
+ "\\alpha": "\u03B1",
259
+ "\\beta": "\u03B2",
260
+ "\\gamma": "\u03B3",
261
+ "\\delta": "\u03B4",
262
+ "\\pi": "\u03C0",
263
+ "\\theta": "\u03B8",
264
+ "\\lambda": "\u03BB",
265
+ "\\mu": "\u03BC",
266
+ "\\sigma": "\u03C3",
267
+ "\\omega": "\u03C9",
268
+ "\\infty": "\u221E",
269
+ "\\partial": "\u2202",
270
+ "\\nabla": "\u2207",
271
+ "\\forall": "\u2200",
272
+ "\\exists": "\u2203",
273
+ "\\times": "\xD7",
274
+ "\\pm": "\xB1",
275
+ "\\neq": "\u2260",
276
+ "\\leq": "\u2264",
277
+ "\\geq": "\u2265",
278
+ "\\approx": "\u2248",
279
+ "\\to": "\u2192",
280
+ "\\Rightarrow": "\u21D2",
281
+ "\\int": "\u222B",
282
+ "\\sum": "\u2211"
283
+ };
284
+ static SUPER_DIGITS = {
285
+ "0": "\u2070",
286
+ "1": "\xB9",
287
+ "2": "\xB2",
288
+ "3": "\xB3",
289
+ "4": "\u2074",
290
+ "5": "\u2075",
291
+ "6": "\u2076",
292
+ "7": "\u2077",
293
+ "8": "\u2078",
294
+ "9": "\u2079",
295
+ "+": "\u207A",
296
+ "-": "\u207B"
297
+ };
298
+ static SUB_DIGITS = {
299
+ "0": "\u2080",
300
+ "1": "\u2081",
301
+ "2": "\u2082",
302
+ "3": "\u2083",
303
+ "4": "\u2084",
304
+ "5": "\u2085",
305
+ "6": "\u2086",
306
+ "7": "\u2087",
307
+ "8": "\u2088",
308
+ "9": "\u2089",
309
+ "+": "\u208A",
310
+ "-": "\u208B"
311
+ };
312
+ render(root, _options) {
313
+ const unicode = this.renderNode(root);
314
+ return { value: unicode, format: "unicode" };
315
+ }
316
+ renderNode(node) {
317
+ const n = node;
318
+ switch (n.kind) {
319
+ case "root":
320
+ return n.children.map((c) => this.renderNode(c)).join("");
321
+ case "symbol":
322
+ return _UnicodeRenderer.GREEK_MAP[n.value] ?? n.value;
323
+ case "frac":
324
+ return `(${this.renderNode(n.numerator)})/(${this.renderNode(n.denominator)})`;
325
+ case "script": {
326
+ let result = this.renderNode(n.base);
327
+ if (n.sub) result += this.toSub(this.renderNode(n.sub));
328
+ if (n.super) result += this.toSuper(this.renderNode(n.super));
329
+ return result;
330
+ }
331
+ case "radical":
332
+ if (n.index) {
333
+ return `${this.toSuper(this.renderNode(n.index))}\u221A(${this.renderNode(n.radicand)})`;
334
+ }
335
+ return `\u221A(${this.renderNode(n.radicand)})`;
336
+ case "delimiter":
337
+ return n.left + this.renderNode(n.body) + n.right;
338
+ case "text":
339
+ return n.content;
340
+ case "space":
341
+ return " ";
342
+ case "accent":
343
+ return this.renderNode(n.base);
344
+ case "largeop":
345
+ return this.renderNode(n.body ?? { kind: "text", content: "" });
346
+ case "group":
347
+ return n.children.map((c) => this.renderNode(c)).join("");
348
+ case "chem-element": {
349
+ let result = n.symbol;
350
+ if (n.count && n.count > 1) result += this.toSub(String(n.count));
351
+ if (n.charge) result += this.toSuper(`${n.charge}${n.chargeSign ?? "+"}`);
352
+ return result;
353
+ }
354
+ case "chem-bond":
355
+ return this.renderNode(n.left) + "-" + this.renderNode(n.right);
356
+ case "chem-reaction":
357
+ return n.reactants.map((r) => this.renderNode(r)).join(" + ") + " \u2192 " + n.products.map((p) => this.renderNode(p)).join(" + ");
358
+ case "chem-group":
359
+ return n.atoms.map((a) => this.renderNode(a)).join("");
360
+ default:
361
+ return "";
362
+ }
363
+ }
364
+ toSuper(text) {
365
+ let r = "";
366
+ for (const ch of text) r += _UnicodeRenderer.SUPER_DIGITS[ch] ?? ch;
367
+ return r;
368
+ }
369
+ toSub(text) {
370
+ let r = "";
371
+ for (const ch of text) r += _UnicodeRenderer.SUB_DIGITS[ch] ?? ch;
372
+ return r;
373
+ }
374
+ };
375
+
376
+ // src/mathml-renderer.ts
377
+ var MathMLRenderer = class {
378
+ id = "mathml";
379
+ outputFormat = "mathml";
380
+ render(root, options) {
381
+ const display = options?.displayMode ?? root.metadata?.displayMode ?? false;
382
+ const body = this.renderNode(root);
383
+ const mathml = display ? `<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">${body}</math>` : `<math xmlns="http://www.w3.org/1998/Math/MathML">${body}</math>`;
384
+ return { value: mathml, format: "mathml" };
385
+ }
386
+ renderNode(node) {
387
+ const n = node;
388
+ switch (n.kind) {
389
+ case "root":
390
+ return n.children.length === 1 ? this.renderNode(n.children[0]) : `<mrow>${n.children.map((c) => this.renderNode(c)).join("")}</mrow>`;
391
+ case "symbol": {
392
+ const value = this.escapeXml(n.value);
393
+ const greek = {
394
+ "\\alpha": "\u03B1",
395
+ "\\beta": "\u03B2",
396
+ "\\gamma": "\u03B3",
397
+ "\\delta": "\u03B4",
398
+ "\\pi": "\u03C0",
399
+ "\\theta": "\u03B8",
400
+ "\\lambda": "\u03BB",
401
+ "\\mu": "\u03BC",
402
+ "\\sigma": "\u03C3",
403
+ "\\omega": "\u03C9",
404
+ "\\infty": "\u221E",
405
+ "\\partial": "\u2202"
406
+ };
407
+ if (greek[n.value]) {
408
+ return `<mi>${greek[n.value]}</mi>`;
409
+ }
410
+ if (/^\d+(\.\d+)?$/.test(value)) {
411
+ return `<mn>${value}</mn>`;
412
+ }
413
+ return `<mi>${value}</mi>`;
414
+ }
415
+ case "frac":
416
+ return `<mfrac><mrow>${this.renderNode(n.numerator)}</mrow><mrow>${this.renderNode(n.denominator)}</mrow></mfrac>`;
417
+ case "script": {
418
+ let result = this.renderNode(n.base);
419
+ if (n.sub) {
420
+ result = `<msub>${result}<mrow>${this.renderNode(n.sub)}</mrow></msub>`;
421
+ }
422
+ if (n.super) {
423
+ result = `<msup>${result}<mrow>${this.renderNode(n.super)}</mrow></msup>`;
424
+ }
425
+ if (n.sub && n.super) {
426
+ result = `<msubsup>${this.renderNode(n.base)}<mrow>${this.renderNode(n.sub)}</mrow><mrow>${this.renderNode(n.super)}</mrow></msubsup>`;
427
+ }
428
+ return result;
429
+ }
430
+ case "radical": {
431
+ if (n.index) {
432
+ return `<mroot><mrow>${this.renderNode(n.radicand)}</mrow><mrow>${this.renderNode(n.index)}</mrow></mroot>`;
433
+ }
434
+ return `<msqrt><mrow>${this.renderNode(n.radicand)}</mrow></msqrt>`;
435
+ }
436
+ case "delimiter":
437
+ return `<mfenced open="${this.escapeXml(n.left)}" close="${this.escapeXml(n.right)}">${this.renderNode(n.body)}</mfenced>`;
438
+ case "matrix": {
439
+ const rows = n.rows.map((row) => `<mtr>${row.map((cell) => `<mtd>${this.renderNode(cell)}</mtd>`).join("")}</mtr>`).join("");
440
+ return `<mtable>${rows}</mtable>`;
441
+ }
442
+ case "text":
443
+ return `<mtext>${this.escapeXml(n.content)}</mtext>`;
444
+ case "space":
445
+ return `<mspace width="${n.width}em"/>`;
446
+ case "accent": {
447
+ const accentMap = {
448
+ hat: "^",
449
+ tilde: "~",
450
+ bar: "\xAF",
451
+ dot: "\u02D9",
452
+ ddot: "\xA8",
453
+ vec: "\u2192"
454
+ };
455
+ return `<mover accent="true"><mrow>${this.renderNode(n.base)}</mrow><mo>${accentMap[n.accentType] ?? "^"}</mo></mover>`;
456
+ }
457
+ case "largeop": {
458
+ let result = `<mo>${n.opType}</mo>`;
459
+ if (n.body) {
460
+ result = `<mrow>${result}<mrow>${this.renderNode(n.body)}</mrow></mrow>`;
461
+ }
462
+ if (n.lower) {
463
+ result = `<munder>${result}<mrow>${this.renderNode(n.lower)}</mrow></munder>`;
464
+ }
465
+ if (n.upper) {
466
+ result = `<mover>${result}<mrow>${this.renderNode(n.upper)}</mrow></mover>`;
467
+ }
468
+ return result;
469
+ }
470
+ case "group":
471
+ return `<mrow>${n.children.map((c) => this.renderNode(c)).join("")}</mrow>`;
472
+ case "chem-element":
473
+ return `<mi mathvariant="normal">${this.escapeXml(n.symbol)}</mi>` + (n.count ? `<mn>${n.count}</mn>` : "");
474
+ default:
475
+ return "";
476
+ }
477
+ }
478
+ escapeXml(text) {
479
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
480
+ }
481
+ };
482
+ // Annotate the CommonJS export names for ESM import in node:
483
+ 0 && (module.exports = {
484
+ KatexRenderer,
485
+ LatexReverseRenderer,
486
+ MathMLRenderer,
487
+ RendererRegistry,
488
+ UnicodeRenderer
489
+ });
@@ -0,0 +1,151 @@
1
+ import { ExpressionRoot, ExpressionNode, SourceLocation } from '@sciexpr/core';
2
+
3
+ /** 渲染输出格式 */
4
+ type RenderOutputFormat = 'html' | 'mathml' | 'latex' | 'unicode' | 'omml' | 'svg';
5
+ /** 渲染结果 */
6
+ interface RenderResult {
7
+ /** 渲染输出值 */
8
+ value: string;
9
+ /** 输出格式 */
10
+ format: RenderOutputFormat;
11
+ /** 警告信息 */
12
+ warnings?: RenderWarning[];
13
+ }
14
+ /** 渲染警告 */
15
+ interface RenderWarning {
16
+ message: string;
17
+ code: string;
18
+ }
19
+ /** 渲染错误 */
20
+ interface RenderError {
21
+ message: string;
22
+ node?: ExpressionNode;
23
+ source?: SourceLocation;
24
+ }
25
+ /** 渲染钩子 */
26
+ interface RenderHooks {
27
+ /** 渲染前转换 AST */
28
+ beforeRender?: (node: ExpressionNode) => ExpressionNode;
29
+ /** 渲染后处理结果 */
30
+ afterRender?: (result: RenderResult) => RenderResult;
31
+ /** 错误回调 */
32
+ onError?: (error: RenderError) => void;
33
+ }
34
+ /** 渲染选项 */
35
+ interface RenderOptions {
36
+ /** 是否为 display 模式(块级公式) */
37
+ displayMode?: boolean;
38
+ /** 错误时是否抛异常 */
39
+ throwOnError?: boolean;
40
+ /** 自定义宏 */
41
+ macros?: Record<string, string>;
42
+ /** 渲染钩子 */
43
+ hooks?: RenderHooks;
44
+ }
45
+ /**
46
+ * 渲染器接口
47
+ *
48
+ * 所有渲染适配器的统一接口。
49
+ * 实现此接口即可将 AST 渲染为特定输出格式。
50
+ */
51
+ interface IRenderer {
52
+ /** 渲染器唯一标识 */
53
+ readonly id: string;
54
+ /** 输出格式 */
55
+ readonly outputFormat: RenderOutputFormat;
56
+ /**
57
+ * 渲染 AST 为最终格式
58
+ * @param root AST 根节点
59
+ * @param options 渲染选项
60
+ */
61
+ render(root: ExpressionRoot, options?: RenderOptions): RenderResult;
62
+ }
63
+ /**
64
+ * 渲染器注册表
65
+ * 管理所有渲染器实例,支持按格式查找和注册自定义渲染器
66
+ */
67
+ declare class RendererRegistry {
68
+ private renderers;
69
+ /** 注册渲染器 */
70
+ register(renderer: IRenderer): void;
71
+ /** 移除渲染器 */
72
+ unregister(id: string): boolean;
73
+ /** 按 ID 获取 */
74
+ get(id: string): IRenderer | undefined;
75
+ /** 按输出格式查找 */
76
+ findByFormat(format: RenderOutputFormat): IRenderer | undefined;
77
+ /**
78
+ * 渲染 AST
79
+ * @param root AST 根节点
80
+ * @param format 目标输出格式
81
+ * @param options 渲染选项
82
+ */
83
+ render(root: ExpressionRoot, format: RenderOutputFormat, options?: RenderOptions): RenderResult;
84
+ /** 列出所有已注册格式 */
85
+ listFormats(): RenderOutputFormat[];
86
+ }
87
+
88
+ /**
89
+ * AST → LaTeX 反向渲染器
90
+ *
91
+ * 将内部 AST 还原为 LaTeX 源码。
92
+ * 支持所有 AST 节点类型的双向转换。
93
+ */
94
+ declare class LatexReverseRenderer implements IRenderer {
95
+ readonly id = "latex-reverse";
96
+ readonly outputFormat: "latex";
97
+ render(root: ExpressionRoot, _options?: RenderOptions): RenderResult;
98
+ private renderNode;
99
+ }
100
+
101
+ /**
102
+ * KaTeX HTML 渲染器
103
+ *
104
+ * 将 AST 先还原为 LaTeX,再通过 KaTeX 渲染为 HTML。
105
+ * KaTeX 为 peerDependency,使用方需自行安装。
106
+ */
107
+ declare class KatexRenderer implements IRenderer {
108
+ readonly id = "katex";
109
+ readonly outputFormat: "html";
110
+ private latexRenderer;
111
+ private katexInstance;
112
+ constructor();
113
+ private loadKatex;
114
+ /** 手动设置 KaTeX 实例 */
115
+ setKatex(katex: any): void;
116
+ /** 检查 KaTeX 是否可用 */
117
+ isAvailable(): boolean;
118
+ render(root: ExpressionRoot, options?: RenderOptions): RenderResult;
119
+ private escapeHtml;
120
+ }
121
+
122
+ /**
123
+ * Unicode 纯文本渲染器
124
+ */
125
+ declare class UnicodeRenderer implements IRenderer {
126
+ readonly id = "unicode";
127
+ readonly outputFormat: "unicode";
128
+ private static readonly GREEK_MAP;
129
+ private static readonly SUPER_DIGITS;
130
+ private static readonly SUB_DIGITS;
131
+ render(root: ExpressionRoot, _options?: RenderOptions): RenderResult;
132
+ private renderNode;
133
+ private toSuper;
134
+ private toSub;
135
+ }
136
+
137
+ /**
138
+ * MathML 渲染器
139
+ *
140
+ * 将 AST 渲染为 MathML 3.0 标准格式。
141
+ * 适用于无障碍场景、Word 导入和需要语义化公式的场合。
142
+ */
143
+ declare class MathMLRenderer implements IRenderer {
144
+ readonly id = "mathml";
145
+ readonly outputFormat: "mathml";
146
+ render(root: ExpressionRoot, options?: RenderOptions): RenderResult;
147
+ private renderNode;
148
+ private escapeXml;
149
+ }
150
+
151
+ export { type IRenderer, KatexRenderer, LatexReverseRenderer, MathMLRenderer, type RenderError, type RenderHooks, type RenderOptions, type RenderOutputFormat, type RenderResult, type RenderWarning, RendererRegistry, UnicodeRenderer };
@@ -0,0 +1,151 @@
1
+ import { ExpressionRoot, ExpressionNode, SourceLocation } from '@sciexpr/core';
2
+
3
+ /** 渲染输出格式 */
4
+ type RenderOutputFormat = 'html' | 'mathml' | 'latex' | 'unicode' | 'omml' | 'svg';
5
+ /** 渲染结果 */
6
+ interface RenderResult {
7
+ /** 渲染输出值 */
8
+ value: string;
9
+ /** 输出格式 */
10
+ format: RenderOutputFormat;
11
+ /** 警告信息 */
12
+ warnings?: RenderWarning[];
13
+ }
14
+ /** 渲染警告 */
15
+ interface RenderWarning {
16
+ message: string;
17
+ code: string;
18
+ }
19
+ /** 渲染错误 */
20
+ interface RenderError {
21
+ message: string;
22
+ node?: ExpressionNode;
23
+ source?: SourceLocation;
24
+ }
25
+ /** 渲染钩子 */
26
+ interface RenderHooks {
27
+ /** 渲染前转换 AST */
28
+ beforeRender?: (node: ExpressionNode) => ExpressionNode;
29
+ /** 渲染后处理结果 */
30
+ afterRender?: (result: RenderResult) => RenderResult;
31
+ /** 错误回调 */
32
+ onError?: (error: RenderError) => void;
33
+ }
34
+ /** 渲染选项 */
35
+ interface RenderOptions {
36
+ /** 是否为 display 模式(块级公式) */
37
+ displayMode?: boolean;
38
+ /** 错误时是否抛异常 */
39
+ throwOnError?: boolean;
40
+ /** 自定义宏 */
41
+ macros?: Record<string, string>;
42
+ /** 渲染钩子 */
43
+ hooks?: RenderHooks;
44
+ }
45
+ /**
46
+ * 渲染器接口
47
+ *
48
+ * 所有渲染适配器的统一接口。
49
+ * 实现此接口即可将 AST 渲染为特定输出格式。
50
+ */
51
+ interface IRenderer {
52
+ /** 渲染器唯一标识 */
53
+ readonly id: string;
54
+ /** 输出格式 */
55
+ readonly outputFormat: RenderOutputFormat;
56
+ /**
57
+ * 渲染 AST 为最终格式
58
+ * @param root AST 根节点
59
+ * @param options 渲染选项
60
+ */
61
+ render(root: ExpressionRoot, options?: RenderOptions): RenderResult;
62
+ }
63
+ /**
64
+ * 渲染器注册表
65
+ * 管理所有渲染器实例,支持按格式查找和注册自定义渲染器
66
+ */
67
+ declare class RendererRegistry {
68
+ private renderers;
69
+ /** 注册渲染器 */
70
+ register(renderer: IRenderer): void;
71
+ /** 移除渲染器 */
72
+ unregister(id: string): boolean;
73
+ /** 按 ID 获取 */
74
+ get(id: string): IRenderer | undefined;
75
+ /** 按输出格式查找 */
76
+ findByFormat(format: RenderOutputFormat): IRenderer | undefined;
77
+ /**
78
+ * 渲染 AST
79
+ * @param root AST 根节点
80
+ * @param format 目标输出格式
81
+ * @param options 渲染选项
82
+ */
83
+ render(root: ExpressionRoot, format: RenderOutputFormat, options?: RenderOptions): RenderResult;
84
+ /** 列出所有已注册格式 */
85
+ listFormats(): RenderOutputFormat[];
86
+ }
87
+
88
+ /**
89
+ * AST → LaTeX 反向渲染器
90
+ *
91
+ * 将内部 AST 还原为 LaTeX 源码。
92
+ * 支持所有 AST 节点类型的双向转换。
93
+ */
94
+ declare class LatexReverseRenderer implements IRenderer {
95
+ readonly id = "latex-reverse";
96
+ readonly outputFormat: "latex";
97
+ render(root: ExpressionRoot, _options?: RenderOptions): RenderResult;
98
+ private renderNode;
99
+ }
100
+
101
+ /**
102
+ * KaTeX HTML 渲染器
103
+ *
104
+ * 将 AST 先还原为 LaTeX,再通过 KaTeX 渲染为 HTML。
105
+ * KaTeX 为 peerDependency,使用方需自行安装。
106
+ */
107
+ declare class KatexRenderer implements IRenderer {
108
+ readonly id = "katex";
109
+ readonly outputFormat: "html";
110
+ private latexRenderer;
111
+ private katexInstance;
112
+ constructor();
113
+ private loadKatex;
114
+ /** 手动设置 KaTeX 实例 */
115
+ setKatex(katex: any): void;
116
+ /** 检查 KaTeX 是否可用 */
117
+ isAvailable(): boolean;
118
+ render(root: ExpressionRoot, options?: RenderOptions): RenderResult;
119
+ private escapeHtml;
120
+ }
121
+
122
+ /**
123
+ * Unicode 纯文本渲染器
124
+ */
125
+ declare class UnicodeRenderer implements IRenderer {
126
+ readonly id = "unicode";
127
+ readonly outputFormat: "unicode";
128
+ private static readonly GREEK_MAP;
129
+ private static readonly SUPER_DIGITS;
130
+ private static readonly SUB_DIGITS;
131
+ render(root: ExpressionRoot, _options?: RenderOptions): RenderResult;
132
+ private renderNode;
133
+ private toSuper;
134
+ private toSub;
135
+ }
136
+
137
+ /**
138
+ * MathML 渲染器
139
+ *
140
+ * 将 AST 渲染为 MathML 3.0 标准格式。
141
+ * 适用于无障碍场景、Word 导入和需要语义化公式的场合。
142
+ */
143
+ declare class MathMLRenderer implements IRenderer {
144
+ readonly id = "mathml";
145
+ readonly outputFormat: "mathml";
146
+ render(root: ExpressionRoot, options?: RenderOptions): RenderResult;
147
+ private renderNode;
148
+ private escapeXml;
149
+ }
150
+
151
+ export { type IRenderer, KatexRenderer, LatexReverseRenderer, MathMLRenderer, type RenderError, type RenderHooks, type RenderOptions, type RenderOutputFormat, type RenderResult, type RenderWarning, RendererRegistry, UnicodeRenderer };
package/dist/index.js ADDED
@@ -0,0 +1,448 @@
1
+ // src/types.ts
2
+ var RendererRegistry = class {
3
+ renderers = /* @__PURE__ */ new Map();
4
+ /** 注册渲染器 */
5
+ register(renderer) {
6
+ this.renderers.set(renderer.id, renderer);
7
+ }
8
+ /** 移除渲染器 */
9
+ unregister(id) {
10
+ return this.renderers.delete(id);
11
+ }
12
+ /** 按 ID 获取 */
13
+ get(id) {
14
+ return this.renderers.get(id);
15
+ }
16
+ /** 按输出格式查找 */
17
+ findByFormat(format) {
18
+ for (const r of this.renderers.values()) {
19
+ if (r.outputFormat === format) return r;
20
+ }
21
+ return void 0;
22
+ }
23
+ /**
24
+ * 渲染 AST
25
+ * @param root AST 根节点
26
+ * @param format 目标输出格式
27
+ * @param options 渲染选项
28
+ */
29
+ render(root, format, options) {
30
+ const renderer = this.findByFormat(format);
31
+ if (!renderer) {
32
+ throw new Error(`No renderer registered for format: ${format}`);
33
+ }
34
+ return renderer.render(root, options);
35
+ }
36
+ /** 列出所有已注册格式 */
37
+ listFormats() {
38
+ return Array.from(this.renderers.values()).map((r) => r.outputFormat);
39
+ }
40
+ };
41
+
42
+ // src/latex-renderer.ts
43
+ var LatexReverseRenderer = class {
44
+ id = "latex-reverse";
45
+ outputFormat = "latex";
46
+ render(root, _options) {
47
+ const latex = this.renderNode(root);
48
+ return { value: latex, format: "latex" };
49
+ }
50
+ renderNode(node) {
51
+ const n = node;
52
+ switch (n.kind) {
53
+ case "root":
54
+ return n.children.map((c) => this.renderNode(c)).join("");
55
+ case "symbol":
56
+ return n.value;
57
+ case "frac":
58
+ return `\\frac{${this.renderNode(n.numerator)}}{${this.renderNode(n.denominator)}}`;
59
+ case "script": {
60
+ let result = this.renderNode(n.base);
61
+ if (n.sub) {
62
+ result += `_{${this.renderNode(n.sub)}}`;
63
+ }
64
+ if (n.super) {
65
+ result += `^{${this.renderNode(n.super)}}`;
66
+ }
67
+ return result;
68
+ }
69
+ case "radical": {
70
+ if (n.index) {
71
+ return `\\sqrt[${this.renderNode(n.index)}]{${this.renderNode(n.radicand)}}`;
72
+ }
73
+ return `\\sqrt{${this.renderNode(n.radicand)}}`;
74
+ }
75
+ case "delimiter":
76
+ return `${n.left}${this.renderNode(n.body)}${n.right}`;
77
+ case "matrix": {
78
+ const cols = n.columnAlign ? `{${n.columnAlign.join("")}}` : "";
79
+ const type = n.matrixType ?? "matrix";
80
+ const rows = n.rows.map((row) => row.map((cell) => this.renderNode(cell)).join(" & ")).join(" \\\\ ");
81
+ return `\\begin{${type}}${cols}${rows}\\end{${type}}`;
82
+ }
83
+ case "text":
84
+ return `\\text{${n.content}}`;
85
+ case "space":
86
+ return n.width === 1 ? "\\," : "\\quad";
87
+ case "accent":
88
+ return `\\${n.accentType}{${this.renderNode(n.base)}}`;
89
+ case "largeop": {
90
+ let result = `\\${n.opType}`;
91
+ if (n.lower) {
92
+ result += `_{${this.renderNode(n.lower)}}`;
93
+ }
94
+ if (n.upper) {
95
+ result += `^{${this.renderNode(n.upper)}}`;
96
+ }
97
+ if (n.body) {
98
+ result += `{${this.renderNode(n.body)}}`;
99
+ }
100
+ return result;
101
+ }
102
+ case "color":
103
+ return `\\color{${n.color}}{${this.renderNode(n.body)}}`;
104
+ case "group":
105
+ return `{${n.children.map((c) => this.renderNode(c)).join("")}}`;
106
+ case "chem-element": {
107
+ let result = n.symbol;
108
+ if (n.count && n.count > 1) {
109
+ result += n.count;
110
+ }
111
+ if (n.charge) {
112
+ result += `^{${n.charge}${n.chargeSign ?? "+"}}`;
113
+ }
114
+ return result;
115
+ }
116
+ case "chem-bond": {
117
+ const bondMap = {
118
+ single: "-",
119
+ double: "=",
120
+ triple: "\\equiv ",
121
+ aromatic: "~",
122
+ coordinate: "->"
123
+ };
124
+ return `${this.renderNode(n.left)}${bondMap[n.bondType] ?? "-"}${this.renderNode(n.right)}`;
125
+ }
126
+ case "chem-reaction": {
127
+ const arrowMap = {
128
+ "->": "->",
129
+ "<-": "<-",
130
+ "<=>": "<=>",
131
+ "<->": "<->",
132
+ "->[": "->[",
133
+ "<-[": "<-[",
134
+ "<=>>": "<=>>",
135
+ "<<=>": "<<=>"
136
+ };
137
+ const react = n.reactants.map((r) => this.renderNode(r)).join(" + ");
138
+ const prod = n.products.map((p) => this.renderNode(p)).join(" + ");
139
+ return `\\ce{${react} ${arrowMap[n.arrowType] ?? "->"} ${prod}}`;
140
+ }
141
+ case "chem-group":
142
+ return n.atoms.map((a) => this.renderNode(a)).join("");
143
+ default:
144
+ return "";
145
+ }
146
+ }
147
+ };
148
+
149
+ // src/katex-renderer.ts
150
+ var KatexRenderer = class {
151
+ id = "katex";
152
+ outputFormat = "html";
153
+ latexRenderer = new LatexReverseRenderer();
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ katexInstance = null;
156
+ constructor() {
157
+ this.loadKatex();
158
+ }
159
+ async loadKatex() {
160
+ try {
161
+ this.katexInstance = await import("katex");
162
+ } catch {
163
+ this.katexInstance = null;
164
+ }
165
+ }
166
+ /** 手动设置 KaTeX 实例 */
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ setKatex(katex) {
169
+ this.katexInstance = katex;
170
+ }
171
+ /** 检查 KaTeX 是否可用 */
172
+ isAvailable() {
173
+ return this.katexInstance !== null;
174
+ }
175
+ render(root, options) {
176
+ if (!this.katexInstance) {
177
+ const latex = this.latexRenderer.render(root).value;
178
+ return {
179
+ value: `<code class="sciexpr-latex-fallback">${this.escapeHtml(latex)}</code>`,
180
+ format: "html",
181
+ warnings: [{ message: "KaTeX not available, showing LaTeX source", code: "KATEX_UNAVAILABLE" }]
182
+ };
183
+ }
184
+ const latexSource = this.latexRenderer.render(root).value;
185
+ try {
186
+ const html = this.katexInstance.renderToString(latexSource, {
187
+ displayMode: options?.displayMode ?? root.metadata?.displayMode ?? false,
188
+ throwOnError: options?.throwOnError ?? false,
189
+ macros: options?.macros,
190
+ trust: true
191
+ });
192
+ return { value: html, format: "html" };
193
+ } catch (err) {
194
+ const error = {
195
+ message: err instanceof Error ? err.message : String(err)
196
+ };
197
+ options?.hooks?.onError?.(error);
198
+ if (options?.throwOnError) {
199
+ throw error;
200
+ }
201
+ return {
202
+ value: `<span class="sciexpr-error">${this.escapeHtml(latexSource)}</span>`,
203
+ format: "html",
204
+ warnings: [{ message: error.message, code: "KATEX_RENDER_ERROR" }]
205
+ };
206
+ }
207
+ }
208
+ escapeHtml(text) {
209
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
210
+ }
211
+ };
212
+
213
+ // src/unicode-renderer.ts
214
+ var UnicodeRenderer = class _UnicodeRenderer {
215
+ id = "unicode";
216
+ outputFormat = "unicode";
217
+ static GREEK_MAP = {
218
+ "\\alpha": "\u03B1",
219
+ "\\beta": "\u03B2",
220
+ "\\gamma": "\u03B3",
221
+ "\\delta": "\u03B4",
222
+ "\\pi": "\u03C0",
223
+ "\\theta": "\u03B8",
224
+ "\\lambda": "\u03BB",
225
+ "\\mu": "\u03BC",
226
+ "\\sigma": "\u03C3",
227
+ "\\omega": "\u03C9",
228
+ "\\infty": "\u221E",
229
+ "\\partial": "\u2202",
230
+ "\\nabla": "\u2207",
231
+ "\\forall": "\u2200",
232
+ "\\exists": "\u2203",
233
+ "\\times": "\xD7",
234
+ "\\pm": "\xB1",
235
+ "\\neq": "\u2260",
236
+ "\\leq": "\u2264",
237
+ "\\geq": "\u2265",
238
+ "\\approx": "\u2248",
239
+ "\\to": "\u2192",
240
+ "\\Rightarrow": "\u21D2",
241
+ "\\int": "\u222B",
242
+ "\\sum": "\u2211"
243
+ };
244
+ static SUPER_DIGITS = {
245
+ "0": "\u2070",
246
+ "1": "\xB9",
247
+ "2": "\xB2",
248
+ "3": "\xB3",
249
+ "4": "\u2074",
250
+ "5": "\u2075",
251
+ "6": "\u2076",
252
+ "7": "\u2077",
253
+ "8": "\u2078",
254
+ "9": "\u2079",
255
+ "+": "\u207A",
256
+ "-": "\u207B"
257
+ };
258
+ static SUB_DIGITS = {
259
+ "0": "\u2080",
260
+ "1": "\u2081",
261
+ "2": "\u2082",
262
+ "3": "\u2083",
263
+ "4": "\u2084",
264
+ "5": "\u2085",
265
+ "6": "\u2086",
266
+ "7": "\u2087",
267
+ "8": "\u2088",
268
+ "9": "\u2089",
269
+ "+": "\u208A",
270
+ "-": "\u208B"
271
+ };
272
+ render(root, _options) {
273
+ const unicode = this.renderNode(root);
274
+ return { value: unicode, format: "unicode" };
275
+ }
276
+ renderNode(node) {
277
+ const n = node;
278
+ switch (n.kind) {
279
+ case "root":
280
+ return n.children.map((c) => this.renderNode(c)).join("");
281
+ case "symbol":
282
+ return _UnicodeRenderer.GREEK_MAP[n.value] ?? n.value;
283
+ case "frac":
284
+ return `(${this.renderNode(n.numerator)})/(${this.renderNode(n.denominator)})`;
285
+ case "script": {
286
+ let result = this.renderNode(n.base);
287
+ if (n.sub) result += this.toSub(this.renderNode(n.sub));
288
+ if (n.super) result += this.toSuper(this.renderNode(n.super));
289
+ return result;
290
+ }
291
+ case "radical":
292
+ if (n.index) {
293
+ return `${this.toSuper(this.renderNode(n.index))}\u221A(${this.renderNode(n.radicand)})`;
294
+ }
295
+ return `\u221A(${this.renderNode(n.radicand)})`;
296
+ case "delimiter":
297
+ return n.left + this.renderNode(n.body) + n.right;
298
+ case "text":
299
+ return n.content;
300
+ case "space":
301
+ return " ";
302
+ case "accent":
303
+ return this.renderNode(n.base);
304
+ case "largeop":
305
+ return this.renderNode(n.body ?? { kind: "text", content: "" });
306
+ case "group":
307
+ return n.children.map((c) => this.renderNode(c)).join("");
308
+ case "chem-element": {
309
+ let result = n.symbol;
310
+ if (n.count && n.count > 1) result += this.toSub(String(n.count));
311
+ if (n.charge) result += this.toSuper(`${n.charge}${n.chargeSign ?? "+"}`);
312
+ return result;
313
+ }
314
+ case "chem-bond":
315
+ return this.renderNode(n.left) + "-" + this.renderNode(n.right);
316
+ case "chem-reaction":
317
+ return n.reactants.map((r) => this.renderNode(r)).join(" + ") + " \u2192 " + n.products.map((p) => this.renderNode(p)).join(" + ");
318
+ case "chem-group":
319
+ return n.atoms.map((a) => this.renderNode(a)).join("");
320
+ default:
321
+ return "";
322
+ }
323
+ }
324
+ toSuper(text) {
325
+ let r = "";
326
+ for (const ch of text) r += _UnicodeRenderer.SUPER_DIGITS[ch] ?? ch;
327
+ return r;
328
+ }
329
+ toSub(text) {
330
+ let r = "";
331
+ for (const ch of text) r += _UnicodeRenderer.SUB_DIGITS[ch] ?? ch;
332
+ return r;
333
+ }
334
+ };
335
+
336
+ // src/mathml-renderer.ts
337
+ var MathMLRenderer = class {
338
+ id = "mathml";
339
+ outputFormat = "mathml";
340
+ render(root, options) {
341
+ const display = options?.displayMode ?? root.metadata?.displayMode ?? false;
342
+ const body = this.renderNode(root);
343
+ const mathml = display ? `<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">${body}</math>` : `<math xmlns="http://www.w3.org/1998/Math/MathML">${body}</math>`;
344
+ return { value: mathml, format: "mathml" };
345
+ }
346
+ renderNode(node) {
347
+ const n = node;
348
+ switch (n.kind) {
349
+ case "root":
350
+ return n.children.length === 1 ? this.renderNode(n.children[0]) : `<mrow>${n.children.map((c) => this.renderNode(c)).join("")}</mrow>`;
351
+ case "symbol": {
352
+ const value = this.escapeXml(n.value);
353
+ const greek = {
354
+ "\\alpha": "\u03B1",
355
+ "\\beta": "\u03B2",
356
+ "\\gamma": "\u03B3",
357
+ "\\delta": "\u03B4",
358
+ "\\pi": "\u03C0",
359
+ "\\theta": "\u03B8",
360
+ "\\lambda": "\u03BB",
361
+ "\\mu": "\u03BC",
362
+ "\\sigma": "\u03C3",
363
+ "\\omega": "\u03C9",
364
+ "\\infty": "\u221E",
365
+ "\\partial": "\u2202"
366
+ };
367
+ if (greek[n.value]) {
368
+ return `<mi>${greek[n.value]}</mi>`;
369
+ }
370
+ if (/^\d+(\.\d+)?$/.test(value)) {
371
+ return `<mn>${value}</mn>`;
372
+ }
373
+ return `<mi>${value}</mi>`;
374
+ }
375
+ case "frac":
376
+ return `<mfrac><mrow>${this.renderNode(n.numerator)}</mrow><mrow>${this.renderNode(n.denominator)}</mrow></mfrac>`;
377
+ case "script": {
378
+ let result = this.renderNode(n.base);
379
+ if (n.sub) {
380
+ result = `<msub>${result}<mrow>${this.renderNode(n.sub)}</mrow></msub>`;
381
+ }
382
+ if (n.super) {
383
+ result = `<msup>${result}<mrow>${this.renderNode(n.super)}</mrow></msup>`;
384
+ }
385
+ if (n.sub && n.super) {
386
+ result = `<msubsup>${this.renderNode(n.base)}<mrow>${this.renderNode(n.sub)}</mrow><mrow>${this.renderNode(n.super)}</mrow></msubsup>`;
387
+ }
388
+ return result;
389
+ }
390
+ case "radical": {
391
+ if (n.index) {
392
+ return `<mroot><mrow>${this.renderNode(n.radicand)}</mrow><mrow>${this.renderNode(n.index)}</mrow></mroot>`;
393
+ }
394
+ return `<msqrt><mrow>${this.renderNode(n.radicand)}</mrow></msqrt>`;
395
+ }
396
+ case "delimiter":
397
+ return `<mfenced open="${this.escapeXml(n.left)}" close="${this.escapeXml(n.right)}">${this.renderNode(n.body)}</mfenced>`;
398
+ case "matrix": {
399
+ const rows = n.rows.map((row) => `<mtr>${row.map((cell) => `<mtd>${this.renderNode(cell)}</mtd>`).join("")}</mtr>`).join("");
400
+ return `<mtable>${rows}</mtable>`;
401
+ }
402
+ case "text":
403
+ return `<mtext>${this.escapeXml(n.content)}</mtext>`;
404
+ case "space":
405
+ return `<mspace width="${n.width}em"/>`;
406
+ case "accent": {
407
+ const accentMap = {
408
+ hat: "^",
409
+ tilde: "~",
410
+ bar: "\xAF",
411
+ dot: "\u02D9",
412
+ ddot: "\xA8",
413
+ vec: "\u2192"
414
+ };
415
+ return `<mover accent="true"><mrow>${this.renderNode(n.base)}</mrow><mo>${accentMap[n.accentType] ?? "^"}</mo></mover>`;
416
+ }
417
+ case "largeop": {
418
+ let result = `<mo>${n.opType}</mo>`;
419
+ if (n.body) {
420
+ result = `<mrow>${result}<mrow>${this.renderNode(n.body)}</mrow></mrow>`;
421
+ }
422
+ if (n.lower) {
423
+ result = `<munder>${result}<mrow>${this.renderNode(n.lower)}</mrow></munder>`;
424
+ }
425
+ if (n.upper) {
426
+ result = `<mover>${result}<mrow>${this.renderNode(n.upper)}</mrow></mover>`;
427
+ }
428
+ return result;
429
+ }
430
+ case "group":
431
+ return `<mrow>${n.children.map((c) => this.renderNode(c)).join("")}</mrow>`;
432
+ case "chem-element":
433
+ return `<mi mathvariant="normal">${this.escapeXml(n.symbol)}</mi>` + (n.count ? `<mn>${n.count}</mn>` : "");
434
+ default:
435
+ return "";
436
+ }
437
+ }
438
+ escapeXml(text) {
439
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
440
+ }
441
+ };
442
+ export {
443
+ KatexRenderer,
444
+ LatexReverseRenderer,
445
+ MathMLRenderer,
446
+ RendererRegistry,
447
+ UnicodeRenderer
448
+ };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@sciexpr/renderer",
3
+ "version": "0.1.0",
4
+ "description": "Scientific Expression Engine - Multi-format renderer (KaTeX, Unicode, MathML, LaTeX)",
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
+ "renderer",
25
+ "katex",
26
+ "mathml",
27
+ "unicode"
28
+ ],
29
+ "license": "MIT",
30
+ "author": {
31
+ "name": "haoguodong09-svg",
32
+ "email": "haoguodong09@gmail.com"
33
+ },
34
+ "homepage": "https://github.com/haoguodong09-svg/scientific-expression-engine#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/haoguodong09-svg/scientific-expression-engine/issues"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/haoguodong09-svg/scientific-expression-engine",
41
+ "directory": "packages/renderer"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "@sciexpr/core": "0.1.0"
48
+ },
49
+ "peerDependencies": {
50
+ "katex": "^0.16.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "katex": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "devDependencies": {
58
+ "@types/katex": "^0.16.0",
59
+ "katex": "^0.16.47",
60
+ "rimraf": "^5.0.0",
61
+ "tsup": "^8.0.0",
62
+ "typescript": "^5.5.0",
63
+ "vitest": "^2.0.0"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup",
67
+ "dev": "tsup --watch",
68
+ "lint": "tsc --noEmit",
69
+ "clean": "rimraf dist",
70
+ "test": "vitest run"
71
+ }
72
+ }