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,229 @@
|
|
|
1
|
+
import { parseCanonicalListMarkdown, serializeCanonicalListHtml } from "../list/formats.js";
|
|
2
|
+
const listTags = new Set(["UL", "OL"]);
|
|
3
|
+
const blockSelector = ":scope > p, :scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > h5, :scope > h6, :scope > ul, :scope > ol, :scope > blockquote, :scope > pre, :scope > table, :scope > div";
|
|
4
|
+
const msoListPattern = /(?:^|;)\s*mso-list\s*:\s*([^\s;]+)\s+level(\d+)\s+([^\s;]+)/i;
|
|
5
|
+
const unwrap = (element) => element.replaceWith(...Array.from(element.childNodes));
|
|
6
|
+
const normalizeTransparentContainers = (document) => {
|
|
7
|
+
const docsWrapper = document.body.querySelector('[id^="docs-internal-guid-"]');
|
|
8
|
+
if (docsWrapper && docsWrapper.parentElement === document.body)
|
|
9
|
+
unwrap(docsWrapper);
|
|
10
|
+
[...document.body.querySelectorAll("div")].reverse().forEach((element) => {
|
|
11
|
+
if (element.querySelector(blockSelector))
|
|
12
|
+
unwrap(element);
|
|
13
|
+
else if (element.parentElement === document.body || element.parentElement?.tagName === "LI") {
|
|
14
|
+
const paragraph = document.createElement("p");
|
|
15
|
+
paragraph.append(...Array.from(element.childNodes));
|
|
16
|
+
element.replaceWith(paragraph);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const directNodes = Array.from(document.body.childNodes);
|
|
20
|
+
let paragraph = null;
|
|
21
|
+
directNodes.forEach((node) => {
|
|
22
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent) {
|
|
23
|
+
paragraph || (paragraph = document.createElement("p"));
|
|
24
|
+
if (!paragraph.parentNode)
|
|
25
|
+
document.body.insertBefore(paragraph, node);
|
|
26
|
+
paragraph.append(node);
|
|
27
|
+
}
|
|
28
|
+
else if (node.nodeType === Node.ELEMENT_NODE && !["BR", "SPAN", "B", "STRONG", "I", "EM", "U", "S", "A", "CODE"].includes(node.tagName)) {
|
|
29
|
+
paragraph = null;
|
|
30
|
+
}
|
|
31
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
32
|
+
paragraph || (paragraph = document.createElement("p"));
|
|
33
|
+
if (!paragraph.parentNode)
|
|
34
|
+
document.body.insertBefore(paragraph, node);
|
|
35
|
+
paragraph.append(node);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const declaredLevel = (item) => {
|
|
40
|
+
const value = Number(item.getAttribute("data-aria-level") || item.getAttribute("aria-level"));
|
|
41
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
42
|
+
};
|
|
43
|
+
const listShell = (source) => {
|
|
44
|
+
const shell = source.cloneNode(false);
|
|
45
|
+
shell.removeAttribute("start");
|
|
46
|
+
return shell;
|
|
47
|
+
};
|
|
48
|
+
const msoListEntry = (element) => {
|
|
49
|
+
if (element.tagName !== "P")
|
|
50
|
+
return null;
|
|
51
|
+
const match = msoListPattern.exec(element.getAttribute("style") || "");
|
|
52
|
+
if (!match)
|
|
53
|
+
return null;
|
|
54
|
+
const markerElement = Array.from(element.querySelectorAll("span")).find((span) => /(?:^|;)\s*mso-list\s*:\s*Ignore(?:\s*;|\s*$)/i.test(span.getAttribute("style") || ""));
|
|
55
|
+
const marker = markerElement?.textContent?.replace(/\u00a0/g, " ").trim() || "";
|
|
56
|
+
const ordered = /^(?:\d+|[a-z]+|[ivxlcdm]+)[.)]/i.test(marker);
|
|
57
|
+
const start = ordered ? Number.parseInt(marker, 10) : undefined;
|
|
58
|
+
return {
|
|
59
|
+
element: element,
|
|
60
|
+
markerElement: markerElement ?? null,
|
|
61
|
+
listKey: `${match[1]}:${match[3]}`,
|
|
62
|
+
level: Math.max(1, Number(match[2]) || 1),
|
|
63
|
+
tagName: ordered ? "OL" : "UL",
|
|
64
|
+
...(start !== undefined && Number.isFinite(start) && start > 1 ? { start } : {}),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/** Converts real Office `mso-list` paragraph runs before generic wrappers move them. */
|
|
68
|
+
const normalizeMsoLists = (document) => {
|
|
69
|
+
const visit = (parent) => {
|
|
70
|
+
const children = Array.from(parent.children);
|
|
71
|
+
for (let index = 0; index < children.length;) {
|
|
72
|
+
const first = msoListEntry(children[index]);
|
|
73
|
+
if (!first) {
|
|
74
|
+
visit(children[index]);
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const entries = [first];
|
|
79
|
+
let cursor = index + 1;
|
|
80
|
+
while (cursor < children.length) {
|
|
81
|
+
const next = msoListEntry(children[cursor]);
|
|
82
|
+
if (!next || next.listKey !== first.listKey)
|
|
83
|
+
break;
|
|
84
|
+
entries.push(next);
|
|
85
|
+
cursor += 1;
|
|
86
|
+
}
|
|
87
|
+
const roots = [];
|
|
88
|
+
const lists = [];
|
|
89
|
+
const lastItems = [];
|
|
90
|
+
entries.forEach((entry) => {
|
|
91
|
+
entry.markerElement?.remove();
|
|
92
|
+
Array.from(entry.element.children).forEach((child) => {
|
|
93
|
+
if (!child.textContent?.trim() && !child.querySelector("img,br,table"))
|
|
94
|
+
child.remove();
|
|
95
|
+
});
|
|
96
|
+
const level = Math.min(entry.level, lists.length + 1);
|
|
97
|
+
const parentItem = level > 1 ? lastItems[level - 2] : undefined;
|
|
98
|
+
let list = lists[level - 1];
|
|
99
|
+
const needsList = !list || list.tagName !== entry.tagName || (level > 1 && list.parentElement !== parentItem);
|
|
100
|
+
if (needsList) {
|
|
101
|
+
list = document.createElement(entry.tagName.toLowerCase());
|
|
102
|
+
if (entry.tagName === "OL" && entry.start)
|
|
103
|
+
list.setAttribute("start", String(entry.start));
|
|
104
|
+
if (level === 1)
|
|
105
|
+
roots.push(list);
|
|
106
|
+
else
|
|
107
|
+
parentItem?.append(list);
|
|
108
|
+
lists[level - 1] = list;
|
|
109
|
+
}
|
|
110
|
+
lists.length = level;
|
|
111
|
+
lastItems.length = level;
|
|
112
|
+
const item = document.createElement("li");
|
|
113
|
+
item.append(...Array.from(entry.element.childNodes));
|
|
114
|
+
list.append(item);
|
|
115
|
+
lastItems[level - 1] = item;
|
|
116
|
+
});
|
|
117
|
+
entries[0].element.replaceWith(...roots);
|
|
118
|
+
entries.slice(1).forEach((entry) => entry.element.remove());
|
|
119
|
+
roots.forEach(visit);
|
|
120
|
+
index = cursor;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
visit(document.body);
|
|
124
|
+
};
|
|
125
|
+
/** Regroups Office/Docs runs that encode each visual item as a sibling list. */
|
|
126
|
+
const normalizeDeclaredLevelLists = (document) => {
|
|
127
|
+
const visit = (parent) => {
|
|
128
|
+
const children = Array.from(parent.children);
|
|
129
|
+
for (let index = 0; index < children.length;) {
|
|
130
|
+
const first = children[index];
|
|
131
|
+
const firstItems = listTags.has(first.tagName) ? Array.from(first.children).filter((child) => child.tagName === "LI") : [];
|
|
132
|
+
if (!firstItems.some((item) => declaredLevel(item) !== null)) {
|
|
133
|
+
index += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const run = [];
|
|
137
|
+
while (index < children.length && listTags.has(children[index].tagName))
|
|
138
|
+
run.push(children[index++]);
|
|
139
|
+
const entries = run.flatMap((list) => Array.from(list.children)
|
|
140
|
+
.filter((child) => child.tagName === "LI")
|
|
141
|
+
.map((item) => ({ level: declaredLevel(item) || 1, list, item: item })));
|
|
142
|
+
if (!entries.length)
|
|
143
|
+
continue;
|
|
144
|
+
const roots = [];
|
|
145
|
+
const stacks = [];
|
|
146
|
+
const lastItems = [];
|
|
147
|
+
entries.forEach((entry) => {
|
|
148
|
+
const level = Math.min(entry.level, stacks.length + 1);
|
|
149
|
+
const parentItem = level > 1 ? lastItems[level - 2] : undefined;
|
|
150
|
+
let target = stacks[level - 1];
|
|
151
|
+
const needsList = !target || target.tagName !== entry.list.tagName || (level > 1 && target.parentElement !== parentItem);
|
|
152
|
+
if (needsList) {
|
|
153
|
+
target = listShell(entry.list);
|
|
154
|
+
if (level === 1)
|
|
155
|
+
roots.push(target);
|
|
156
|
+
else
|
|
157
|
+
parentItem?.append(target);
|
|
158
|
+
stacks[level - 1] = target;
|
|
159
|
+
}
|
|
160
|
+
stacks.length = level;
|
|
161
|
+
lastItems.length = level;
|
|
162
|
+
entry.item.removeAttribute("aria-level");
|
|
163
|
+
entry.item.removeAttribute("data-aria-level");
|
|
164
|
+
target.append(entry.item);
|
|
165
|
+
lastItems[level - 1] = entry.item;
|
|
166
|
+
});
|
|
167
|
+
run[0].replaceWith(...roots);
|
|
168
|
+
run.slice(1).forEach((list) => list.remove());
|
|
169
|
+
}
|
|
170
|
+
Array.from(parent.children).forEach((child) => visit(child));
|
|
171
|
+
};
|
|
172
|
+
visit(document.body);
|
|
173
|
+
};
|
|
174
|
+
const normalizedHtml = (payload, id) => {
|
|
175
|
+
normalizeMsoLists(payload.document);
|
|
176
|
+
normalizeTransparentContainers(payload.document);
|
|
177
|
+
normalizeDeclaredLevelLists(payload.document);
|
|
178
|
+
return { html: payload.document.body.innerHTML, plainText: payload.plainText, repairs: [`${id}:transparent-containers-and-declared-list-levels`] };
|
|
179
|
+
};
|
|
180
|
+
export const wordClipboardNormalizer = {
|
|
181
|
+
id: "word",
|
|
182
|
+
sources: ["word"],
|
|
183
|
+
normalize: (payload) => normalizedHtml(payload, "word"),
|
|
184
|
+
};
|
|
185
|
+
export const googleDocsClipboardNormalizer = {
|
|
186
|
+
id: "google-docs",
|
|
187
|
+
sources: ["google-docs"],
|
|
188
|
+
normalize: (payload) => normalizedHtml(payload, "google-docs"),
|
|
189
|
+
};
|
|
190
|
+
export const spreadsheetClipboardNormalizer = {
|
|
191
|
+
id: "spreadsheet",
|
|
192
|
+
sources: ["spreadsheet"],
|
|
193
|
+
normalize(payload) {
|
|
194
|
+
normalizeTransparentContainers(payload.document);
|
|
195
|
+
return { html: payload.document.body.innerHTML, plainText: payload.plainText, repairs: ["spreadsheet:transparent-containers"] };
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
export const htmlClipboardNormalizer = {
|
|
199
|
+
id: "generic-html",
|
|
200
|
+
sources: ["html", "native"],
|
|
201
|
+
normalize: (payload) => normalizedHtml(payload, "generic-html"),
|
|
202
|
+
};
|
|
203
|
+
export const markdownClipboardNormalizer = {
|
|
204
|
+
id: "markdown",
|
|
205
|
+
sources: ["markdown"],
|
|
206
|
+
normalize(payload) {
|
|
207
|
+
const document = parseCanonicalListMarkdown(payload.plainText);
|
|
208
|
+
return {
|
|
209
|
+
html: serializeCanonicalListHtml(document, { clean: true, fragment: true }),
|
|
210
|
+
plainText: payload.plainText,
|
|
211
|
+
repairs: ["markdown:gfm-parse"],
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
export const plainTextClipboardNormalizer = {
|
|
216
|
+
id: "plain-text",
|
|
217
|
+
sources: ["plain-text"],
|
|
218
|
+
normalize(payload) {
|
|
219
|
+
return { html: payload.html, plainText: payload.plainText, repairs: [] };
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
export const capturedSourceNormalizers = Object.freeze([
|
|
223
|
+
wordClipboardNormalizer,
|
|
224
|
+
googleDocsClipboardNormalizer,
|
|
225
|
+
spreadsheetClipboardNormalizer,
|
|
226
|
+
markdownClipboardNormalizer,
|
|
227
|
+
htmlClipboardNormalizer,
|
|
228
|
+
plainTextClipboardNormalizer,
|
|
229
|
+
]);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClipboardFragment, ClipboardPipelineOptions, RawClipboardPayload } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_MAX_CLIPBOARD_BYTES: number;
|
|
3
|
+
export declare class ClipboardPayloadTooLargeError extends Error {
|
|
4
|
+
readonly actualBytes: number;
|
|
5
|
+
readonly maximumBytes: number;
|
|
6
|
+
constructor(actualBytes: number, maximumBytes: number);
|
|
7
|
+
}
|
|
8
|
+
export declare const estimateClipboardPayloadBytes: (payload: RawClipboardPayload) => number;
|
|
9
|
+
/**
|
|
10
|
+
* The only public raw-payload entry point. Its call graph fixes the security
|
|
11
|
+
* order as detect -> sanitize -> normalize -> parse -> schema repair.
|
|
12
|
+
*/
|
|
13
|
+
export declare const parseClipboardPayload: (payload: RawClipboardPayload, options: ClipboardPipelineOptions) => ClipboardFragment;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createNodeId } from "../identity.js";
|
|
2
|
+
import { parseCanonicalListHtml } from "../list/formats.js";
|
|
3
|
+
import { foundationSchema, repair } from "../schema.js";
|
|
4
|
+
import { detectClipboardSource } from "./detection.js";
|
|
5
|
+
import { capturedSourceNormalizers } from "./normalizers.js";
|
|
6
|
+
import { sanitizeClipboardHtml } from "./sanitizer.js";
|
|
7
|
+
import { parseNativeClipboardDocument } from "./serialization.js";
|
|
8
|
+
export const DEFAULT_MAX_CLIPBOARD_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
export class ClipboardPayloadTooLargeError extends Error {
|
|
10
|
+
constructor(actualBytes, maximumBytes) {
|
|
11
|
+
super(`Clipboard payload is ${actualBytes} bytes; the limit is ${maximumBytes} bytes.`);
|
|
12
|
+
this.actualBytes = actualBytes;
|
|
13
|
+
this.maximumBytes = maximumBytes;
|
|
14
|
+
this.name = "ClipboardPayloadTooLargeError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const estimateClipboardPayloadBytes = (payload) => {
|
|
18
|
+
const values = payload.representations
|
|
19
|
+
? Object.values(payload.representations)
|
|
20
|
+
: [payload.html || "", payload.plainText || "", payload.native || ""];
|
|
21
|
+
return values.reduce((total, value) => total + new TextEncoder().encode(value).byteLength, 0);
|
|
22
|
+
};
|
|
23
|
+
const genericNormalizer = {
|
|
24
|
+
id: "generic-html",
|
|
25
|
+
sources: ["native", "word", "google-docs", "spreadsheet", "markdown", "html", "plain-text"],
|
|
26
|
+
normalize(payload) {
|
|
27
|
+
return { html: payload.html, plainText: payload.plainText, repairs: [] };
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const textAsHtml = (ownerDocument, text) => text.split(/\r?\n\r?\n/).map((paragraph) => {
|
|
31
|
+
const element = ownerDocument.createElement("p");
|
|
32
|
+
element.textContent = paragraph;
|
|
33
|
+
return element.outerHTML;
|
|
34
|
+
}).join("");
|
|
35
|
+
/**
|
|
36
|
+
* The only public raw-payload entry point. Its call graph fixes the security
|
|
37
|
+
* order as detect -> sanitize -> normalize -> parse -> schema repair.
|
|
38
|
+
*/
|
|
39
|
+
export const parseClipboardPayload = (payload, options) => {
|
|
40
|
+
const bytes = estimateClipboardPayloadBytes(payload);
|
|
41
|
+
const maximum = Math.max(1, Math.floor(options.maxBytes ?? DEFAULT_MAX_CLIPBOARD_BYTES));
|
|
42
|
+
if (bytes > maximum)
|
|
43
|
+
throw new ClipboardPayloadTooLargeError(bytes, maximum);
|
|
44
|
+
const detection = detectClipboardSource(payload);
|
|
45
|
+
const plainText = payload.plainText || "";
|
|
46
|
+
const rawHtml = payload.html || textAsHtml(options.ownerDocument, plainText);
|
|
47
|
+
const sanitized = sanitizeClipboardHtml(options.ownerDocument, detection.source, rawHtml, plainText);
|
|
48
|
+
const normalizer = options.normalizerMode === "generic" ? genericNormalizer
|
|
49
|
+
: options.normalizers?.find((candidate) => candidate.sources.includes(detection.source))
|
|
50
|
+
|| capturedSourceNormalizers.find((candidate) => candidate.sources.includes(detection.source))
|
|
51
|
+
|| genericNormalizer;
|
|
52
|
+
const normalized = normalizer.normalize(sanitized);
|
|
53
|
+
const parsed = detection.source === "native" && payload.native
|
|
54
|
+
? parseNativeClipboardDocument(payload.native)
|
|
55
|
+
: parseCanonicalListHtml(normalized.html);
|
|
56
|
+
const repaired = repair({ ...parsed, id: parsed.id || createNodeId() }, options.schema ?? foundationSchema);
|
|
57
|
+
return { source: detection.source, document: repaired.doc, repairs: [...normalized.repairs, ...repaired.repairs] };
|
|
58
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ClipboardSource, SanitizedClipboardPayload } from "./types.js";
|
|
2
|
+
/** Security boundary. Source normalizers can only receive the branded result. */
|
|
3
|
+
export declare const sanitizeClipboardHtml: (ownerDocument: Document, source: ClipboardSource, html: string, plainText: string) => SanitizedClipboardPayload;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import createDOMPurify from "dompurify";
|
|
2
|
+
import { sanitizeLinkHref, sanitizeResourceUrl } from "../security/urlPolicy.js";
|
|
3
|
+
const allowedClipboardImageData = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
4
|
+
const dangerousCss = /(?:expression\s*\(|url\s*\(|@import\b|behavior\s*:|-moz-binding\s*:)/i;
|
|
5
|
+
const parseSanitizedDocument = (ownerDocument, html) => {
|
|
6
|
+
const parser = new ownerDocument.defaultView.DOMParser();
|
|
7
|
+
return parser.parseFromString(html, "text/html");
|
|
8
|
+
};
|
|
9
|
+
/** Security boundary. Source normalizers can only receive the branded result. */
|
|
10
|
+
export const sanitizeClipboardHtml = (ownerDocument, source, html, plainText) => {
|
|
11
|
+
if (!ownerDocument.defaultView)
|
|
12
|
+
throw new Error("Clipboard sanitization requires a DOM window.");
|
|
13
|
+
const purifier = createDOMPurify(ownerDocument.defaultView);
|
|
14
|
+
purifier.addHook("uponSanitizeAttribute", (_node, event) => {
|
|
15
|
+
const name = event.attrName.toLowerCase();
|
|
16
|
+
if (name.startsWith("on")) {
|
|
17
|
+
event.keepAttr = false;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (name === "style" && dangerousCss.test(event.attrValue)) {
|
|
21
|
+
event.keepAttr = false;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (name === "href" || name === "xlink:href") {
|
|
25
|
+
const safe = sanitizeLinkHref(event.attrValue);
|
|
26
|
+
if (safe)
|
|
27
|
+
event.attrValue = safe;
|
|
28
|
+
else
|
|
29
|
+
event.keepAttr = false;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (name === "src" || name === "poster") {
|
|
33
|
+
const safe = sanitizeResourceUrl(event.attrValue, { allowedDataMimeTypes: allowedClipboardImageData });
|
|
34
|
+
if (safe)
|
|
35
|
+
event.attrValue = safe;
|
|
36
|
+
else
|
|
37
|
+
event.keepAttr = false;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const sanitized = purifier.sanitize(html, {
|
|
41
|
+
ALLOWED_NAMESPACES: ["http://www.w3.org/1999/xhtml", "http://www.w3.org/1998/Math/MathML"],
|
|
42
|
+
ALLOW_DATA_ATTR: true,
|
|
43
|
+
ALLOW_UNKNOWN_PROTOCOLS: false,
|
|
44
|
+
FORBID_TAGS: ["script", "iframe", "object", "embed", "form", "style", "svg"],
|
|
45
|
+
FORBID_CONTENTS: ["script", "iframe", "object", "embed", "form", "style", "svg"],
|
|
46
|
+
CUSTOM_ELEMENT_HANDLING: {
|
|
47
|
+
tagNameCheck: /^x-[a-z0-9-]+$/,
|
|
48
|
+
attributeNameCheck: null,
|
|
49
|
+
allowCustomizedBuiltInElements: false,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
source,
|
|
54
|
+
html: sanitized,
|
|
55
|
+
plainText,
|
|
56
|
+
document: parseSanitizedDocument(ownerDocument, sanitized),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SmartDocument } from "../types.js";
|
|
2
|
+
import { type ClipboardRepresentations } from "./types.js";
|
|
3
|
+
export declare const serializeNativeClipboardDocument: (document: SmartDocument) => string;
|
|
4
|
+
export declare const parseNativeClipboardDocument: (value: string) => SmartDocument;
|
|
5
|
+
export declare const serializeClipboardRepresentations: (document: SmartDocument) => ClipboardRepresentations;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isTextNode } from "../identity.js";
|
|
2
|
+
import { serializeCanonicalListHtml } from "../list/formats.js";
|
|
3
|
+
import { NATIVE_CLIPBOARD_MIME } from "./types.js";
|
|
4
|
+
const nodeText = (node) => {
|
|
5
|
+
if (isTextNode(node))
|
|
6
|
+
return node.text;
|
|
7
|
+
if (node.type === "hard_break")
|
|
8
|
+
return "\n";
|
|
9
|
+
if (node.type === "image" || node.type === "block_image")
|
|
10
|
+
return String(node.attrs?.alt || "");
|
|
11
|
+
if (node.type === "formula" || node.type === "block_formula")
|
|
12
|
+
return String(node.attrs?.source || "");
|
|
13
|
+
const separator = node.type === "table_cell" ? "\t" : node.type === "paragraph" || node.type === "heading"
|
|
14
|
+
|| node.type === "code_block" || node.type === "list_item" || node.type === "table_row" ? "\n" : "";
|
|
15
|
+
return (node.children || []).map(nodeText).join(separator === "\t" ? "" : "") + separator;
|
|
16
|
+
};
|
|
17
|
+
export const serializeNativeClipboardDocument = (document) => JSON.stringify({
|
|
18
|
+
format: "smartrte-canonical-fragment",
|
|
19
|
+
version: 1,
|
|
20
|
+
document,
|
|
21
|
+
});
|
|
22
|
+
export const parseNativeClipboardDocument = (value) => {
|
|
23
|
+
const envelope = JSON.parse(value);
|
|
24
|
+
if (envelope.format !== "smartrte-canonical-fragment" || envelope.version !== 1 || envelope.document?.type !== "doc") {
|
|
25
|
+
throw new Error("Invalid native Smart RTE clipboard fragment.");
|
|
26
|
+
}
|
|
27
|
+
return structuredClone(envelope.document);
|
|
28
|
+
};
|
|
29
|
+
export const serializeClipboardRepresentations = (document) => ({
|
|
30
|
+
[NATIVE_CLIPBOARD_MIME]: serializeNativeClipboardDocument(document),
|
|
31
|
+
"text/html": serializeCanonicalListHtml(document, { clean: true }),
|
|
32
|
+
"text/plain": document.children.map(nodeText).join("").replace(/\n+$/g, ""),
|
|
33
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PositionLookup } from "../scope/types.js";
|
|
2
|
+
import type { SmartDocument, SmartOperation, SmartSelection } from "../types.js";
|
|
3
|
+
export declare const sliceClipboardSelection: (document: SmartDocument, selection: SmartSelection) => SmartDocument;
|
|
4
|
+
export interface ClipboardDeletionResult {
|
|
5
|
+
readonly operations: readonly SmartOperation[];
|
|
6
|
+
readonly selectionTarget: {
|
|
7
|
+
readonly ownerId: string;
|
|
8
|
+
readonly offset: number;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare const deleteClipboardSelection: (document: SmartDocument, selection: SmartSelection, positions: PositionLookup) => ClipboardDeletionResult;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { isTextNode } from "../identity.js";
|
|
2
|
+
import { comparePos, nodeAtPath } from "../positions.js";
|
|
3
|
+
const samePath = (left, right) => left.length === right.length && left.every((part, index) => part === right[index]);
|
|
4
|
+
const size = (node) => isTextNode(node) ? node.text.length : 1;
|
|
5
|
+
const split = (children, offset) => {
|
|
6
|
+
const before = [];
|
|
7
|
+
const after = [];
|
|
8
|
+
let cursor = 0;
|
|
9
|
+
children.forEach((node) => {
|
|
10
|
+
const end = cursor + size(node);
|
|
11
|
+
if (end <= offset)
|
|
12
|
+
before.push(structuredClone(node));
|
|
13
|
+
else if (cursor >= offset)
|
|
14
|
+
after.push(structuredClone(node));
|
|
15
|
+
else if (isTextNode(node)) {
|
|
16
|
+
const local = offset - cursor;
|
|
17
|
+
if (local)
|
|
18
|
+
before.push({ ...node, text: node.text.slice(0, local) });
|
|
19
|
+
if (local < node.text.length)
|
|
20
|
+
after.push({ ...node, text: node.text.slice(local) });
|
|
21
|
+
}
|
|
22
|
+
cursor = end;
|
|
23
|
+
});
|
|
24
|
+
return [before, after];
|
|
25
|
+
};
|
|
26
|
+
const range = (selection) => comparePos(selection.anchor, selection.head) <= 0
|
|
27
|
+
? { from: selection.anchor, to: selection.head } : { from: selection.head, to: selection.anchor };
|
|
28
|
+
const element = (document, pos) => {
|
|
29
|
+
const node = nodeAtPath(document, pos.path);
|
|
30
|
+
if (!node || isTextNode(node))
|
|
31
|
+
throw new Error("Clipboard selection owner is invalid.");
|
|
32
|
+
return node;
|
|
33
|
+
};
|
|
34
|
+
export const sliceClipboardSelection = (document, selection) => {
|
|
35
|
+
const selected = range(selection);
|
|
36
|
+
if (selection.type === "node" || selection.type === "cell") {
|
|
37
|
+
const parent = nodeAtPath(document, selected.from.path);
|
|
38
|
+
if (!parent || isTextNode(parent) || !parent.children)
|
|
39
|
+
throw new Error("Clipboard node selection parent is invalid.");
|
|
40
|
+
return { type: "doc", id: document.id, children: parent.children.slice(selected.from.offset, selected.to.offset).map((node) => structuredClone(node)) };
|
|
41
|
+
}
|
|
42
|
+
const fromOwner = element(document, selected.from);
|
|
43
|
+
if (samePath(selected.from.path, selected.to.path)) {
|
|
44
|
+
const [, tail] = split(fromOwner.children || [], selected.from.offset);
|
|
45
|
+
const [content] = split(tail, selected.to.offset - selected.from.offset);
|
|
46
|
+
return { type: "doc", id: document.id, children: [{ ...fromOwner, children: content }] };
|
|
47
|
+
}
|
|
48
|
+
const parentPath = selected.from.path.slice(0, -1);
|
|
49
|
+
if (!samePath(parentPath, selected.to.path.slice(0, -1)))
|
|
50
|
+
throw new Error("Clipboard copy is clamped to one structural parent.");
|
|
51
|
+
const parent = nodeAtPath(document, parentPath);
|
|
52
|
+
const toOwner = element(document, selected.to);
|
|
53
|
+
if (!parent || isTextNode(parent) || !parent.children)
|
|
54
|
+
throw new Error("Clipboard selection parent is invalid.");
|
|
55
|
+
const fromIndex = selected.from.path[selected.from.path.length - 1];
|
|
56
|
+
const toIndex = selected.to.path[selected.to.path.length - 1];
|
|
57
|
+
const [, first] = split(fromOwner.children || [], selected.from.offset);
|
|
58
|
+
const [last] = split(toOwner.children || [], selected.to.offset);
|
|
59
|
+
return {
|
|
60
|
+
type: "doc", id: document.id,
|
|
61
|
+
children: [
|
|
62
|
+
{ ...fromOwner, children: first },
|
|
63
|
+
...parent.children.slice(fromIndex + 1, toIndex).map((node) => structuredClone(node)),
|
|
64
|
+
{ ...toOwner, children: last },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
export const deleteClipboardSelection = (document, selection, positions) => {
|
|
69
|
+
const selected = range(selection);
|
|
70
|
+
if (selection.type === "node" || selection.type === "cell") {
|
|
71
|
+
const parent = nodeAtPath(document, selected.from.path);
|
|
72
|
+
if (!parent || isTextNode(parent) || !parent.children)
|
|
73
|
+
throw new Error("Clipboard node deletion parent is invalid.");
|
|
74
|
+
const operations = [];
|
|
75
|
+
for (let index = selected.to.offset - 1; index >= selected.from.offset; index -= 1)
|
|
76
|
+
operations.push({
|
|
77
|
+
type: "removeNode", pos: { path: [...selected.from.path], offset: index }, node: parent.children[index],
|
|
78
|
+
});
|
|
79
|
+
return { operations, selectionTarget: { ownerId: parent.id, offset: selected.from.offset } };
|
|
80
|
+
}
|
|
81
|
+
const owner = element(document, selected.from);
|
|
82
|
+
const ownerPosition = positions.positionOf(owner.id);
|
|
83
|
+
if (!ownerPosition)
|
|
84
|
+
throw new Error("Clipboard deletion owner position is unavailable.");
|
|
85
|
+
if (samePath(selected.from.path, selected.to.path)) {
|
|
86
|
+
const [before] = split(owner.children || [], selected.from.offset);
|
|
87
|
+
const [, after] = split(owner.children || [], selected.to.offset);
|
|
88
|
+
return {
|
|
89
|
+
operations: [{ type: "replaceNode", pos: ownerPosition.pos, before: owner, after: { ...owner, children: [...before, ...after] } }],
|
|
90
|
+
selectionTarget: { ownerId: owner.id, offset: selected.from.offset },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const parentPath = selected.from.path.slice(0, -1);
|
|
94
|
+
const parent = nodeAtPath(document, parentPath);
|
|
95
|
+
const toOwner = element(document, selected.to);
|
|
96
|
+
if (!samePath(parentPath, selected.to.path.slice(0, -1)) || !parent || isTextNode(parent) || !parent.children) {
|
|
97
|
+
throw new Error("Clipboard deletion is clamped to one structural parent.");
|
|
98
|
+
}
|
|
99
|
+
const fromIndex = selected.from.path[selected.from.path.length - 1];
|
|
100
|
+
const toIndex = selected.to.path[selected.to.path.length - 1];
|
|
101
|
+
const [before] = split(owner.children || [], selected.from.offset);
|
|
102
|
+
const [, after] = split(toOwner.children || [], selected.to.offset);
|
|
103
|
+
const operations = [];
|
|
104
|
+
for (let index = toIndex; index > fromIndex; index -= 1)
|
|
105
|
+
operations.push({ type: "removeNode", pos: { path: [...parentPath], offset: index }, node: parent.children[index] });
|
|
106
|
+
operations.push({ type: "replaceNode", pos: ownerPosition.pos, before: owner, after: { ...owner, children: [...before, ...after] } });
|
|
107
|
+
return { operations, selectionTarget: { ownerId: owner.id, offset: selected.from.offset } };
|
|
108
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Repair, SmartDocument, SmartSchema } from "../types.js";
|
|
2
|
+
export declare const NATIVE_CLIPBOARD_MIME = "application/x-smart-rte+json";
|
|
3
|
+
export type ClipboardSource = "native" | "word" | "google-docs" | "spreadsheet" | "markdown" | "html" | "plain-text";
|
|
4
|
+
export interface RawClipboardPayload {
|
|
5
|
+
readonly html?: string;
|
|
6
|
+
readonly plainText?: string;
|
|
7
|
+
readonly native?: string;
|
|
8
|
+
readonly types?: readonly string[];
|
|
9
|
+
/** Complete MIME map when available; used by size guards and adapters. */
|
|
10
|
+
readonly representations?: Readonly<Record<string, string>>;
|
|
11
|
+
}
|
|
12
|
+
export interface ClipboardDetection {
|
|
13
|
+
/** A normalizer-selection hint. Correctness must not depend on this value. */
|
|
14
|
+
readonly source: ClipboardSource;
|
|
15
|
+
readonly signals: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
declare const sanitizedClipboardBrand: unique symbol;
|
|
18
|
+
/** Only the sanitizer stage can construct this input for a source normalizer. */
|
|
19
|
+
export interface SanitizedClipboardPayload {
|
|
20
|
+
readonly [sanitizedClipboardBrand]: true;
|
|
21
|
+
readonly source: ClipboardSource;
|
|
22
|
+
readonly html: string;
|
|
23
|
+
readonly plainText: string;
|
|
24
|
+
readonly document: Document;
|
|
25
|
+
}
|
|
26
|
+
export interface NormalizedClipboardPayload {
|
|
27
|
+
readonly html: string;
|
|
28
|
+
readonly plainText: string;
|
|
29
|
+
readonly repairs: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export interface SourceNormalizer {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly sources: readonly ClipboardSource[];
|
|
34
|
+
normalize(payload: SanitizedClipboardPayload): NormalizedClipboardPayload;
|
|
35
|
+
}
|
|
36
|
+
export interface ClipboardFragment {
|
|
37
|
+
readonly source: ClipboardSource;
|
|
38
|
+
readonly document: SmartDocument;
|
|
39
|
+
readonly repairs: readonly (Repair | string)[];
|
|
40
|
+
}
|
|
41
|
+
export interface ClipboardRepresentations {
|
|
42
|
+
readonly [NATIVE_CLIPBOARD_MIME]: string;
|
|
43
|
+
readonly "text/html": string;
|
|
44
|
+
readonly "text/plain": string;
|
|
45
|
+
}
|
|
46
|
+
export interface ClipboardPipelineOptions {
|
|
47
|
+
readonly ownerDocument: Document;
|
|
48
|
+
readonly normalizers?: readonly SourceNormalizer[];
|
|
49
|
+
/** Test/audit switch proving detection is never required for correctness. */
|
|
50
|
+
readonly normalizerMode?: "detected" | "generic";
|
|
51
|
+
readonly maxBytes?: number;
|
|
52
|
+
/**
|
|
53
|
+
* The schema pasted content is repaired against - defaults to
|
|
54
|
+
* `foundationSchema` (the full built-in set) for full backward
|
|
55
|
+
* compatibility. An editor instance constructed with a restricted plugin
|
|
56
|
+
* set (see packages/react's capabilityPresets.ts) must pass its own
|
|
57
|
+
* `editor.schema` here, or pasted content matching an *excluded* plugin's
|
|
58
|
+
* node types (e.g. a pasted `<table>` under a table-less preset) would
|
|
59
|
+
* incorrectly validate against the full schema instead of demoting to
|
|
60
|
+
* `unknown` - defeating Phase 10's disable-safety guarantee specifically
|
|
61
|
+
* for the paste path.
|
|
62
|
+
*/
|
|
63
|
+
readonly schema?: SmartSchema;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const NATIVE_CLIPBOARD_MIME = "application/x-smart-rte+json";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SmartTransaction } from "../types.js";
|
|
2
|
+
import type { RebaseResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Rebases `transaction` (whose `baseRevision` is behind the current
|
|
5
|
+
* revision) through every transaction committed since, in commit order.
|
|
6
|
+
* Each of `transaction`'s own operations is threaded through every
|
|
7
|
+
* operation of every missed transaction in sequence - the standard
|
|
8
|
+
* single-step operational-transform composition, built on `mapOperation`/
|
|
9
|
+
* `transformOperation`.
|
|
10
|
+
*
|
|
11
|
+
* Atomic per transaction: if any one operation conflicts, the whole
|
|
12
|
+
* transaction is rejected as a conflict rather than partially applied -
|
|
13
|
+
* matching `applyTransactionAtomic`'s own existing all-or-nothing
|
|
14
|
+
* semantics, and avoiding a confusing state where only part of a user's
|
|
15
|
+
* intent materializes.
|
|
16
|
+
*
|
|
17
|
+
* Does not check retention/resync - `missed` is whatever the caller
|
|
18
|
+
* supplies; a caller backed by a bounded revision log is responsible for
|
|
19
|
+
* returning `resync-required` itself before ever calling this when the
|
|
20
|
+
* requested history has already been evicted (see editor.ts's dispatch).
|
|
21
|
+
*
|
|
22
|
+
* `newBaseRevision` is the revision the rebased transaction should be
|
|
23
|
+
* stamped with (i.e. the live revision the caller is about to apply
|
|
24
|
+
* against) - taken explicitly from the caller rather than inferred from
|
|
25
|
+
* `missed`'s own entries, so a gap or reordering bug in how the caller
|
|
26
|
+
* assembled `missed` can't silently produce a wrong revision stamp here.
|
|
27
|
+
*/
|
|
28
|
+
export declare const rebaseTransaction: (transaction: SmartTransaction, missed: readonly SmartTransaction[], newBaseRevision: number) => RebaseResult;
|