smartrte-react 0.3.4 → 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.
Files changed (97) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +23 -0
  3. package/dist/adapters/domChecklistCommandBridge.d.ts +6 -0
  4. package/dist/adapters/domChecklistCommandBridge.js +33 -0
  5. package/dist/adapters/domCommandBridge.d.ts +19 -0
  6. package/dist/adapters/domCommandBridge.js +33 -0
  7. package/dist/adapters/domInlineAtomCommandBridge.d.ts +15 -0
  8. package/dist/adapters/domInlineAtomCommandBridge.js +205 -0
  9. package/dist/adapters/domInlineImageCommandBridge.d.ts +14 -0
  10. package/dist/adapters/domInlineImageCommandBridge.js +6 -0
  11. package/dist/adapters/domMoveCommandBridge.d.ts +7 -0
  12. package/dist/adapters/domMoveCommandBridge.js +54 -0
  13. package/dist/adapters/domSelectionBridge.d.ts +5 -3
  14. package/dist/adapters/domSelectionBridge.js +96 -44
  15. package/dist/adapters/domSmartDocument.d.ts +4 -4
  16. package/dist/adapters/domSmartDocument.js +159 -17
  17. package/dist/adapters/domTableCommandBridge.d.ts +61 -0
  18. package/dist/adapters/domTableCommandBridge.js +231 -0
  19. package/dist/adapters/internalFlags.d.ts +0 -8
  20. package/dist/adapters/internalFlags.js +0 -24
  21. package/dist/adapters/legacyListShadowComparator.d.ts +26 -0
  22. package/dist/adapters/legacyListShadowComparator.js +316 -0
  23. package/dist/adapters/pdfPrint.d.ts +8 -0
  24. package/dist/adapters/pdfPrint.js +22 -0
  25. package/dist/adapters/shadowMode.d.ts +3 -3
  26. package/dist/adapters/shadowMode.js +1 -1
  27. package/dist/canonicalEditorRuntime.d.ts +129 -0
  28. package/dist/canonicalEditorRuntime.js +331 -0
  29. package/dist/capabilityPresets.d.ts +27 -0
  30. package/dist/capabilityPresets.js +21 -0
  31. package/dist/collabTransport.d.ts +66 -0
  32. package/dist/collabTransport.js +1 -0
  33. package/dist/commentProvider.d.ts +24 -0
  34. package/dist/commentProvider.js +1 -0
  35. package/dist/components/CanonicalAuthorityEditor.d.ts +59 -0
  36. package/dist/components/CanonicalAuthorityEditor.js +1492 -0
  37. package/dist/components/ClassicEditorAuthority.d.ts +109 -0
  38. package/dist/components/ClassicEditorAuthority.js +17 -0
  39. package/dist/components/ColorPickerPopover.d.ts +39 -0
  40. package/dist/components/ColorPickerPopover.js +285 -0
  41. package/dist/components/CommentMarkers.d.ts +25 -0
  42. package/dist/components/CommentMarkers.js +105 -0
  43. package/dist/components/CommentThreadPanel.d.ts +26 -0
  44. package/dist/components/CommentThreadPanel.js +47 -0
  45. package/dist/components/ContextMenu.d.ts +23 -0
  46. package/dist/components/ContextMenu.js +141 -0
  47. package/dist/components/FormulaLibraryPopover.d.ts +16 -0
  48. package/dist/components/FormulaLibraryPopover.js +107 -0
  49. package/dist/components/LinkEditorPopover.d.ts +3 -1
  50. package/dist/components/LinkEditorPopover.js +22 -6
  51. package/dist/components/MediaManager.d.ts +4 -20
  52. package/dist/components/MediaManager.js +35 -6
  53. package/dist/components/MediaOverlay.d.ts +36 -0
  54. package/dist/components/MediaOverlay.js +163 -0
  55. package/dist/components/MediaPicker.d.ts +10 -0
  56. package/dist/components/MediaPicker.js +16 -0
  57. package/dist/components/SpecialCharacterPopover.d.ts +18 -0
  58. package/dist/components/SpecialCharacterPopover.js +99 -0
  59. package/dist/components/StructuralSuggestionMarkers.d.ts +20 -0
  60. package/dist/components/StructuralSuggestionMarkers.js +56 -0
  61. package/dist/components/SuggestionPanel.d.ts +26 -0
  62. package/dist/components/SuggestionPanel.js +33 -0
  63. package/dist/components/TableBorderPopover.d.ts +59 -0
  64. package/dist/components/TableBorderPopover.js +147 -0
  65. package/dist/components/TableResizeHandles.d.ts +25 -0
  66. package/dist/components/TableResizeHandles.js +301 -0
  67. package/dist/components/TableSizePickerPopover.d.ts +16 -0
  68. package/dist/components/TableSizePickerPopover.js +126 -0
  69. package/dist/components/ToolbarPrimitives.d.ts +129 -0
  70. package/dist/components/ToolbarPrimitives.js +146 -0
  71. package/dist/components/VersionHistoryPanel.d.ts +9 -0
  72. package/dist/components/VersionHistoryPanel.js +157 -0
  73. package/dist/formulaLibrary.d.ts +31 -0
  74. package/dist/formulaLibrary.js +90 -0
  75. package/dist/index.d.ts +23 -1
  76. package/dist/index.js +14 -1
  77. package/dist/mediaManagerAdapter.d.ts +12 -0
  78. package/dist/mediaManagerAdapter.js +34 -0
  79. package/dist/mediaProvider.d.ts +59 -0
  80. package/dist/mediaProvider.js +1 -0
  81. package/dist/specialCharacters.d.ts +22 -0
  82. package/dist/specialCharacters.js +86 -0
  83. package/dist/standalone/classic-editor-embed.js +19 -12
  84. package/dist/suggestionProvider.d.ts +17 -0
  85. package/dist/suggestionProvider.js +1 -0
  86. package/dist/theme.d.ts +1 -1
  87. package/dist/theme.js +161 -1
  88. package/dist/versionProvider.d.ts +29 -0
  89. package/dist/versionProvider.js +1 -0
  90. package/package.json +28 -14
  91. package/dist/adapters/coreBold.d.ts +0 -5
  92. package/dist/adapters/coreBold.js +0 -3
  93. package/dist/adapters/inlineMarkCoreExecution.d.ts +0 -9
  94. package/dist/adapters/inlineMarkCoreExecution.js +0 -25
  95. package/dist/components/ClassicEditor.d.ts +0 -66
  96. package/dist/components/ClassicEditor.js +0 -6617
  97. package/dist/standalone/editor.js +0 -841
