smartrte-core 0.2.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 +16 -0
- package/README.md +23 -0
- package/dist/command.d.ts +8 -8
- package/dist/editor.d.ts +32 -0
- package/dist/editor.js +121 -0
- package/dist/foundation/annotations/index.d.ts +2 -0
- package/dist/foundation/annotations/index.js +2 -0
- package/dist/foundation/annotations/range.d.ts +54 -0
- package/dist/foundation/annotations/range.js +158 -0
- package/dist/foundation/annotations/types.d.ts +33 -0
- package/dist/foundation/annotations/types.js +1 -0
- package/dist/foundation/atom/commands.d.ts +11 -0
- package/dist/foundation/atom/commands.js +118 -0
- package/dist/foundation/atom/composition.d.ts +8 -0
- package/dist/foundation/atom/composition.js +22 -0
- package/dist/foundation/atom/declarations.d.ts +2 -0
- package/dist/foundation/atom/declarations.js +17 -0
- package/dist/foundation/atom/formats.d.ts +24 -0
- package/dist/foundation/atom/formats.js +105 -0
- package/dist/foundation/atom/index.d.ts +10 -0
- package/dist/foundation/atom/index.js +10 -0
- package/dist/foundation/atom/lifecycle.d.ts +12 -0
- package/dist/foundation/atom/lifecycle.js +41 -0
- package/dist/foundation/atom/persistence.d.ts +3 -0
- package/dist/foundation/atom/persistence.js +20 -0
- package/dist/foundation/atom/plugin.d.ts +20 -0
- package/dist/foundation/atom/plugin.js +55 -0
- package/dist/foundation/atom/schema.d.ts +3 -0
- package/dist/foundation/atom/schema.js +29 -0
- package/dist/foundation/atom/security.d.ts +10 -0
- package/dist/foundation/atom/security.js +20 -0
- package/dist/foundation/atom/selection.d.ts +3 -0
- package/dist/foundation/atom/selection.js +4 -0
- package/dist/foundation/atom/types.d.ts +47 -0
- package/dist/foundation/atom/types.js +1 -0
- package/dist/foundation/block/commands.d.ts +17 -0
- package/dist/foundation/block/commands.js +197 -0
- package/dist/foundation/block/formats.d.ts +33 -0
- package/dist/foundation/block/formats.js +57 -0
- package/dist/foundation/block/index.d.ts +5 -0
- package/dist/foundation/block/index.js +5 -0
- package/dist/foundation/block/input.d.ts +19 -0
- package/dist/foundation/block/input.js +67 -0
- package/dist/foundation/block/plugin.d.ts +26 -0
- package/dist/foundation/block/plugin.js +79 -0
- package/dist/foundation/block/schema.d.ts +49 -0
- package/dist/foundation/block/schema.js +27 -0
- package/dist/foundation/block/types.d.ts +40 -0
- package/dist/foundation/block/types.js +1 -0
- package/dist/foundation/boundaries.d.ts +11 -0
- package/dist/foundation/boundaries.js +117 -0
- package/dist/foundation/clipboard/detection.d.ts +3 -0
- package/dist/foundation/clipboard/detection.js +29 -0
- package/dist/foundation/clipboard/diagnostics.d.ts +24 -0
- package/dist/foundation/clipboard/diagnostics.js +59 -0
- package/dist/foundation/clipboard/index.d.ts +8 -0
- package/dist/foundation/clipboard/index.js +8 -0
- package/dist/foundation/clipboard/insertion.d.ts +22 -0
- package/dist/foundation/clipboard/insertion.js +210 -0
- package/dist/foundation/clipboard/normalizers.d.ts +8 -0
- package/dist/foundation/clipboard/normalizers.js +229 -0
- package/dist/foundation/clipboard/pipeline.d.ts +13 -0
- package/dist/foundation/clipboard/pipeline.js +58 -0
- package/dist/foundation/clipboard/sanitizer.d.ts +3 -0
- package/dist/foundation/clipboard/sanitizer.js +58 -0
- package/dist/foundation/clipboard/serialization.d.ts +5 -0
- package/dist/foundation/clipboard/serialization.js +33 -0
- package/dist/foundation/clipboard/transfer.d.ts +11 -0
- package/dist/foundation/clipboard/transfer.js +108 -0
- package/dist/foundation/clipboard/types.d.ts +65 -0
- package/dist/foundation/clipboard/types.js +1 -0
- package/dist/foundation/collab/index.d.ts +3 -0
- package/dist/foundation/collab/index.js +3 -0
- package/dist/foundation/collab/rebase.d.ts +28 -0
- package/dist/foundation/collab/rebase.js +82 -0
- package/dist/foundation/collab/transform.d.ts +73 -0
- package/dist/foundation/collab/transform.js +251 -0
- package/dist/foundation/collab/types.d.ts +52 -0
- package/dist/foundation/collab/types.js +1 -0
- package/dist/foundation/comments/fromSelection.d.ts +16 -0
- package/dist/foundation/comments/fromSelection.js +24 -0
- package/dist/foundation/comments/index.d.ts +3 -0
- package/dist/foundation/comments/index.js +3 -0
- package/dist/foundation/comments/rebase.d.ts +31 -0
- package/dist/foundation/comments/rebase.js +43 -0
- package/dist/foundation/comments/types.d.ts +37 -0
- package/dist/foundation/comments/types.js +1 -0
- package/dist/foundation/diff/documentDiff.d.ts +12 -0
- package/dist/foundation/diff/documentDiff.js +148 -0
- package/dist/foundation/diff/index.d.ts +3 -0
- package/dist/foundation/diff/index.js +3 -0
- package/dist/foundation/diff/textDiff.d.ts +12 -0
- package/dist/foundation/diff/textDiff.js +114 -0
- package/dist/foundation/diff/types.d.ts +74 -0
- package/dist/foundation/diff/types.js +1 -0
- package/dist/foundation/editor.d.ts +138 -0
- package/dist/foundation/editor.js +398 -0
- package/dist/foundation/formats/codec.d.ts +45 -0
- package/dist/foundation/formats/codec.js +1 -0
- package/dist/foundation/formats/docx/export.d.ts +4 -0
- package/dist/foundation/formats/docx/export.js +285 -0
- package/dist/foundation/formats/docx/import.d.ts +9 -0
- package/dist/foundation/formats/docx/import.js +37 -0
- package/dist/foundation/formats/docx/nestingGuard.d.ts +40 -0
- package/dist/foundation/formats/docx/nestingGuard.js +66 -0
- package/dist/foundation/formats/docx/portableAtoms.d.ts +4 -0
- package/dist/foundation/formats/docx/portableAtoms.js +66 -0
- package/dist/foundation/formats/docx/styledImport.d.ts +10 -0
- package/dist/foundation/formats/docx/styledImport.js +205 -0
- package/dist/foundation/formats/featureCodecs.d.ts +37 -0
- package/dist/foundation/formats/featureCodecs.js +69 -0
- package/dist/foundation/formats/fidelity.d.ts +31 -0
- package/dist/foundation/formats/fidelity.js +123 -0
- package/dist/foundation/formats/index.d.ts +7 -0
- package/dist/foundation/formats/index.js +7 -0
- package/dist/foundation/formats/pdf/format.d.ts +29 -0
- package/dist/foundation/formats/pdf/format.js +190 -0
- package/dist/foundation/history.d.ts +20 -0
- package/dist/foundation/history.js +121 -0
- package/dist/foundation/identity.d.ts +8 -0
- package/dist/foundation/identity.js +30 -0
- package/dist/foundation/index.d.ts +30 -0
- package/dist/foundation/index.js +30 -0
- package/dist/foundation/list/commands.d.ts +28 -0
- package/dist/foundation/list/commands.js +422 -0
- package/dist/foundation/list/formats.d.ts +21 -0
- package/dist/foundation/list/formats.js +813 -0
- package/dist/foundation/list/index.d.ts +8 -0
- package/dist/foundation/list/index.js +8 -0
- package/dist/foundation/list/input.d.ts +35 -0
- package/dist/foundation/list/input.js +320 -0
- package/dist/foundation/list/plugin.d.ts +21 -0
- package/dist/foundation/list/plugin.js +71 -0
- package/dist/foundation/list/presets.d.ts +16 -0
- package/dist/foundation/list/presets.js +21 -0
- package/dist/foundation/list/schema.d.ts +35 -0
- package/dist/foundation/list/schema.js +29 -0
- package/dist/foundation/list/shadow.d.ts +54 -0
- package/dist/foundation/list/shadow.js +75 -0
- package/dist/foundation/list/shadowCorpus.d.ts +10 -0
- package/dist/foundation/list/shadowCorpus.js +41 -0
- package/dist/foundation/list/types.d.ts +50 -0
- package/dist/foundation/list/types.js +1 -0
- package/dist/foundation/mapping.d.ts +11 -0
- package/dist/foundation/mapping.js +40 -0
- package/dist/foundation/marks/canonical.d.ts +9 -0
- package/dist/foundation/marks/canonical.js +72 -0
- package/dist/foundation/marks/commands.d.ts +21 -0
- package/dist/foundation/marks/commands.js +136 -0
- package/dist/foundation/marks/dom.d.ts +2 -0
- package/dist/foundation/marks/dom.js +46 -0
- package/dist/foundation/marks/editor.d.ts +8 -0
- package/dist/foundation/marks/editor.js +50 -0
- package/dist/foundation/marks/formats.d.ts +37 -0
- package/dist/foundation/marks/formats.js +49 -0
- package/dist/foundation/marks/hardBreak.d.ts +8 -0
- package/dist/foundation/marks/hardBreak.js +80 -0
- package/dist/foundation/marks/index.d.ts +11 -0
- package/dist/foundation/marks/index.js +11 -0
- package/dist/foundation/marks/normalization.d.ts +2 -0
- package/dist/foundation/marks/normalization.js +49 -0
- package/dist/foundation/marks/plugin.d.ts +14 -0
- package/dist/foundation/marks/plugin.js +63 -0
- package/dist/foundation/marks/resolveMarkRun.d.ts +8 -0
- package/dist/foundation/marks/resolveMarkRun.js +47 -0
- package/dist/foundation/marks/schema.d.ts +167 -0
- package/dist/foundation/marks/schema.js +50 -0
- package/dist/foundation/marks/stored.d.ts +2 -0
- package/dist/foundation/marks/stored.js +31 -0
- package/dist/foundation/marks/types.d.ts +40 -0
- package/dist/foundation/marks/types.js +1 -0
- package/dist/foundation/modelDom.d.ts +38 -0
- package/dist/foundation/modelDom.js +327 -0
- package/dist/foundation/normalization.d.ts +22 -0
- package/dist/foundation/normalization.js +132 -0
- package/dist/foundation/operations.d.ts +28 -0
- package/dist/foundation/operations.js +568 -0
- package/dist/foundation/plugin/builtins.d.ts +10 -0
- package/dist/foundation/plugin/builtins.js +27 -0
- package/dist/foundation/plugin/dispatch.d.ts +33 -0
- package/dist/foundation/plugin/dispatch.js +34 -0
- package/dist/foundation/plugin/index.d.ts +4 -0
- package/dist/foundation/plugin/index.js +4 -0
- package/dist/foundation/plugin/registry.d.ts +36 -0
- package/dist/foundation/plugin/registry.js +115 -0
- package/dist/foundation/plugin/types.d.ts +141 -0
- package/dist/foundation/plugin/types.js +13 -0
- package/dist/foundation/positions.d.ts +20 -0
- package/dist/foundation/positions.js +121 -0
- package/dist/foundation/schema.d.ts +57 -0
- package/dist/foundation/schema.js +371 -0
- package/dist/foundation/schemaBuilder.d.ts +41 -0
- package/dist/foundation/schemaBuilder.js +96 -0
- package/dist/foundation/scope/index.d.ts +2 -0
- package/dist/foundation/scope/index.js +2 -0
- package/dist/foundation/scope/resolveScope.d.ts +13 -0
- package/dist/foundation/scope/resolveScope.js +860 -0
- package/dist/foundation/scope/types.d.ts +122 -0
- package/dist/foundation/scope/types.js +1 -0
- package/dist/foundation/security/urlPolicy.d.ts +21 -0
- package/dist/foundation/security/urlPolicy.js +75 -0
- package/dist/foundation/structural/contentTarget.d.ts +21 -0
- package/dist/foundation/structural/contentTarget.js +51 -0
- package/dist/foundation/structural/index.d.ts +2 -0
- package/dist/foundation/structural/index.js +2 -0
- package/dist/foundation/structural/move.d.ts +7 -0
- package/dist/foundation/structural/move.js +35 -0
- package/dist/foundation/suggestions/index.d.ts +3 -0
- package/dist/foundation/suggestions/index.js +3 -0
- package/dist/foundation/suggestions/inline.d.ts +64 -0
- package/dist/foundation/suggestions/inline.js +116 -0
- package/dist/foundation/suggestions/structural.d.ts +40 -0
- package/dist/foundation/suggestions/structural.js +47 -0
- package/dist/foundation/suggestions/types.d.ts +19 -0
- package/dist/foundation/suggestions/types.js +1 -0
- package/dist/foundation/surface/index.d.ts +3 -0
- package/dist/foundation/surface/index.js +3 -0
- package/dist/foundation/surface/input.d.ts +127 -0
- package/dist/foundation/surface/input.js +1761 -0
- package/dist/foundation/surface/renderer.d.ts +84 -0
- package/dist/foundation/surface/renderer.js +974 -0
- package/dist/foundation/surface/types.d.ts +48 -0
- package/dist/foundation/surface/types.js +1 -0
- package/dist/foundation/table/commands.d.ts +31 -0
- package/dist/foundation/table/commands.js +470 -0
- package/dist/foundation/table/formats.d.ts +27 -0
- package/dist/foundation/table/formats.js +40 -0
- package/dist/foundation/table/grid.d.ts +8 -0
- package/dist/foundation/table/grid.js +178 -0
- package/dist/foundation/table/index.d.ts +6 -0
- package/dist/foundation/table/index.js +6 -0
- package/dist/foundation/table/plugin.d.ts +23 -0
- package/dist/foundation/table/plugin.js +119 -0
- package/dist/foundation/table/schema.d.ts +2 -0
- package/dist/foundation/table/schema.js +38 -0
- package/dist/foundation/table/selection.d.ts +7 -0
- package/dist/foundation/table/selection.js +35 -0
- package/dist/foundation/table/types.d.ts +109 -0
- package/dist/foundation/table/types.js +1 -0
- package/dist/foundation/transactions.d.ts +41 -0
- package/dist/foundation/transactions.js +90 -0
- package/dist/foundation/types.d.ts +261 -0
- package/dist/foundation/types.js +1 -0
- package/dist/foundation/versioning/diffVersions.d.ts +5 -0
- package/dist/foundation/versioning/diffVersions.js +3 -0
- package/dist/foundation/versioning/index.d.ts +2 -0
- package/dist/foundation/versioning/index.js +2 -0
- package/dist/foundation/versioning/types.d.ts +17 -0
- package/dist/foundation/versioning/types.js +1 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +165 -0
- package/dist/html/compatibility.js +7 -1
- package/dist/index.d.ts +1 -14
- package/dist/index.js +9 -14
- package/dist/legacy/index.d.ts +45 -0
- package/dist/legacy/index.js +45 -0
- package/dist/legacyCommands/alignment.d.ts +20 -0
- package/dist/{commands → legacyCommands}/alignment.js +17 -0
- package/dist/legacyCommands/blocks.d.ts +14 -0
- package/dist/legacyCommands/blocks.js +105 -0
- package/dist/legacyCommands/checklist.d.ts +11 -0
- package/dist/legacyCommands/checklist.js +45 -0
- package/dist/legacyCommands/code.d.ts +15 -0
- package/dist/{commands → legacyCommands}/code.js +25 -1
- package/dist/legacyCommands/formula.d.ts +7 -0
- package/dist/legacyCommands/formula.js +67 -0
- package/dist/legacyCommands/inlineAtoms.d.ts +7 -0
- package/dist/legacyCommands/inlineAtoms.js +58 -0
- package/dist/legacyCommands/list.d.ts +38 -0
- package/dist/legacyCommands/list.js +477 -0
- package/dist/{commands → legacyCommands}/marks.d.ts +3 -0
- package/dist/{commands → legacyCommands}/marks.js +28 -11
- package/dist/legacyCommands/media.d.ts +31 -0
- package/dist/legacyCommands/media.js +205 -0
- package/dist/legacyCommands/move.d.ts +14 -0
- package/dist/legacyCommands/move.js +91 -0
- package/dist/legacyCommands/table.d.ts +91 -0
- package/dist/legacyCommands/table.js +547 -0
- package/dist/listPresets.d.ts +12 -0
- package/dist/listPresets.js +6 -0
- package/dist/listScope.d.ts +27 -0
- package/dist/listScope.js +104 -0
- package/dist/markdown/compatibility.d.ts +2 -0
- package/dist/markdown/compatibility.js +94 -2
- package/dist/marks.d.ts +5 -5
- package/dist/model.d.ts +65 -9
- package/dist/plugin.d.ts +15 -0
- package/dist/plugin.js +31 -0
- package/dist/plugins/alignment.d.ts +3 -0
- package/dist/plugins/alignment.js +6 -0
- package/dist/plugins/basicFormatting.d.ts +9 -0
- package/dist/plugins/basicFormatting.js +24 -0
- package/dist/plugins/blockType.d.ts +3 -0
- package/dist/plugins/blockType.js +6 -0
- package/dist/plugins/blockquote.d.ts +3 -0
- package/dist/plugins/blockquote.js +6 -0
- package/dist/plugins/checklist.d.ts +3 -0
- package/dist/plugins/checklist.js +10 -0
- package/dist/plugins/codeBlock.d.ts +3 -0
- package/dist/plugins/codeBlock.js +6 -0
- package/dist/plugins/formula.d.ts +3 -0
- package/dist/plugins/formula.js +9 -0
- package/dist/plugins/list.d.ts +3 -0
- package/dist/plugins/list.js +10 -0
- package/dist/plugins/media.d.ts +3 -0
- package/dist/plugins/media.js +12 -0
- package/dist/plugins/move.d.ts +3 -0
- package/dist/plugins/move.js +9 -0
- package/dist/plugins/table.d.ts +3 -0
- package/dist/plugins/table.js +22 -0
- package/dist/preset.d.ts +12 -0
- package/dist/preset.js +40 -0
- package/dist/schema.d.ts +22 -0
- package/dist/schema.js +333 -0
- package/dist/security/urlPolicy.d.ts +1 -17
- package/dist/security/urlPolicy.js +3 -69
- package/dist/selection.d.ts +1 -1
- package/dist/selectionMapping.d.ts +4 -0
- package/dist/selectionMapping.js +222 -0
- package/dist/table.d.ts +3 -3
- package/dist/transaction.d.ts +35 -12
- package/dist/transaction.js +174 -28
- package/dist/tree.d.ts +8 -2
- package/dist/tree.js +44 -0
- package/package.json +27 -8
- package/dist/commands/alignment.d.ts +0 -18
- package/dist/commands/code.d.ts +0 -13
- package/dist/commands/list.d.ts +0 -20
- package/dist/commands/list.js +0 -52
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
import { parseFragment, serializeOuter } from "parse5";
|
|
2
|
+
import { unified } from "unified";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkParse from "remark-parse";
|
|
5
|
+
import { createNodeId, isTextNode } from "../identity.js";
|
|
6
|
+
import { canonicalMarkAttrs, canonicalMarkOrder } from "../marks/canonical.js";
|
|
7
|
+
import { occupancyGridFor } from "../table/grid.js";
|
|
8
|
+
import { atomToHtml, atomToMarkdown } from "../atom/formats.js";
|
|
9
|
+
import { sanitizeAtomSource } from "../atom/security.js";
|
|
10
|
+
import { foundationListStyleForPresetDepth, isFoundationSmartListPreset } from "./presets.js";
|
|
11
|
+
const escapeHtml = (value) => String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
12
|
+
const attr = (node, name) => node.attrs?.find((candidate) => candidate.name === name)?.value;
|
|
13
|
+
const generatedId = (node, prefix) => attr(node, "data-smart-id") || `${prefix}-${createNodeId()}`;
|
|
14
|
+
const isEditorUiNode = (node) => attr(node, "data-smart-ui") !== undefined || attr(node, "data-srte-check") !== undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Real-world exports (Google Docs, Word, ChatGPT, Sootr) routinely wrap
|
|
17
|
+
* meaningful block content - most often a <table> - in one or more <div>s
|
|
18
|
+
* (table-wrapper divs, layout divs) that carry no semantic meaning of
|
|
19
|
+
* their own. parseBlock has no case for any of these tags; see
|
|
20
|
+
* parseBlockList below for how they're unwrapped instead of swallowed.
|
|
21
|
+
*/
|
|
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;
|
|
25
|
+
const serializeInline = (node) => {
|
|
26
|
+
if (!isTextNode(node)) {
|
|
27
|
+
if (node.type === "hard_break")
|
|
28
|
+
return `<br data-smart-id="${escapeHtml(node.id)}" data-smart-type="hard_break">`;
|
|
29
|
+
if (["image", "formula"].includes(node.type))
|
|
30
|
+
return atomToHtml(node, { renderFormulaHtml: renderFormulaHtmlMode });
|
|
31
|
+
const raw = node.attrs?.raw;
|
|
32
|
+
if (typeof raw?.html === "string")
|
|
33
|
+
return raw.html;
|
|
34
|
+
return `<span data-smart-id="${escapeHtml(node.id)}" data-smart-atomic="true"></span>`;
|
|
35
|
+
}
|
|
36
|
+
let html = escapeHtml(node.text);
|
|
37
|
+
[...canonicalMarkOrder(node.marks)].reverse().forEach((mark) => {
|
|
38
|
+
if (mark.type === "bold")
|
|
39
|
+
html = `<strong>${html}</strong>`;
|
|
40
|
+
else if (mark.type === "italic")
|
|
41
|
+
html = `<em>${html}</em>`;
|
|
42
|
+
else if (mark.type === "underline")
|
|
43
|
+
html = `<u>${html}</u>`;
|
|
44
|
+
else if (mark.type === "strike")
|
|
45
|
+
html = `<s>${html}</s>`;
|
|
46
|
+
else if (mark.type === "code")
|
|
47
|
+
html = `<code>${html}</code>`;
|
|
48
|
+
else if (mark.type === "superscript")
|
|
49
|
+
html = `<sup>${html}</sup>`;
|
|
50
|
+
else if (mark.type === "subscript")
|
|
51
|
+
html = `<sub>${html}</sub>`;
|
|
52
|
+
else if (mark.type === "textColor")
|
|
53
|
+
html = `<span style="color:${escapeHtml(mark.attrs?.value || "")}">${html}</span>`;
|
|
54
|
+
else if (mark.type === "backgroundColor")
|
|
55
|
+
html = `<span style="background-color:${escapeHtml(mark.attrs?.value || "")}">${html}</span>`;
|
|
56
|
+
else if (mark.type === "fontSize")
|
|
57
|
+
html = `<span style="font-size:${escapeHtml(mark.attrs?.valuePx || "")}px">${html}</span>`;
|
|
58
|
+
else if (mark.type === "fontFamily")
|
|
59
|
+
html = `<span style="font-family:${escapeHtml(mark.attrs?.value || "")}">${html}</span>`;
|
|
60
|
+
else if (mark.type === "link")
|
|
61
|
+
html = `<a href="${escapeHtml(mark.attrs?.href || "")}"${mark.attrs?.target ? ` target="${escapeHtml(mark.attrs.target)}"` : ""}>${html}</a>`;
|
|
62
|
+
});
|
|
63
|
+
return html;
|
|
64
|
+
};
|
|
65
|
+
const orderedList = (node) => {
|
|
66
|
+
const marker = String(node.attrs?.style || node.attrs?.preset || "");
|
|
67
|
+
return /^(?:decimal|lower-|upper-|ordered)/.test(marker) || node.attrs?.start !== undefined;
|
|
68
|
+
};
|
|
69
|
+
const effectiveStyle = (node, depth) => {
|
|
70
|
+
if (typeof node.attrs?.style === "string")
|
|
71
|
+
return node.attrs.style;
|
|
72
|
+
return isFoundationSmartListPreset(node.attrs?.preset)
|
|
73
|
+
? foundationListStyleForPresetDepth(node.attrs.preset, depth)
|
|
74
|
+
: undefined;
|
|
75
|
+
};
|
|
76
|
+
const blockAttributes = (node) => {
|
|
77
|
+
const declarations = [
|
|
78
|
+
typeof node.attrs?.htmlStyle === "string" ? node.attrs.htmlStyle.replace(/;\s*$/, "") : "",
|
|
79
|
+
typeof node.attrs?.align === "string" ? `text-align:${node.attrs.align}` : "",
|
|
80
|
+
Number(node.attrs?.indentLevel) > 0 ? `margin-inline-start:${Number(node.attrs?.indentLevel) * 2}em` : "",
|
|
81
|
+
].filter(Boolean);
|
|
82
|
+
return [
|
|
83
|
+
node.attrs?.align ? ` data-smart-align="${escapeHtml(node.attrs.align)}"` : "",
|
|
84
|
+
Number(node.attrs?.indentLevel) > 0 ? ` data-smart-indent="${escapeHtml(node.attrs?.indentLevel)}"` : "",
|
|
85
|
+
declarations.length ? ` style="${escapeHtml(declarations.join(";"))}"` : "",
|
|
86
|
+
].join("");
|
|
87
|
+
};
|
|
88
|
+
const serializeBlock = (node, includeIds, listDepth = 0, tableCellA11y) => {
|
|
89
|
+
if (node.type === "unknown") {
|
|
90
|
+
const raw = node.attrs?.raw;
|
|
91
|
+
if (typeof raw?.html === "string")
|
|
92
|
+
return raw.html;
|
|
93
|
+
}
|
|
94
|
+
const id = includeIds ? ` data-smart-id="${escapeHtml(node.id)}"` : "";
|
|
95
|
+
if (["block_image", "block_formula", "video", "audio", "divider"].includes(node.type))
|
|
96
|
+
return atomToHtml(node, { renderFormulaHtml: renderFormulaHtmlMode });
|
|
97
|
+
if (node.type === "paragraph" || node.type === "heading") {
|
|
98
|
+
const tag = node.type === "heading" ? `h${String(node.attrs?.level || 1)}` : "p";
|
|
99
|
+
return `<${tag}${id}${blockAttributes(node)}>${(node.children || []).map(serializeInline).join("")}</${tag}>`;
|
|
100
|
+
}
|
|
101
|
+
if (node.type === "blockquote") {
|
|
102
|
+
return `<blockquote${id}${blockAttributes(node)}>${(node.children || []).map((child) => isTextNode(child) ? serializeInline(child) : serializeBlock(child, includeIds, listDepth)).join("")}</blockquote>`;
|
|
103
|
+
}
|
|
104
|
+
if (node.type === "code_block") {
|
|
105
|
+
const language = typeof node.attrs?.language === "string" && node.attrs.language ? node.attrs.language : undefined;
|
|
106
|
+
const languageAttrs = language
|
|
107
|
+
? ` data-smart-language="${escapeHtml(language)}"><code class="language-${escapeHtml(language)}"`
|
|
108
|
+
: "><code";
|
|
109
|
+
const text = (node.children || []).map((child) => isTextNode(child) ? child.text : child.type === "hard_break" ? "\n" : "").join("");
|
|
110
|
+
return `<pre${id}${blockAttributes(node)}${languageAttrs}>${escapeHtml(text)}</code></pre>`;
|
|
111
|
+
}
|
|
112
|
+
if (node.type === "table") {
|
|
113
|
+
const grid = occupancyGridFor(node);
|
|
114
|
+
let headerRows = 0;
|
|
115
|
+
while (headerRows < grid.rows && Array.from({ length: grid.columns }, (_, column) => grid.at(headerRows, column)?.node.attrs?.header === true).every(Boolean))
|
|
116
|
+
headerRows += 1;
|
|
117
|
+
let headerColumns = 0;
|
|
118
|
+
while (headerColumns < grid.columns && Array.from({ length: grid.rows }, (_, row) => grid.at(row, headerColumns)?.node.attrs?.header === true).every(Boolean))
|
|
119
|
+
headerColumns += 1;
|
|
120
|
+
const headerIds = new Map(grid.anchors.filter((cell) => cell.node.attrs?.header === true).map((cell) => [cell.cellId, `smart-header-${node.id}-${cell.cellId}`]));
|
|
121
|
+
const cellA11y = (cell) => {
|
|
122
|
+
const anchor = grid.anchors.find((candidate) => candidate.cellId === cell.id);
|
|
123
|
+
if (!anchor)
|
|
124
|
+
return {};
|
|
125
|
+
if (cell.attrs?.header === true)
|
|
126
|
+
return { id: headerIds.get(cell.id), scope: anchor.left < headerColumns && anchor.top >= headerRows ? "row" : "col" };
|
|
127
|
+
const headers = grid.anchors.filter((header) => header.node.attrs?.header === true && (header.top < headerRows && header.left <= anchor.left && header.right > anchor.left
|
|
128
|
+
|| header.left < headerColumns && header.top <= anchor.top && header.bottom > anchor.top)).map((header) => headerIds.get(header.cellId)).filter((value) => Boolean(value));
|
|
129
|
+
return headers.length ? { headers: headers.join(" ") } : {};
|
|
130
|
+
};
|
|
131
|
+
const widths = Array.isArray(node.attrs?.columnWidths) ? node.attrs.columnWidths : [];
|
|
132
|
+
const colgroup = widths.length ? `<colgroup>${widths.map((width) => `<col style="width:${escapeHtml(width)}px">`).join("")}</colgroup>` : "";
|
|
133
|
+
const caption = typeof node.attrs?.caption === "string" && node.attrs.caption ? `<caption>${escapeHtml(node.attrs.caption)}</caption>` : "";
|
|
134
|
+
const layout = node.attrs?.layout ? ` data-smart-layout="${escapeHtml(node.attrs.layout)}" style="table-layout:${escapeHtml(node.attrs.layout)}"` : "";
|
|
135
|
+
const rows = (node.children || []).map((row) => isTextNode(row) ? "" : `<tr${includeIds ? ` data-smart-id="${escapeHtml(row.id)}"` : ""}${Number(row.attrs?.height) > 0 ? ` style="height:${Number(row.attrs?.height)}px"` : ""}>${(row.children || []).map((cell) => isTextNode(cell) ? "" : serializeBlock(cell, includeIds, listDepth, cellA11y(cell))).join("")}</tr>`).join("");
|
|
136
|
+
return `<table${id}${layout}>${caption}${colgroup}<tbody>${rows}</tbody></table>`;
|
|
137
|
+
}
|
|
138
|
+
if (node.type === "table_row") {
|
|
139
|
+
const height = Number(node.attrs?.height);
|
|
140
|
+
return `<tr${id}${Number.isFinite(height) && height > 0 ? ` style="height:${height}px"` : ""}>${(node.children || []).map((child) => isTextNode(child) ? "" : serializeBlock(child, includeIds, listDepth)).join("")}</tr>`;
|
|
141
|
+
}
|
|
142
|
+
if (node.type === "table_cell") {
|
|
143
|
+
const tag = node.attrs?.header === true ? "th" : "td";
|
|
144
|
+
const rowspan = Math.max(1, Number(node.attrs?.rowspan) || 1);
|
|
145
|
+
const colspan = Math.max(1, Number(node.attrs?.colspan) || 1);
|
|
146
|
+
const styles = [
|
|
147
|
+
node.attrs?.background ? `background:${String(node.attrs.background)}` : "",
|
|
148
|
+
node.attrs?.textColor ? `color:${String(node.attrs.textColor)}` : "",
|
|
149
|
+
node.attrs?.borders ? `border:${String(node.attrs.borders)}` : "",
|
|
150
|
+
// Per-side overrides emit after the uniform shorthand, same
|
|
151
|
+
// last-write-wins order the renderer applies them in.
|
|
152
|
+
node.attrs?.borderTop ? `border-top:${String(node.attrs.borderTop)}` : "",
|
|
153
|
+
node.attrs?.borderRight ? `border-right:${String(node.attrs.borderRight)}` : "",
|
|
154
|
+
node.attrs?.borderBottom ? `border-bottom:${String(node.attrs.borderBottom)}` : "",
|
|
155
|
+
node.attrs?.borderLeft ? `border-left:${String(node.attrs.borderLeft)}` : "",
|
|
156
|
+
node.attrs?.verticalAlign ? `vertical-align:${String(node.attrs.verticalAlign)}` : "",
|
|
157
|
+
].filter(Boolean).join(";");
|
|
158
|
+
return `<${tag}${id}${tableCellA11y?.id ? ` id="${escapeHtml(tableCellA11y.id)}"` : ""}${tableCellA11y?.scope ? ` scope="${tableCellA11y.scope}"` : ""}${tableCellA11y?.headers ? ` headers="${escapeHtml(tableCellA11y.headers)}"` : ""}${rowspan > 1 ? ` rowspan="${rowspan}"` : ""}${colspan > 1 ? ` colspan="${colspan}"` : ""}${styles ? ` style="${escapeHtml(styles)}"` : ""}>${(node.children || []).map((child) => isTextNode(child) ? serializeInline(child) : serializeBlock(child, includeIds, listDepth)).join("")}</${tag}>`;
|
|
159
|
+
}
|
|
160
|
+
if (node.type === "list") {
|
|
161
|
+
const tag = orderedList(node) ? "ol" : "ul";
|
|
162
|
+
const style = effectiveStyle(node, listDepth);
|
|
163
|
+
const attrs = [
|
|
164
|
+
id,
|
|
165
|
+
node.attrs?.preset ? ` data-smart-list-preset="${escapeHtml(node.attrs.preset)}" data-srte-list-preset="${escapeHtml(node.attrs.preset)}" data-srte-list-depth="${listDepth}"` : "",
|
|
166
|
+
node.attrs?.style ? ` data-smart-list-style="${escapeHtml(node.attrs.style)}"` : "",
|
|
167
|
+
style ? ` style="list-style-type:${escapeHtml(style)}"` : "",
|
|
168
|
+
node.attrs?.start !== undefined ? ` start="${escapeHtml(node.attrs.start)}"` : "",
|
|
169
|
+
node.attrs?.checkable === true ? ` data-smart-checkable="true" data-srte-checklist="true"` : "",
|
|
170
|
+
].join("");
|
|
171
|
+
return `<${tag}${attrs}>${(node.children || []).map((child) => isTextNode(child) ? "" : serializeBlock(child, includeIds, listDepth)).join("")}</${tag}>`;
|
|
172
|
+
}
|
|
173
|
+
if (node.type === "list_item") {
|
|
174
|
+
const attrs = [
|
|
175
|
+
id,
|
|
176
|
+
node.attrs?.checked !== undefined ? ` data-smart-checked="${node.attrs.checked === true ? "true" : "false"}" data-checked="${node.attrs.checked === true ? "true" : "false"}"` : "",
|
|
177
|
+
typeof node.attrs?.htmlStyle === "string" ? ` style="${escapeHtml(node.attrs.htmlStyle)}"` : "",
|
|
178
|
+
node.attrs?.numberOverride !== undefined ? ` value="${escapeHtml(node.attrs.numberOverride)}"` : "",
|
|
179
|
+
].join("");
|
|
180
|
+
return `<li${attrs}>${(node.children || []).map((child) => isTextNode(child) ? serializeInline(child) : serializeBlock(child, includeIds, child.type === "list" ? listDepth + 1 : listDepth)).join("")}</li>`;
|
|
181
|
+
}
|
|
182
|
+
return `<div${id}>${(node.children || []).map((child) => isTextNode(child) ? serializeInline(child) : serializeBlock(child, includeIds)).join("")}</div>`;
|
|
183
|
+
};
|
|
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
|
+
};
|
|
206
|
+
const textWithMarks = (node, inherited = []) => {
|
|
207
|
+
if (isEditorUiNode(node))
|
|
208
|
+
return [];
|
|
209
|
+
if (node.nodeName === "#text")
|
|
210
|
+
return node.value ? [{ type: "text", text: node.value, ...(inherited.length ? { marks: [...inherited] } : {}) }] : [];
|
|
211
|
+
const tag = node.tagName || "";
|
|
212
|
+
if (tag === "br" || attr(node, "data-smart-type") === "hard_break")
|
|
213
|
+
return [{
|
|
214
|
+
type: "hard_break", id: generatedId(node, "break"),
|
|
215
|
+
}];
|
|
216
|
+
const declaredAtom = attr(node, "data-smart-type");
|
|
217
|
+
if (tag === "img" || declaredAtom === "image" || declaredAtom === "formula") {
|
|
218
|
+
if (declaredAtom === "formula")
|
|
219
|
+
return [{ type: "formula", id: generatedId(node, "formula"), attrs: { source: attr(node, "data-smart-formula") || rawText(node), notation: attr(node, "data-smart-notation") === "mathml" ? "mathml" : "latex" } }];
|
|
220
|
+
const src = sanitizeAtomSource(attr(node, "src"), { kind: "image", allowBlobPreview: attr(node, "data-smart-status") === "pending" });
|
|
221
|
+
if (src) {
|
|
222
|
+
// Real-world sources (Google Docs, most web pages, this app's own
|
|
223
|
+
// exported markup) far more commonly size an <img> via inline
|
|
224
|
+
// `style="width:...;height:...;"` than the legacy HTML width/height
|
|
225
|
+
// attributes - style must win when both are present. naturalWidth/
|
|
226
|
+
// naturalHeight are deliberately not consulted: paste parsing is
|
|
227
|
+
// synchronous and the image hasn't loaded yet at this point.
|
|
228
|
+
const width = parsePixelWidth(styleValue(node, "width")) ?? parsePixelWidth(attr(node, "width"));
|
|
229
|
+
const height = parsePixelWidth(styleValue(node, "height")) ?? parsePixelWidth(attr(node, "height"));
|
|
230
|
+
return [{ type: "image", id: generatedId(node, "image"), attrs: {
|
|
231
|
+
src, alt: attr(node, "alt") || "", status: attr(node, "data-smart-status") || "ready",
|
|
232
|
+
...(attr(node, "data-smart-decorative") === "true" ? { decorative: true } : {}),
|
|
233
|
+
...(attr(node, "title") ? { title: attr(node, "title") } : {}),
|
|
234
|
+
...(attr(node, "data-smart-align") ? { align: attr(node, "data-smart-align") } : {}),
|
|
235
|
+
...(width !== null ? { width } : {}), ...(height !== null ? { height } : {}),
|
|
236
|
+
} }];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (attr(node, "data-smart-atomic") === "true")
|
|
240
|
+
return [{
|
|
241
|
+
type: "unknown", id: generatedId(node, "atom"),
|
|
242
|
+
attrs: { originalType: attr(node, "data-smart-unknown-type") || tag || "inline-atom", originalGroup: "inline", raw: { html: serializeOuter(node) }, editable: false },
|
|
243
|
+
}];
|
|
244
|
+
let marks = [...inherited];
|
|
245
|
+
const push = (mark) => { if (mark)
|
|
246
|
+
marks.push(mark); };
|
|
247
|
+
if (tag === "strong" || tag === "b")
|
|
248
|
+
push({ type: "bold" });
|
|
249
|
+
if (tag === "em" || tag === "i")
|
|
250
|
+
push({ type: "italic" });
|
|
251
|
+
if (tag === "u")
|
|
252
|
+
push({ type: "underline" });
|
|
253
|
+
if (tag === "s" || tag === "strike" || tag === "del")
|
|
254
|
+
push({ type: "strike" });
|
|
255
|
+
if (tag === "code")
|
|
256
|
+
push({ type: "code" });
|
|
257
|
+
if (tag === "sup")
|
|
258
|
+
push({ type: "superscript" });
|
|
259
|
+
if (tag === "sub")
|
|
260
|
+
push({ type: "subscript" });
|
|
261
|
+
if (tag === "a")
|
|
262
|
+
push({ type: "link", attrs: {
|
|
263
|
+
href: attr(node, "href") || "",
|
|
264
|
+
...(attr(node, "target") ? { target: attr(node, "target") } : {}),
|
|
265
|
+
} });
|
|
266
|
+
// Real-world exports (this includes our own DOCX import - styledImport.ts's
|
|
267
|
+
// runStyle already emits font-weight/font-style/text-decoration for
|
|
268
|
+
// <w:b>/<w:i>/<w:u> runs, on the assumption these get parsed back into
|
|
269
|
+
// marks) commonly signal bold/italic/underline via inline style rather
|
|
270
|
+
// than <strong>/<em>/<u>. "bolder" is technically relative to the
|
|
271
|
+
// inherited weight, but without full cascade resolution here, treating it
|
|
272
|
+
// (and >=700, matching what styledImport.ts itself generates) as bold is
|
|
273
|
+
// the same approximation color/background-color/font-size/font-family
|
|
274
|
+
// below already make from raw style values.
|
|
275
|
+
const weight = styleValue(node, "font-weight")?.toLowerCase();
|
|
276
|
+
if (weight && !marks.some((mark) => mark.type === "bold") && (weight === "bold" || weight === "bolder" || Number(weight) >= 700)) {
|
|
277
|
+
push({ type: "bold" });
|
|
278
|
+
}
|
|
279
|
+
const style = styleValue(node, "font-style")?.toLowerCase();
|
|
280
|
+
if (style && !marks.some((mark) => mark.type === "italic") && (style === "italic" || style === "oblique")) {
|
|
281
|
+
push({ type: "italic" });
|
|
282
|
+
}
|
|
283
|
+
const decoration = styleValue(node, "text-decoration")?.toLowerCase();
|
|
284
|
+
if (decoration?.includes("underline") && !marks.some((mark) => mark.type === "underline"))
|
|
285
|
+
push({ type: "underline" });
|
|
286
|
+
if (decoration?.includes("line-through") && !marks.some((mark) => mark.type === "strike"))
|
|
287
|
+
push({ type: "strike" });
|
|
288
|
+
const declared = attr(node, "data-smart-mark");
|
|
289
|
+
if (declared && !marks.some((mark) => mark.type === declared)) {
|
|
290
|
+
const raw = attr(node, "data-smart-mark-attrs");
|
|
291
|
+
try {
|
|
292
|
+
push({ type: declared, ...(raw ? { attrs: JSON.parse(raw) } : {}) });
|
|
293
|
+
}
|
|
294
|
+
catch { /* invalid metadata is ignored */ }
|
|
295
|
+
}
|
|
296
|
+
const color = styleValue(node, "color");
|
|
297
|
+
const background = styleValue(node, "background-color");
|
|
298
|
+
const size = styleValue(node, "font-size");
|
|
299
|
+
const family = styleValue(node, "font-family");
|
|
300
|
+
const attributed = [
|
|
301
|
+
...(color ? [["textColor", { value: color }]] : []),
|
|
302
|
+
...(background ? [["backgroundColor", { value: background }]] : []),
|
|
303
|
+
...(size ? [["fontSize", { valuePx: size }]] : []),
|
|
304
|
+
...(family ? [["fontFamily", { value: family }]] : []),
|
|
305
|
+
];
|
|
306
|
+
attributed.forEach(([type, attrs]) => {
|
|
307
|
+
const canonical = canonicalMarkAttrs(type, attrs);
|
|
308
|
+
if (canonical && !marks.some((mark) => mark.type === type))
|
|
309
|
+
push({ type, attrs: canonical });
|
|
310
|
+
});
|
|
311
|
+
marks = canonicalMarkOrder(marks);
|
|
312
|
+
return (node.childNodes || []).flatMap((child) => textWithMarks(child, marks));
|
|
313
|
+
};
|
|
314
|
+
const elementChildren = (node) => (node.childNodes || []).filter((child) => Boolean(child.tagName) && !isEditorUiNode(child));
|
|
315
|
+
const styleValue = (node, property) => attr(node, "style")?.split(";").map((part) => part.split(":"))
|
|
316
|
+
.find(([name]) => name?.trim().toLowerCase() === property)?.[1]?.trim();
|
|
317
|
+
/**
|
|
318
|
+
* A bare number (the legacy HTML `width` attribute's convention) or an
|
|
319
|
+
* explicit "Npx" CSS value - anything else (a percentage, "auto", em,
|
|
320
|
+
* missing) is rejected rather than guessed at. `Number.parseFloat("50%")`
|
|
321
|
+
* would silently read as "50 real pixels", a drastic understatement once
|
|
322
|
+
* a table's own rendered width is pinned to the literal sum of its column
|
|
323
|
+
* widths (surface/renderer.ts) - this is what previously made a pasted
|
|
324
|
+
* table with percentage or unspecified column widths shrink far below its
|
|
325
|
+
* real size instead of being left to render at its natural default.
|
|
326
|
+
*/
|
|
327
|
+
const parsePixelWidth = (raw) => {
|
|
328
|
+
const match = raw ? /^\s*([\d.]+)\s*(?:px)?\s*$/i.exec(raw) : null;
|
|
329
|
+
if (!match)
|
|
330
|
+
return null;
|
|
331
|
+
const width = Number.parseFloat(match[1]);
|
|
332
|
+
return Number.isFinite(width) && width > 0 ? width : null;
|
|
333
|
+
};
|
|
334
|
+
const withoutListMarkerStyle = (style) => {
|
|
335
|
+
if (!style)
|
|
336
|
+
return undefined;
|
|
337
|
+
const retained = style.split(";")
|
|
338
|
+
.map((declaration) => declaration.trim())
|
|
339
|
+
.filter((declaration) => declaration && !/^list-style(?:-type)?\s*:/i.test(declaration));
|
|
340
|
+
return retained.length ? retained.join(";") : undefined;
|
|
341
|
+
};
|
|
342
|
+
const parsedBlockAttrs = (node) => {
|
|
343
|
+
const attrs = {};
|
|
344
|
+
const align = attr(node, "data-smart-align") || styleValue(node, "text-align");
|
|
345
|
+
const explicitIndent = Number(attr(node, "data-smart-indent"));
|
|
346
|
+
const legacyIndent = styleValue(node, "margin-left");
|
|
347
|
+
const logicalIndent = styleValue(node, "margin-inline-start");
|
|
348
|
+
const cssIndent = legacyIndent || logicalIndent;
|
|
349
|
+
const computedIndent = cssIndent?.endsWith("px") ? Number.parseFloat(cssIndent) / 24
|
|
350
|
+
: cssIndent?.endsWith("em") ? Number.parseFloat(cssIndent) / 2 : 0;
|
|
351
|
+
const indent = Number.isFinite(explicitIndent) && explicitIndent > 0 ? explicitIndent : computedIndent;
|
|
352
|
+
if (align && ["left", "center", "right", "justify"].includes(align))
|
|
353
|
+
attrs.align = align;
|
|
354
|
+
if (Number.isInteger(indent) && indent > 0)
|
|
355
|
+
attrs.indentLevel = indent;
|
|
356
|
+
return attrs;
|
|
357
|
+
};
|
|
358
|
+
const rawText = (node) => node.nodeName === "#text" ? node.value || ""
|
|
359
|
+
: node.tagName === "br" ? "\n"
|
|
360
|
+
: (node.childNodes || []).filter((child) => !isEditorUiNode(child)).map(rawText).join("");
|
|
361
|
+
const parseBlock = (node) => {
|
|
362
|
+
const tag = node.tagName || "";
|
|
363
|
+
const declaredAtom = attr(node, "data-smart-type");
|
|
364
|
+
if (declaredAtom === "block_image") {
|
|
365
|
+
const src = sanitizeAtomSource(attr(node, "src"), { kind: "image" });
|
|
366
|
+
if (!src)
|
|
367
|
+
return null;
|
|
368
|
+
// Mirrors the inline `image` atom's width/height parsing just above -
|
|
369
|
+
// style wins over the legacy HTML attributes, and this is also what
|
|
370
|
+
// this app's own copy output writes them as (atom/formats.ts's
|
|
371
|
+
// atomToHtml), so a self-copy/self-paste round-trip depends on this.
|
|
372
|
+
const width = parsePixelWidth(styleValue(node, "width")) ?? parsePixelWidth(attr(node, "width"));
|
|
373
|
+
const height = parsePixelWidth(styleValue(node, "height")) ?? parsePixelWidth(attr(node, "height"));
|
|
374
|
+
return { type: "block_image", id: generatedId(node, "image"), attrs: {
|
|
375
|
+
src, alt: attr(node, "alt") || "", status: attr(node, "data-smart-status") || "ready",
|
|
376
|
+
...(attr(node, "data-smart-decorative") === "true" ? { decorative: true } : {}),
|
|
377
|
+
...(width !== null ? { width } : {}), ...(height !== null ? { height } : {}),
|
|
378
|
+
} };
|
|
379
|
+
}
|
|
380
|
+
if (declaredAtom === "block_formula")
|
|
381
|
+
return { type: "block_formula", id: generatedId(node, "formula"), attrs: { source: attr(node, "data-smart-formula") || rawText(node), notation: attr(node, "data-smart-notation") === "mathml" ? "mathml" : "latex" } };
|
|
382
|
+
if (tag === "hr")
|
|
383
|
+
return { type: "divider", id: generatedId(node, "divider") };
|
|
384
|
+
// A third-party <img> sitting directly at block level (not wrapped in a
|
|
385
|
+
// <p>) - the ordinary shape for a standalone content photo on most real
|
|
386
|
+
// websites, e.g. a bare <img> between paragraphs or inside a <figure>
|
|
387
|
+
// (now a TRANSPARENT_CONTAINER_TAGS entry, so its <img> reaches here
|
|
388
|
+
// directly). Previously had no case at all - every recognized block tag
|
|
389
|
+
// above and below is checked first, and a bare "img" tag matches none of
|
|
390
|
+
// them, so it fell all the way to the generic "unrecognized tag"
|
|
391
|
+
// fallback at the bottom of this function, producing "[Unsupported:
|
|
392
|
+
// img]"/"[Unsupported: figure]" instead of an actual image. Mirrors the
|
|
393
|
+
// inline `image` atom's own src/width/height parsing just above in
|
|
394
|
+
// textWithMarks (declaredAtom === "block_image" above only fires for
|
|
395
|
+
// this app's own round-tripped export, which explicitly marks the tag -
|
|
396
|
+
// third-party HTML never does).
|
|
397
|
+
if (tag === "img") {
|
|
398
|
+
const src = sanitizeAtomSource(attr(node, "src"), { kind: "image" });
|
|
399
|
+
if (src) {
|
|
400
|
+
const width = parsePixelWidth(styleValue(node, "width")) ?? parsePixelWidth(attr(node, "width"));
|
|
401
|
+
const height = parsePixelWidth(styleValue(node, "height")) ?? parsePixelWidth(attr(node, "height"));
|
|
402
|
+
return { type: "block_image", id: generatedId(node, "image"), attrs: {
|
|
403
|
+
src, alt: attr(node, "alt") || "", status: "ready",
|
|
404
|
+
...(width !== null ? { width } : {}), ...(height !== null ? { height } : {}),
|
|
405
|
+
} };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Real, captured evidence (an actual browser "select the image element,
|
|
409
|
+
// copy" gesture - the closest scriptable equivalent of a native
|
|
410
|
+
// right-click "Copy image" - against a live Wikipedia photo): the
|
|
411
|
+
// clipboard payload was a bare <a href="..."><img ...></a>, no <p>/<td>
|
|
412
|
+
// wrapper at all. A link wrapping nothing but a single image, sitting at
|
|
413
|
+
// block level, unwraps to that image the same way the bare-<img> case
|
|
414
|
+
// above does - the link target (usually the source page/file, not
|
|
415
|
+
// meaningful to preserve as a clickable wrapper around an atomic block
|
|
416
|
+
// image in this schema) is intentionally dropped rather than attempted
|
|
417
|
+
// to be represented some other way.
|
|
418
|
+
if (tag === "a") {
|
|
419
|
+
const onlyChild = elementChildren(node);
|
|
420
|
+
if (onlyChild.length === 1 && onlyChild[0].tagName === "img") {
|
|
421
|
+
const image = parseBlock(onlyChild[0]);
|
|
422
|
+
if (image)
|
|
423
|
+
return image;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (tag === "video" || tag === "audio") {
|
|
427
|
+
const src = sanitizeAtomSource(attr(node, "src"), { kind: tag });
|
|
428
|
+
return src ? { type: tag, id: generatedId(node, tag), attrs: { src, status: "ready", ...(tag === "video" && attr(node, "poster") ? { poster: attr(node, "poster") } : {}) } } : null;
|
|
429
|
+
}
|
|
430
|
+
if (tag === "p" || /^h[1-6]$/.test(tag))
|
|
431
|
+
return {
|
|
432
|
+
type: tag === "p" ? "paragraph" : "heading",
|
|
433
|
+
id: generatedId(node, tag),
|
|
434
|
+
...((tag !== "p" || Object.keys(parsedBlockAttrs(node)).length) ? { attrs: {
|
|
435
|
+
...(tag !== "p" ? { level: Number(tag.slice(1)) } : {}),
|
|
436
|
+
...parsedBlockAttrs(node),
|
|
437
|
+
} } : {}),
|
|
438
|
+
children: (node.childNodes || []).flatMap((child) => textWithMarks(child)),
|
|
439
|
+
};
|
|
440
|
+
if (tag === "blockquote") {
|
|
441
|
+
// Real-world exports (Sootr among them) put inline content - <span>
|
|
442
|
+
// wrapper runs, bare text, <br> - directly inside <blockquote> with no
|
|
443
|
+
// wrapping <p>. Without this split, elementChildren+parseBlock alone
|
|
444
|
+
// sent every such child through the generic "unrecognized tag" fallback
|
|
445
|
+
// at the bottom of this function, producing an unknown block node per
|
|
446
|
+
// span (rendered as "[Unsupported: span]") instead of parsed text/marks
|
|
447
|
+
// - the same directInline pattern td/li already use below.
|
|
448
|
+
const blockTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "blockquote", "pre", "table"];
|
|
449
|
+
const isBlockLike = (child) => blockTags.includes(child.tagName || "") || TRANSPARENT_CONTAINER_TAGS.includes(child.tagName || "");
|
|
450
|
+
const children = parseBlockList(elementChildren(node).filter(isBlockLike));
|
|
451
|
+
const directInline = (node.childNodes || []).filter((child) => !child.tagName || !isBlockLike(child)).flatMap((child) => textWithMarks(child));
|
|
452
|
+
if (directInline.length)
|
|
453
|
+
children.unshift({ type: "paragraph", id: createNodeId(), children: directInline });
|
|
454
|
+
return {
|
|
455
|
+
type: "blockquote", id: generatedId(node, "quote"),
|
|
456
|
+
...(Object.keys(parsedBlockAttrs(node)).length ? { attrs: parsedBlockAttrs(node) } : {}),
|
|
457
|
+
children: children.length ? children : [{ type: "paragraph", id: createNodeId(), children: [] }],
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (tag === "pre") {
|
|
461
|
+
const code = elementChildren(node).find((child) => child.tagName === "code");
|
|
462
|
+
const language = attr(node, "data-smart-language")
|
|
463
|
+
|| attr(code || node, "data-smart-language")
|
|
464
|
+
|| attr(code || node, "class")?.split(/\s+/).find((value) => value.startsWith("language-"))?.slice(9);
|
|
465
|
+
const attrs = { ...parsedBlockAttrs(node), ...(language ? { language } : {}) };
|
|
466
|
+
const text = rawText(code || node);
|
|
467
|
+
return {
|
|
468
|
+
type: "code_block", id: generatedId(node, "code"),
|
|
469
|
+
...(Object.keys(attrs).length ? { attrs } : {}),
|
|
470
|
+
children: text ? [{ type: "text", text }] : [],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (tag === "table") {
|
|
474
|
+
const rowNodes = elementChildren(node).flatMap((child) => child.tagName === "tr" ? [child]
|
|
475
|
+
: ["thead", "tbody", "tfoot"].includes(child.tagName || "") ? elementChildren(child).filter((candidate) => candidate.tagName === "tr") : []);
|
|
476
|
+
const columns = elementChildren(node).find((child) => child.tagName === "colgroup");
|
|
477
|
+
// All-or-nothing: a table where only some (or none) of its <col>s
|
|
478
|
+
// specify a real pixel width is rendered at its natural/stretched
|
|
479
|
+
// default (no columnWidths at all) rather than mixing real values
|
|
480
|
+
// with a fabricated fallback for the rest - see parsePixelWidth's
|
|
481
|
+
// comment for why a fallback here caused pasted tables to shrink.
|
|
482
|
+
const parsedColumnWidths = columns
|
|
483
|
+
? elementChildren(columns).filter((child) => child.tagName === "col")
|
|
484
|
+
.map((col) => parsePixelWidth(styleValue(col, "width")) ?? parsePixelWidth(attr(col, "width")))
|
|
485
|
+
: [];
|
|
486
|
+
const columnWidths = parsedColumnWidths.length && parsedColumnWidths.every((width) => width !== null)
|
|
487
|
+
? parsedColumnWidths
|
|
488
|
+
: [];
|
|
489
|
+
const captionNode = elementChildren(node).find((child) => child.tagName === "caption");
|
|
490
|
+
const tableAttrs = {};
|
|
491
|
+
if (columnWidths.length)
|
|
492
|
+
tableAttrs.columnWidths = columnWidths;
|
|
493
|
+
const caption = captionNode ? rawText(captionNode).trim() : "";
|
|
494
|
+
if (caption)
|
|
495
|
+
tableAttrs.caption = caption;
|
|
496
|
+
const layout = attr(node, "data-smart-layout") || styleValue(node, "table-layout");
|
|
497
|
+
if (layout)
|
|
498
|
+
tableAttrs.layout = layout;
|
|
499
|
+
const rows = rowNodes.map((rowNode) => parseBlock(rowNode)).filter((row) => Boolean(row));
|
|
500
|
+
return { type: "table", id: generatedId(node, "table"), ...(Object.keys(tableAttrs).length ? { attrs: tableAttrs } : {}), children: rows };
|
|
501
|
+
}
|
|
502
|
+
if (tag === "tr") {
|
|
503
|
+
const height = Number.parseFloat(styleValue(node, "height") || attr(node, "height") || "");
|
|
504
|
+
const cells = elementChildren(node).filter((child) => child.tagName === "td" || child.tagName === "th")
|
|
505
|
+
.map((cell) => parseBlock(cell)).filter((cell) => Boolean(cell));
|
|
506
|
+
return { type: "table_row", id: generatedId(node, "row"), ...(Number.isFinite(height) && height > 0 ? { attrs: { height } } : {}), children: cells };
|
|
507
|
+
}
|
|
508
|
+
if (tag === "td" || tag === "th") {
|
|
509
|
+
const cellAttrs = {
|
|
510
|
+
rowspan: Math.max(1, Number(attr(node, "rowspan")) || 1),
|
|
511
|
+
colspan: Math.max(1, Number(attr(node, "colspan")) || 1),
|
|
512
|
+
header: tag === "th",
|
|
513
|
+
};
|
|
514
|
+
const background = styleValue(node, "background") || styleValue(node, "background-color");
|
|
515
|
+
const borders = styleValue(node, "border");
|
|
516
|
+
const textColor = styleValue(node, "color");
|
|
517
|
+
const verticalAlign = styleValue(node, "vertical-align");
|
|
518
|
+
if (background)
|
|
519
|
+
cellAttrs.background = background;
|
|
520
|
+
if (borders)
|
|
521
|
+
cellAttrs.borders = borders;
|
|
522
|
+
// Per-side declarations (e.g. from a source that styled only one edge)
|
|
523
|
+
// parse independently of the uniform shorthand above - both can be
|
|
524
|
+
// present at once, matching how the renderer layers them.
|
|
525
|
+
const borderTop = styleValue(node, "border-top");
|
|
526
|
+
const borderRight = styleValue(node, "border-right");
|
|
527
|
+
const borderBottom = styleValue(node, "border-bottom");
|
|
528
|
+
const borderLeft = styleValue(node, "border-left");
|
|
529
|
+
if (borderTop)
|
|
530
|
+
cellAttrs.borderTop = borderTop;
|
|
531
|
+
if (borderRight)
|
|
532
|
+
cellAttrs.borderRight = borderRight;
|
|
533
|
+
if (borderBottom)
|
|
534
|
+
cellAttrs.borderBottom = borderBottom;
|
|
535
|
+
if (borderLeft)
|
|
536
|
+
cellAttrs.borderLeft = borderLeft;
|
|
537
|
+
if (textColor)
|
|
538
|
+
cellAttrs.textColor = textColor;
|
|
539
|
+
if (verticalAlign)
|
|
540
|
+
cellAttrs.verticalAlign = verticalAlign;
|
|
541
|
+
const blockTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "blockquote", "pre", "table"];
|
|
542
|
+
const isBlockLike = (child) => blockTags.includes(child.tagName || "") || TRANSPARENT_CONTAINER_TAGS.includes(child.tagName || "");
|
|
543
|
+
const children = parseBlockList(elementChildren(node).filter(isBlockLike));
|
|
544
|
+
const directInline = (node.childNodes || []).filter((child) => !child.tagName || !isBlockLike(child)).flatMap((child) => textWithMarks(child));
|
|
545
|
+
if (directInline.length)
|
|
546
|
+
children.unshift({ type: "paragraph", id: createNodeId(), children: directInline });
|
|
547
|
+
if (!children.length)
|
|
548
|
+
children.push({ type: "paragraph", id: createNodeId(), children: [] });
|
|
549
|
+
return { type: "table_cell", id: generatedId(node, "cell"), attrs: cellAttrs, children };
|
|
550
|
+
}
|
|
551
|
+
if (tag === "ul" || tag === "ol") {
|
|
552
|
+
const listAttrs = {};
|
|
553
|
+
const requestedPreset = attr(node, "data-smart-list-preset") || attr(node, "data-srte-list-preset");
|
|
554
|
+
const preset = isFoundationSmartListPreset(requestedPreset) ? requestedPreset : undefined;
|
|
555
|
+
const explicitStyle = attr(node, "data-smart-list-style");
|
|
556
|
+
const style = explicitStyle || (!preset ? styleValue(node, "list-style-type") : undefined) || (!preset ? (tag === "ol" ? "decimal" : "disc") : undefined);
|
|
557
|
+
const start = Number(attr(node, "start"));
|
|
558
|
+
if (preset)
|
|
559
|
+
listAttrs.preset = preset;
|
|
560
|
+
if (style)
|
|
561
|
+
listAttrs.style = style;
|
|
562
|
+
if (Number.isInteger(start) && start >= 1)
|
|
563
|
+
listAttrs.start = start;
|
|
564
|
+
if (attr(node, "data-smart-checkable") === "true" || attr(node, "data-srte-checklist") === "true")
|
|
565
|
+
listAttrs.checkable = true;
|
|
566
|
+
const items = [];
|
|
567
|
+
elementChildren(node).forEach((child) => {
|
|
568
|
+
if (child.tagName === "li") {
|
|
569
|
+
const parsed = parseBlock(child);
|
|
570
|
+
if (parsed)
|
|
571
|
+
items.push(parsed);
|
|
572
|
+
}
|
|
573
|
+
else if ((child.tagName === "ul" || child.tagName === "ol") && items.length) {
|
|
574
|
+
const nested = parseBlock(child);
|
|
575
|
+
if (nested) {
|
|
576
|
+
const last = items[items.length - 1];
|
|
577
|
+
items[items.length - 1] = { ...last, children: [...(last.children || []), nested] };
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
return { type: "list", id: generatedId(node, "list"), attrs: listAttrs, children: items.length ? items : [{ type: "list_item", id: createNodeId(), children: [{ type: "paragraph", id: createNodeId(), children: [] }] }] };
|
|
582
|
+
}
|
|
583
|
+
if (tag === "li") {
|
|
584
|
+
const attrs = {};
|
|
585
|
+
const checked = attr(node, "data-smart-checked") || attr(node, "data-srte-checked") || attr(node, "data-checked");
|
|
586
|
+
const value = Number(attr(node, "value"));
|
|
587
|
+
if (checked !== undefined)
|
|
588
|
+
attrs.checked = checked === "true";
|
|
589
|
+
const htmlStyle = withoutListMarkerStyle(attr(node, "style"));
|
|
590
|
+
if (htmlStyle)
|
|
591
|
+
attrs.htmlStyle = htmlStyle;
|
|
592
|
+
if (Number.isInteger(value) && value >= 1)
|
|
593
|
+
attrs.numberOverride = value;
|
|
594
|
+
const children = [];
|
|
595
|
+
const blockTags = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "blockquote", "pre", "div", "table", "figure"];
|
|
596
|
+
const inlineNodes = (node.childNodes || []).filter((child) => !child.tagName || !blockTags.includes(child.tagName));
|
|
597
|
+
const directText = inlineNodes.flatMap((child) => textWithMarks(child));
|
|
598
|
+
if (directText.length)
|
|
599
|
+
children.push({ type: "paragraph", id: createNodeId(), children: directText });
|
|
600
|
+
// blockTags already lists "div"/"figure" as block-worthy, but parseBlock
|
|
601
|
+
// itself has no case for either - parseBlockList is what actually
|
|
602
|
+
// unwraps them (rather than swallowing their content into one opaque
|
|
603
|
+
// `unknown` node) instead of parsing them directly.
|
|
604
|
+
children.push(...parseBlockList(elementChildren(node).filter((child) => blockTags.includes(child.tagName || ""))));
|
|
605
|
+
if (!children.length)
|
|
606
|
+
children.push({ type: "paragraph", id: createNodeId(), children: [] });
|
|
607
|
+
return { type: "list_item", id: generatedId(node, "item"), ...(Object.keys(attrs).length ? { attrs } : {}), children };
|
|
608
|
+
}
|
|
609
|
+
if (tag)
|
|
610
|
+
return {
|
|
611
|
+
type: "unknown", id: generatedId(node, "unknown"),
|
|
612
|
+
attrs: { originalType: attr(node, "data-smart-unknown-type") || tag, originalGroup: "block", raw: { html: serializeOuter(node) }, editable: false },
|
|
613
|
+
};
|
|
614
|
+
return null;
|
|
615
|
+
};
|
|
616
|
+
/**
|
|
617
|
+
* A thin wrapper around parseBlock for "list of block-level children"
|
|
618
|
+
* call sites: a transparent container (TRANSPARENT_CONTAINER_TAGS) is
|
|
619
|
+
* recursed into and its own children spliced in flat, rather than parsed
|
|
620
|
+
* as one opaque node - a div wrapping N real blocks (a table, N
|
|
621
|
+
* paragraphs, or nothing at all) produces exactly those N blocks instead
|
|
622
|
+
* of a single `unknown` placeholder that hides all of them.
|
|
623
|
+
*/
|
|
624
|
+
const parseBlockList = (nodes) => nodes.flatMap((node) => {
|
|
625
|
+
if (TRANSPARENT_CONTAINER_TAGS.includes(node.tagName || ""))
|
|
626
|
+
return parseBlockList(elementChildren(node));
|
|
627
|
+
const parsed = parseBlock(node);
|
|
628
|
+
return parsed ? [parsed] : [];
|
|
629
|
+
});
|
|
630
|
+
export const parseCanonicalListHtml = (html) => {
|
|
631
|
+
const fragment = parseFragment(html);
|
|
632
|
+
const wrapper = elementChildren(fragment).find((node) => attr(node, "data-smart-document") === "true");
|
|
633
|
+
const source = wrapper || fragment;
|
|
634
|
+
const children = parseBlockList(elementChildren(source));
|
|
635
|
+
return { type: "doc", id: attr(source, "data-smart-id") || createNodeId(), children: children.length ? children : [{ type: "paragraph", id: createNodeId(), children: [] }] };
|
|
636
|
+
};
|
|
637
|
+
const plainText = (node) => (node.children || []).map((child) => isTextNode(child) ? child.text : child.type === "hard_break" ? "\n" : "").join("");
|
|
638
|
+
const markdownInlineText = (node) => (node.children || []).map((child) => {
|
|
639
|
+
if (!isTextNode(child)) {
|
|
640
|
+
if (child.type === "hard_break")
|
|
641
|
+
return " \n";
|
|
642
|
+
if (child.type === "image" || child.type === "formula")
|
|
643
|
+
return atomToMarkdown(child);
|
|
644
|
+
return "";
|
|
645
|
+
}
|
|
646
|
+
let value = child.text;
|
|
647
|
+
[...canonicalMarkOrder(child.marks)].reverse().forEach((mark) => {
|
|
648
|
+
if (mark.type === "code")
|
|
649
|
+
value = `\`${value}\``;
|
|
650
|
+
else if (mark.type === "bold")
|
|
651
|
+
value = `**${value}**`;
|
|
652
|
+
else if (mark.type === "italic")
|
|
653
|
+
value = `*${value}*`;
|
|
654
|
+
else if (mark.type === "strike")
|
|
655
|
+
value = `~~${value}~~`;
|
|
656
|
+
else if (mark.type === "link")
|
|
657
|
+
value = `[${value}](${String(mark.attrs?.href || "")})`;
|
|
658
|
+
});
|
|
659
|
+
return value;
|
|
660
|
+
}).join("");
|
|
661
|
+
const markdownList = (list, depth) => (list.children || []).flatMap((child, index) => {
|
|
662
|
+
if (isTextNode(child))
|
|
663
|
+
return [];
|
|
664
|
+
const content = (child.children || []).filter((node) => !isTextNode(node) && node.type !== "list");
|
|
665
|
+
const marker = orderedList(list) ? `${Number(list.attrs?.start || 1) + index}.` : "-";
|
|
666
|
+
const task = list.attrs?.checkable === true ? `[${child.attrs?.checked === true ? "x" : " "}] ` : "";
|
|
667
|
+
const indentation = " ".repeat(depth);
|
|
668
|
+
const line = `${indentation}${marker} ${task}${content[0] ? markdownInlineText(content[0]) : ""}`;
|
|
669
|
+
const continuation = content.slice(1).flatMap((block) => ["", `${indentation} ${markdownInlineText(block)}`]);
|
|
670
|
+
const nested = (child.children || []).filter((node) => !isTextNode(node) && node.type === "list")
|
|
671
|
+
.flatMap((node) => markdownList(node, depth + 1));
|
|
672
|
+
return [line, ...continuation, ...nested];
|
|
673
|
+
});
|
|
674
|
+
const markdownBlock = (node) => {
|
|
675
|
+
if (["block_image", "block_formula", "video", "audio", "divider"].includes(node.type))
|
|
676
|
+
return [atomToMarkdown(node)];
|
|
677
|
+
if (node.type === "list")
|
|
678
|
+
return markdownList(node, 0);
|
|
679
|
+
if (node.type === "heading")
|
|
680
|
+
return [`${"#".repeat(Math.max(1, Math.min(6, Number(node.attrs?.level) || 1)))} ${markdownInlineText(node)}`];
|
|
681
|
+
if (node.type === "code_block") {
|
|
682
|
+
const language = typeof node.attrs?.language === "string" ? node.attrs.language : "";
|
|
683
|
+
return [`\`\`\`${language}\n${plainText(node)}\n\`\`\``];
|
|
684
|
+
}
|
|
685
|
+
if (node.type === "blockquote") {
|
|
686
|
+
const content = (node.children || []).filter((child) => !isTextNode(child))
|
|
687
|
+
.flatMap(markdownBlock).join("\n");
|
|
688
|
+
return [content.split("\n").map((line) => `> ${line}`).join("\n")];
|
|
689
|
+
}
|
|
690
|
+
if (node.type === "table") {
|
|
691
|
+
const rows = (node.children || []).filter((child) => !isTextNode(child) && child.type === "table_row");
|
|
692
|
+
const logical = rows.map((row) => (row.children || []).filter((child) => !isTextNode(child) && child.type === "table_cell")
|
|
693
|
+
.flatMap((cell) => [markdownInlineText((cell.children || []).find((child) => !isTextNode(child)) || cell), ...Array(Math.max(0, Number(cell.attrs?.colspan) || 1) - 1).fill("")]));
|
|
694
|
+
const width = logical.reduce((max, row) => Math.max(max, row.length), 0);
|
|
695
|
+
if (!width)
|
|
696
|
+
return [""];
|
|
697
|
+
const padded = logical.map((row) => [...row, ...Array(width - row.length).fill("")]);
|
|
698
|
+
const first = padded[0] || Array(width).fill("");
|
|
699
|
+
return [[`| ${first.join(" | ")} |`, `| ${Array(width).fill("---").join(" | ")} |`, ...padded.slice(1).map((row) => `| ${row.join(" | ")} |`)].join("\n")];
|
|
700
|
+
}
|
|
701
|
+
return [markdownInlineText(node)];
|
|
702
|
+
};
|
|
703
|
+
/** Alignment and indent are unsupported in Markdown; block content survives semantically. */
|
|
704
|
+
export const serializeCanonicalListMarkdown = (document) => document.children.flatMap((node) => isTextNode(node) ? [node.text] : markdownBlock(node)).join("\n");
|
|
705
|
+
const markdownText = (node) => node.value || (node.children || []).map(markdownText).join("");
|
|
706
|
+
/**
|
|
707
|
+
* remark's core parser has no math extension, so `$x^2$` survives parsing
|
|
708
|
+
* as literal text on a plain text node - split any dollar-delimited
|
|
709
|
+
* formula regions out of a text run into formula atoms, post-parse. Only
|
|
710
|
+
* applied to unmarked (mark-free) text, matching how the serializer only
|
|
711
|
+
* emits `$source$` at the top level, never inside bold/italic/etc.
|
|
712
|
+
*/
|
|
713
|
+
const splitInlineFormulas = (text, marks) => {
|
|
714
|
+
if (marks.length)
|
|
715
|
+
return [{ type: "text", text, marks: canonicalMarkOrder(marks) }];
|
|
716
|
+
// Block math ($$\n...\n$$, atomToMarkdown's block_formula form) first,
|
|
717
|
+
// since it would otherwise be swallowed as three separate single-$ matches;
|
|
718
|
+
// then inline math ($...$) over what's left.
|
|
719
|
+
const pattern = /\$\$\n([^]*?)\n\$\$|\$([^$\n]+)\$/g;
|
|
720
|
+
const output = [];
|
|
721
|
+
let cursor = 0;
|
|
722
|
+
let match;
|
|
723
|
+
while ((match = pattern.exec(text))) {
|
|
724
|
+
if (match.index > cursor)
|
|
725
|
+
output.push({ type: "text", text: text.slice(cursor, match.index) });
|
|
726
|
+
output.push({ type: "formula", id: createNodeId(), attrs: { source: match[1] ?? match[2] ?? "", notation: "latex" } });
|
|
727
|
+
cursor = pattern.lastIndex;
|
|
728
|
+
}
|
|
729
|
+
if (!output.length)
|
|
730
|
+
return [{ type: "text", text }];
|
|
731
|
+
if (cursor < text.length)
|
|
732
|
+
output.push({ type: "text", text: text.slice(cursor) });
|
|
733
|
+
return output;
|
|
734
|
+
};
|
|
735
|
+
const markdownInline = (node, inherited = []) => {
|
|
736
|
+
if (node.type === "text")
|
|
737
|
+
return node.value ? splitInlineFormulas(node.value, inherited) : [];
|
|
738
|
+
if (node.type === "inlineCode")
|
|
739
|
+
return node.value ? [{ type: "text", text: node.value, marks: canonicalMarkOrder([...inherited, { type: "code" }]) }] : [];
|
|
740
|
+
if (node.type === "break")
|
|
741
|
+
return [{ type: "hard_break", id: createNodeId() }];
|
|
742
|
+
if (node.type === "image")
|
|
743
|
+
return node.url ? [{ type: "image", id: createNodeId(), attrs: { src: node.url, alt: node.alt || "" } }] : [];
|
|
744
|
+
const marks = [...inherited];
|
|
745
|
+
if (node.type === "strong")
|
|
746
|
+
marks.push({ type: "bold" });
|
|
747
|
+
if (node.type === "emphasis")
|
|
748
|
+
marks.push({ type: "italic" });
|
|
749
|
+
if (node.type === "delete")
|
|
750
|
+
marks.push({ type: "strike" });
|
|
751
|
+
if (node.type === "link")
|
|
752
|
+
marks.push({ type: "link", attrs: { href: node.url || "" } });
|
|
753
|
+
return (node.children || []).flatMap((child) => markdownInline(child, marks));
|
|
754
|
+
};
|
|
755
|
+
const listFromMarkdown = (node) => {
|
|
756
|
+
const checkable = (node.children || []).some((item) => item.checked !== null && item.checked !== undefined);
|
|
757
|
+
return {
|
|
758
|
+
type: "list", id: createNodeId(), attrs: {
|
|
759
|
+
style: node.ordered ? "decimal" : "disc",
|
|
760
|
+
...(node.ordered && node.start && node.start !== 1 ? { start: node.start } : {}),
|
|
761
|
+
...(checkable ? { checkable: true } : {}),
|
|
762
|
+
}, children: (node.children || []).map((itemNode) => {
|
|
763
|
+
const blocks = [];
|
|
764
|
+
(itemNode.children || []).forEach((child) => {
|
|
765
|
+
blocks.push(...blocksFromMarkdown(child));
|
|
766
|
+
});
|
|
767
|
+
return { type: "list_item", id: createNodeId(), ...(checkable ? { attrs: { checked: itemNode.checked === true } } : {}), children: blocks.length ? blocks : [{ type: "paragraph", id: createNodeId(), children: [] }] };
|
|
768
|
+
}),
|
|
769
|
+
};
|
|
770
|
+
};
|
|
771
|
+
const blocksFromMarkdown = (node) => {
|
|
772
|
+
if (node.type === "list")
|
|
773
|
+
return [listFromMarkdown(node)];
|
|
774
|
+
if (node.type === "heading")
|
|
775
|
+
return [{ type: "heading", id: createNodeId(), attrs: { level: Math.max(1, Math.min(6, node.depth || 1)) }, children: (node.children || []).flatMap((inline) => markdownInline(inline)) }];
|
|
776
|
+
if (node.type === "code")
|
|
777
|
+
return [{ type: "code_block", id: createNodeId(), ...(node.lang ? { attrs: { language: node.lang } } : {}), children: node.value ? [{ type: "text", text: node.value }] : [] }];
|
|
778
|
+
if (node.type === "blockquote") {
|
|
779
|
+
const children = (node.children || []).flatMap(blocksFromMarkdown);
|
|
780
|
+
return [{ type: "blockquote", id: createNodeId(), children: children.length ? children : [{ type: "paragraph", id: createNodeId(), children: [] }] }];
|
|
781
|
+
}
|
|
782
|
+
return [{ type: "paragraph", id: createNodeId(), children: (node.children || []).flatMap((inline) => markdownInline(inline)) }];
|
|
783
|
+
};
|
|
784
|
+
export const parseCanonicalListMarkdown = (markdown) => {
|
|
785
|
+
const tree = unified().use(remarkParse).use(remarkGfm).parse(markdown);
|
|
786
|
+
const children = (tree.children || []).flatMap(blocksFromMarkdown);
|
|
787
|
+
return { type: "doc", id: createNodeId(), children: children.length ? children : [{ type: "paragraph", id: createNodeId(), children: [] }] };
|
|
788
|
+
};
|
|
789
|
+
/** DOCX has numId/ilvl but no Smart preset identity; presets fall back semantically. */
|
|
790
|
+
export const canonicalListToDocxNumbering = (document) => {
|
|
791
|
+
const entries = [];
|
|
792
|
+
let numId = 1;
|
|
793
|
+
const visit = (list, depth, currentNumId) => {
|
|
794
|
+
for (const child of list.children || []) {
|
|
795
|
+
if (isTextNode(child))
|
|
796
|
+
continue;
|
|
797
|
+
const content = (child.children || []).find((node) => !isTextNode(node) && node.type !== "list");
|
|
798
|
+
entries.push({
|
|
799
|
+
itemId: child.id, numId: currentNumId, ilvl: depth,
|
|
800
|
+
marker: orderedList(list) ? "decimal" : "bullet",
|
|
801
|
+
text: content ? plainText(content) : "",
|
|
802
|
+
...(list.attrs?.checkable === true ? { checked: child.attrs?.checked === true } : {}),
|
|
803
|
+
});
|
|
804
|
+
(child.children || []).forEach((node) => { if (!isTextNode(node) && node.type === "list")
|
|
805
|
+
visit(node, depth + 1, currentNumId); });
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
document.children.forEach((node) => { if (!isTextNode(node) && node.type === "list")
|
|
809
|
+
visit(node, 0, numId++); });
|
|
810
|
+
return entries;
|
|
811
|
+
};
|
|
812
|
+
export const canonicalListPdfText = (document) => canonicalListToDocxNumbering(document)
|
|
813
|
+
.map((entry) => `${" ".repeat(entry.ilvl)}${entry.marker === "decimal" ? "1." : "•"} ${entry.text}`).join("\n");
|