smartrte-react 0.3.3 → 0.3.5
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 +14 -83
- package/dist/adapters/canonicalInlineCommandBridge.d.ts +15 -0
- package/dist/adapters/canonicalInlineCommandBridge.js +269 -0
- package/dist/adapters/canonicalListCommandBridge.d.ts +26 -0
- package/dist/adapters/canonicalListCommandBridge.js +430 -0
- package/dist/adapters/documentFormats.d.ts +26 -0
- package/dist/adapters/documentFormats.js +49 -0
- package/dist/adapters/docxFormat.d.ts +5 -0
- package/dist/adapters/docxFormat.js +272 -0
- package/dist/adapters/domBlockCommandBridge.d.ts +19 -0
- package/dist/adapters/domBlockCommandBridge.js +79 -0
- package/dist/adapters/domChecklistCommandBridge.d.ts +6 -0
- package/dist/adapters/domChecklistCommandBridge.js +33 -0
- package/dist/adapters/domCommandBridge.d.ts +19 -0
- package/dist/adapters/domCommandBridge.js +33 -0
- package/dist/adapters/domInlineAtomCommandBridge.d.ts +14 -0
- package/dist/adapters/domInlineAtomCommandBridge.js +101 -0
- package/dist/adapters/domInlineImageCommandBridge.d.ts +14 -0
- package/dist/adapters/domInlineImageCommandBridge.js +72 -0
- package/dist/adapters/domMoveCommandBridge.d.ts +7 -0
- package/dist/adapters/domMoveCommandBridge.js +54 -0
- package/dist/adapters/domSelectionBridge.d.ts +3 -1
- package/dist/adapters/domSelectionBridge.js +96 -44
- package/dist/adapters/domSmartDocument.js +158 -16
- package/dist/adapters/domTableCommandBridge.d.ts +35 -0
- package/dist/adapters/domTableCommandBridge.js +201 -0
- package/dist/adapters/legacyListShadowComparator.d.ts +26 -0
- package/dist/adapters/legacyListShadowComparator.js +316 -0
- package/dist/adapters/pdfFormat.d.ts +23 -0
- package/dist/adapters/pdfFormat.js +204 -0
- package/dist/adapters/portableDocxAtoms.d.ts +4 -0
- package/dist/adapters/portableDocxAtoms.js +58 -0
- package/dist/adapters/styledDocxFormat.d.ts +8 -0
- package/dist/adapters/styledDocxFormat.js +190 -0
- package/dist/builtInFormatDefinitions.d.ts +5 -0
- package/dist/builtInFormatDefinitions.js +82 -0
- package/dist/canonicalAuthorityFlag.d.ts +32 -0
- package/dist/canonicalAuthorityFlag.js +49 -0
- package/dist/canonicalClipboardRuntime.d.ts +15 -0
- package/dist/canonicalClipboardRuntime.js +76 -0
- package/dist/canonicalEditorRuntime.d.ts +77 -0
- package/dist/canonicalEditorRuntime.js +274 -0
- package/dist/components/CanonicalAuthorityEditor.d.ts +25 -0
- package/dist/components/CanonicalAuthorityEditor.js +518 -0
- package/dist/components/ClassicEditor.d.ts +13 -3
- package/dist/components/ClassicEditor.js +1309 -1159
- package/dist/components/ClassicEditorAuthority.d.ts +38 -0
- package/dist/components/ClassicEditorAuthority.js +49 -0
- package/dist/components/MediaPicker.d.ts +10 -0
- package/dist/components/MediaPicker.js +16 -0
- package/dist/editorController.d.ts +78 -0
- package/dist/editorController.js +298 -0
- package/dist/formatFidelity.d.ts +14 -0
- package/dist/formatFidelity.js +107 -0
- package/dist/formatRuntime.d.ts +50 -0
- package/dist/formatRuntime.js +20 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +10 -0
- package/dist/mediaProvider.d.ts +39 -0
- package/dist/mediaProvider.js +1 -0
- package/dist/pluginRuntime.d.ts +68 -0
- package/dist/pluginRuntime.js +78 -0
- package/dist/standalone/editor.js +722 -495
- package/dist/test-harness/atomShadowComparator.d.ts +14 -0
- package/dist/test-harness/atomShadowComparator.js +63 -0
- package/dist/test-harness/blockShadowComparator.d.ts +30 -0
- package/dist/test-harness/blockShadowComparator.js +140 -0
- package/dist/test-harness/clipboardShadowComparator.d.ts +12 -0
- package/dist/test-harness/clipboardShadowComparator.js +46 -0
- package/dist/test-harness/inlineShadowComparator.d.ts +32 -0
- package/dist/test-harness/inlineShadowComparator.js +152 -0
- package/dist/test-harness/legacyAtomEngine.d.ts +10 -0
- package/dist/test-harness/legacyAtomEngine.js +15 -0
- package/dist/test-harness/legacyBlockEngine.d.ts +31 -0
- package/dist/test-harness/legacyBlockEngine.js +31 -0
- package/dist/test-harness/legacyClipboardEngine.d.ts +14 -0
- package/dist/test-harness/legacyClipboardEngine.js +67 -0
- package/dist/test-harness/legacyInlineEngine.d.ts +19 -0
- package/dist/test-harness/legacyInlineEngine.js +49 -0
- package/dist/test-harness/legacyTableEngine.d.ts +12 -0
- package/dist/test-harness/legacyTableEngine.js +25 -0
- package/dist/test-harness/tableShadowComparator.d.ts +29 -0
- package/dist/test-harness/tableShadowComparator.js +101 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +28 -6
- package/package.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { createSmartEditor, tablePlugin, } from "smartrte-core";
|
|
2
|
+
import { serializeSmartDocument, smartDocumentFromHtml } from "./domSmartDocument.js";
|
|
3
|
+
const cellsOf = (row) => Array.from(row.children).filter((node) => node instanceof HTMLTableCellElement);
|
|
4
|
+
const cellGrid = (table) => {
|
|
5
|
+
const rows = Array.from(table.rows);
|
|
6
|
+
const grid = [];
|
|
7
|
+
rows.forEach((row, rowIndex) => {
|
|
8
|
+
grid[rowIndex] || (grid[rowIndex] = []);
|
|
9
|
+
let column = 0;
|
|
10
|
+
cellsOf(row).forEach((cell) => {
|
|
11
|
+
var _a;
|
|
12
|
+
while (grid[rowIndex][column])
|
|
13
|
+
column += 1;
|
|
14
|
+
const rowspan = Math.max(1, cell.rowSpan || 1);
|
|
15
|
+
const colspan = Math.max(1, cell.colSpan || 1);
|
|
16
|
+
for (let rowOffset = 0; rowOffset < rowspan; rowOffset += 1) {
|
|
17
|
+
grid[_a = rowIndex + rowOffset] || (grid[_a] = []);
|
|
18
|
+
for (let columnOffset = 0; columnOffset < colspan; columnOffset += 1) {
|
|
19
|
+
grid[rowIndex + rowOffset][column + columnOffset] = cell;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
column += colspan;
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
return grid;
|
|
26
|
+
};
|
|
27
|
+
const copyVisualAttributes = (from, to) => {
|
|
28
|
+
const style = from.getAttribute("style");
|
|
29
|
+
const className = from.getAttribute("class");
|
|
30
|
+
if (style)
|
|
31
|
+
to.setAttribute("style", style);
|
|
32
|
+
if (className)
|
|
33
|
+
to.setAttribute("class", className);
|
|
34
|
+
};
|
|
35
|
+
const reconcileHeaderStyle = (from, to) => {
|
|
36
|
+
if (from.tagName === to.tagName)
|
|
37
|
+
return;
|
|
38
|
+
if (to.tagName === "TH") {
|
|
39
|
+
to.style.fontWeight = "700";
|
|
40
|
+
to.style.background = to.style.background || "#f3f4f6";
|
|
41
|
+
to.style.textAlign = to.style.textAlign || "left";
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (to.style.fontWeight === "700" || to.style.fontWeight === "bold")
|
|
45
|
+
to.style.fontWeight = "";
|
|
46
|
+
if (to.style.background === "rgb(243, 244, 246)" || to.style.background === "#f3f4f6") {
|
|
47
|
+
to.style.background = "";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Executes a core table command while replacing only the affected table.
|
|
52
|
+
* Visual table/cell attributes are transferred by logical grid position.
|
|
53
|
+
*/
|
|
54
|
+
export const executeDomTableCommand = (table, command) => {
|
|
55
|
+
const document = smartDocumentFromHtml(table.outerHTML, table.ownerDocument);
|
|
56
|
+
if (document.children.length !== 1 || document.children[0].type !== "table")
|
|
57
|
+
return null;
|
|
58
|
+
const editor = createSmartEditor({
|
|
59
|
+
state: { document, selection: { type: "node", path: [0] } },
|
|
60
|
+
plugins: [tablePlugin],
|
|
61
|
+
});
|
|
62
|
+
if (!editor.execute(command.id, { ...command.input, tablePath: [0] }))
|
|
63
|
+
return null;
|
|
64
|
+
if (command.id === "table.column.width.set") {
|
|
65
|
+
let group = table.querySelector(":scope > colgroup");
|
|
66
|
+
if (!group) {
|
|
67
|
+
group = table.ownerDocument.createElement("colgroup");
|
|
68
|
+
table.insertBefore(group, table.firstChild);
|
|
69
|
+
}
|
|
70
|
+
const logicalWidth = Math.max(...cellGrid(table).map((row) => row.length));
|
|
71
|
+
while (group.children.length < logicalWidth) {
|
|
72
|
+
group.appendChild(table.ownerDocument.createElement("col"));
|
|
73
|
+
}
|
|
74
|
+
const column = group.children[command.input.index];
|
|
75
|
+
if (!column)
|
|
76
|
+
return null;
|
|
77
|
+
column.style.width = `${command.input.widthPx}px`;
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
cellGrid(table).forEach((row) => {
|
|
80
|
+
const target = row[command.input.index];
|
|
81
|
+
if (!target || seen.has(target))
|
|
82
|
+
return;
|
|
83
|
+
seen.add(target);
|
|
84
|
+
target.style.width = `${command.input.widthPx}px`;
|
|
85
|
+
target.style.minWidth = `${command.input.widthPx}px`;
|
|
86
|
+
target.style.maxWidth = `${command.input.widthPx}px`;
|
|
87
|
+
});
|
|
88
|
+
return table;
|
|
89
|
+
}
|
|
90
|
+
if (command.id === "table.row.height.set") {
|
|
91
|
+
const row = table.rows[command.input.index];
|
|
92
|
+
if (!row)
|
|
93
|
+
return null;
|
|
94
|
+
row.style.height = `${command.input.heightPx}px`;
|
|
95
|
+
cellsOf(row).forEach((target) => {
|
|
96
|
+
target.style.height = `${command.input.heightPx}px`;
|
|
97
|
+
});
|
|
98
|
+
return table;
|
|
99
|
+
}
|
|
100
|
+
if (command.id === "table.cell.style.set") {
|
|
101
|
+
const end = command.input.end || command.input.start;
|
|
102
|
+
const top = Math.min(command.input.start.row, end.row);
|
|
103
|
+
const bottom = Math.max(command.input.start.row, end.row);
|
|
104
|
+
const left = Math.min(command.input.start.column, end.column);
|
|
105
|
+
const right = Math.max(command.input.start.column, end.column);
|
|
106
|
+
const grid = cellGrid(table);
|
|
107
|
+
const targets = new Set();
|
|
108
|
+
for (let row = top; row <= bottom; row += 1) {
|
|
109
|
+
for (let column = left; column <= right; column += 1) {
|
|
110
|
+
const target = grid[row]?.[column];
|
|
111
|
+
if (target)
|
|
112
|
+
targets.add(target);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
targets.forEach((target) => {
|
|
116
|
+
if (command.input.backgroundColor !== undefined) {
|
|
117
|
+
target.style.backgroundColor = command.input.backgroundColor || "";
|
|
118
|
+
}
|
|
119
|
+
if (command.input.textColor !== undefined) {
|
|
120
|
+
target.style.color = command.input.textColor || "";
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return table;
|
|
124
|
+
}
|
|
125
|
+
if (command.id === "table.cell.border.toggle") {
|
|
126
|
+
const end = command.input.end || command.input.start;
|
|
127
|
+
const top = Math.min(command.input.start.row, end.row);
|
|
128
|
+
const bottom = Math.max(command.input.start.row, end.row);
|
|
129
|
+
const left = Math.min(command.input.start.column, end.column);
|
|
130
|
+
const right = Math.max(command.input.start.column, end.column);
|
|
131
|
+
const grid = cellGrid(table);
|
|
132
|
+
const targets = new Set();
|
|
133
|
+
for (let row = top; row <= bottom; row += 1) {
|
|
134
|
+
for (let column = left; column <= right; column += 1) {
|
|
135
|
+
const target = grid[row]?.[column];
|
|
136
|
+
if (target)
|
|
137
|
+
targets.add(target);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
targets.forEach((target) => {
|
|
141
|
+
target.style.border = target.style.border && target.style.border !== "none"
|
|
142
|
+
? "none"
|
|
143
|
+
: command.input.visibleBorder || "1px solid #d1d5db";
|
|
144
|
+
});
|
|
145
|
+
return table;
|
|
146
|
+
}
|
|
147
|
+
const container = table.ownerDocument.createElement("div");
|
|
148
|
+
container.innerHTML = serializeSmartDocument(editor.state.document);
|
|
149
|
+
const replacement = container.querySelector("table");
|
|
150
|
+
if (!(replacement instanceof HTMLTableElement))
|
|
151
|
+
return null;
|
|
152
|
+
copyVisualAttributes(table, replacement);
|
|
153
|
+
const previousGrid = cellGrid(table);
|
|
154
|
+
const nextGrid = cellGrid(replacement);
|
|
155
|
+
const previousPosition = (row, column) => {
|
|
156
|
+
if (command.id === "table.row.add") {
|
|
157
|
+
if (row === command.input.index)
|
|
158
|
+
return null;
|
|
159
|
+
return { row: row > command.input.index ? row - 1 : row, column };
|
|
160
|
+
}
|
|
161
|
+
if (command.id === "table.row.remove") {
|
|
162
|
+
return { row: row >= command.input.index ? row + 1 : row, column };
|
|
163
|
+
}
|
|
164
|
+
if (command.id === "table.column.add") {
|
|
165
|
+
if (column === command.input.index)
|
|
166
|
+
return null;
|
|
167
|
+
return { row, column: column > command.input.index ? column - 1 : column };
|
|
168
|
+
}
|
|
169
|
+
if (command.id === "table.column.remove") {
|
|
170
|
+
return { row, column: column >= command.input.index ? column + 1 : column };
|
|
171
|
+
}
|
|
172
|
+
return { row, column };
|
|
173
|
+
};
|
|
174
|
+
const copied = new Set();
|
|
175
|
+
nextGrid.forEach((row, rowIndex) => row.forEach((nextCell, column) => {
|
|
176
|
+
if (!nextCell || copied.has(nextCell))
|
|
177
|
+
return;
|
|
178
|
+
const position = previousPosition(rowIndex, column);
|
|
179
|
+
const previousCell = position ? previousGrid[position.row]?.[position.column] : undefined;
|
|
180
|
+
if (previousCell) {
|
|
181
|
+
copyVisualAttributes(previousCell, nextCell);
|
|
182
|
+
reconcileHeaderStyle(previousCell, nextCell);
|
|
183
|
+
}
|
|
184
|
+
copied.add(nextCell);
|
|
185
|
+
}));
|
|
186
|
+
table.replaceWith(replacement);
|
|
187
|
+
return replacement;
|
|
188
|
+
};
|
|
189
|
+
export const executeDomTableRemoval = (table) => {
|
|
190
|
+
const document = smartDocumentFromHtml(table.outerHTML, table.ownerDocument);
|
|
191
|
+
if (document.children.length !== 1 || document.children[0].type !== "table")
|
|
192
|
+
return false;
|
|
193
|
+
const editor = createSmartEditor({
|
|
194
|
+
state: { document, selection: { type: "node", path: [0] } },
|
|
195
|
+
plugins: [tablePlugin],
|
|
196
|
+
});
|
|
197
|
+
if (!editor.execute("table.remove", { tablePath: [0] }))
|
|
198
|
+
return false;
|
|
199
|
+
table.remove();
|
|
200
|
+
return true;
|
|
201
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { shadowLogRecord } from "smartrte-core";
|
|
2
|
+
export interface NamedListIntentReplay {
|
|
3
|
+
readonly intent: string;
|
|
4
|
+
readonly equivalent: boolean;
|
|
5
|
+
readonly selectionCompared: boolean;
|
|
6
|
+
readonly classification?: string;
|
|
7
|
+
readonly hash: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Replays the named list intents against the retained command snapshots and
|
|
11
|
+
* the canonical commands. The retained selection can be a node selection
|
|
12
|
+
* after list toggles; it is intentionally converted to the first text owner
|
|
13
|
+
* so both engines are compared at the same semantic editing point rather than
|
|
14
|
+
* comparing legacy path identity or operation streams.
|
|
15
|
+
*/
|
|
16
|
+
export declare const runNamedListIntent: (intent: string) => NamedListIntentReplay;
|
|
17
|
+
export declare const runNamedListIntentCorpus: () => NamedListIntentReplay[];
|
|
18
|
+
export interface DualEngineShadowSummary {
|
|
19
|
+
readonly seed: number;
|
|
20
|
+
readonly scenarios: number;
|
|
21
|
+
readonly equivalent: number;
|
|
22
|
+
readonly divergences: Readonly<Record<string, number>>;
|
|
23
|
+
readonly logs: readonly ReturnType<typeof shadowLogRecord>[];
|
|
24
|
+
}
|
|
25
|
+
/** Executes both the legacy model commands and canonical pure commands. */
|
|
26
|
+
export declare const runDualEngineListShadowCorpus: (scenarios?: number, seed?: number) => DualEngineShadowSummary;
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { applyOperations, compareShadowDocuments, createTransactionMap, createList, createScopeIndex, foundationSchema, indentList, moveListItems, outdentList, setListChecked, setListPreset, setListStyle, shadowLogRecord, unwrapList, isTextNode, } from "smartrte-core";
|
|
2
|
+
import { applyTransaction as applyLegacyTransaction, indentListItems as legacyIndentListItems, moveBlocks as legacyMoveBlocks, outdentListItems as legacyOutdentListItems, setChecklistItemChecked as legacySetChecklistItemChecked, toggleList as legacyToggleList, } from "smartrte-core/legacy";
|
|
3
|
+
const p = (id, text) => ({ type: "paragraph", id, children: [{ type: "text", text }] });
|
|
4
|
+
const item = (id, text, nested) => ({ type: "list_item", id, children: [p(`${id}-p`, text), ...(nested ? [nested] : [])] });
|
|
5
|
+
const list = (id, children, style = "decimal") => ({ type: "list", id, attrs: { style }, children });
|
|
6
|
+
const scope = (listId, ids, depth = 0) => ({
|
|
7
|
+
kind: "list-selection", listId, items: ids.map((itemId) => ({ itemId, depth, hasChildList: false })),
|
|
8
|
+
partialSubtree: false, promotedFromPartial: false,
|
|
9
|
+
range: { from: { path: [], offset: 0 }, to: { path: [], offset: 0 } }, isolatingAncestorId: null, clamped: false,
|
|
10
|
+
});
|
|
11
|
+
const ctx = (document) => ({ schema: foundationSchema, positions: createScopeIndex().positions(document, foundationSchema) });
|
|
12
|
+
const inlineToLegacy = (node) => isTextNode(node)
|
|
13
|
+
? { type: "text", text: node.text }
|
|
14
|
+
: { type: "formula", value: String(node.attrs?.originalType || node.type) };
|
|
15
|
+
const blockToLegacy = (node) => {
|
|
16
|
+
if (node.type === "paragraph")
|
|
17
|
+
return { type: "paragraph", children: (node.children || []).map(inlineToLegacy) };
|
|
18
|
+
if (node.type === "heading")
|
|
19
|
+
return { type: "heading", level: Number(node.attrs?.level || 1), children: (node.children || []).map(inlineToLegacy) };
|
|
20
|
+
if (node.type === "list")
|
|
21
|
+
return {
|
|
22
|
+
type: "list", style: String(node.attrs?.style || "disc"),
|
|
23
|
+
...(typeof node.attrs?.preset === "string" ? { preset: node.attrs.preset } : {}),
|
|
24
|
+
...(node.attrs?.checkable === true ? { checklist: true } : {}),
|
|
25
|
+
children: (node.children || []).filter((child) => !isTextNode(child)).map((child) => ({
|
|
26
|
+
type: "listItem", ...(child.attrs?.checked !== undefined ? { checked: child.attrs.checked === true } : {}),
|
|
27
|
+
children: (child.children || []).filter((entry) => !isTextNode(entry)).map(blockToLegacy),
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
return { type: "paragraph", children: [{ type: "text", text: "" }] };
|
|
31
|
+
};
|
|
32
|
+
const toLegacy = (document) => ({
|
|
33
|
+
type: "doc", children: document.children.filter((node) => !isTextNode(node)).map(blockToLegacy),
|
|
34
|
+
});
|
|
35
|
+
let canonicalId = 0;
|
|
36
|
+
const inlineFromLegacy = (node) => node.type === "text"
|
|
37
|
+
? { type: "text", text: node.text }
|
|
38
|
+
: { type: "unknown", id: `legacy-atom-${canonicalId++}`, attrs: { originalType: node.type, originalGroup: "inline", raw: node, editable: false } };
|
|
39
|
+
const blockFromLegacy = (node) => {
|
|
40
|
+
const id = `legacy-${canonicalId++}`;
|
|
41
|
+
if (node.type === "paragraph")
|
|
42
|
+
return { type: "paragraph", id, children: node.children.map(inlineFromLegacy) };
|
|
43
|
+
if (node.type === "heading")
|
|
44
|
+
return { type: "heading", id, attrs: { level: node.level }, children: node.children.map(inlineFromLegacy) };
|
|
45
|
+
if (node.type === "list")
|
|
46
|
+
return {
|
|
47
|
+
type: "list", id, attrs: {
|
|
48
|
+
style: node.style,
|
|
49
|
+
...(node.preset ? { preset: node.preset } : {}),
|
|
50
|
+
...(node.checklist ? { checkable: true } : {}),
|
|
51
|
+
}, children: node.children.map((entry) => ({
|
|
52
|
+
type: "list_item", id: `legacy-${canonicalId++}`,
|
|
53
|
+
...(entry.checked !== undefined ? { attrs: { checked: entry.checked } } : {}),
|
|
54
|
+
children: entry.children.map(blockFromLegacy),
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
return { type: "paragraph", id, children: [{ type: "text", text: "" }] };
|
|
58
|
+
};
|
|
59
|
+
const fromLegacy = (document) => {
|
|
60
|
+
canonicalId = 0;
|
|
61
|
+
return { type: "doc", id: "legacy-doc", children: document.children.map(blockFromLegacy) };
|
|
62
|
+
};
|
|
63
|
+
const firstOwner = (document) => {
|
|
64
|
+
const visit = (nodes, path) => {
|
|
65
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
66
|
+
const node = nodes[index];
|
|
67
|
+
if (isTextNode(node))
|
|
68
|
+
continue;
|
|
69
|
+
const next = [...path, index];
|
|
70
|
+
if (node.type === "paragraph" || node.type === "heading")
|
|
71
|
+
return next;
|
|
72
|
+
const found = visit(node.children || [], next);
|
|
73
|
+
if (found)
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
return visit(document.children, []) || [];
|
|
79
|
+
};
|
|
80
|
+
const firstOwnerUnder = (document, path) => {
|
|
81
|
+
let node = document;
|
|
82
|
+
for (const index of path) {
|
|
83
|
+
if (isTextNode(node) || !node.children)
|
|
84
|
+
break;
|
|
85
|
+
node = node.children[index];
|
|
86
|
+
}
|
|
87
|
+
if (!node || isTextNode(node))
|
|
88
|
+
return firstOwner(document);
|
|
89
|
+
const visit = (candidate, candidatePath) => {
|
|
90
|
+
if (!isTextNode(candidate) && (candidate.type === "paragraph" || candidate.type === "heading"))
|
|
91
|
+
return candidatePath;
|
|
92
|
+
if (isTextNode(candidate))
|
|
93
|
+
return null;
|
|
94
|
+
for (let index = 0; index < (candidate.children || []).length; index += 1) {
|
|
95
|
+
const found = visit(candidate.children[index], [...candidatePath, index]);
|
|
96
|
+
if (found)
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
101
|
+
return visit(node, [...path]) || firstOwner(document);
|
|
102
|
+
};
|
|
103
|
+
const legacyPointToFoundation = (document, point) => {
|
|
104
|
+
const path = point.path.length ? [...point.path.slice(0, -1)] : firstOwner(document);
|
|
105
|
+
return { path, offset: point.offset };
|
|
106
|
+
};
|
|
107
|
+
const legacySelectionToFoundation = (document, selection) => {
|
|
108
|
+
if (selection.type === "text") {
|
|
109
|
+
const anchor = legacyPointToFoundation(document, selection.anchor);
|
|
110
|
+
const head = legacyPointToFoundation(document, selection.focus);
|
|
111
|
+
return { type: "text", anchor, head };
|
|
112
|
+
}
|
|
113
|
+
if (selection.type === "node") {
|
|
114
|
+
const owner = firstOwnerUnder(document, selection.path);
|
|
115
|
+
return { type: "text", anchor: { path: owner, offset: 0 }, head: { path: owner, offset: 0 } };
|
|
116
|
+
}
|
|
117
|
+
const owner = firstOwner(document);
|
|
118
|
+
return { type: "text", anchor: { path: owner, offset: 0 }, head: { path: owner, offset: 0 } };
|
|
119
|
+
};
|
|
120
|
+
const replayHash = (value) => {
|
|
121
|
+
const input = JSON.stringify(value) ?? String(value);
|
|
122
|
+
let state = 2166136261;
|
|
123
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
124
|
+
state ^= input.charCodeAt(index);
|
|
125
|
+
state = Math.imul(state, 16777619);
|
|
126
|
+
}
|
|
127
|
+
return (state >>> 0).toString(16).padStart(8, "0");
|
|
128
|
+
};
|
|
129
|
+
const listScope = (listId, itemIds, depth = 0) => scope(listId, itemIds, depth);
|
|
130
|
+
/**
|
|
131
|
+
* Replays the named list intents against the retained command snapshots and
|
|
132
|
+
* the canonical commands. The retained selection can be a node selection
|
|
133
|
+
* after list toggles; it is intentionally converted to the first text owner
|
|
134
|
+
* so both engines are compared at the same semantic editing point rather than
|
|
135
|
+
* comparing legacy path identity or operation streams.
|
|
136
|
+
*/
|
|
137
|
+
export const runNamedListIntent = (intent) => {
|
|
138
|
+
const suffix = intent.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
|
|
139
|
+
let canonical;
|
|
140
|
+
let legacy;
|
|
141
|
+
let legacySelection;
|
|
142
|
+
let canonicalOperations = [];
|
|
143
|
+
const compare = (initialSelection, transaction) => {
|
|
144
|
+
const nextLegacy = applyLegacyTransaction({ document: legacy, selection: legacySelection }, transaction);
|
|
145
|
+
const legacyDocument = fromLegacy(nextLegacy.document);
|
|
146
|
+
const legacySemanticSelection = legacySelectionToFoundation(legacyDocument, nextLegacy.selection);
|
|
147
|
+
const canonicalSemanticSelection = createTransactionMap(canonicalOperations).mapSelection(initialSelection);
|
|
148
|
+
const result = compareShadowDocuments({
|
|
149
|
+
legacyDocument,
|
|
150
|
+
legacySelection: legacySemanticSelection,
|
|
151
|
+
canonicalDocument: canonical,
|
|
152
|
+
canonicalSelection: canonicalSemanticSelection,
|
|
153
|
+
schema: foundationSchema,
|
|
154
|
+
});
|
|
155
|
+
const classification = !result.documentEquivalent && intent === "list.setPreset"
|
|
156
|
+
? "expected-normalization"
|
|
157
|
+
: result.classification;
|
|
158
|
+
return {
|
|
159
|
+
intent,
|
|
160
|
+
equivalent: result.equivalent,
|
|
161
|
+
selectionCompared: true,
|
|
162
|
+
...(classification ? { classification } : {}),
|
|
163
|
+
hash: replayHash([result.legacyStructureHash, result.canonicalStructureHash, result.legacySelectionHash, result.canonicalSelectionHash]),
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
if (intent === "list.create" || intent === "list.create.numbered") {
|
|
167
|
+
canonical = { type: "doc", id: `doc-${suffix}`, children: [p(`a-${suffix}`, "a"), p(`b-${suffix}`, "b")] };
|
|
168
|
+
legacy = toLegacy(canonical);
|
|
169
|
+
legacySelection = { type: "text", anchor: { path: [0, 0], offset: 0 }, focus: { path: [1, 0], offset: 0 } };
|
|
170
|
+
const initialSelection = { type: "text", anchor: { path: [0], offset: 0 }, head: { path: [1], offset: 0 } };
|
|
171
|
+
canonicalOperations = createList(canonical, {
|
|
172
|
+
kind: "block-range", blockIds: [`a-${suffix}`, `b-${suffix}`], promotedFromPartial: true,
|
|
173
|
+
commonParentId: canonical.id, range: { from: { path: [], offset: 0 }, to: { path: [], offset: 2 } }, isolatingAncestorId: null, clamped: false,
|
|
174
|
+
}, { listIds: [`list-${suffix}`], itemIds: [`ia-${suffix}`, `ib-${suffix}`], style: intent.endsWith("numbered") ? "decimal" : "disc" }, ctx(canonical));
|
|
175
|
+
canonical = applyOperations(canonical, canonicalOperations);
|
|
176
|
+
const legacyTransaction = legacyToggleList.execute({ document: legacy, selection: legacySelection }, { style: intent.endsWith("numbered") ? "decimal" : "disc" });
|
|
177
|
+
return compare(initialSelection, legacyTransaction);
|
|
178
|
+
}
|
|
179
|
+
const baseList = (attrs = {}, includeNested = false) => ({
|
|
180
|
+
type: "doc", id: `doc-${suffix}`, children: [list(`list-${suffix}`, [
|
|
181
|
+
item(`a-${suffix}`, "a", includeNested ? list(`nested-${suffix}`, [item(`nested-item-${suffix}`, "nested")]) : undefined),
|
|
182
|
+
item(`b-${suffix}`, "b"), item(`c-${suffix}`, "c"),
|
|
183
|
+
], String(attrs.style || "disc"))],
|
|
184
|
+
});
|
|
185
|
+
if (intent === "list.setPreset") {
|
|
186
|
+
canonical = baseList({ style: "disc" });
|
|
187
|
+
legacy = toLegacy(canonical);
|
|
188
|
+
legacySelection = { type: "text", anchor: { path: [0, 1, 0, 0], offset: 0 }, focus: { path: [0, 1, 0, 0], offset: 0 } };
|
|
189
|
+
const initialSelection = { type: "text", anchor: { path: [0, 1, 0], offset: 0 }, head: { path: [0, 1, 0], offset: 0 } };
|
|
190
|
+
canonicalOperations = setListPreset(canonical, listScope(`list-${suffix}`, [`b-${suffix}`]), { preset: "bullet-disc" }, ctx(canonical));
|
|
191
|
+
canonical = applyOperations(canonical, canonicalOperations);
|
|
192
|
+
return compare(initialSelection, legacyToggleList.execute({ document: legacy, selection: legacySelection }, { style: "disc", preset: "bullet-disc" }));
|
|
193
|
+
}
|
|
194
|
+
if (intent === "list.setStyle") {
|
|
195
|
+
canonical = baseList({ style: "disc" });
|
|
196
|
+
legacy = toLegacy(canonical);
|
|
197
|
+
legacySelection = { type: "text", anchor: { path: [0, 1, 0, 0], offset: 0 }, focus: { path: [0, 1, 0, 0], offset: 0 } };
|
|
198
|
+
const initialSelection = { type: "text", anchor: { path: [0, 1, 0], offset: 0 }, head: { path: [0, 1, 0], offset: 0 } };
|
|
199
|
+
canonicalOperations = setListStyle(canonical, listScope(`list-${suffix}`, [`b-${suffix}`]), { style: "upper-roman" }, ctx(canonical));
|
|
200
|
+
canonical = applyOperations(canonical, canonicalOperations);
|
|
201
|
+
return compare(initialSelection, legacyToggleList.execute({ document: legacy, selection: legacySelection }, { style: "upper-roman" }));
|
|
202
|
+
}
|
|
203
|
+
if (intent === "list.indent") {
|
|
204
|
+
canonical = baseList();
|
|
205
|
+
legacy = toLegacy(canonical);
|
|
206
|
+
legacySelection = { type: "text", anchor: { path: [0, 1, 0, 0], offset: 0 }, focus: { path: [0, 1, 0, 0], offset: 0 } };
|
|
207
|
+
const initialSelection = { type: "text", anchor: { path: [0, 1, 0], offset: 0 }, head: { path: [0, 1, 0], offset: 0 } };
|
|
208
|
+
canonicalOperations = indentList(canonical, listScope(`list-${suffix}`, [`b-${suffix}`]), { nestedListIds: [`nested-${suffix}`] }, ctx(canonical));
|
|
209
|
+
canonical = applyOperations(canonical, canonicalOperations);
|
|
210
|
+
return compare(initialSelection, legacyIndentListItems.execute({ document: legacy, selection: legacySelection }));
|
|
211
|
+
}
|
|
212
|
+
if (intent === "list.outdent") {
|
|
213
|
+
canonical = baseList({}, true);
|
|
214
|
+
legacy = toLegacy(canonical);
|
|
215
|
+
legacySelection = { type: "text", anchor: { path: [0, 0, 1, 0, 0, 0], offset: 0 }, focus: { path: [0, 0, 1, 0, 0, 0], offset: 0 } };
|
|
216
|
+
const initialSelection = { type: "text", anchor: { path: [0, 0, 1, 0, 0], offset: 0 }, head: { path: [0, 0, 1, 0, 0], offset: 0 } };
|
|
217
|
+
canonicalOperations = outdentList(canonical, listScope(`nested-${suffix}`, [`nested-item-${suffix}`], 1), {}, ctx(canonical));
|
|
218
|
+
canonical = applyOperations(canonical, canonicalOperations);
|
|
219
|
+
return compare(initialSelection, legacyOutdentListItems.execute({ document: legacy, selection: legacySelection }));
|
|
220
|
+
}
|
|
221
|
+
if (intent === "list.move" || intent === "list.move.reverse") {
|
|
222
|
+
canonical = baseList();
|
|
223
|
+
legacy = toLegacy(canonical);
|
|
224
|
+
legacySelection = { type: "text", anchor: { path: [0, 1, 0, 0], offset: 0 }, focus: { path: [0, 1, 0, 0], offset: 0 } };
|
|
225
|
+
const initialSelection = { type: "text", anchor: { path: [0, 1, 0], offset: 0 }, head: { path: [0, 1, 0], offset: 0 } };
|
|
226
|
+
const direction = intent === "list.move" ? "up" : "down";
|
|
227
|
+
canonicalOperations = moveListItems(canonical, listScope(`list-${suffix}`, [`b-${suffix}`]), { direction }, ctx(canonical));
|
|
228
|
+
canonical = applyOperations(canonical, canonicalOperations);
|
|
229
|
+
return compare(initialSelection, legacyMoveBlocks.execute({ document: legacy, selection: legacySelection }, { parentPath: [0], blockIndexes: [1], direction }));
|
|
230
|
+
}
|
|
231
|
+
if (intent === "list.setChecked") {
|
|
232
|
+
const checklistBase = baseList();
|
|
233
|
+
const checklistNode = checklistBase.children[0];
|
|
234
|
+
canonical = { ...checklistBase, children: [{ ...checklistNode, attrs: { ...(checklistNode.attrs || {}), checkable: true } }] };
|
|
235
|
+
legacy = toLegacy(canonical);
|
|
236
|
+
legacySelection = { type: "text", anchor: { path: [0, 1, 0, 0], offset: 0 }, focus: { path: [0, 1, 0, 0], offset: 0 } };
|
|
237
|
+
const initialSelection = { type: "text", anchor: { path: [0, 1, 0], offset: 0 }, head: { path: [0, 1, 0], offset: 0 } };
|
|
238
|
+
canonicalOperations = setListChecked(canonical, listScope(`list-${suffix}`, [`b-${suffix}`]), { checked: true }, ctx(canonical));
|
|
239
|
+
canonical = applyOperations(canonical, canonicalOperations);
|
|
240
|
+
return compare(initialSelection, legacySetChecklistItemChecked.execute({ document: legacy, selection: legacySelection }, { path: [0, 1], checked: true }));
|
|
241
|
+
}
|
|
242
|
+
if (intent === "list.unwrap") {
|
|
243
|
+
canonical = baseList({ style: "decimal" });
|
|
244
|
+
legacy = toLegacy(canonical);
|
|
245
|
+
legacySelection = { type: "text", anchor: { path: [0, 0, 0, 0], offset: 0 }, focus: { path: [0, 2, 0, 0], offset: 0 } };
|
|
246
|
+
const initialSelection = { type: "text", anchor: { path: [0, 0, 0], offset: 0 }, head: { path: [0, 2, 0], offset: 0 } };
|
|
247
|
+
canonicalOperations = unwrapList(canonical, listScope(`list-${suffix}`, [`a-${suffix}`, `b-${suffix}`, `c-${suffix}`]), {}, ctx(canonical));
|
|
248
|
+
canonical = applyOperations(canonical, canonicalOperations);
|
|
249
|
+
return compare(initialSelection, legacyToggleList.execute({ document: legacy, selection: legacySelection }, { style: "decimal" }));
|
|
250
|
+
}
|
|
251
|
+
throw new Error(`Unknown named list replay intent: ${intent}`);
|
|
252
|
+
};
|
|
253
|
+
export const runNamedListIntentCorpus = () => [
|
|
254
|
+
"list.create", "list.setPreset", "list.setStyle", "list.indent", "list.outdent", "list.move", "list.move.reverse", "list.create.numbered", "list.setChecked", "list.unwrap",
|
|
255
|
+
].map(runNamedListIntent);
|
|
256
|
+
const applyLegacy = (document, selection, transaction) => applyLegacyTransaction({ document, selection }, transaction).document;
|
|
257
|
+
/** Executes both the legacy model commands and canonical pure commands. */
|
|
258
|
+
export const runDualEngineListShadowCorpus = (scenarios = 1000, seed = 3500212992) => {
|
|
259
|
+
const logs = [];
|
|
260
|
+
const divergences = {};
|
|
261
|
+
let equivalent = 0;
|
|
262
|
+
for (let index = 0; index < scenarios; index += 1) {
|
|
263
|
+
const label = ((seed ^ Math.imul(index + 1, 1103515245)) >>> 0).toString(36);
|
|
264
|
+
const mode = index % 5;
|
|
265
|
+
const nested = list(`nested-${label}`, [item(`nested-item-${label}`, `nested-${label}`)]);
|
|
266
|
+
let canonical = mode === 2
|
|
267
|
+
? { type: "doc", id: `doc-${label}`, children: [p(`a-${label}`, `a-${label}`), p(`b-${label}`, `b-${label}`)] }
|
|
268
|
+
: mode === 1
|
|
269
|
+
? { type: "doc", id: `doc-${label}`, children: [list(`list-${label}`, [item(`a-${label}`, `a-${label}`, nested), item(`c-${label}`, `c-${label}`)])] }
|
|
270
|
+
: { type: "doc", id: `doc-${label}`, children: [list(`list-${label}`, [item(`a-${label}`, `a-${label}`), item(`b-${label}`, `b-${label}`), item(`c-${label}`, `c-${label}`)])] };
|
|
271
|
+
let legacy = toLegacy(canonical);
|
|
272
|
+
const beforeCanonical = canonical;
|
|
273
|
+
if (mode === 0) {
|
|
274
|
+
const legacySelection = { type: "text", anchor: { path: [0, 1, 0, 0], offset: 0 }, focus: { path: [0, 1, 0, 0], offset: 0 } };
|
|
275
|
+
legacy = applyLegacy(legacy, legacySelection, legacyIndentListItems.execute({ document: legacy, selection: legacySelection }));
|
|
276
|
+
canonical = applyOperations(canonical, indentList(canonical, scope(`list-${label}`, [`b-${label}`]), { nestedListIds: [`new-${label}`] }, ctx(canonical)));
|
|
277
|
+
}
|
|
278
|
+
else if (mode === 1) {
|
|
279
|
+
const legacySelection = { type: "text", anchor: { path: [0, 0, 1, 0, 0, 0], offset: 0 }, focus: { path: [0, 0, 1, 0, 0, 0], offset: 0 } };
|
|
280
|
+
legacy = applyLegacy(legacy, legacySelection, legacyOutdentListItems.execute({ document: legacy, selection: legacySelection }));
|
|
281
|
+
canonical = applyOperations(canonical, outdentList(canonical, scope(`nested-${label}`, [`nested-item-${label}`], 1), {}, ctx(canonical)));
|
|
282
|
+
}
|
|
283
|
+
else if (mode === 2) {
|
|
284
|
+
const legacySelection = { type: "text", anchor: { path: [0, 0], offset: 0 }, focus: { path: [1, 0], offset: 0 } };
|
|
285
|
+
legacy = applyLegacy(legacy, legacySelection, legacyToggleList.execute({ document: legacy, selection: legacySelection }, { style: "disc" }));
|
|
286
|
+
canonical = applyOperations(canonical, createList(canonical, {
|
|
287
|
+
kind: "block-range", blockIds: [`a-${label}`, `b-${label}`], promotedFromPartial: true, commonParentId: canonical.id,
|
|
288
|
+
range: { from: { path: [], offset: 0 }, to: { path: [], offset: 2 } }, isolatingAncestorId: null, clamped: false,
|
|
289
|
+
}, { listIds: [`list-${label}`], itemIds: [`ia-${label}`, `ib-${label}`], style: "disc" }, ctx(canonical)));
|
|
290
|
+
}
|
|
291
|
+
else if (mode === 3) {
|
|
292
|
+
const legacySelection = { type: "text", anchor: { path: [0, 0, 0, 0], offset: 0 }, focus: { path: [0, 2, 0, 0], offset: 0 } };
|
|
293
|
+
legacy = applyLegacy(legacy, legacySelection, legacyToggleList.execute({ document: legacy, selection: legacySelection }, { style: "decimal" }));
|
|
294
|
+
canonical = applyOperations(canonical, unwrapList(canonical, scope(`list-${label}`, [`a-${label}`, `b-${label}`, `c-${label}`]), {}, ctx(canonical)));
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
const legacySelection = { type: "text", anchor: { path: [0, 1, 0, 0], offset: 0 }, focus: { path: [0, 1, 0, 0], offset: 0 } };
|
|
298
|
+
legacy = applyLegacy(legacy, legacySelection, legacyToggleList.execute({ document: legacy, selection: legacySelection }, { style: "upper-roman" }));
|
|
299
|
+
canonical = applyOperations(canonical, setListStyle(canonical, scope(`list-${label}`, [`b-${label}`]), { style: "upper-roman" }, ctx(canonical)));
|
|
300
|
+
}
|
|
301
|
+
const legacyCanonical = fromLegacy(legacy);
|
|
302
|
+
const first = { path: firstOwner(canonical), offset: 0 };
|
|
303
|
+
const result = compareShadowDocuments({
|
|
304
|
+
legacyDocument: legacyCanonical, legacySelection: { type: "text", anchor: first, head: first },
|
|
305
|
+
canonicalDocument: canonical, canonicalSelection: { type: "text", anchor: first, head: first }, schema: foundationSchema,
|
|
306
|
+
classification: "semantic",
|
|
307
|
+
});
|
|
308
|
+
if (result.equivalent)
|
|
309
|
+
equivalent += 1;
|
|
310
|
+
else
|
|
311
|
+
divergences[result.classification || "unknown"] = (divergences[result.classification || "unknown"] || 0) + 1;
|
|
312
|
+
logs.push(shadowLogRecord(`dual-${mode}-${index}`, result));
|
|
313
|
+
void beforeCanonical;
|
|
314
|
+
}
|
|
315
|
+
return { seed, scenarios, equivalent, divergences, logs };
|
|
316
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SmartDocument } from "smartrte-core";
|
|
2
|
+
export declare const PDF_MEDIA_TYPE = "application/pdf";
|
|
3
|
+
export interface PdfTextItemSnapshot {
|
|
4
|
+
text: string;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
fontFamily?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PdfPageSnapshot {
|
|
12
|
+
width: number;
|
|
13
|
+
items: readonly PdfTextItemSnapshot[];
|
|
14
|
+
}
|
|
15
|
+
export interface PdfImportResult {
|
|
16
|
+
document: SmartDocument;
|
|
17
|
+
layoutHtml: string;
|
|
18
|
+
pages: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const reconstructPdfPages: (pages: readonly PdfPageSnapshot[], ownerDocument: Document) => PdfImportResult;
|
|
21
|
+
export declare const importPdfDocument: (arrayBuffer: ArrayBuffer, ownerDocument: Document) => Promise<PdfImportResult>;
|
|
22
|
+
export declare const buildPdfPrintDocument: (document: SmartDocument) => string;
|
|
23
|
+
export declare const printSmartDocumentAsPdf: (document: SmartDocument, hostWindow: Window, delayMs?: number) => boolean;
|