@@ -0,0 +1,231 @@
1
+ import { applyOperations, createNodeId, createScopeIndex, foundationSchema, insertTableColumnCommand, insertTableCommand, insertTableRowCommand, mergeTableCellsCommand, occupancyGridFor, parseCanonicalTableHtml, removeTableColumnCommand, removeTableRowCommand, removeTableCommand, repair, serializeCanonicalTableHtml, setTableCellAttributesCommand, setTableColumnWidthCommand, setTableHeaderCommand, setTableRowHeightCommand, splitTableCellCommand, } from "smartrte-core/foundation";
2
+ const canonicalTable = (element) => {
3
+ const repaired = repair(parseCanonicalTableHtml(element.outerHTML)).doc;
4
+ const table = repaired.children[0];
5
+ return table && table.type === "table" ? { document: repaired, table: table } : null;
6
+ };
7
+ const tableScope = (table, start, end = start) => {
8
+ const grid = occupancyGridFor(table);
9
+ const top = Math.max(0, Math.min(start.row, end.row));
10
+ const bottom = Math.min(grid.rows - 1, Math.max(start.row, end.row));
11
+ const left = Math.max(0, Math.min(start.column, end.column));
12
+ const right = Math.min(grid.columns - 1, Math.max(start.column, end.column));
13
+ const exclusive = { top, left, bottom: bottom + 1, right: right + 1 };
14
+ return {
15
+ kind: "table-grid", tableId: table.id, rect: { top, left, bottom, right },
16
+ cellIds: grid.anchors.filter((cell) => cell.top <= bottom && cell.bottom > top && cell.left <= right && cell.right > left).map((cell) => cell.cellId),
17
+ coveredCellIds: grid.coveredIn(exclusive).map((cell) => cell.cellId),
18
+ rectangular: grid.isRectangular(exclusive), range: { from: { path: [0], offset: 0 }, to: { path: [0], offset: 1 } },
19
+ isolatingAncestorId: null, clamped: false,
20
+ };
21
+ };
22
+ const commandContext = (document) => ({ schema: foundationSchema, positions: createScopeIndex().positions(document, foundationSchema) });
23
+ const generated = (count, prefix) => Array.from({ length: count }, () => `${prefix}-${createNodeId()}`);
24
+ const domGrid = (table) => {
25
+ const slots = [];
26
+ Array.from(table.rows).forEach((row, rowIndex) => {
27
+ slots[rowIndex] || (slots[rowIndex] = []);
28
+ let column = 0;
29
+ Array.from(row.cells).forEach((cell) => {
30
+ while (slots[rowIndex][column])
31
+ column += 1;
32
+ for (let r = rowIndex; r < rowIndex + Math.max(1, cell.rowSpan); r += 1) {
33
+ slots[r] || (slots[r] = []);
34
+ for (let c = column; c < column + Math.max(1, cell.colSpan); c += 1)
35
+ slots[r][c] = cell;
36
+ }
37
+ column += Math.max(1, cell.colSpan);
38
+ });
39
+ });
40
+ return slots;
41
+ };
42
+ const copyVisualAttributes = (from, to) => {
43
+ if (from.className)
44
+ to.className = from.className;
45
+ const retained = from.getAttribute("style");
46
+ if (retained)
47
+ for (const declaration of retained.split(";")) {
48
+ const separator = declaration.indexOf(":");
49
+ if (separator > 0)
50
+ to.style.setProperty(declaration.slice(0, separator).trim(), declaration.slice(separator + 1).trim());
51
+ }
52
+ };
53
+ const projectAttributesInPlace = (element, after) => {
54
+ const table = after.children[0];
55
+ const modelGrid = occupancyGridFor(table);
56
+ const grid = domGrid(element);
57
+ const widths = Array.isArray(table.attrs?.columnWidths) ? table.attrs.columnWidths : [];
58
+ if (widths.length) {
59
+ let group = element.querySelector(":scope > colgroup");
60
+ if (!group) {
61
+ group = element.ownerDocument.createElement("colgroup");
62
+ element.prepend(group);
63
+ }
64
+ while (group.children.length < widths.length)
65
+ group.appendChild(element.ownerDocument.createElement("col"));
66
+ while (group.children.length > widths.length)
67
+ group.lastElementChild?.remove();
68
+ widths.forEach((width, index) => { group.children[index].style.width = `${width}px`; });
69
+ }
70
+ (table.children || []).forEach((row, rowIndex) => {
71
+ if (row.type !== "table_row")
72
+ return;
73
+ const height = Number(row.attrs?.height);
74
+ if (Number.isFinite(height) && height > 0)
75
+ element.rows[rowIndex].style.height = `${height}px`;
76
+ });
77
+ modelGrid.anchors.forEach((cell) => {
78
+ const target = grid[cell.top]?.[cell.left];
79
+ if (!target)
80
+ return;
81
+ target.style.background = cell.node.attrs?.background ? String(cell.node.attrs.background) : "";
82
+ target.style.color = cell.node.attrs?.textColor ? String(cell.node.attrs.textColor) : "";
83
+ target.style.border = cell.node.attrs?.borders ? String(cell.node.attrs.borders) : "";
84
+ target.style.verticalAlign = cell.node.attrs?.verticalAlign ? String(cell.node.attrs.verticalAlign) : "";
85
+ if (widths.length) {
86
+ const width = widths.slice(cell.left, cell.right).reduce((sum, value) => sum + value, 0);
87
+ target.style.width = `${width}px`;
88
+ target.style.minWidth = `${width}px`;
89
+ target.style.maxWidth = `${width}px`;
90
+ }
91
+ const rowHeight = Number(table.children?.[cell.top]?.attrs?.height);
92
+ if (Number.isFinite(rowHeight) && rowHeight > 0)
93
+ target.style.height = `${rowHeight}px`;
94
+ });
95
+ return element;
96
+ };
97
+ const replaceDomTable = (table, document) => {
98
+ const container = table.ownerDocument.createElement("div");
99
+ container.innerHTML = serializeCanonicalTableHtml(document, { fragment: true });
100
+ const replacement = container.querySelector("table");
101
+ if (!(replacement instanceof HTMLTableElement))
102
+ return null;
103
+ copyVisualAttributes(table, replacement);
104
+ const beforeGrid = domGrid(table);
105
+ const afterGrid = domGrid(replacement);
106
+ const copied = new Set();
107
+ afterGrid.forEach((row, rowIndex) => row.forEach((cell, column) => {
108
+ if (!cell || copied.has(cell))
109
+ return;
110
+ const source = beforeGrid[rowIndex]?.[column];
111
+ if (source)
112
+ copyVisualAttributes(source, cell);
113
+ if (cell.tagName === "TH" && source?.tagName !== "TH") {
114
+ cell.style.fontWeight = "700";
115
+ cell.style.background = cell.style.background || "#f3f4f6";
116
+ cell.style.textAlign = cell.style.textAlign || "left";
117
+ }
118
+ else if (cell.tagName === "TD" && source?.tagName === "TH") {
119
+ if (["700", "bold"].includes(cell.style.fontWeight))
120
+ cell.style.fontWeight = "";
121
+ if (["rgb(243, 244, 246)", "#f3f4f6"].includes(cell.style.background))
122
+ cell.style.background = "";
123
+ if (cell.style.textAlign === "left")
124
+ cell.style.textAlign = "";
125
+ }
126
+ copied.add(cell);
127
+ }));
128
+ table.replaceWith(replacement);
129
+ return replacement;
130
+ };
131
+ export const executeDomTableCommand = (element, command) => {
132
+ if (command.id === "table.remove")
133
+ return null;
134
+ const parsed = canonicalTable(element);
135
+ if (!parsed)
136
+ return null;
137
+ const { document, table } = parsed;
138
+ const grid = occupancyGridFor(table);
139
+ const start = "start" in command.input ? command.input.start : "row" in command.input ? command.input : { row: "index" in command.input ? command.input.index : 0, column: 0 };
140
+ const end = "end" in command.input && command.input.end ? command.input.end : start;
141
+ const scope = tableScope(table, start, end);
142
+ const ctx = commandContext(document);
143
+ let operations = [];
144
+ if (command.id === "table.row.add")
145
+ operations = insertTableRowCommand(document, scope, {
146
+ rowIndex: command.input.index, rowId: `row-${createNodeId()}`,
147
+ cellIds: generated(grid.columns, "cell"), paragraphIds: generated(grid.columns, "paragraph"),
148
+ }, ctx);
149
+ else if (command.id === "table.row.remove")
150
+ operations = removeTableRowCommand(document, scope, { rowIndex: command.input.index }, ctx);
151
+ else if (command.id === "table.column.add")
152
+ operations = insertTableColumnCommand(document, scope, {
153
+ columnIndex: command.input.index, cellIds: generated(grid.rows, "cell"), paragraphIds: generated(grid.rows, "paragraph"),
154
+ }, ctx);
155
+ else if (command.id === "table.column.remove")
156
+ operations = removeTableColumnCommand(document, scope, { columnIndex: command.input.index }, ctx);
157
+ else if (command.id === "table.cell.merge")
158
+ operations = mergeTableCellsCommand(document, scope, {}, ctx);
159
+ else if (command.id === "table.cell.split") {
160
+ const target = grid.at(command.input.row, command.input.column);
161
+ const count = target ? (target.bottom - target.top) * (target.right - target.left) - 1 : 0;
162
+ operations = splitTableCellCommand(document, scope, { cellIds: generated(count, "cell"), paragraphIds: generated(count, "paragraph") }, ctx);
163
+ }
164
+ else if (command.id.startsWith("table.header.")) {
165
+ const point = command.input;
166
+ const targetCells = command.id === "table.header.cell.toggle" ? [grid.at(point.row, point.column)].filter(Boolean)
167
+ : command.id === "table.header.row.toggle" ? grid.anchors.filter((cell) => cell.top <= point.row && cell.bottom > point.row)
168
+ : grid.anchors.filter((cell) => cell.left <= point.column && cell.right > point.column);
169
+ const makeHeader = targetCells.some((cell) => cell?.node.attrs?.header !== true);
170
+ if (command.id === "table.header.cell.toggle" && point.row > 0 && point.column > 0)
171
+ return null;
172
+ const headerScope = { ...scope, cellIds: targetCells.map((cell) => cell.cellId) };
173
+ operations = command.id === "table.header.cell.toggle"
174
+ ? setTableCellAttributesCommand(document, headerScope, { attrs: { header: makeHeader } }, ctx)
175
+ : setTableHeaderCommand(document, scope, { target: makeHeader ? command.id === "table.header.row.toggle" ? "row" : "column" : "none" }, ctx);
176
+ }
177
+ else if (command.id === "table.cell.style.set")
178
+ operations = setTableCellAttributesCommand(document, scope, { attrs: {
179
+ ...(command.input.backgroundColor !== undefined ? { background: command.input.backgroundColor || undefined } : {}),
180
+ ...(command.input.textColor !== undefined ? { textColor: command.input.textColor || undefined } : {}),
181
+ } }, ctx);
182
+ else if (command.id === "table.cell.border.toggle") {
183
+ const visible = scope.cellIds.some((id) => grid.anchors.find((cell) => cell.cellId === id)?.node.attrs?.borders && grid.anchors.find((cell) => cell.cellId === id)?.node.attrs?.borders !== "none");
184
+ operations = setTableCellAttributesCommand(document, scope, { attrs: { borders: visible ? "none" : command.input.visibleBorder || "1px solid #d1d5db" } }, ctx);
185
+ }
186
+ else if (command.id === "table.column.width.set")
187
+ operations = setTableColumnWidthCommand(document, scope, { index: command.input.index, width: command.input.widthPx }, ctx);
188
+ else if (command.id === "table.row.height.set")
189
+ operations = setTableRowHeightCommand(document, scope, { index: command.input.index, height: command.input.heightPx }, ctx);
190
+ if (!operations.length)
191
+ return null;
192
+ const after = applyOperations(document, operations);
193
+ if (["table.cell.style.set", "table.cell.border.toggle", "table.column.width.set", "table.row.height.set"].includes(command.id)) {
194
+ return projectAttributesInPlace(element, after);
195
+ }
196
+ return replaceDomTable(element, after);
197
+ };
198
+ export const executeDomTableRemoval = (element) => {
199
+ const parsed = canonicalTable(element);
200
+ if (!parsed)
201
+ return false;
202
+ const scope = tableScope(parsed.table, { row: 0, column: 0 });
203
+ if (!removeTableCommand(parsed.document, scope, {}, commandContext(parsed.document)).length)
204
+ return false;
205
+ element.remove();
206
+ return true;
207
+ };
208
+ export const executeDomTableInsert = (root, afterBlockIndex, rows, columns, withHeader = false) => {
209
+ if (rows < 1 || columns < 1)
210
+ return null;
211
+ const document = repair(parseCanonicalTableHtml(root.innerHTML)).doc;
212
+ const index = Math.max(0, Math.min(afterBlockIndex, document.children.length - 1));
213
+ const target = document.children[index];
214
+ if (!target || !("id" in target))
215
+ return null;
216
+ const blockScope = {
217
+ kind: "block-range", blockIds: [target.id], promotedFromPartial: false,
218
+ commonParentId: document.id, range: { from: { path: [], offset: index }, to: { path: [], offset: index + 1 } },
219
+ isolatingAncestorId: null, clamped: false,
220
+ };
221
+ const total = rows * columns;
222
+ const tableId = `table-${createNodeId()}`;
223
+ const operations = insertTableCommand(document, blockScope, {
224
+ rows, columns, withHeader, placement: "after",
225
+ ids: { tableId, rowIds: generated(rows, "row"), cellIds: generated(total, "cell"), paragraphIds: generated(total, "paragraph") },
226
+ }, commandContext(document));
227
+ if (!operations.length)
228
+ return null;
229
+ root.innerHTML = serializeCanonicalTableHtml(applyOperations(document, operations), { fragment: true });
230
+ return root.querySelector(`table[data-smart-id="${tableId}"]`);
231
+ };
@@ -1,13 +1,5 @@
1
- import type { CoreInlineMark } from "./inlineMarkCoreExecution.js";
2
1
  export interface SmartRteInternalFlags {
3
- coreBold?: boolean;
4
- coreItalic?: boolean;
5
- coreUnderline?: boolean;
6
- coreSuperscript?: boolean;
7
- coreSubscript?: boolean;
8
- coreInlineMarks?: boolean;
9
2
  shadowMode?: boolean;
10
3
  }
