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,38 @@
|
|
|
1
|
+
import type { ModelDomMapping, SmartDocument, SmartElementNode, SmartNode, SmartPos } from "./types.js";
|
|
2
|
+
export declare const SMART_UI_ATTRIBUTE = "data-smart-ui";
|
|
3
|
+
export declare const SMART_PROJECTION_ATTRIBUTE = "data-smart-projection";
|
|
4
|
+
export declare const SMART_NODE_ID_ATTRIBUTE = "data-smart-id";
|
|
5
|
+
export declare const SMART_EMPTY_LINE_ATTRIBUTE = "data-smart-empty-line";
|
|
6
|
+
/**
|
|
7
|
+
* Correctness renderer for the mapping contract. The production strategy is
|
|
8
|
+
* contentEditable plus subtree diffing. Benchmark content-visibility before
|
|
9
|
+
* considering windowing; destructive editable-region virtualization is out of
|
|
10
|
+
* scope unless measurements prove it unavoidable.
|
|
11
|
+
*/
|
|
12
|
+
export declare const renderDocumentNaively: (root: HTMLElement, document: SmartDocument) => FoundationModelDomMapping;
|
|
13
|
+
export declare class FoundationModelDomMapping implements ModelDomMapping {
|
|
14
|
+
private root;
|
|
15
|
+
private document;
|
|
16
|
+
private readonly byId;
|
|
17
|
+
private readonly byDom;
|
|
18
|
+
private readonly pathById;
|
|
19
|
+
rebuild(root: HTMLElement, document: SmartDocument): void;
|
|
20
|
+
/** Incremental renderer hook. Paths passed here are already current. */
|
|
21
|
+
beginUpdate(document: SmartDocument): void;
|
|
22
|
+
track(node: SmartElementNode, path: readonly number[], element: HTMLElement): void;
|
|
23
|
+
forget(nodeId: string): void;
|
|
24
|
+
nodeToDom(nodeId: string): HTMLElement | null;
|
|
25
|
+
domToNode(input: Node): {
|
|
26
|
+
nodeId: string;
|
|
27
|
+
node: SmartNode;
|
|
28
|
+
} | null;
|
|
29
|
+
private modelDomChildren;
|
|
30
|
+
private isInlineOwner;
|
|
31
|
+
private textDescendant;
|
|
32
|
+
posToDom(pos: SmartPos): {
|
|
33
|
+
node: Node;
|
|
34
|
+
offset: number;
|
|
35
|
+
} | null;
|
|
36
|
+
domToPos(node: Node, offset: number): SmartPos | null;
|
|
37
|
+
isEditorUiNode(node: Node): boolean;
|
|
38
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { isTextNode } from "./identity.js";
|
|
2
|
+
import { nodeAtPath } from "./positions.js";
|
|
3
|
+
import { renderMarkedText } from "./marks/dom.js";
|
|
4
|
+
import { sanitizeAtomSource } from "./atom/security.js";
|
|
5
|
+
const atomTypes = new Set(["image", "block_image", "formula", "block_formula", "video", "audio", "divider"]);
|
|
6
|
+
export const SMART_UI_ATTRIBUTE = "data-smart-ui";
|
|
7
|
+
export const SMART_PROJECTION_ATTRIBUTE = "data-smart-projection";
|
|
8
|
+
export const SMART_NODE_ID_ATTRIBUTE = "data-smart-id";
|
|
9
|
+
export const SMART_EMPTY_LINE_ATTRIBUTE = "data-smart-empty-line";
|
|
10
|
+
const tagForNode = (node) => {
|
|
11
|
+
if (node.type === "paragraph")
|
|
12
|
+
return "p";
|
|
13
|
+
if (node.type === "heading")
|
|
14
|
+
return `h${String(node.attrs?.level || 1)}`;
|
|
15
|
+
if (node.type === "blockquote")
|
|
16
|
+
return "blockquote";
|
|
17
|
+
if (node.type === "code_block")
|
|
18
|
+
return "pre";
|
|
19
|
+
if (node.type === "list") {
|
|
20
|
+
const marker = String(node.attrs?.style || node.attrs?.preset || "");
|
|
21
|
+
return /^(?:decimal|lower-|upper-|ordered)/.test(marker) || node.attrs?.start !== undefined ? "ol" : "ul";
|
|
22
|
+
}
|
|
23
|
+
if (node.type === "list_item")
|
|
24
|
+
return "li";
|
|
25
|
+
if (node.type === "hard_break")
|
|
26
|
+
return "br";
|
|
27
|
+
if (node.type === "table")
|
|
28
|
+
return "table";
|
|
29
|
+
if (node.type === "table_row")
|
|
30
|
+
return "tr";
|
|
31
|
+
if (node.type === "table_cell")
|
|
32
|
+
return node.attrs?.header === true ? "th" : "td";
|
|
33
|
+
if (node.type === "image" || node.type === "block_image")
|
|
34
|
+
return "img";
|
|
35
|
+
if (node.type === "formula")
|
|
36
|
+
return "span";
|
|
37
|
+
if (node.type === "block_formula")
|
|
38
|
+
return "div";
|
|
39
|
+
if (node.type === "video" || node.type === "audio")
|
|
40
|
+
return node.type;
|
|
41
|
+
return "div";
|
|
42
|
+
};
|
|
43
|
+
const renderNode = (node, ownerDocument) => {
|
|
44
|
+
if (isTextNode(node))
|
|
45
|
+
return renderMarkedText(node, ownerDocument);
|
|
46
|
+
const element = ownerDocument.createElement(tagForNode(node));
|
|
47
|
+
element.setAttribute(SMART_NODE_ID_ATTRIBUTE, node.id);
|
|
48
|
+
element.setAttribute("data-smart-type", node.type);
|
|
49
|
+
if (node.attrs?.align)
|
|
50
|
+
element.style.textAlign = String(node.attrs.align);
|
|
51
|
+
if (node.attrs?.indentLevel)
|
|
52
|
+
element.style.marginInlineStart = `${Number(node.attrs.indentLevel) * 2}em`;
|
|
53
|
+
if (node.type === "code_block") {
|
|
54
|
+
const language = typeof node.attrs?.language === "string" && node.attrs.language.trim()
|
|
55
|
+
? node.attrs.language.trim()
|
|
56
|
+
: null;
|
|
57
|
+
if (language)
|
|
58
|
+
element.setAttribute("data-smart-language", language);
|
|
59
|
+
element.setAttribute("aria-label", language ? `Code block, ${language}` : "Code block");
|
|
60
|
+
}
|
|
61
|
+
if (node.type === "list") {
|
|
62
|
+
if (typeof node.attrs?.preset === "string")
|
|
63
|
+
element.setAttribute("data-smart-list-preset", node.attrs.preset);
|
|
64
|
+
if (typeof node.attrs?.style === "string") {
|
|
65
|
+
element.setAttribute("data-smart-list-style", node.attrs.style);
|
|
66
|
+
element.style.listStyleType = node.attrs.style;
|
|
67
|
+
}
|
|
68
|
+
if (node.attrs?.start !== undefined)
|
|
69
|
+
element.setAttribute("start", String(node.attrs.start));
|
|
70
|
+
element.setAttribute("data-smart-checkable", node.attrs?.checkable === true ? "true" : "false");
|
|
71
|
+
}
|
|
72
|
+
if (node.type === "table_cell") {
|
|
73
|
+
const rowspan = Math.max(1, Number(node.attrs?.rowspan) || 1);
|
|
74
|
+
const colspan = Math.max(1, Number(node.attrs?.colspan) || 1);
|
|
75
|
+
if (rowspan > 1)
|
|
76
|
+
element.setAttribute("rowspan", String(rowspan));
|
|
77
|
+
if (colspan > 1)
|
|
78
|
+
element.setAttribute("colspan", String(colspan));
|
|
79
|
+
if (node.attrs?.background)
|
|
80
|
+
element.style.background = String(node.attrs.background);
|
|
81
|
+
if (node.attrs?.borders)
|
|
82
|
+
element.style.border = String(node.attrs.borders);
|
|
83
|
+
["Top", "Right", "Bottom", "Left"].forEach((side) => {
|
|
84
|
+
const value = node.attrs?.[`border${side}`];
|
|
85
|
+
if (value)
|
|
86
|
+
element.style.setProperty(`border-${side.toLowerCase()}`, String(value));
|
|
87
|
+
});
|
|
88
|
+
if (node.attrs?.verticalAlign)
|
|
89
|
+
element.style.verticalAlign = String(node.attrs.verticalAlign);
|
|
90
|
+
}
|
|
91
|
+
if (node.type === "image" || node.type === "block_image") {
|
|
92
|
+
const source = sanitizeAtomSource(String(node.attrs?.src || ""), { kind: "image", allowBlobPreview: node.attrs?.status === "pending" });
|
|
93
|
+
if (source)
|
|
94
|
+
element.setAttribute("src", source);
|
|
95
|
+
element.setAttribute("alt", typeof node.attrs?.alt === "string" ? node.attrs.alt : "");
|
|
96
|
+
}
|
|
97
|
+
if (node.type === "formula" || node.type === "block_formula") {
|
|
98
|
+
const source = String(node.attrs?.source || "");
|
|
99
|
+
element.setAttribute("role", "math");
|
|
100
|
+
element.setAttribute("aria-label", `Mathematical formula: ${source}`);
|
|
101
|
+
element.setAttribute("data-smart-formula", source);
|
|
102
|
+
element.textContent = source;
|
|
103
|
+
}
|
|
104
|
+
if (node.type === "video" || node.type === "audio") {
|
|
105
|
+
const source = sanitizeAtomSource(String(node.attrs?.src || ""), { kind: node.type });
|
|
106
|
+
if (source)
|
|
107
|
+
element.setAttribute("src", source);
|
|
108
|
+
element.setAttribute("controls", "");
|
|
109
|
+
element.setAttribute("aria-label", node.type === "video" ? "Video player" : "Audio player");
|
|
110
|
+
}
|
|
111
|
+
if (node.type === "hard_break") {
|
|
112
|
+
element.setAttribute("data-smart-atomic", "true");
|
|
113
|
+
return element;
|
|
114
|
+
}
|
|
115
|
+
if (node.type === "unknown") {
|
|
116
|
+
element.contentEditable = "false";
|
|
117
|
+
element.setAttribute("data-smart-atomic", "true");
|
|
118
|
+
element.textContent = `[Unsupported: ${String(node.attrs?.originalType || "unknown")}]`;
|
|
119
|
+
return element;
|
|
120
|
+
}
|
|
121
|
+
if (atomTypes.has(node.type)) {
|
|
122
|
+
element.contentEditable = "false";
|
|
123
|
+
element.setAttribute("data-smart-atomic", "true");
|
|
124
|
+
return element;
|
|
125
|
+
}
|
|
126
|
+
node.children?.forEach((child) => element.appendChild(renderNode(child, ownerDocument)));
|
|
127
|
+
return element;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Correctness renderer for the mapping contract. The production strategy is
|
|
131
|
+
* contentEditable plus subtree diffing. Benchmark content-visibility before
|
|
132
|
+
* considering windowing; destructive editable-region virtualization is out of
|
|
133
|
+
* scope unless measurements prove it unavoidable.
|
|
134
|
+
*/
|
|
135
|
+
export const renderDocumentNaively = (root, document) => {
|
|
136
|
+
while (root.firstChild)
|
|
137
|
+
root.removeChild(root.firstChild);
|
|
138
|
+
document.children.forEach((node) => root.appendChild(renderNode(node, root.ownerDocument)));
|
|
139
|
+
const mapping = new FoundationModelDomMapping();
|
|
140
|
+
mapping.rebuild(root, document);
|
|
141
|
+
return mapping;
|
|
142
|
+
};
|
|
143
|
+
export class FoundationModelDomMapping {
|
|
144
|
+
constructor() {
|
|
145
|
+
this.root = null;
|
|
146
|
+
this.document = null;
|
|
147
|
+
this.byId = new Map();
|
|
148
|
+
this.byDom = new WeakMap();
|
|
149
|
+
this.pathById = new Map();
|
|
150
|
+
}
|
|
151
|
+
rebuild(root, document) {
|
|
152
|
+
this.root = root;
|
|
153
|
+
this.document = document;
|
|
154
|
+
this.byId.clear();
|
|
155
|
+
this.pathById.clear();
|
|
156
|
+
this.byId.set(document.id, root);
|
|
157
|
+
this.byDom.set(root, document);
|
|
158
|
+
this.pathById.set(document.id, []);
|
|
159
|
+
const elements = new Map();
|
|
160
|
+
root.querySelectorAll(`[${SMART_NODE_ID_ATTRIBUTE}]`).forEach((element) => {
|
|
161
|
+
const id = element.getAttribute(SMART_NODE_ID_ATTRIBUTE);
|
|
162
|
+
if (id)
|
|
163
|
+
elements.set(id, element);
|
|
164
|
+
});
|
|
165
|
+
const visit = (node, path) => {
|
|
166
|
+
if (isTextNode(node))
|
|
167
|
+
return;
|
|
168
|
+
const element = path.length ? elements.get(node.id) || null : root;
|
|
169
|
+
if (element) {
|
|
170
|
+
this.byId.set(node.id, element);
|
|
171
|
+
this.byDom.set(element, node);
|
|
172
|
+
this.pathById.set(node.id, path);
|
|
173
|
+
}
|
|
174
|
+
node.children?.forEach((child, index) => visit(child, [...path, index]));
|
|
175
|
+
};
|
|
176
|
+
visit(document, []);
|
|
177
|
+
}
|
|
178
|
+
/** Incremental renderer hook. Paths passed here are already current. */
|
|
179
|
+
beginUpdate(document) {
|
|
180
|
+
this.document = document;
|
|
181
|
+
if (this.root)
|
|
182
|
+
this.byDom.set(this.root, document);
|
|
183
|
+
}
|
|
184
|
+
track(node, path, element) {
|
|
185
|
+
this.byId.set(node.id, element);
|
|
186
|
+
this.byDom.set(element, node);
|
|
187
|
+
this.pathById.set(node.id, [...path]);
|
|
188
|
+
}
|
|
189
|
+
forget(nodeId) {
|
|
190
|
+
this.byId.delete(nodeId);
|
|
191
|
+
this.pathById.delete(nodeId);
|
|
192
|
+
}
|
|
193
|
+
nodeToDom(nodeId) { return this.byId.get(nodeId) || null; }
|
|
194
|
+
domToNode(input) {
|
|
195
|
+
let node = input;
|
|
196
|
+
while (node && node !== this.root) {
|
|
197
|
+
if (this.isEditorUiNode(node))
|
|
198
|
+
return null;
|
|
199
|
+
const model = this.byDom.get(node);
|
|
200
|
+
if (model && !isTextNode(model))
|
|
201
|
+
return { nodeId: model.id, node: model };
|
|
202
|
+
node = node.parentNode;
|
|
203
|
+
}
|
|
204
|
+
const rootModel = this.root ? this.byDom.get(this.root) : undefined;
|
|
205
|
+
return rootModel && !isTextNode(rootModel) ? { nodeId: rootModel.id, node: rootModel } : null;
|
|
206
|
+
}
|
|
207
|
+
modelDomChildren(element) {
|
|
208
|
+
return [...element.childNodes].filter((child) => !this.isEditorUiNode(child)
|
|
209
|
+
&& !(child instanceof Element && child.hasAttribute(SMART_PROJECTION_ATTRIBUTE)));
|
|
210
|
+
}
|
|
211
|
+
isInlineOwner(node) {
|
|
212
|
+
if (node.type === "paragraph" || node.type === "heading")
|
|
213
|
+
return true;
|
|
214
|
+
return Boolean(node.children?.length) && node.children.every((child) => isTextNode(child) || child.type === "hard_break" || child.type === "image" || child.type === "formula" || child.attrs?.originalGroup === "inline");
|
|
215
|
+
}
|
|
216
|
+
textDescendant(node) {
|
|
217
|
+
if (node.nodeType === node.TEXT_NODE)
|
|
218
|
+
return node;
|
|
219
|
+
const walker = node.ownerDocument?.createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
220
|
+
return walker?.nextNode();
|
|
221
|
+
}
|
|
222
|
+
posToDom(pos) {
|
|
223
|
+
if (!this.document)
|
|
224
|
+
return null;
|
|
225
|
+
const model = nodeAtPath(this.document, pos.path);
|
|
226
|
+
if (!model || isTextNode(model))
|
|
227
|
+
return null;
|
|
228
|
+
const element = this.byId.get(model.id);
|
|
229
|
+
if (!element)
|
|
230
|
+
return null;
|
|
231
|
+
if (!this.isInlineOwner(model)) {
|
|
232
|
+
const children = this.modelDomChildren(element);
|
|
233
|
+
return pos.offset <= children.length ? { node: element, offset: pos.offset } : null;
|
|
234
|
+
}
|
|
235
|
+
let remaining = pos.offset;
|
|
236
|
+
const domChildren = this.modelDomChildren(element);
|
|
237
|
+
for (let index = 0; index < (model.children || []).length; index += 1) {
|
|
238
|
+
const modelChild = model.children?.[index];
|
|
239
|
+
const child = domChildren[index];
|
|
240
|
+
if (!modelChild || !child)
|
|
241
|
+
return null;
|
|
242
|
+
if (isTextNode(modelChild)) {
|
|
243
|
+
const length = modelChild.text.length;
|
|
244
|
+
const text = this.textDescendant(child);
|
|
245
|
+
if (remaining <= length && text)
|
|
246
|
+
return { node: text, offset: remaining };
|
|
247
|
+
remaining -= length;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
const directIndex = [...element.childNodes].indexOf(child);
|
|
251
|
+
if (remaining === 0)
|
|
252
|
+
return { node: element, offset: directIndex };
|
|
253
|
+
if (remaining === 1)
|
|
254
|
+
return { node: element, offset: directIndex + 1 };
|
|
255
|
+
remaining -= 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (remaining !== 0)
|
|
259
|
+
return null;
|
|
260
|
+
const lastModelChild = domChildren[domChildren.length - 1];
|
|
261
|
+
return {
|
|
262
|
+
node: element,
|
|
263
|
+
// Renderer-owned UI projections do not consume model offsets. An empty
|
|
264
|
+
// inline owner therefore maps to offset zero even when it has a visual
|
|
265
|
+
// caret-line projection.
|
|
266
|
+
offset: lastModelChild ? [...element.childNodes].indexOf(lastModelChild) + 1 : 0,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
domToPos(node, offset) {
|
|
270
|
+
if (!this.root || !this.document || this.isEditorUiNode(node))
|
|
271
|
+
return null;
|
|
272
|
+
const owner = this.domToNode(node);
|
|
273
|
+
if (!owner)
|
|
274
|
+
return null;
|
|
275
|
+
if (isTextNode(owner.node))
|
|
276
|
+
return null;
|
|
277
|
+
const ownerNode = owner.node;
|
|
278
|
+
const path = this.pathById.get(owner.nodeId);
|
|
279
|
+
const element = this.byId.get(owner.nodeId);
|
|
280
|
+
if (!path || !element)
|
|
281
|
+
return null;
|
|
282
|
+
if (!this.isInlineOwner(ownerNode)) {
|
|
283
|
+
if (node !== element) {
|
|
284
|
+
const direct = node.nodeType === node.ELEMENT_NODE ? node : node.parentElement;
|
|
285
|
+
const child = direct?.closest(`[${SMART_NODE_ID_ATTRIBUTE}]`);
|
|
286
|
+
const children = [...element.children].filter((candidate) => !this.isEditorUiNode(candidate));
|
|
287
|
+
const index = child ? children.indexOf(child) : -1;
|
|
288
|
+
return index >= 0 ? { path: [...path], offset: index + (offset > 0 ? 1 : 0) } : null;
|
|
289
|
+
}
|
|
290
|
+
return { path: [...path], offset };
|
|
291
|
+
}
|
|
292
|
+
if (node === element) {
|
|
293
|
+
const direct = this.modelDomChildren(element);
|
|
294
|
+
const domChildren = [...element.childNodes];
|
|
295
|
+
const count = direct.filter((child) => domChildren.indexOf(child) < offset).length;
|
|
296
|
+
const total = (ownerNode.children || []).slice(0, count)
|
|
297
|
+
.reduce((sum, child) => sum + (isTextNode(child) ? child.text.length : 1), 0);
|
|
298
|
+
return { path: [...path], offset: total };
|
|
299
|
+
}
|
|
300
|
+
let direct = node;
|
|
301
|
+
while (direct?.parentNode && direct.parentNode !== element)
|
|
302
|
+
direct = direct.parentNode;
|
|
303
|
+
if (!direct || direct.parentNode !== element)
|
|
304
|
+
return null;
|
|
305
|
+
const directIndex = this.modelDomChildren(element).indexOf(direct);
|
|
306
|
+
const modelChild = ownerNode.children?.[directIndex];
|
|
307
|
+
if (!modelChild)
|
|
308
|
+
return null;
|
|
309
|
+
const base = (ownerNode.children || []).slice(0, directIndex)
|
|
310
|
+
.reduce((sum, child) => sum + (isTextNode(child) ? child.text.length : 1), 0);
|
|
311
|
+
if (!isTextNode(modelChild))
|
|
312
|
+
return { path: [...path], offset: base + (offset > 0 ? 1 : 0) };
|
|
313
|
+
try {
|
|
314
|
+
const range = element.ownerDocument.createRange();
|
|
315
|
+
range.selectNodeContents(direct);
|
|
316
|
+
range.setEnd(node, offset);
|
|
317
|
+
return { path: [...path], offset: base + Math.min(modelChild.text.length, range.toString().length) };
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
isEditorUiNode(node) {
|
|
324
|
+
const element = node.nodeType === node.ELEMENT_NODE ? node : node.parentElement;
|
|
325
|
+
return Boolean(element?.closest(`[${SMART_UI_ATTRIBUTE}]`));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NormalizerRegistration, SmartDocument, SmartOperation, SmartSchema } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Normalization is deterministic, idempotent, terminating, and scoped to the
|
|
4
|
+
* smallest affected ancestor unless a document-wide invariant requires otherwise.
|
|
5
|
+
*/
|
|
6
|
+
export declare const NORMALIZATION_PASS_CAP = 3;
|
|
7
|
+
export declare const smallestAffectedAncestor: (operations: readonly SmartOperation[]) => number[];
|
|
8
|
+
export interface NormalizationRun {
|
|
9
|
+
document: SmartDocument;
|
|
10
|
+
operations: SmartOperation[];
|
|
11
|
+
affectedPath: number[];
|
|
12
|
+
passes: number;
|
|
13
|
+
fullDocumentTraversals: number;
|
|
14
|
+
ranNormalizers: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare const runNormalization: (options: {
|
|
17
|
+
document: SmartDocument;
|
|
18
|
+
originatingOperations: readonly SmartOperation[];
|
|
19
|
+
schema: SmartSchema;
|
|
20
|
+
normalizers?: readonly NormalizerRegistration[];
|
|
21
|
+
onDocumentWideRun?: (normalizerId: string) => void;
|
|
22
|
+
}) => NormalizationRun;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { applyOperation } from "./operations.js";
|
|
2
|
+
import { nodeAtPath } from "./positions.js";
|
|
3
|
+
import { repair, validate } from "./schema.js";
|
|
4
|
+
/**
|
|
5
|
+
* Normalization is deterministic, idempotent, terminating, and scoped to the
|
|
6
|
+
* smallest affected ancestor unless a document-wide invariant requires otherwise.
|
|
7
|
+
*/
|
|
8
|
+
export const NORMALIZATION_PASS_CAP = 3;
|
|
9
|
+
const commonPrefix = (paths) => {
|
|
10
|
+
if (!paths.length)
|
|
11
|
+
return [];
|
|
12
|
+
const result = [];
|
|
13
|
+
for (let index = 0; index < Math.min(...paths.map((path) => path.length)); index += 1) {
|
|
14
|
+
if (!paths.every((path) => path[index] === paths[0][index]))
|
|
15
|
+
break;
|
|
16
|
+
result.push(paths[0][index]);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
20
|
+
const operationPaths = (operation) => {
|
|
21
|
+
if (operation.type === "moveNode")
|
|
22
|
+
return [operation.from.path, operation.to.path];
|
|
23
|
+
if (operation.type === "addMark" || operation.type === "removeMark")
|
|
24
|
+
return [operation.range.from.path, operation.range.to.path];
|
|
25
|
+
return [operation.pos.path];
|
|
26
|
+
};
|
|
27
|
+
/** Paths whose existing content an emitted repair changes. Node operations
|
|
28
|
+
* address their parent plus a child offset, unlike text operations whose path
|
|
29
|
+
* already addresses the inline owner. */
|
|
30
|
+
const repairedContentPaths = (operation) => {
|
|
31
|
+
if (operation.type === "replaceNode" || operation.type === "removeNode") {
|
|
32
|
+
return [[...operation.pos.path, operation.pos.offset]];
|
|
33
|
+
}
|
|
34
|
+
return operationPaths(operation);
|
|
35
|
+
};
|
|
36
|
+
export const smallestAffectedAncestor = (operations) => commonPrefix(operations.flatMap(operationPaths));
|
|
37
|
+
const structuralRepairOperations = (before, after, affectedPath) => {
|
|
38
|
+
if (affectedPath.length) {
|
|
39
|
+
const beforeNode = nodeAtPath(before, affectedPath);
|
|
40
|
+
const afterNode = nodeAtPath(after, affectedPath);
|
|
41
|
+
if (beforeNode && afterNode && JSON.stringify(beforeNode) !== JSON.stringify(afterNode)) {
|
|
42
|
+
return [{
|
|
43
|
+
type: "replaceNode",
|
|
44
|
+
pos: { path: affectedPath.slice(0, -1), offset: affectedPath[affectedPath.length - 1] },
|
|
45
|
+
before: beforeNode,
|
|
46
|
+
after: afterNode,
|
|
47
|
+
}];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const operations = [];
|
|
51
|
+
const shared = Math.min(before.children.length, after.children.length);
|
|
52
|
+
for (let index = 0; index < shared; index += 1) {
|
|
53
|
+
if (JSON.stringify(before.children[index]) !== JSON.stringify(after.children[index])) {
|
|
54
|
+
operations.push({ type: "replaceNode", pos: { path: [], offset: index }, before: before.children[index], after: after.children[index] });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (let index = before.children.length - 1; index >= after.children.length; index -= 1) {
|
|
58
|
+
operations.push({ type: "removeNode", pos: { path: [], offset: index }, node: before.children[index] });
|
|
59
|
+
}
|
|
60
|
+
for (let index = shared; index < after.children.length; index += 1) {
|
|
61
|
+
operations.push({ type: "insertNode", pos: { path: [], offset: index }, node: after.children[index] });
|
|
62
|
+
}
|
|
63
|
+
return operations;
|
|
64
|
+
};
|
|
65
|
+
export const runNormalization = (options) => {
|
|
66
|
+
const affectedPath = smallestAffectedAncestor(options.originatingOperations);
|
|
67
|
+
let document = options.document;
|
|
68
|
+
const appended = [];
|
|
69
|
+
let fullDocumentTraversals = 0;
|
|
70
|
+
const ranNormalizers = [];
|
|
71
|
+
const registrations = (options.normalizers || [])
|
|
72
|
+
.map((normalizer, registrationOrder) => ({ normalizer, registrationOrder }))
|
|
73
|
+
.sort((left, right) => left.normalizer.priority - right.normalizer.priority || left.registrationOrder - right.registrationOrder);
|
|
74
|
+
for (let pass = 0; pass < NORMALIZATION_PASS_CAP; pass += 1) {
|
|
75
|
+
const changedBy = [];
|
|
76
|
+
// Structural schema repair always precedes registered normalizers. For a
|
|
77
|
+
// valid operation sequence this is a no-op; invalid output fails loudly
|
|
78
|
+
// rather than allowing an unrecorded whole-document mutation.
|
|
79
|
+
const textOnly = options.originatingOperations.every((operation) => operation.type === "insertText" || operation.type === "deleteText" || operation.type === "addMark" || operation.type === "removeMark");
|
|
80
|
+
if (!textOnly) {
|
|
81
|
+
const structural = repair(document, options.schema);
|
|
82
|
+
if (structural.repairs.length) {
|
|
83
|
+
const repairs = structuralRepairOperations(document, structural.doc, affectedPath);
|
|
84
|
+
if (!repairs.length)
|
|
85
|
+
throw new Error(`Schema repair could not be represented as operations: ${structural.repairs[0].message}`);
|
|
86
|
+
repairs.forEach((operation) => {
|
|
87
|
+
document = applyOperation(document, operation);
|
|
88
|
+
appended.push(operation);
|
|
89
|
+
});
|
|
90
|
+
changedBy.push("schema");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const { normalizer } of registrations) {
|
|
94
|
+
const documentWide = normalizer.documentWide === true;
|
|
95
|
+
if (documentWide) {
|
|
96
|
+
fullDocumentTraversals += 1;
|
|
97
|
+
options.onDocumentWideRun?.(normalizer.id);
|
|
98
|
+
}
|
|
99
|
+
else if (affectedPath.length && !nodeAtPath(document, affectedPath)) {
|
|
100
|
+
throw new Error(`Affected normalization path for "${normalizer.id}" no longer exists.`);
|
|
101
|
+
}
|
|
102
|
+
ranNormalizers.push(normalizer.id);
|
|
103
|
+
const result = normalizer.normalize(document, {
|
|
104
|
+
schema: options.schema,
|
|
105
|
+
affectedPath,
|
|
106
|
+
pass,
|
|
107
|
+
documentWide,
|
|
108
|
+
});
|
|
109
|
+
for (const operation of result.operations) {
|
|
110
|
+
const paths = repairedContentPaths(operation);
|
|
111
|
+
if (!documentWide && affectedPath.length && paths.some((path) => !affectedPath.every((part, index) => path[index] === part))) {
|
|
112
|
+
throw new Error(`Normalizer "${normalizer.id}" emitted an operation outside the affected subtree.`);
|
|
113
|
+
}
|
|
114
|
+
document = applyOperation(document, operation);
|
|
115
|
+
appended.push(operation);
|
|
116
|
+
changedBy.push(normalizer.id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!changedBy.length) {
|
|
120
|
+
if (!textOnly) {
|
|
121
|
+
const errors = validate(document, options.schema);
|
|
122
|
+
if (errors.length)
|
|
123
|
+
throw new Error(`Normalization produced invalid content: ${errors[0].message}`);
|
|
124
|
+
}
|
|
125
|
+
return { document, operations: appended, affectedPath, passes: pass + 1, fullDocumentTraversals, ranNormalizers };
|
|
126
|
+
}
|
|
127
|
+
if (pass === NORMALIZATION_PASS_CAP - 1) {
|
|
128
|
+
throw new Error(`Normalization exceeded ${NORMALIZATION_PASS_CAP} passes; oscillating normalizers: ${[...new Set(changedBy)].join(", ")}.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new Error("Unreachable normalization state.");
|
|
132
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SmartDocument, SmartOperation, SmartPos } from "./types.js";
|
|
2
|
+
export declare const applyOperation: (input: SmartDocument, operation: SmartOperation) => SmartDocument;
|
|
3
|
+
export declare const invertOperation: (operation: SmartOperation) => SmartOperation;
|
|
4
|
+
export declare const applyOperations: (document: SmartDocument, operations: readonly SmartOperation[]) => SmartDocument;
|
|
5
|
+
export declare const invertOperations: (operations: readonly SmartOperation[]) => SmartOperation[];
|
|
6
|
+
export declare const mapPosThroughOperation: (pos: SmartPos, operation: SmartOperation, bias?: -1 | 1) => {
|
|
7
|
+
pos: SmartPos;
|
|
8
|
+
deleted: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* `tieBreakBias` only affects the exact-tie case (operation's own structural
|
|
12
|
+
* position coincides exactly with `through`'s insertion point - e.g. two
|
|
13
|
+
* concurrent insertNode ops at the identical offset) - every other case's
|
|
14
|
+
* bias is unaffected. Defaults to 1 (through's content sorts first),
|
|
15
|
+
* preserving this function's existing, already-tested behavior exactly for
|
|
16
|
+
* every caller that doesn't pass it. A concurrent-rebase caller should
|
|
17
|
+
* derive this from a total order over the two operations' authors (e.g.
|
|
18
|
+
* lexicographic authorId comparison) so both transform directions agree:
|
|
19
|
+
* mapOperation(A, B, {tieBreakBias: A-sorts-first ? -1 : 1}) and
|
|
20
|
+
* mapOperation(B, A, {tieBreakBias: B-sorts-first ? -1 : 1}) must reach the
|
|
21
|
+
* same final order regardless of which one is "operation" and which is
|
|
22
|
+
* "through". This does not affect the addMark/removeMark range-endpoint
|
|
23
|
+
* biases below (-1/+1), which serve an unrelated purpose (range-growth
|
|
24
|
+
* semantics at an insertion boundary) and are deliberately left hardcoded.
|
|
25
|
+
*/
|
|
26
|
+
export declare const mapOperation: (operation: SmartOperation, through: SmartOperation, options?: {
|
|
27
|
+
tieBreakBias?: -1 | 1;
|
|
28
|
+
}) => SmartOperation | null;
|