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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
-
|
|
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
|
-
|
|
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 = {}) =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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;
|