11
4
  export declare const getSmartRteInternalFlags: () => SmartRteInternalFlags;
12
- export declare const isCoreInlineMarkFlagEnabled: (mark: CoreInlineMark) => boolean;
13
5
  export declare const isShadowModeFlagEnabled: () => boolean;
@@ -1,30 +1,6 @@
1
- const markFlagNames = {
2
- bold: "coreBold",
3
- italic: "coreItalic",
4
- underline: "coreUnderline",
5
- superscript: "coreSuperscript",
6
- subscript: "coreSubscript",
7
- };
8
- const legacyMarkFlagNames = {
9
- bold: "__SMART_RTE_CORE_BOLD__",
10
- italic: "__SMART_RTE_CORE_ITALIC__",
11
- underline: "__SMART_RTE_CORE_UNDERLINE__",
12
- superscript: "__SMART_RTE_CORE_SUPERSCRIPT__",
13
- subscript: "__SMART_RTE_CORE_SUBSCRIPT__",
14
- };
15
1
  const getGlobal = () => globalThis;
16
2
  const readBoolean = (value) => typeof value === "boolean" ? value : undefined;
17
3
  export const getSmartRteInternalFlags = () => getGlobal().__SMART_RTE_INTERNAL_FLAGS__ || {};
18
- export const isCoreInlineMarkFlagEnabled = (mark) => {
19
- const flags = getSmartRteInternalFlags();
20
- const individual = readBoolean(flags[markFlagNames[mark]]);
21
- if (individual !== undefined)
22
- return individual;
23
- const grouped = readBoolean(flags.coreInlineMarks);
24
- if (grouped !== undefined)
25
- return grouped;
26
- return getGlobal()[legacyMarkFlagNames[mark]] === true;
27
- };
28
4
  export const isShadowModeFlagEnabled = () => {
29
5
  const configured = readBoolean(getSmartRteInternalFlags().shadowMode);
30
6
  if (configured !== undefined)
@@ -0,0 +1,26 @@
1
+ import { shadowLogRecord } from "smartrte-core/foundation";
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/foundation";
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,8 @@
1
+ import type { SmartDocument } from "smartrte-core/foundation";
2
+ /**
3
+ * The only genuinely browser-specific piece of PDF export: opening a print
4
+ * window is a windowing operation, not format logic, so it stays here
5
+ * rather than in packages/core. buildPdfPrintDocument (the actual HTML
6
+ * content) is framework-agnostic and lives in core.
7
+ */
8
+ export declare const printSmartDocumentAsPdf: (document: SmartDocument, hostWindow: Window, delayMs?: number) => boolean;
@@ -0,0 +1,22 @@
1
+ import { buildPdfPrintDocument } from "smartrte-core/foundation";
2
+ /**
3
+ * The only genuinely browser-specific piece of PDF export: opening a print
4
+ * window is a windowing operation, not format logic, so it stays here
5
+ * rather than in packages/core. buildPdfPrintDocument (the actual HTML
6
+ * content) is framework-agnostic and lives in core.
7
+ */
8
+ export const printSmartDocumentAsPdf = (document, hostWindow, delayMs = 150) => {
9
+ const printWindow = hostWindow.open("", "_blank", "width=900,height=700");
10
+ if (!printWindow)
11
+ return false;
12
+ printWindow.addEventListener("load", () => {
13
+ printWindow.setTimeout(() => {
14
+ printWindow.focus();
15
+ printWindow.print();
16
+ }, delayMs);
17
+ }, { once: true });
18
+ printWindow.document.open();
19
+ printWindow.document.write(buildPdfPrintDocument(document));
20
+ printWindow.document.close();
21
+ return true;
22
+ };