smartrte-core 1.0.0-beta.1 → 1.0.0-beta.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-beta.2
4
+
5
+ - Add an opt-in `renderFormulaHtml` option to `serializeCanonicalListHtml` that bakes real KaTeX-rendered HTML into exported formula elements, instead of leaving an empty placeholder — for consumers (e.g. a read-only preview) that display this HTML directly without also running KaTeX against it themselves. Off by default; no change for existing consumers.
6
+
3
7
  ## 1.0.0-beta.1
4
8
 
5
9
  **BREAKING CHANGE.** This is the first release built on the canonical, schema-driven document model. Every published version through 0.2.1 was built on the older discriminated-union model; none of the work below has shipped before.
@@ -1,5 +1,8 @@
1
+ import "katex/contrib/mhchem";
1
2
  import type { SmartElementNode } from "../types.js";
2
- export declare const atomToHtml: (node: SmartElementNode) => string;
3
+ export declare const atomToHtml: (node: SmartElementNode, options?: {
4
+ renderFormulaHtml?: boolean;
5
+ }) => string;
3
6
  export declare const atomFromHtmlElement: (element: Element) => SmartElementNode | null;
4
7
  export declare const atomToMarkdown: (node: SmartElementNode) => string;
5
8
  export interface AtomDocxRun {
@@ -1,16 +1,44 @@
1
+ import katex from "katex";
2
+ // Registers \ce{...} chemistry-equation notation on the shared katex module
3
+ // instance, matching surface/renderer.ts's own live-render import - a
4
+ // formula baked here via renderFormulaHtml must support the exact same
5
+ // notation the live editor does, or a chemistry entry would render live but
6
+ // fall back to plain text in the exported/baked HTML.
7
+ import "katex/contrib/mhchem";
1
8
  import { createNodeId } from "../identity.js";
2
9
  import { sanitizeAtomSource } from "./security.js";
3
10
  const escape = (value) => String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4
11
  const attr = (name, value) => value === undefined ? "" : ` ${name}="${escape(value)}"`;
5
12
  const dimensions = (node) => `${attr("width", node.attrs?.width)}${attr("height", node.attrs?.height)}`;
6
- export const atomToHtml = (node) => {
13
+ // Mirrors surface/renderer.ts's KATEX_OPTIONS and try/catch-to-plain-text
14
+ // fallback exactly - a formula that fails to render live must fail the same
15
+ // way when baked into exported HTML, not throw and abort the whole export.
16
+ const KATEX_OPTIONS = { trust: false, strict: "error" };
17
+ const renderFormulaToHtml = (source) => {
18
+ try {
19
+ return katex.renderToString(source, KATEX_OPTIONS);
20
+ }
21
+ catch {
22
+ return escape(source);
23
+ }
24
+ };
25
+ export const atomToHtml = (node, options) => {
7
26
  if (node.type === "image" || node.type === "block_image") {
8
27
  const src = sanitizeAtomSource(String(node.attrs?.src || ""), { kind: "image", allowBlobPreview: node.attrs?.status === "pending" }) || "";
9
28
  return `<img data-smart-id="${escape(node.id)}" data-smart-type="${node.type}" src="${escape(src)}" alt="${escape(node.attrs?.alt)}"${dimensions(node)}${attr("data-smart-status", node.attrs?.status || "ready")}${node.attrs?.decorative === true ? ' data-smart-decorative="true"' : ""}${attr("data-smart-align", node.attrs?.align)}>`;
10
29
  }
11
30
  if (node.type === "formula" || node.type === "block_formula") {
12
31
  const tag = node.type === "formula" ? "span" : "div";
13
- return `<${tag} data-smart-id="${escape(node.id)}" data-smart-type="${node.type}" data-smart-notation="${escape(node.attrs?.notation || "latex")}" data-smart-formula="${escape(node.attrs?.source)}" role="math" aria-label="${escape(`Mathematical formula: ${String(node.attrs?.source || "")}`)}"></${tag}>`;
32
+ const source = String(node.attrs?.source || "");
33
+ // The rendered KaTeX HTML is purely a self-contained visual convenience
34
+ // for consumers that display this HTML string directly without also
35
+ // running katex.render() against it themselves (docs/bugs/
36
+ // formula-not-rendered-in-static-html-consumers.md) - data-smart-formula
37
+ // stays the single source of truth for round-tripping back into the
38
+ // model (list/formats.ts's parser reads the attribute, never this
39
+ // inner content), so baking this in can never desync source vs. render.
40
+ const inner = options?.renderFormulaHtml ? renderFormulaToHtml(source) : "";
41
+ return `<${tag} data-smart-id="${escape(node.id)}" data-smart-type="${node.type}" data-smart-notation="${escape(node.attrs?.notation || "latex")}" data-smart-formula="${escape(source)}" role="math" aria-label="${escape(`Mathematical formula: ${source}`)}">${inner}</${tag}>`;
14
42
  }
15
43
  if (node.type === "video" || node.type === "audio") {
16
44
  const src = sanitizeAtomSource(String(node.attrs?.src || ""), { kind: node.type }) || "";
@@ -2,6 +2,7 @@ import type { SmartDocument } from "../types.js";
2
2
  export declare const serializeCanonicalListHtml: (document: SmartDocument, options?: {
3
3
  clean?: boolean;
4
4
  fragment?: boolean;
5
+ renderFormulaHtml?: boolean;
5
6
  }) => string;
6
7
  export declare const parseCanonicalListHtml: (html: string) => SmartDocument;
7
8
  /** Alignment and indent are unsupported in Markdown; block content survives semantically. */
@@ -20,12 +20,14 @@ const isEditorUiNode = (node) => attr(node, "data-smart-ui") !== undefined || at
20
20
  * parseBlockList below for how they're unwrapped instead of swallowed.
21
21
  */
22
22
  const TRANSPARENT_CONTAINER_TAGS = ["div", "section", "article", "figure"];
23
+ /** See serializeCanonicalListHtml's own doc comment for why this is a module-scoped flag rather than a threaded parameter. */
24
+ let renderFormulaHtmlMode = false;
23
25
  const serializeInline = (node) => {
24
26
  if (!isTextNode(node)) {
25
27
  if (node.type === "hard_break")
26
28
  return `<br data-smart-id="${escapeHtml(node.id)}" data-smart-type="hard_break">`;
27
29
  if (["image", "formula"].includes(node.type))
28
- return atomToHtml(node);
30
+ return atomToHtml(node, { renderFormulaHtml: renderFormulaHtmlMode });
29
31
  const raw = node.attrs?.raw;
30
32
  if (typeof raw?.html === "string")
31
33
  return raw.html;
@@ -91,7 +93,7 @@ const serializeBlock = (node, includeIds, listDepth = 0, tableCellA11y) => {
91
93
  }
92
94
  const id = includeIds ? ` data-smart-id="${escapeHtml(node.id)}"` : "";
93
95
  if (["block_image", "block_formula", "video", "audio", "divider"].includes(node.type))
94
- return atomToHtml(node);
96
+ return atomToHtml(node, { renderFormulaHtml: renderFormulaHtmlMode });
95
97
  if (node.type === "paragraph" || node.type === "heading") {
96
98
  const tag = node.type === "heading" ? `h${String(node.attrs?.level || 1)}` : "p";
97
99
  return `<${tag}${id}${blockAttributes(node)}>${(node.children || []).map(serializeInline).join("")}</${tag}>`;
@@ -179,12 +181,28 @@ const serializeBlock = (node, includeIds, listDepth = 0, tableCellA11y) => {
179
181
  }
180
182
  return `<div${id}>${(node.children || []).map((child) => isTextNode(child) ? serializeInline(child) : serializeBlock(child, includeIds)).join("")}</div>`;
181
183
  };
182
- export const serializeCanonicalListHtml = (document, options = {}) => options.clean === true
183
- ? document.children.map((node) => isTextNode(node) ? serializeInline(node) : serializeBlock(node, false)).join("")
184
- .replace(/\sdata-smart-id=(?:"[^"]*"|'[^']*')/g, "")
185
- : options.fragment === true
186
- ? document.children.map((node) => isTextNode(node) ? serializeInline(node) : serializeBlock(node, true)).join("")
187
- : `<div data-smart-document="true" data-smart-id="${escapeHtml(document.id)}">${document.children.map((node) => isTextNode(node) ? serializeInline(node) : serializeBlock(node, true)).join("")}</div>`;
184
+ export const serializeCanonicalListHtml = (document, options = {}) => {
185
+ // A module-scoped flag, not a threaded parameter: serializeInline/
186
+ // serializeBlock recurse through dozens of call sites (lists, tables,
187
+ // blockquotes) - threading a new parameter through every one of them is
188
+ // exactly the kind of change that's easy to miss at one nested call site
189
+ // and silently break there. Safe here because serialization is
190
+ // synchronous, single-threaded, non-reentrant tree recursion - the flag
191
+ // is always restored in `finally` before this function ever returns.
192
+ const previous = renderFormulaHtmlMode;
193
+ renderFormulaHtmlMode = options.renderFormulaHtml === true;
194
+ try {
195
+ return options.clean === true
196
+ ? document.children.map((node) => isTextNode(node) ? serializeInline(node) : serializeBlock(node, false)).join("")
197
+ .replace(/\sdata-smart-id=(?:"[^"]*"|'[^']*')/g, "")
198
+ : options.fragment === true
199
+ ? document.children.map((node) => isTextNode(node) ? serializeInline(node) : serializeBlock(node, true)).join("")
200
+ : `<div data-smart-document="true" data-smart-id="${escapeHtml(document.id)}">${document.children.map((node) => isTextNode(node) ? serializeInline(node) : serializeBlock(node, true)).join("")}</div>`;
201
+ }
202
+ finally {
203
+ renderFormulaHtmlMode = previous;
204
+ }
205
+ };
188
206
  const textWithMarks = (node, inherited = []) => {
189
207
  if (isEditorUiNode(node))
190
208
  return [];
@@ -3,6 +3,7 @@ export declare const parseCanonicalTableHtml: (html: string) => SmartDocument;
3
3
  export declare const serializeCanonicalTableHtml: (document: SmartDocument, options?: {
4
4
  clean?: boolean;
5
5
  fragment?: boolean;
6
+ renderFormulaHtml?: boolean;
6
7
  }) => string;
7
8
  /** GFM is intentionally lossy: anchor content survives and covered coordinates are emitted empty. */
8
9
  export declare const serializeCanonicalTableMarkdown: (document: SmartDocument) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartrte-core",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Framework-agnostic document model and editing primitives for Smart RTE",
5
5
  "repository": {
6
6
  "type": "git",