smartrte-core 0.2.0 → 1.0.0-beta.1
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 +21 -0
- package/dist/foundation/atom/formats.js +77 -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 +20 -0
- package/dist/foundation/list/formats.js +795 -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 +26 -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 +45 -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,75 @@
|
|
|
1
|
+
import { resolvePos } from "../positions.js";
|
|
2
|
+
import { repair } from "../schema.js";
|
|
3
|
+
import { isTextNode } from "../identity.js";
|
|
4
|
+
const sortedRecord = (value) => {
|
|
5
|
+
if (!value)
|
|
6
|
+
return undefined;
|
|
7
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, value[key]]));
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Comparator equivalence is normalized document structure with IDs stripped,
|
|
11
|
+
* plus semantic selection position. Operation streams are intentionally not
|
|
12
|
+
* compared: canonical editing is allowed to express the same user intent with
|
|
13
|
+
* a different operation sequence, and the legacy engine has no stable IDs.
|
|
14
|
+
*/
|
|
15
|
+
export const normalizedStructureWithoutIds = (document, schema) => {
|
|
16
|
+
const repaired = repair(document, schema).doc;
|
|
17
|
+
const strip = (node) => isTextNode(node)
|
|
18
|
+
? { type: "text", text: node.text, ...(node.marks?.length ? { marks: node.marks.map((mark) => ({ type: mark.type, attrs: sortedRecord(mark.attrs) })) } : {}) }
|
|
19
|
+
: {
|
|
20
|
+
type: node.type,
|
|
21
|
+
...(node.attrs && Object.keys(node.attrs).length ? { attrs: sortedRecord(node.attrs) } : {}),
|
|
22
|
+
...(node.children ? { children: node.children.map(strip) } : {}),
|
|
23
|
+
};
|
|
24
|
+
return strip(repaired);
|
|
25
|
+
};
|
|
26
|
+
export const semanticSelectionPosition = (document, selection) => {
|
|
27
|
+
const endpoint = (pos) => {
|
|
28
|
+
const resolved = resolvePos(document, pos);
|
|
29
|
+
return { path: [...pos.path], offset: pos.offset, ownerType: resolved.parent.type, kind: resolved.kind };
|
|
30
|
+
};
|
|
31
|
+
return { type: selection.type, anchor: endpoint(selection.anchor), head: endpoint(selection.head) };
|
|
32
|
+
};
|
|
33
|
+
const hash = (value) => {
|
|
34
|
+
const input = JSON.stringify(value);
|
|
35
|
+
let state = 2166136261;
|
|
36
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
37
|
+
state ^= input.charCodeAt(index);
|
|
38
|
+
state = Math.imul(state, 16777619);
|
|
39
|
+
}
|
|
40
|
+
return (state >>> 0).toString(16).padStart(8, "0");
|
|
41
|
+
};
|
|
42
|
+
export const compareShadowDocuments = (args) => {
|
|
43
|
+
const legacyStructure = normalizedStructureWithoutIds(args.legacyDocument, args.schema);
|
|
44
|
+
const canonicalStructure = normalizedStructureWithoutIds(args.canonicalDocument, args.schema);
|
|
45
|
+
const legacySelection = semanticSelectionPosition(args.legacyDocument, args.legacySelection);
|
|
46
|
+
const canonicalSelection = semanticSelectionPosition(args.canonicalDocument, args.canonicalSelection);
|
|
47
|
+
const legacyStructureJson = JSON.stringify(legacyStructure);
|
|
48
|
+
const canonicalStructureJson = JSON.stringify(canonicalStructure);
|
|
49
|
+
const legacySelectionJson = JSON.stringify(legacySelection);
|
|
50
|
+
const canonicalSelectionJson = JSON.stringify(canonicalSelection);
|
|
51
|
+
const documentEquivalent = legacyStructureJson === canonicalStructureJson;
|
|
52
|
+
const selectionEquivalent = legacySelectionJson === canonicalSelectionJson;
|
|
53
|
+
return {
|
|
54
|
+
equivalent: documentEquivalent && selectionEquivalent,
|
|
55
|
+
documentEquivalent,
|
|
56
|
+
selectionEquivalent,
|
|
57
|
+
legacyStructureHash: hash(legacyStructure),
|
|
58
|
+
canonicalStructureHash: hash(canonicalStructure),
|
|
59
|
+
legacySelectionHash: hash(legacySelection),
|
|
60
|
+
canonicalSelectionHash: hash(canonicalSelection),
|
|
61
|
+
...(!documentEquivalent || !selectionEquivalent ? { classification: args.classification || (documentEquivalent ? "selection-only" : "unknown") } : {}),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/** Privacy-safe logger payload: hashes and classification, never document text. */
|
|
65
|
+
export const shadowLogRecord = (scenarioId, result) => ({
|
|
66
|
+
scenarioId,
|
|
67
|
+
equivalent: result.equivalent,
|
|
68
|
+
documentEquivalent: result.documentEquivalent,
|
|
69
|
+
selectionEquivalent: result.selectionEquivalent,
|
|
70
|
+
legacyStructureHash: result.legacyStructureHash,
|
|
71
|
+
canonicalStructureHash: result.canonicalStructureHash,
|
|
72
|
+
legacySelectionHash: result.legacySelectionHash,
|
|
73
|
+
canonicalSelectionHash: result.canonicalSelectionHash,
|
|
74
|
+
...(result.classification ? { classification: result.classification } : {}),
|
|
75
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { shadowLogRecord } from "./shadow.js";
|
|
2
|
+
export interface ShadowCorpusSummary {
|
|
3
|
+
readonly seed: number;
|
|
4
|
+
readonly scenarios: number;
|
|
5
|
+
readonly equivalent: number;
|
|
6
|
+
readonly divergences: Readonly<Record<string, number>>;
|
|
7
|
+
readonly logs: readonly ReturnType<typeof shadowLogRecord>[];
|
|
8
|
+
}
|
|
9
|
+
/** Browser-neutral replay of the production legacy-HTML comparator boundary. */
|
|
10
|
+
export declare const runListShadowReplayCorpus: (scenarios?: number, seed?: number) => ShadowCorpusSummary;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { foundationSchema } from "../schema.js";
|
|
2
|
+
import { compareShadowDocuments, shadowLogRecord } from "./shadow.js";
|
|
3
|
+
import { parseCanonicalListHtml, serializeCanonicalListHtml } from "./formats.js";
|
|
4
|
+
/** Browser-neutral replay of the production legacy-HTML comparator boundary. */
|
|
5
|
+
export const runListShadowReplayCorpus = (scenarios = 1000, seed = 1369441024) => {
|
|
6
|
+
const logs = [];
|
|
7
|
+
const divergences = {};
|
|
8
|
+
let equivalent = 0;
|
|
9
|
+
for (let index = 0; index < scenarios; index += 1) {
|
|
10
|
+
const label = ((seed ^ Math.imul(index + 1, 2654435761)) >>> 0).toString(36);
|
|
11
|
+
const nested = {
|
|
12
|
+
type: "list", id: `nested-${label}`, attrs: { style: index % 2 ? "lower-alpha" : "circle" }, children: [{
|
|
13
|
+
type: "list_item", id: `nested-item-${label}`, children: [{ type: "paragraph", id: `nested-p-${label}`, children: [{ type: "text", text: `nested-${label}` }] }],
|
|
14
|
+
}],
|
|
15
|
+
};
|
|
16
|
+
const canonical = { type: "doc", id: `doc-${label}`, children: [{
|
|
17
|
+
type: "list", id: `list-${label}`, attrs: {
|
|
18
|
+
style: index % 2 ? "decimal" : "disc",
|
|
19
|
+
...(index % 3 === 0 ? { checkable: true } : {}),
|
|
20
|
+
...(index % 5 === 0 ? { start: 3 } : {}),
|
|
21
|
+
}, children: [0, 1, 2].map((itemIndex) => ({
|
|
22
|
+
type: "list_item", id: `item-${label}-${itemIndex}`,
|
|
23
|
+
...(index % 3 === 0 ? { attrs: { checked: itemIndex === 1 } } : {}),
|
|
24
|
+
children: [{ type: itemIndex === 0 && index % 4 === 0 ? "heading" : "paragraph", id: `p-${label}-${itemIndex}`, ...(itemIndex === 0 && index % 4 === 0 ? { attrs: { level: 2 } } : {}), children: [{ type: "text", text: `content-${label}-${itemIndex}` }] }, ...(itemIndex === 1 ? [nested] : [])],
|
|
25
|
+
})),
|
|
26
|
+
}] };
|
|
27
|
+
const legacy = parseCanonicalListHtml(serializeCanonicalListHtml(canonical, { clean: true }));
|
|
28
|
+
const pos = { path: [0, 0, 0], offset: 0 };
|
|
29
|
+
const result = compareShadowDocuments({
|
|
30
|
+
legacyDocument: legacy, legacySelection: { type: "text", anchor: pos, head: pos },
|
|
31
|
+
canonicalDocument: canonical, canonicalSelection: { type: "text", anchor: pos, head: pos },
|
|
32
|
+
schema: foundationSchema,
|
|
33
|
+
});
|
|
34
|
+
if (result.equivalent)
|
|
35
|
+
equivalent += 1;
|
|
36
|
+
else
|
|
37
|
+
divergences[result.classification || "unknown"] = (divergences[result.classification || "unknown"] || 0) + 1;
|
|
38
|
+
logs.push(shadowLogRecord(`browser-${index}`, result));
|
|
39
|
+
}
|
|
40
|
+
return { seed, scenarios, equivalent, divergences, logs };
|
|
41
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { PositionLookup, ResolvedScope } from "../scope/types.js";
|
|
2
|
+
import type { SmartDocument, SmartOperation, SmartSchema } from "../types.js";
|
|
3
|
+
export interface CommandContext {
|
|
4
|
+
readonly schema: SmartSchema;
|
|
5
|
+
readonly positions: PositionLookup;
|
|
6
|
+
}
|
|
7
|
+
export type ListCommand<P> = (document: SmartDocument, scope: ResolvedScope, params: P, ctx: CommandContext) => SmartOperation[];
|
|
8
|
+
export interface CreateListParams {
|
|
9
|
+
readonly listIds: readonly string[];
|
|
10
|
+
readonly itemIds: readonly string[];
|
|
11
|
+
readonly preset?: string;
|
|
12
|
+
readonly style?: string;
|
|
13
|
+
readonly start?: number;
|
|
14
|
+
readonly checkable?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface UnwrapListParams {
|
|
17
|
+
/** New ID used only when unwrapping a middle run splits one list in two. */
|
|
18
|
+
readonly splitListIds?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
export interface IndentListParams {
|
|
21
|
+
/** One deterministic ID per selected list that needs a new nested list. */
|
|
22
|
+
readonly nestedListIds?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export interface OutdentListParams extends UnwrapListParams {
|
|
25
|
+
}
|
|
26
|
+
export interface SetListPresetParams {
|
|
27
|
+
readonly preset?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface SetListStyleParams {
|
|
30
|
+
readonly style?: string;
|
|
31
|
+
/** Optional list-level checklist mode used by the list toolbar adapter. */
|
|
32
|
+
readonly checkable?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface SetListCheckedParams {
|
|
35
|
+
readonly checked: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface MoveListItemsParams {
|
|
38
|
+
readonly direction: "up" | "down";
|
|
39
|
+
}
|
|
40
|
+
export interface RestartListNumberingParams {
|
|
41
|
+
readonly start: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ContinueListNumberingParams {
|
|
44
|
+
}
|
|
45
|
+
export interface InsertListFragmentParams {
|
|
46
|
+
readonly fragment: SmartDocument;
|
|
47
|
+
readonly position: "before" | "after" | "start" | "end";
|
|
48
|
+
/** Deterministic IDs used when plain fragment blocks must become items. */
|
|
49
|
+
readonly itemIds?: readonly string[];
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SmartOperation, SmartPos, SmartRange, SmartSelection, SmartTransaction, TransactionMap } from "./types.js";
|
|
2
|
+
export declare class FoundationTransactionMap implements TransactionMap {
|
|
3
|
+
private readonly operations;
|
|
4
|
+
private readonly deletedKeys;
|
|
5
|
+
constructor(transactionOrOperations: SmartTransaction | readonly SmartOperation[]);
|
|
6
|
+
map(pos: SmartPos, bias?: -1 | 1): SmartPos;
|
|
7
|
+
mapRange(range: SmartRange): SmartRange;
|
|
8
|
+
mapSelection(selection: SmartSelection): SmartSelection;
|
|
9
|
+
deleted(pos: SmartPos): boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const createTransactionMap: (transactionOrOperations: SmartTransaction | readonly SmartOperation[]) => TransactionMap;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mapPosThroughOperation } from "./operations.js";
|
|
2
|
+
export class FoundationTransactionMap {
|
|
3
|
+
constructor(transactionOrOperations) {
|
|
4
|
+
this.deletedKeys = new Set();
|
|
5
|
+
this.operations = "operations" in transactionOrOperations
|
|
6
|
+
? transactionOrOperations.operations
|
|
7
|
+
: transactionOrOperations;
|
|
8
|
+
}
|
|
9
|
+
map(pos, bias = 1) {
|
|
10
|
+
let current = { path: [...pos.path], offset: pos.offset };
|
|
11
|
+
let wasDeleted = false;
|
|
12
|
+
this.operations.forEach((operation) => {
|
|
13
|
+
const result = mapPosThroughOperation(current, operation, bias);
|
|
14
|
+
current = result.pos;
|
|
15
|
+
wasDeleted || (wasDeleted = result.deleted);
|
|
16
|
+
});
|
|
17
|
+
if (wasDeleted)
|
|
18
|
+
this.deletedKeys.add(JSON.stringify(pos));
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
mapRange(range) {
|
|
22
|
+
return { from: this.map(range.from, -1), to: this.map(range.to, 1) };
|
|
23
|
+
}
|
|
24
|
+
mapSelection(selection) {
|
|
25
|
+
return { ...selection, anchor: this.map(selection.anchor, -1), head: this.map(selection.head, 1) };
|
|
26
|
+
}
|
|
27
|
+
deleted(pos) {
|
|
28
|
+
if (this.deletedKeys.has(JSON.stringify(pos)))
|
|
29
|
+
return true;
|
|
30
|
+
let current = pos;
|
|
31
|
+
for (const operation of this.operations) {
|
|
32
|
+
const result = mapPosThroughOperation(current, operation);
|
|
33
|
+
if (result.deleted)
|
|
34
|
+
return true;
|
|
35
|
+
current = result.pos;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const createTransactionMap = (transactionOrOperations) => new FoundationTransactionMap(transactionOrOperations);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Attrs, SmartMark } from "../types.js";
|
|
2
|
+
export declare const stableValue: (value: unknown) => string;
|
|
3
|
+
export declare const markKey: (mark: SmartMark) => string;
|
|
4
|
+
export declare const compareMarks: (left: SmartMark, right: SmartMark) => number;
|
|
5
|
+
export declare const canonicalMarkOrder: (marks: readonly SmartMark[] | undefined) => SmartMark[];
|
|
6
|
+
export declare const canonicalColor: (input: unknown) => string | null;
|
|
7
|
+
export declare const canonicalFontSize: (input: unknown) => number | null;
|
|
8
|
+
export declare const canonicalFontFamily: (input: unknown) => string | null;
|
|
9
|
+
export declare const canonicalMarkAttrs: (markType: string, attrs: Attrs | undefined) => Attrs | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const namedColors = Object.freeze({
|
|
2
|
+
black: "#000000", white: "#ffffff", red: "#ff0000", green: "#008000",
|
|
3
|
+
blue: "#0000ff", yellow: "#ffff00", gray: "#808080", grey: "#808080",
|
|
4
|
+
transparent: "#00000000",
|
|
5
|
+
});
|
|
6
|
+
export const stableValue = (value) => {
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return `[${value.map(stableValue).join(",")}]`;
|
|
9
|
+
if (value && typeof value === "object")
|
|
10
|
+
return `{${Object.entries(value)
|
|
11
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
12
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableValue(item)}`).join(",")}}`;
|
|
13
|
+
return JSON.stringify(value);
|
|
14
|
+
};
|
|
15
|
+
export const markKey = (mark) => `${mark.type}:${stableValue(mark.attrs || {})}`;
|
|
16
|
+
export const compareMarks = (left, right) => left.type.localeCompare(right.type) || stableValue(left.attrs || {}).localeCompare(stableValue(right.attrs || {}));
|
|
17
|
+
export const canonicalMarkOrder = (marks) => [...(marks || [])].sort(compareMarks).map((mark) => ({ type: mark.type, ...(mark.attrs ? { attrs: { ...mark.attrs } } : {}) }));
|
|
18
|
+
export const canonicalColor = (input) => {
|
|
19
|
+
if (typeof input !== "string")
|
|
20
|
+
return null;
|
|
21
|
+
const value = input.trim().toLowerCase();
|
|
22
|
+
if (namedColors[value])
|
|
23
|
+
return namedColors[value];
|
|
24
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(value);
|
|
25
|
+
if (hex) {
|
|
26
|
+
const raw = hex[1].length === 3 ? hex[1].split("").map((part) => part + part).join("") : hex[1];
|
|
27
|
+
return `#${raw}`;
|
|
28
|
+
}
|
|
29
|
+
const rgb = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(0|1|0?\.\d+))?\s*\)$/.exec(value);
|
|
30
|
+
if (!rgb)
|
|
31
|
+
return null;
|
|
32
|
+
const parts = rgb.slice(1, 4).map(Number);
|
|
33
|
+
if (parts.some((part) => part > 255))
|
|
34
|
+
return null;
|
|
35
|
+
const base = `#${parts.map((part) => part.toString(16).padStart(2, "0")).join("")}`;
|
|
36
|
+
if (rgb[4] === undefined || Number(rgb[4]) === 1)
|
|
37
|
+
return base;
|
|
38
|
+
return `${base}${Math.round(Number(rgb[4]) * 255).toString(16).padStart(2, "0")}`;
|
|
39
|
+
};
|
|
40
|
+
export const canonicalFontSize = (input) => {
|
|
41
|
+
const raw = typeof input === "number" ? input : typeof input === "string"
|
|
42
|
+
? (() => {
|
|
43
|
+
const match = /^(\d+(?:\.\d+)?)(px|pt)?$/i.exec(input.trim());
|
|
44
|
+
if (!match)
|
|
45
|
+
return Number.NaN;
|
|
46
|
+
return Number(match[1]) * (match[2]?.toLowerCase() === "pt" ? 4 / 3 : 1);
|
|
47
|
+
})()
|
|
48
|
+
: Number.NaN;
|
|
49
|
+
return Number.isFinite(raw) && raw >= 1 && raw <= 512 ? Number(raw.toFixed(3)) : null;
|
|
50
|
+
};
|
|
51
|
+
export const canonicalFontFamily = (input) => {
|
|
52
|
+
if (typeof input !== "string")
|
|
53
|
+
return null;
|
|
54
|
+
const value = input.split(",").map((family) => family.trim().replace(/^(['"])(.*)\1$/, "$2").replace(/\s+/g, " ").toLowerCase())
|
|
55
|
+
.filter(Boolean).join(", ");
|
|
56
|
+
return value || null;
|
|
57
|
+
};
|
|
58
|
+
export const canonicalMarkAttrs = (markType, attrs) => {
|
|
59
|
+
if (markType === "textColor" || markType === "backgroundColor") {
|
|
60
|
+
const value = canonicalColor(attrs?.value);
|
|
61
|
+
return value ? { value } : null;
|
|
62
|
+
}
|
|
63
|
+
if (markType === "fontSize") {
|
|
64
|
+
const valuePx = canonicalFontSize(attrs?.valuePx);
|
|
65
|
+
return valuePx === null ? null : { valuePx };
|
|
66
|
+
}
|
|
67
|
+
if (markType === "fontFamily") {
|
|
68
|
+
const value = canonicalFontFamily(attrs?.value);
|
|
69
|
+
return value ? { value } : null;
|
|
70
|
+
}
|
|
71
|
+
return attrs ? Object.fromEntries(Object.entries(attrs).sort(([left], [right]) => left.localeCompare(right))) : {};
|
|
72
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ResolvedScope } from "../scope/types.js";
|
|
2
|
+
import type { SmartDocument } from "../types.js";
|
|
3
|
+
import type { MarkApplyParams, MarkClearAllParams, MarkCommand, MarkCommandContext, MarkRemoveParams, MarkSetAttrsParams, MarkToggleParams, LinkEditParams, MarkApplicationReport } from "./types.js";
|
|
4
|
+
/** Reports atoms skipped by mark application without changing the command contract. */
|
|
5
|
+
export declare const reportMarkApplication: (document: SmartDocument, scope: ResolvedScope, markType: string, ctx: MarkCommandContext) => MarkApplicationReport;
|
|
6
|
+
export declare const applyMarkCommand: MarkCommand<MarkApplyParams>;
|
|
7
|
+
export declare const removeMarkCommand: MarkCommand<MarkRemoveParams>;
|
|
8
|
+
export declare const toggleMarkCommand: MarkCommand<MarkToggleParams>;
|
|
9
|
+
export declare const setMarkAttrsCommand: MarkCommand<MarkSetAttrsParams>;
|
|
10
|
+
export declare const clearAllMarksCommand: MarkCommand<MarkClearAllParams>;
|
|
11
|
+
export declare const removeLinkCommand: MarkCommand<MarkRemoveParams>;
|
|
12
|
+
export declare const editLinkCommand: MarkCommand<LinkEditParams>;
|
|
13
|
+
export declare const markCommands: {
|
|
14
|
+
readonly "mark.apply": MarkCommand<MarkApplyParams>;
|
|
15
|
+
readonly "mark.remove": MarkCommand<MarkRemoveParams>;
|
|
16
|
+
readonly "mark.toggle": MarkCommand<MarkToggleParams>;
|
|
17
|
+
readonly "mark.setAttrs": MarkCommand<MarkSetAttrsParams>;
|
|
18
|
+
readonly "mark.clearAll": MarkCommand<MarkClearAllParams>;
|
|
19
|
+
readonly "link.remove": MarkCommand<MarkRemoveParams>;
|
|
20
|
+
readonly "link.edit": MarkCommand<LinkEditParams>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { isTextNode } from "../identity.js";
|
|
2
|
+
import { nodeAtPath } from "../positions.js";
|
|
3
|
+
import { normalizeLinkInput, sanitizeLinkTarget } from "../security/urlPolicy.js";
|
|
4
|
+
import { canonicalMarkAttrs, markKey } from "./canonical.js";
|
|
5
|
+
import { resolveMarkRun } from "./resolveMarkRun.js";
|
|
6
|
+
const inlineScopes = (scope) => {
|
|
7
|
+
if (scope.kind === "inline-range")
|
|
8
|
+
return [scope];
|
|
9
|
+
if (scope.kind !== "mixed")
|
|
10
|
+
return [];
|
|
11
|
+
return scope.parts.flatMap(inlineScopes);
|
|
12
|
+
};
|
|
13
|
+
const markFor = (params, ctx) => {
|
|
14
|
+
const spec = ctx.schema.marks[params.markType];
|
|
15
|
+
if (!spec)
|
|
16
|
+
throw new Error(`Unknown mark type "${params.markType}".`);
|
|
17
|
+
let attrs = canonicalMarkAttrs(params.markType, params.attrs);
|
|
18
|
+
if (params.markType === "link") {
|
|
19
|
+
const href = normalizeLinkInput(String(params.attrs?.href || "")).href;
|
|
20
|
+
if (!href)
|
|
21
|
+
throw new Error("Link mark requires a safe URL.");
|
|
22
|
+
attrs = { href, ...(sanitizeLinkTarget(params.attrs?.target) ? { target: sanitizeLinkTarget(params.attrs?.target) } : {}) };
|
|
23
|
+
}
|
|
24
|
+
if (attrs === null)
|
|
25
|
+
throw new Error(`Invalid attributes for mark "${params.markType}".`);
|
|
26
|
+
const errors = Object.entries(spec.attributes || {}).filter(([name, attribute]) => attribute.required && attrs?.[name] === undefined || attrs?.[name] !== undefined && attribute.validate && !attribute.validate(attrs[name]));
|
|
27
|
+
if (errors.length)
|
|
28
|
+
throw new Error(`Invalid attributes for mark "${params.markType}": ${errors[0][0]}.`);
|
|
29
|
+
return { type: params.markType, ...(Object.keys(attrs).length ? { attrs } : {}) };
|
|
30
|
+
};
|
|
31
|
+
const textMarksInRange = (document, range) => {
|
|
32
|
+
const owner = nodeAtPath(document, range.from.path);
|
|
33
|
+
if (!owner || isTextNode(owner) || range.from.path.some((part, index) => part !== range.to.path[index]))
|
|
34
|
+
return [];
|
|
35
|
+
let offset = 0;
|
|
36
|
+
const marks = [];
|
|
37
|
+
(owner.children || []).forEach((node) => {
|
|
38
|
+
const width = isTextNode(node) ? node.text.length : 1;
|
|
39
|
+
if (isTextNode(node) && offset < range.to.offset && offset + width > range.from.offset)
|
|
40
|
+
marks.push(...(node.marks || []));
|
|
41
|
+
offset += width;
|
|
42
|
+
});
|
|
43
|
+
return [...new Map(marks.map((mark) => [markKey(mark), mark])).values()];
|
|
44
|
+
};
|
|
45
|
+
const ranges = (scope, ctx) => inlineScopes(scope).flatMap((part) => part.runs.filter((run) => run.to > run.from).flatMap((run) => {
|
|
46
|
+
const content = ctx.positions.contentRangeOf(run.ownerNodeId);
|
|
47
|
+
return content ? [{ from: { path: [...content.from.path], offset: run.from }, to: { path: [...content.to.path], offset: run.to } }] : [];
|
|
48
|
+
}));
|
|
49
|
+
const markAllowedInOwner = (document, range, markType, ctx) => {
|
|
50
|
+
const owner = nodeAtPath(document, range.from.path);
|
|
51
|
+
if (!owner || isTextNode(owner))
|
|
52
|
+
return false;
|
|
53
|
+
const allowed = ctx.schema.nodes[owner.type]?.marks;
|
|
54
|
+
return allowed !== "" && (allowed === undefined || allowed === "_all" || allowed.includes(markType));
|
|
55
|
+
};
|
|
56
|
+
/** Reports atoms skipped by mark application without changing the command contract. */
|
|
57
|
+
export const reportMarkApplication = (document, scope, markType, ctx) => {
|
|
58
|
+
const ownerIds = new Set();
|
|
59
|
+
const skippedOwners = new Set();
|
|
60
|
+
const skipped = new Set();
|
|
61
|
+
for (const part of inlineScopes(scope))
|
|
62
|
+
for (const run of part.runs) {
|
|
63
|
+
ownerIds.add(run.ownerNodeId);
|
|
64
|
+
const content = ctx.positions.contentRangeOf(run.ownerNodeId);
|
|
65
|
+
if (content && !markAllowedInOwner(document, content, markType, ctx))
|
|
66
|
+
skippedOwners.add(run.ownerNodeId);
|
|
67
|
+
if (!run.containsAtoms)
|
|
68
|
+
continue;
|
|
69
|
+
const owner = content ? nodeAtPath(document, content.from.path) : undefined;
|
|
70
|
+
if (!owner || isTextNode(owner))
|
|
71
|
+
continue;
|
|
72
|
+
let offset = 0;
|
|
73
|
+
for (const child of owner.children || []) {
|
|
74
|
+
const width = isTextNode(child) ? child.text.length : 1;
|
|
75
|
+
if (!isTextNode(child) && offset < run.to && offset + width > run.from) {
|
|
76
|
+
const allowed = ctx.schema.nodes[child.type]?.marks;
|
|
77
|
+
if (allowed !== "_all" && (!Array.isArray(allowed) || !allowed.includes(markType)))
|
|
78
|
+
skipped.add(child.id);
|
|
79
|
+
}
|
|
80
|
+
offset += width;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ownerCount: ownerIds.size,
|
|
85
|
+
ownerIdsSkipped: [...skippedOwners],
|
|
86
|
+
atomOwnersSkipped: [...skipped],
|
|
87
|
+
partial: skipped.size > 0 || skippedOwners.size > 0,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const removalsFor = (document, range, types) => textMarksInRange(document, range).filter((mark) => types.has(mark.type)).map((mark) => ({ type: "removeMark", range, mark }));
|
|
91
|
+
export const applyMarkCommand = (document, scope, params, ctx) => {
|
|
92
|
+
const mark = markFor(params, ctx);
|
|
93
|
+
const excluded = new Set([mark.type, ...(ctx.schema.marks[mark.type]?.excludes || [])]);
|
|
94
|
+
return ranges(scope, ctx).filter((range) => markAllowedInOwner(document, range, params.markType, ctx)).flatMap((range) => [
|
|
95
|
+
...removalsFor(document, range, excluded),
|
|
96
|
+
{ type: "addMark", range, mark },
|
|
97
|
+
]);
|
|
98
|
+
};
|
|
99
|
+
export const removeMarkCommand = (document, scope, params, ctx) => ranges(scope, ctx).flatMap((range) => removalsFor(document, range, new Set([params.markType])));
|
|
100
|
+
export const toggleMarkCommand = (document, scope, params, ctx) => params.coverage === "all"
|
|
101
|
+
? removeMarkCommand(document, scope, params, ctx)
|
|
102
|
+
: applyMarkCommand(document, scope, params, ctx);
|
|
103
|
+
export const setMarkAttrsCommand = applyMarkCommand;
|
|
104
|
+
export const clearAllMarksCommand = (document, scope, _params, ctx) => ranges(scope, ctx).flatMap((range) => textMarksInRange(document, range).map((mark) => ({ type: "removeMark", range, mark })));
|
|
105
|
+
const collapsedLinkRun = (document, scope) => {
|
|
106
|
+
const inline = inlineScopes(scope)[0];
|
|
107
|
+
return inline?.collapsed && inline.storedMarkAnchor
|
|
108
|
+
? resolveMarkRun(document, inline.storedMarkAnchor.pos, "link")
|
|
109
|
+
: null;
|
|
110
|
+
};
|
|
111
|
+
export const removeLinkCommand = (document, scope, params, ctx) => {
|
|
112
|
+
const run = collapsedLinkRun(document, scope);
|
|
113
|
+
return run
|
|
114
|
+
? [{ type: "removeMark", range: run.range, mark: run.mark }]
|
|
115
|
+
: removeMarkCommand(document, scope, { markType: params.markType || "link" }, ctx);
|
|
116
|
+
};
|
|
117
|
+
export const editLinkCommand = (document, scope, params, ctx) => {
|
|
118
|
+
const run = collapsedLinkRun(document, scope);
|
|
119
|
+
const attrs = { href: params.href, ...(params.target ? { target: params.target } : {}) };
|
|
120
|
+
if (!run)
|
|
121
|
+
return applyMarkCommand(document, scope, { markType: "link", attrs }, ctx);
|
|
122
|
+
const replacement = markFor({ markType: "link", attrs }, ctx);
|
|
123
|
+
return [
|
|
124
|
+
{ type: "removeMark", range: run.range, mark: run.mark },
|
|
125
|
+
{ type: "addMark", range: run.range, mark: replacement },
|
|
126
|
+
];
|
|
127
|
+
};
|
|
128
|
+
export const markCommands = {
|
|
129
|
+
"mark.apply": applyMarkCommand,
|
|
130
|
+
"mark.remove": removeMarkCommand,
|
|
131
|
+
"mark.toggle": toggleMarkCommand,
|
|
132
|
+
"mark.setAttrs": setMarkAttrsCommand,
|
|
133
|
+
"mark.clearAll": clearAllMarksCommand,
|
|
134
|
+
"link.remove": removeLinkCommand,
|
|
135
|
+
"link.edit": editLinkCommand,
|
|
136
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { canonicalMarkOrder, stableValue } from "./canonical.js";
|
|
2
|
+
const elementForMark = (mark, document) => {
|
|
3
|
+
const tag = mark.type === "bold" ? "strong"
|
|
4
|
+
: mark.type === "italic" ? "em"
|
|
5
|
+
: mark.type === "underline" ? "u"
|
|
6
|
+
: mark.type === "strike" ? "s"
|
|
7
|
+
: mark.type === "code" ? "code"
|
|
8
|
+
: mark.type === "superscript" ? "sup"
|
|
9
|
+
: mark.type === "subscript" ? "sub"
|
|
10
|
+
: mark.type === "link" ? "a" : "span";
|
|
11
|
+
const element = document.createElement(tag);
|
|
12
|
+
element.setAttribute("data-smart-mark", mark.type);
|
|
13
|
+
if (mark.attrs)
|
|
14
|
+
element.setAttribute("data-smart-mark-attrs", stableValue(mark.attrs));
|
|
15
|
+
if (mark.type === "link") {
|
|
16
|
+
element.setAttribute("href", String(mark.attrs?.href || ""));
|
|
17
|
+
element.setAttribute("title", "Ctrl/Cmd+click to open link");
|
|
18
|
+
if (mark.attrs?.target)
|
|
19
|
+
element.setAttribute("target", String(mark.attrs.target));
|
|
20
|
+
}
|
|
21
|
+
else if (mark.type === "textColor")
|
|
22
|
+
element.style.color = String(mark.attrs?.value || "");
|
|
23
|
+
else if (mark.type === "backgroundColor")
|
|
24
|
+
element.style.backgroundColor = String(mark.attrs?.value || "");
|
|
25
|
+
else if (mark.type === "fontSize")
|
|
26
|
+
element.style.fontSize = `${String(mark.attrs?.valuePx || "")}px`;
|
|
27
|
+
else if (mark.type === "fontFamily")
|
|
28
|
+
element.style.fontFamily = String(mark.attrs?.value || "");
|
|
29
|
+
else if (mark.type === "suggestion") {
|
|
30
|
+
element.style.textDecoration = mark.attrs?.kind === "delete" ? "line-through" : "underline";
|
|
31
|
+
if (mark.attrs?.authorId)
|
|
32
|
+
element.setAttribute("data-suggestion-author", String(mark.attrs.authorId));
|
|
33
|
+
if (mark.attrs?.kind)
|
|
34
|
+
element.setAttribute("data-suggestion-kind", String(mark.attrs.kind));
|
|
35
|
+
}
|
|
36
|
+
return element;
|
|
37
|
+
};
|
|
38
|
+
export const renderMarkedText = (node, document) => {
|
|
39
|
+
let output = document.createTextNode(node.text);
|
|
40
|
+
[...canonicalMarkOrder(node.marks)].reverse().forEach((mark) => {
|
|
41
|
+
const wrapper = elementForMark(mark, document);
|
|
42
|
+
wrapper.appendChild(output);
|
|
43
|
+
output = wrapper;
|
|
44
|
+
});
|
|
45
|
+
return output;
|
|
46
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FoundationEditor } from "../editor.js";
|
|
2
|
+
import type { Attrs, SmartOperation } from "../types.js";
|
|
3
|
+
import type { InlineToolDeclaration } from "./types.js";
|
|
4
|
+
export type MarkIntent = "apply" | "remove" | "toggle" | "setAttrs" | "clearAll" | "editLink";
|
|
5
|
+
/** Editor-state adapter; the underlying mark commands remain pure and editor-free. */
|
|
6
|
+
export declare const executeMarkTool: (editor: FoundationEditor, declaration: InlineToolDeclaration, intent: MarkIntent, attrs?: Attrs, options?: {
|
|
7
|
+
addToHistory?: boolean;
|
|
8
|
+
}) => SmartOperation[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { canonicalMarkAttrs, canonicalMarkOrder } from "./canonical.js";
|
|
2
|
+
import { applyMarkCommand, clearAllMarksCommand, editLinkCommand, removeLinkCommand, removeMarkCommand, toggleMarkCommand } from "./commands.js";
|
|
3
|
+
import { marksAtInsertion } from "./stored.js";
|
|
4
|
+
const updateCollapsed = (current, declaration, intent, attrs) => {
|
|
5
|
+
if (intent === "clearAll")
|
|
6
|
+
return [];
|
|
7
|
+
const exists = current.some((mark) => mark.type === declaration.markType);
|
|
8
|
+
const remove = intent === "remove" || intent === "toggle" && exists;
|
|
9
|
+
const without = current.filter((mark) => mark.type !== declaration.markType
|
|
10
|
+
&& !(declaration.excludes || []).includes(mark.type));
|
|
11
|
+
if (remove)
|
|
12
|
+
return canonicalMarkOrder(without);
|
|
13
|
+
const canonicalAttrs = canonicalMarkAttrs(declaration.markType, attrs);
|
|
14
|
+
if (canonicalAttrs === null || declaration.validate?.(canonicalAttrs) === false) {
|
|
15
|
+
throw new Error(`Invalid attributes for mark "${declaration.markType}".`);
|
|
16
|
+
}
|
|
17
|
+
return canonicalMarkOrder([...without, {
|
|
18
|
+
type: declaration.markType,
|
|
19
|
+
...(Object.keys(canonicalAttrs).length ? { attrs: canonicalAttrs } : {}),
|
|
20
|
+
}]);
|
|
21
|
+
};
|
|
22
|
+
/** Editor-state adapter; the underlying mark commands remain pure and editor-free. */
|
|
23
|
+
export const executeMarkTool = (editor, declaration, intent, attrs, options = {}) => {
|
|
24
|
+
const scope = editor.resolveScope({ want: "inline-range" });
|
|
25
|
+
if (!("kind" in scope) || scope.kind !== "inline-range")
|
|
26
|
+
return [];
|
|
27
|
+
const inline = scope;
|
|
28
|
+
const description = editor.resolveScope({ want: "describe" });
|
|
29
|
+
if (inline.collapsed && !(declaration.markType === "link" && (intent === "remove" || intent === "editLink"))) {
|
|
30
|
+
const current = editor.storedMarks || marksAtInsertion(editor.document, editor.selection.head, editor.schema);
|
|
31
|
+
editor.setStoredMarks(updateCollapsed(current, declaration, intent, attrs), { source: "toolbar" });
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const context = { schema: editor.schema, positions: editor.positions };
|
|
35
|
+
const coverage = description.marks.some((entry) => entry.mark.type === declaration.markType && entry.coverage === "all")
|
|
36
|
+
? "all"
|
|
37
|
+
: description.marks.some((entry) => entry.mark.type === declaration.markType) ? "partial" : "none";
|
|
38
|
+
const operations = intent === "clearAll" ? clearAllMarksCommand(editor.document, inline, {}, context)
|
|
39
|
+
: intent === "remove" && declaration.markType === "link" ? removeLinkCommand(editor.document, inline, { markType: "link" }, context)
|
|
40
|
+
: intent === "editLink" && declaration.markType === "link" ? editLinkCommand(editor.document, inline, attrs, context)
|
|
41
|
+
: intent === "remove" ? removeMarkCommand(editor.document, inline, { markType: declaration.markType }, context)
|
|
42
|
+
: intent === "toggle" ? toggleMarkCommand(editor.document, inline, { markType: declaration.markType, attrs, coverage }, context)
|
|
43
|
+
: applyMarkCommand(editor.document, inline, { markType: declaration.markType, attrs }, context);
|
|
44
|
+
if (operations.length)
|
|
45
|
+
editor.transact((builder) => {
|
|
46
|
+
builder.operations.push(...operations);
|
|
47
|
+
builder.setSelection(editor.selection);
|
|
48
|
+
}, { source: "toolbar", addToHistory: options.addToHistory ?? true });
|
|
49
|
+
return operations;
|
|
50
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SmartDocument, SmartMark, SmartNode } from "../types.js";
|
|
2
|
+
export declare const MARKDOWN_SUPPORTED_MARKS: readonly ["bold", "italic", "code", "strike", "link"];
|
|
3
|
+
export declare const MARKDOWN_UNSUPPORTED_MARKS: readonly ["underline", "textColor", "backgroundColor", "fontSize", "fontFamily", "superscript", "subscript"];
|
|
4
|
+
export interface CanonicalDocxRunProperties {
|
|
5
|
+
readonly bold?: boolean;
|
|
6
|
+
readonly italic?: boolean;
|
|
7
|
+
readonly underline?: boolean;
|
|
8
|
+
readonly strike?: boolean;
|
|
9
|
+
readonly code?: boolean;
|
|
10
|
+
readonly superscript?: boolean;
|
|
11
|
+
readonly subscript?: boolean;
|
|
12
|
+
readonly color?: string;
|
|
13
|
+
readonly backgroundColor?: string;
|
|
14
|
+
readonly fontSizeHalfPoints?: number;
|
|
15
|
+
readonly fontFamily?: string;
|
|
16
|
+
readonly link?: {
|
|
17
|
+
href: string;
|
|
18
|
+
target?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface CanonicalDocxRun {
|
|
22
|
+
readonly text: string;
|
|
23
|
+
readonly properties: CanonicalDocxRunProperties;
|
|
24
|
+
}
|
|
25
|
+
export declare const docxProperties: (marks: readonly SmartMark[]) => CanonicalDocxRunProperties;
|
|
26
|
+
/**
|
|
27
|
+
* The per-node half of docxProperties, matching FeatureFormatCodec.serialize's
|
|
28
|
+
* (node, ctx) => unknown shape (formats/codec.ts) - Phase 11 Tier 2's marks
|
|
29
|
+
* codec slice. Real single-node function already existed (docxProperties
|
|
30
|
+
* operates on one text node's marks array already); this only adapts its
|
|
31
|
+
* signature to accept a SmartNode directly, it does not add new logic.
|
|
32
|
+
*/
|
|
33
|
+
export declare const markRunDocxProperties: (node: SmartNode) => CanonicalDocxRunProperties;
|
|
34
|
+
/** Semantic DOCX run model. Conversion to OOXML lives in the product adapter. */
|
|
35
|
+
export declare const canonicalMarksToDocxRuns: (document: SmartDocument) => CanonicalDocxRun[];
|
|
36
|
+
/** PDF fidelity is visual/lossy; this payload deliberately contains no mark identity. */
|
|
37
|
+
export declare const canonicalMarksPdfText: (document: SmartDocument) => string;
|