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.
Files changed (87) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +14 -83
  3. package/dist/adapters/canonicalInlineCommandBridge.d.ts +15 -0
  4. package/dist/adapters/canonicalInlineCommandBridge.js +269 -0
  5. package/dist/adapters/canonicalListCommandBridge.d.ts +26 -0
  6. package/dist/adapters/canonicalListCommandBridge.js +430 -0
  7. package/dist/adapters/documentFormats.d.ts +26 -0
  8. package/dist/adapters/documentFormats.js +49 -0
  9. package/dist/adapters/docxFormat.d.ts +5 -0
  10. package/dist/adapters/docxFormat.js +272 -0
  11. package/dist/adapters/domBlockCommandBridge.d.ts +19 -0
  12. package/dist/adapters/domBlockCommandBridge.js +79 -0
  13. package/dist/adapters/domChecklistCommandBridge.d.ts +6 -0
  14. package/dist/adapters/domChecklistCommandBridge.js +33 -0
  15. package/dist/adapters/domCommandBridge.d.ts +19 -0
  16. package/dist/adapters/domCommandBridge.js +33 -0
  17. package/dist/adapters/domInlineAtomCommandBridge.d.ts +14 -0
  18. package/dist/adapters/domInlineAtomCommandBridge.js +101 -0
  19. package/dist/adapters/domInlineImageCommandBridge.d.ts +14 -0
  20. package/dist/adapters/domInlineImageCommandBridge.js +72 -0
  21. package/dist/adapters/domMoveCommandBridge.d.ts +7 -0
  22. package/dist/adapters/domMoveCommandBridge.js +54 -0
  23. package/dist/adapters/domSelectionBridge.d.ts +3 -1
  24. package/dist/adapters/domSelectionBridge.js +96 -44
  25. package/dist/adapters/domSmartDocument.js +158 -16
  26. package/dist/adapters/domTableCommandBridge.d.ts +35 -0
  27. package/dist/adapters/domTableCommandBridge.js +201 -0
  28. package/dist/adapters/legacyListShadowComparator.d.ts +26 -0
  29. package/dist/adapters/legacyListShadowComparator.js +316 -0
  30. package/dist/adapters/pdfFormat.d.ts +23 -0
  31. package/dist/adapters/pdfFormat.js +204 -0
  32. package/dist/adapters/portableDocxAtoms.d.ts +4 -0
  33. package/dist/adapters/portableDocxAtoms.js +58 -0
  34. package/dist/adapters/styledDocxFormat.d.ts +8 -0
  35. package/dist/adapters/styledDocxFormat.js +190 -0
  36. package/dist/builtInFormatDefinitions.d.ts +5 -0
  37. package/dist/builtInFormatDefinitions.js +82 -0
  38. package/dist/canonicalAuthorityFlag.d.ts +32 -0
  39. package/dist/canonicalAuthorityFlag.js +49 -0
  40. package/dist/canonicalClipboardRuntime.d.ts +15 -0
  41. package/dist/canonicalClipboardRuntime.js +76 -0
  42. package/dist/canonicalEditorRuntime.d.ts +77 -0
  43. package/dist/canonicalEditorRuntime.js +274 -0
  44. package/dist/components/CanonicalAuthorityEditor.d.ts +25 -0
  45. package/dist/components/CanonicalAuthorityEditor.js +518 -0
  46. package/dist/components/ClassicEditor.d.ts +13 -3
  47. package/dist/components/ClassicEditor.js +1309 -1159
  48. package/dist/components/ClassicEditorAuthority.d.ts +38 -0
  49. package/dist/components/ClassicEditorAuthority.js +49 -0
  50. package/dist/components/MediaPicker.d.ts +10 -0
  51. package/dist/components/MediaPicker.js +16 -0
  52. package/dist/editorController.d.ts +78 -0
  53. package/dist/editorController.js +298 -0
  54. package/dist/formatFidelity.d.ts +14 -0
  55. package/dist/formatFidelity.js +107 -0
  56. package/dist/formatRuntime.d.ts +50 -0
  57. package/dist/formatRuntime.js +20 -0
  58. package/dist/index.d.ts +24 -0
  59. package/dist/index.js +10 -0
  60. package/dist/mediaProvider.d.ts +39 -0
  61. package/dist/mediaProvider.js +1 -0
  62. package/dist/pluginRuntime.d.ts +68 -0
  63. package/dist/pluginRuntime.js +78 -0
  64. package/dist/standalone/editor.js +722 -495
  65. package/dist/test-harness/atomShadowComparator.d.ts +14 -0
  66. package/dist/test-harness/atomShadowComparator.js +63 -0
  67. package/dist/test-harness/blockShadowComparator.d.ts +30 -0
  68. package/dist/test-harness/blockShadowComparator.js +140 -0
  69. package/dist/test-harness/clipboardShadowComparator.d.ts +12 -0
  70. package/dist/test-harness/clipboardShadowComparator.js +46 -0
  71. package/dist/test-harness/inlineShadowComparator.d.ts +32 -0
  72. package/dist/test-harness/inlineShadowComparator.js +152 -0
  73. package/dist/test-harness/legacyAtomEngine.d.ts +10 -0
  74. package/dist/test-harness/legacyAtomEngine.js +15 -0
  75. package/dist/test-harness/legacyBlockEngine.d.ts +31 -0
  76. package/dist/test-harness/legacyBlockEngine.js +31 -0
  77. package/dist/test-harness/legacyClipboardEngine.d.ts +14 -0
  78. package/dist/test-harness/legacyClipboardEngine.js +67 -0
  79. package/dist/test-harness/legacyInlineEngine.d.ts +19 -0
  80. package/dist/test-harness/legacyInlineEngine.js +49 -0
  81. package/dist/test-harness/legacyTableEngine.d.ts +12 -0
  82. package/dist/test-harness/legacyTableEngine.js +25 -0
  83. package/dist/test-harness/tableShadowComparator.d.ts +29 -0
  84. package/dist/test-harness/tableShadowComparator.js +101 -0
  85. package/dist/theme.d.ts +1 -1
  86. package/dist/theme.js +28 -6
  87. package/package.json +1 -1
@@ -0,0 +1,76 @@
1
+ import { parseClipboardPayload, reportParsedClipboard, reportRejectedClipboard, serializeCanonicalListHtml, } from "smartrte-core/foundation";
2
+ const payloadFromTransfer = (transfer) => {
3
+ const representations = Object.fromEntries(Array.from(transfer.types)
4
+ .filter((type) => type !== "Files")
5
+ .map((type) => [type, transfer.getData(type)]));
6
+ return {
7
+ html: representations["text/html"], plainText: representations["text/plain"],
8
+ native: representations["application/x-smart-rte+json"], types: Array.from(transfer.types), representations,
9
+ };
10
+ };
11
+ /**
12
+ * ROLLBACK_ADAPTER: canonical-clipboard-dom-insert; unreachable from canonical authority.
13
+ * Legacy rollback-only DOM insertion. The canonical product path never imports this module.
14
+ */
15
+ export const insertCanonicalClipboardData = (transfer, ownerDocument, onDiagnostic) => {
16
+ if (!transfer.types.length)
17
+ return false;
18
+ const payload = payloadFromTransfer(transfer);
19
+ try {
20
+ const fragment = parseClipboardPayload(payload, { ownerDocument });
21
+ onDiagnostic?.(reportParsedClipboard(payload, fragment));
22
+ const cleanHtml = serializeCanonicalListHtml(fragment.document, { clean: true });
23
+ if (!cleanHtml)
24
+ return false;
25
+ const selection = ownerDocument.getSelection();
26
+ if (!selection?.rangeCount)
27
+ return false;
28
+ const range = selection.getRangeAt(0);
29
+ range.deleteContents();
30
+ const startElement = range.startContainer.nodeType === Node.ELEMENT_NODE
31
+ ? range.startContainer : range.startContainer.parentElement;
32
+ const inlineOwner = startElement?.closest("p,h1,h2,h3,h4,h5,h6,pre,li,td,th");
33
+ const only = fragment.document.children.length === 1 ? fragment.document.children[0] : null;
34
+ let insertionHtml = cleanHtml;
35
+ if (inlineOwner && only && only.type !== "text" && ["paragraph", "heading"].includes(only.type)) {
36
+ const container = ownerDocument.createElement("div");
37
+ container.innerHTML = cleanHtml;
38
+ insertionHtml = container.firstElementChild?.innerHTML || cleanHtml;
39
+ }
40
+ const fragmentNode = range.createContextualFragment(insertionHtml);
41
+ const last = fragmentNode.lastChild;
42
+ range.insertNode(fragmentNode);
43
+ if (last) {
44
+ range.setStartAfter(last);
45
+ range.collapse(true);
46
+ selection.removeAllRanges();
47
+ selection.addRange(range);
48
+ }
49
+ return true;
50
+ }
51
+ catch (error) {
52
+ onDiagnostic?.(reportRejectedClipboard(payload, error));
53
+ return false;
54
+ }
55
+ };
56
+ /** Owns the product paste listener outside ClassicEditor. */
57
+ export const installCanonicalClipboardRuntime = (root, options) => {
58
+ const onPaste = (event) => {
59
+ const transfer = event.clipboardData;
60
+ if (!transfer)
61
+ return;
62
+ const files = Array.from(transfer.files || []);
63
+ if (files.length && options.onFiles?.(files)) {
64
+ event.preventDefault();
65
+ return;
66
+ }
67
+ if (!transfer.types.length)
68
+ return;
69
+ event.preventDefault();
70
+ options.beforeInsert?.();
71
+ if (insertCanonicalClipboardData(transfer, options.ownerDocument, options.onDiagnostic))
72
+ options.afterInsert?.();
73
+ };
74
+ root.addEventListener("paste", onPaste);
75
+ return () => root.removeEventListener("paste", onPaste);
76
+ };
@@ -0,0 +1,77 @@
1
+ import { type CanonicalInputPipeline, type ClipboardDiagnosticReport, type CanonicalSubtreeRenderer, type FoundationEditor, type PersistedEditorDocument, type SmartMark, type SmartOperation, type SmartSelection, type SmartTransaction } from "smartrte-core/foundation";
2
+ export interface SmartEditorCheckpoint {
3
+ envelope: PersistedEditorDocument;
4
+ selection: SmartSelection;
5
+ storedMarks?: SmartMark[];
6
+ savedRevision: number;
7
+ }
8
+ export interface SmartEditorChange {
9
+ revision: number;
10
+ documentChanged: boolean;
11
+ transaction: SmartTransaction;
12
+ }
13
+ export interface SmartEditorHandle {
14
+ getValue(): PersistedEditorDocument;
15
+ replaceValue(doc: PersistedEditorDocument, opts?: {
16
+ keepSelection?: boolean;
17
+ }): void;
18
+ isDirty(): boolean;
19
+ markSaved(revision: number): void;
20
+ getRevision(): number;
21
+ focus(): void;
22
+ executeOperations(operations: readonly SmartOperation[], opts?: ExecuteOperationsOptions): void;
23
+ createCheckpoint(): SmartEditorCheckpoint;
24
+ restoreCheckpoint(checkpoint: SmartEditorCheckpoint): void;
25
+ }
26
+ export interface ExecuteOperationsOptions {
27
+ historyGroup?: string;
28
+ addToHistory?: boolean;
29
+ /** Keep structural commands anchored to the same stable inline-owner IDs. */
30
+ preserveSelectionById?: boolean;
31
+ /** Put the caret in a newly-created inline owner after the operation. */
32
+ selectionOwnerId?: string;
33
+ selectionOffset?: number;
34
+ }
35
+ export interface CanonicalEditorRuntimeOptions {
36
+ initialValue?: string | PersistedEditorDocument;
37
+ onChange?: (change: SmartEditorChange) => void;
38
+ onHtmlChange?: (html: string) => void;
39
+ onClipboardDiagnostic?: (report: ClipboardDiagnosticReport) => void;
40
+ }
41
+ /** Persistent, React-independent owner for one product editor instance. */
42
+ export declare class CanonicalEditorRuntime implements SmartEditorHandle {
43
+ readonly editor: FoundationEditor;
44
+ private root;
45
+ private renderer;
46
+ private pipeline;
47
+ private unsubscribe;
48
+ private savedRevision;
49
+ private onChange?;
50
+ private onHtmlChange?;
51
+ private readonly onClipboardDiagnostic?;
52
+ private htmlChangeTimer;
53
+ private pendingHtmlDocument;
54
+ constructor(options?: CanonicalEditorRuntimeOptions);
55
+ setCallbacks(onChange?: (change: SmartEditorChange) => void, onHtmlChange?: (html: string) => void): void;
56
+ private scheduleHtmlChange;
57
+ mount(root: HTMLElement): void;
58
+ unmount(): void;
59
+ getValue(): PersistedEditorDocument;
60
+ replaceValue(doc: PersistedEditorDocument, opts?: {
61
+ keepSelection?: boolean;
62
+ }): void;
63
+ isDirty(): boolean;
64
+ markSaved(revision: number): void;
65
+ getRevision(): number;
66
+ focus(): void;
67
+ executeOperations(operations: readonly SmartOperation[], opts?: ExecuteOperationsOptions): void;
68
+ createCheckpoint(): SmartEditorCheckpoint;
69
+ restoreCheckpoint(checkpoint: SmartEditorCheckpoint): void;
70
+ /** Instrumentation for product-path composition and takeover tests. */
71
+ get surface(): {
72
+ root: HTMLElement;
73
+ renderer: CanonicalSubtreeRenderer;
74
+ pipeline: CanonicalInputPipeline;
75
+ };
76
+ }
77
+ export declare const createCanonicalEditorRuntime: (options?: CanonicalEditorRuntimeOptions) => CanonicalEditorRuntime;
@@ -0,0 +1,274 @@
1
+ import { createFoundationEditor, applyOperations, createInputPipeline, createSubtreeRenderer, createTransactionMap, foundationSchema, isTextNode, parseCanonicalListHtml, serializeCanonicalListHtml, } from "smartrte-core/foundation";
2
+ const nodeAtPath = (root, path) => {
3
+ let node = root;
4
+ for (const index of path) {
5
+ if (isTextNode(node) || !node.children?.[index])
6
+ return null;
7
+ node = node.children[index];
8
+ }
9
+ return node;
10
+ };
11
+ const pathOfNode = (root, nodeId, path = []) => {
12
+ if (!isTextNode(root) && root.id === nodeId)
13
+ return path;
14
+ if (isTextNode(root))
15
+ return null;
16
+ for (let index = 0; index < (root.children?.length || 0); index += 1) {
17
+ const found = pathOfNode(root.children[index], nodeId, [...path, index]);
18
+ if (found)
19
+ return found;
20
+ }
21
+ return null;
22
+ };
23
+ const inlineWidth = (node) => isTextNode(node)
24
+ ? node.text.length
25
+ : node.children?.reduce((width, child) => width + (isTextNode(child) ? child.text.length : 1), 0) ?? 0;
26
+ const firstTextSelection = (document) => {
27
+ const visit = (node, path) => {
28
+ if (isTextNode(node))
29
+ return null;
30
+ const spec = foundationSchema.nodes[node.type];
31
+ const children = node.children || [];
32
+ if (spec?.group === "block" && (children.length === 0 || children.every((child) => isTextNode(child) || foundationSchema.nodes[child.type]?.group === "inline")))
33
+ return { path, offset: 0 };
34
+ for (let index = 0; index < children.length; index += 1) {
35
+ const found = visit(children[index], [...path, index]);
36
+ if (found)
37
+ return found;
38
+ }
39
+ return null;
40
+ };
41
+ const pos = visit(document, []) || { path: [], offset: 0 };
42
+ return { type: "text", anchor: pos, head: pos };
43
+ };
44
+ const envelopeFrom = (value) => {
45
+ if (typeof value === "object" && value)
46
+ return structuredClone(value);
47
+ const document = parseCanonicalListHtml(typeof value === "string" ? value : "<p></p>");
48
+ return { schemaVersion: foundationSchema.version, revision: 0, document };
49
+ };
50
+ const sameOperations = (transaction) => transaction.operations.length > 0;
51
+ const cellIdAt = (document, position) => {
52
+ const node = nodeAtPath(document, position.path);
53
+ return node && !isTextNode(node) && node.type === "table_cell" ? node.id : null;
54
+ };
55
+ const cellSelectionIn = (document, anchorId, headId) => {
56
+ const anchorPath = pathOfNode(document, anchorId) || pathOfNode(document, headId);
57
+ const headPath = pathOfNode(document, headId) || anchorPath;
58
+ if (!anchorPath || !headPath)
59
+ return null;
60
+ const anchor = nodeAtPath(document, anchorPath);
61
+ const head = nodeAtPath(document, headPath);
62
+ if (!anchor || isTextNode(anchor) || anchor.type !== "table_cell"
63
+ || !head || isTextNode(head) || head.type !== "table_cell")
64
+ return null;
65
+ return {
66
+ type: "cell",
67
+ anchor: { path: anchorPath, offset: 0 },
68
+ head: { path: headPath, offset: head.children?.length || 0 },
69
+ };
70
+ };
71
+ /** Persistent, React-independent owner for one product editor instance. */
72
+ export class CanonicalEditorRuntime {
73
+ constructor(options = {}) {
74
+ this.root = null;
75
+ this.renderer = null;
76
+ this.pipeline = null;
77
+ this.unsubscribe = null;
78
+ this.htmlChangeTimer = null;
79
+ this.pendingHtmlDocument = null;
80
+ const envelope = envelopeFrom(options.initialValue);
81
+ this.editor = createFoundationEditor({
82
+ document: envelope.document,
83
+ revision: envelope.revision,
84
+ selection: firstTextSelection(envelope.document),
85
+ });
86
+ this.savedRevision = envelope.revision;
87
+ this.onChange = options.onChange;
88
+ this.onHtmlChange = options.onHtmlChange;
89
+ this.onClipboardDiagnostic = options.onClipboardDiagnostic;
90
+ }
91
+ setCallbacks(onChange, onHtmlChange) {
92
+ this.onChange = onChange;
93
+ this.onHtmlChange = onHtmlChange;
94
+ }
95
+ scheduleHtmlChange(document) {
96
+ if (!this.onHtmlChange)
97
+ return;
98
+ this.pendingHtmlDocument = document;
99
+ const view = this.root?.ownerDocument.defaultView;
100
+ if (this.htmlChangeTimer !== null) {
101
+ if (view)
102
+ view.clearTimeout(this.htmlChangeTimer);
103
+ else
104
+ globalThis.clearTimeout(this.htmlChangeTimer);
105
+ }
106
+ const flush = () => {
107
+ this.htmlChangeTimer = null;
108
+ const pending = this.pendingHtmlDocument;
109
+ this.pendingHtmlDocument = null;
110
+ if (pending && this.onHtmlChange)
111
+ this.onHtmlChange(serializeCanonicalListHtml(pending, { clean: true }));
112
+ };
113
+ if (view) {
114
+ // Transitional HTML serialization is intentionally debounced outside the
115
+ // typing frame. The canonical onChange contract remains per transaction.
116
+ this.htmlChangeTimer = view.setTimeout(flush, 250);
117
+ }
118
+ else {
119
+ this.htmlChangeTimer = setTimeout(flush, 250);
120
+ }
121
+ }
122
+ mount(root) {
123
+ if (this.root === root && this.pipeline && this.renderer)
124
+ return;
125
+ this.unmount();
126
+ this.root = root;
127
+ this.renderer = createSubtreeRenderer(root);
128
+ this.pipeline = createInputPipeline(this.editor, this.renderer, root, { onClipboardDiagnostic: this.onClipboardDiagnostic });
129
+ this.unsubscribe = this.editor.subscribe((transaction, state) => {
130
+ this.renderer?.render(this.editor.document, this.editor.selection);
131
+ this.onChange?.({ revision: state.revision, documentChanged: sameOperations(transaction), transaction });
132
+ if (sameOperations(transaction))
133
+ this.scheduleHtmlChange(state.document);
134
+ });
135
+ }
136
+ unmount() {
137
+ this.unsubscribe?.();
138
+ this.unsubscribe = null;
139
+ this.pipeline?.destroy();
140
+ this.pipeline = null;
141
+ this.renderer?.destroy();
142
+ this.renderer = null;
143
+ const view = this.root?.ownerDocument.defaultView;
144
+ if (this.htmlChangeTimer !== null) {
145
+ if (view)
146
+ view.clearTimeout(this.htmlChangeTimer);
147
+ else
148
+ globalThis.clearTimeout(this.htmlChangeTimer);
149
+ }
150
+ this.htmlChangeTimer = null;
151
+ this.pendingHtmlDocument = null;
152
+ this.root?.replaceChildren();
153
+ this.root = null;
154
+ }
155
+ getValue() {
156
+ const state = this.editor.state;
157
+ return { schemaVersion: state.schemaVersion, revision: state.revision, document: state.document };
158
+ }
159
+ replaceValue(doc, opts = {}) {
160
+ let selection = firstTextSelection(doc.document);
161
+ if (opts.keepSelection) {
162
+ const current = this.editor.selection;
163
+ const owner = nodeAtPath(this.editor.document, current.head.path);
164
+ if (owner && !isTextNode(owner)) {
165
+ const find = (node, path) => {
166
+ if (!isTextNode(node) && node.id === owner.id)
167
+ return path;
168
+ if (isTextNode(node))
169
+ return null;
170
+ for (let index = 0; index < (node.children?.length || 0); index += 1) {
171
+ const found = find(node.children[index], [...path, index]);
172
+ if (found)
173
+ return found;
174
+ }
175
+ return null;
176
+ };
177
+ const path = find(doc.document, []);
178
+ if (path) {
179
+ const replacementOwner = nodeAtPath(doc.document, path);
180
+ const offset = Math.min(current.head.offset, inlineWidth(replacementOwner));
181
+ const pos = { path, offset };
182
+ selection = { type: "text", anchor: pos, head: pos };
183
+ }
184
+ }
185
+ }
186
+ this.editor.replaceState(doc, { selection });
187
+ }
188
+ isDirty() { return this.editor.state.revision !== this.savedRevision; }
189
+ markSaved(revision) {
190
+ if (revision > this.editor.state.revision)
191
+ throw new Error("Cannot mark a future revision as saved.");
192
+ this.savedRevision = revision;
193
+ }
194
+ getRevision() { return this.editor.state.revision; }
195
+ focus() { this.root?.focus(); }
196
+ executeOperations(operations, opts = {}) {
197
+ if (!operations.length)
198
+ return;
199
+ const beforeSelection = this.editor.selection;
200
+ let selection = createTransactionMap(operations).mapSelection(beforeSelection);
201
+ if (opts.selectionOwnerId || opts.preserveSelectionById) {
202
+ const beforeDocument = this.editor.document;
203
+ const preview = applyOperations(beforeDocument, operations);
204
+ const beforeCellAnchor = beforeSelection.type === "cell" ? cellIdAt(beforeDocument, beforeSelection.anchor) : null;
205
+ const beforeCellHead = beforeSelection.type === "cell" ? cellIdAt(beforeDocument, beforeSelection.head) : null;
206
+ let preservedCellSelection = false;
207
+ if (opts.preserveSelectionById && beforeCellAnchor && beforeCellHead) {
208
+ // Structural table commands may retire the head cell (merge) while
209
+ // preserving the anchor. Keep the selection on the surviving cell so
210
+ // the next table command still has an unambiguous scope.
211
+ const mappedCells = cellSelectionIn(preview, beforeCellAnchor, beforeCellHead);
212
+ if (mappedCells) {
213
+ selection = mappedCells;
214
+ preservedCellSelection = true;
215
+ }
216
+ }
217
+ const point = (pos, forcedId) => {
218
+ const beforeOwner = nodeAtPath(beforeDocument, pos.path);
219
+ const ownerId = forcedId || (!beforeOwner || isTextNode(beforeOwner) ? undefined : beforeOwner.id);
220
+ if (!ownerId)
221
+ return null;
222
+ const path = pathOfNode(preview, ownerId);
223
+ if (!path)
224
+ return null;
225
+ const owner = nodeAtPath(preview, path);
226
+ if (!owner)
227
+ return null;
228
+ return { path, offset: Math.min(forcedId ? opts.selectionOffset ?? 0 : pos.offset, inlineWidth(owner)) };
229
+ };
230
+ if (preservedCellSelection) {
231
+ // The cell selection was mapped above; do not reinterpret its
232
+ // structural endpoints as a text selection.
233
+ }
234
+ else if (opts.selectionOwnerId) {
235
+ const caret = point(beforeSelection.head, opts.selectionOwnerId);
236
+ if (caret)
237
+ selection = { type: "text", anchor: caret, head: caret };
238
+ }
239
+ else {
240
+ const anchor = point(beforeSelection.anchor);
241
+ const head = point(beforeSelection.head);
242
+ if (anchor && head)
243
+ selection = { ...beforeSelection, anchor, head };
244
+ }
245
+ }
246
+ this.editor.transact((builder) => {
247
+ builder.operations.push(...operations);
248
+ builder.setSelection(selection);
249
+ }, {
250
+ source: "toolbar",
251
+ addToHistory: opts.addToHistory ?? true,
252
+ ...(opts.historyGroup ? { historyGroup: opts.historyGroup } : {}),
253
+ });
254
+ this.focus();
255
+ }
256
+ createCheckpoint() {
257
+ return {
258
+ envelope: this.getValue(),
259
+ selection: this.editor.selection,
260
+ ...(this.editor.storedMarks?.length ? { storedMarks: [...structuredClone(this.editor.storedMarks)] } : {}),
261
+ savedRevision: this.savedRevision,
262
+ };
263
+ }
264
+ restoreCheckpoint(checkpoint) {
265
+ this.editor.replaceState(checkpoint.envelope, {
266
+ selection: checkpoint.selection,
267
+ storedMarks: checkpoint.storedMarks,
268
+ });
269
+ this.savedRevision = checkpoint.savedRevision;
270
+ }
271
+ /** Instrumentation for product-path composition and takeover tests. */
272
+ get surface() { return { root: this.root, renderer: this.renderer, pipeline: this.pipeline }; }
273
+ }
274
+ export const createCanonicalEditorRuntime = (options = {}) => new CanonicalEditorRuntime(options);
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ import { type PersistedEditorDocument, type ClipboardDiagnosticReport } from "smartrte-core/foundation";
3
+ import type { MediaProvider } from "../mediaProvider.js";
4
+ import { type MediaPickerComponent } from "./MediaPicker.js";
5
+ import { CanonicalEditorRuntime, type SmartEditorChange, type SmartEditorHandle } from "../canonicalEditorRuntime.js";
6
+ export interface CanonicalAuthorityEditorProps {
7
+ /** Initial value only. Later replacements must use SmartEditorHandle.replaceValue. */
8
+ defaultValue?: string | PersistedEditorDocument;
9
+ onChange?: (change: SmartEditorChange) => void;
10
+ /** Transitional serialization callback for hosts that still persist HTML. */
11
+ onHtmlChange?: (html: string) => void;
12
+ onClipboardDiagnostic?: (report: ClipboardDiagnosticReport) => void;
13
+ /** Host-owned upload/search/remove boundary for canonical media insertion. */
14
+ mediaProvider?: MediaProvider;
15
+ /** Replaceable picker; the default only selects a local file. */
16
+ mediaPicker?: MediaPickerComponent;
17
+ placeholder?: string;
18
+ minHeight?: number | string;
19
+ maxHeight?: number | string;
20
+ readOnly?: boolean;
21
+ className?: string;
22
+ /** Test/diagnostic hook; not part of the editing contract. */
23
+ onRuntime?: (runtime: CanonicalEditorRuntime) => void;
24
+ }
25
+ export declare const CanonicalAuthorityEditor: React.ForwardRefExoticComponent<CanonicalAuthorityEditorProps & React.RefAttributes<SmartEditorHandle>>;