smartrte-react 0.2.8 → 0.3.0

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 ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ - Rebuild list selection and conversion behavior for checklists, bullets, numbers, alphabetic lists, and Roman numerals.
6
+ - Support list-aware blockquotes and code blocks.
7
+ - Make heading and font-size changes deterministic across carets and multi-block selections.
8
+ - Add left, center, right, and justified block alignment, including list items, code blocks, and table cells.
9
+ - Redesign link insertion and editing with accessible fields, validation, display-text editing, removal, opening, and secure new-tab links.
10
+ - Add regression coverage for formatting, lists, links, table cells, and document serialization.
@@ -0,0 +1,5 @@
1
+ import { type CoreInlineMarkResult } from "./inlineMarkCoreExecution.js";
2
+ export declare const isCoreBoldEnabled: () => boolean;
3
+ /** Executes only the core bold command. Callers own DOM writes and legacy fallback. */
4
+ export type CoreBoldResult = CoreInlineMarkResult;
5
+ export declare const getCoreBoldResult: (root: HTMLElement) => CoreBoldResult | null;
@@ -0,0 +1,3 @@
1
+ import { getCoreInlineMarkResult, isCoreInlineMarkEnabled } from "./inlineMarkCoreExecution.js";
2
+ export const isCoreBoldEnabled = () => isCoreInlineMarkEnabled("bold");
3
+ export const getCoreBoldResult = (root) => getCoreInlineMarkResult(root, "bold");
@@ -0,0 +1,6 @@
1
+ import type { SmartSelection } from "smartrte-core";
2
+ export declare const isEditorOnlyElement: (node: Element) => boolean;
3
+ /** Converts browser selection into a core selection without exposing editor UI nodes. */
4
+ export declare const selectionFromDom: (editor: HTMLElement, selection: Selection | null) => SmartSelection | null;
5
+ /** Restores a text selection from core paths after the editor DOM is rebuilt. */
6
+ export declare const restoreSelectionToDom: (editor: HTMLElement, smartSelection: SmartSelection) => boolean;
@@ -0,0 +1,185 @@
1
+ const LEAF_TAGS = new Set(["p", "h1", "h2", "h3", "h4", "h5", "h6"]);
2
+ const CONTAINER_TAGS = new Set(["blockquote", "ul", "ol", "li", "table", "tr", "td", "th"]);
3
+ export const isEditorOnlyElement = (node) => node.getAttribute("data-table-wrapper") === "true" ||
4
+ Array.from(node.attributes).some((attribute) => attribute.name.startsWith("data-srte-")) ||
5
+ node.matches(".srte-table-resize-handle, .srte-table-resize-overlay, .srte-drag-handle");
6
+ const isSemanticElement = (node) => LEAF_TAGS.has(node.tagName.toLowerCase()) || CONTAINER_TAGS.has(node.tagName.toLowerCase());
7
+ const hasSemanticChild = (node) => Array.from(node.children).some((child) => !isEditorOnlyElement(child) && isSemanticElement(child));
8
+ const isVirtualLeaf = (node) => {
9
+ const tag = node.tagName.toLowerCase();
10
+ return (tag === "li" || tag === "td" || tag === "th") && !hasSemanticChild(node);
11
+ };
12
+ const isLeaf = (node) => LEAF_TAGS.has(node.tagName.toLowerCase()) || isVirtualLeaf(node);
13
+ const closestElement = (node) => {
14
+ if (!node)
15
+ return null;
16
+ return node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
17
+ };
18
+ const closestLeaf = (node, editor) => {
19
+ let element = closestElement(node);
20
+ while (element && element !== editor) {
21
+ if (isEditorOnlyElement(element))
22
+ return null;
23
+ if (isLeaf(element))
24
+ return element;
25
+ element = element.parentElement;
26
+ }
27
+ return null;
28
+ };
29
+ const semanticChildren = (parent) => {
30
+ const result = [];
31
+ Array.from(parent.children).forEach((child) => {
32
+ if (isEditorOnlyElement(child)) {
33
+ result.push(...semanticChildren(child));
34
+ }
35
+ else if (isSemanticElement(child)) {
36
+ result.push(child);
37
+ }
38
+ else {
39
+ result.push(...semanticChildren(child));
40
+ }
41
+ });
42
+ return result;
43
+ };
44
+ const pathForElement = (element, editor) => {
45
+ const findPath = (parent, basePath) => {
46
+ const children = semanticChildren(parent);
47
+ for (let index = 0; index < children.length; index += 1) {
48
+ const child = children[index];
49
+ const path = [...basePath, index];
50
+ if (child === element)
51
+ return path;
52
+ if (child.contains(element)) {
53
+ const nestedPath = findPath(child, path);
54
+ if (nestedPath)
55
+ return nestedPath;
56
+ }
57
+ }
58
+ return null;
59
+ };
60
+ return findPath(editor, []);
61
+ };
62
+ const textNodesIn = (root) => {
63
+ const nodes = [];
64
+ const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
65
+ acceptNode(node) {
66
+ let parent = node.parentElement;
67
+ while (parent && parent !== root) {
68
+ if (isEditorOnlyElement(parent))
69
+ return NodeFilter.FILTER_REJECT;
70
+ parent = parent.parentElement;
71
+ }
72
+ return NodeFilter.FILTER_ACCEPT;
73
+ },
74
+ });
75
+ let node = walker.nextNode();
76
+ while (node) {
77
+ nodes.push(node);
78
+ node = walker.nextNode();
79
+ }
80
+ return nodes;
81
+ };
82
+ const offsetBeforePoint = (leaf, container, offset) => {
83
+ if (!leaf.contains(container) && leaf !== container)
84
+ return null;
85
+ const range = leaf.ownerDocument.createRange();
86
+ range.selectNodeContents(leaf);
87
+ try {
88
+ range.setEnd(container, offset);
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ return range.toString().length;
94
+ };
95
+ const pointForDomPoint = (editor, container, offset) => {
96
+ const leaf = closestLeaf(container, editor);
97
+ if (!leaf)
98
+ return null;
99
+ const leafPath = pathForElement(leaf, editor);
100
+ if (!leafPath)
101
+ return null;
102
+ const textNodes = textNodesIn(leaf);
103
+ if (textNodes.length === 0)
104
+ return { path: [...leafPath, 0], offset: 0 };
105
+ const absoluteOffset = offsetBeforePoint(leaf, container, offset);
106
+ if (absoluteOffset == null)
107
+ return null;
108
+ let remaining = absoluteOffset;
109
+ for (let index = 0; index < textNodes.length; index += 1) {
110
+ const length = textNodes[index].data.length;
111
+ if (remaining <= length)
112
+ return { path: [...leafPath, index], offset: remaining };
113
+ remaining -= length;
114
+ }
115
+ const last = textNodes.length - 1;
116
+ return { path: [...leafPath, last], offset: textNodes[last].data.length };
117
+ };
118
+ const nodeSelectionFromRange = (editor, range) => {
119
+ const nodes = Array.from(editor.querySelectorAll("img, [data-srte-node-selection='true']"));
120
+ const selected = nodes.find((node) => range.intersectsNode(node));
121
+ if (!selected)
122
+ return null;
123
+ const path = pathForElement(selected, editor);
124
+ return path ? { type: "node", path } : null;
125
+ };
126
+ /** Converts browser selection into a core selection without exposing editor UI nodes. */
127
+ export const selectionFromDom = (editor, selection) => {
128
+ if (!selection || selection.rangeCount === 0)
129
+ return null;
130
+ const range = selection.getRangeAt(0);
131
+ if (!editor.contains(range.commonAncestorContainer))
132
+ return null;
133
+ const nodeSelection = nodeSelectionFromRange(editor, range);
134
+ if (nodeSelection)
135
+ return nodeSelection;
136
+ const anchor = pointForDomPoint(editor, range.startContainer, range.startOffset);
137
+ const focus = pointForDomPoint(editor, range.endContainer, range.endOffset);
138
+ if (!anchor || !focus)
139
+ return null;
140
+ return { type: "text", anchor, focus };
141
+ };
142
+ const elementAtPath = (editor, path) => {
143
+ let current = editor;
144
+ for (const index of path) {
145
+ const next = semanticChildren(current)[index];
146
+ if (!next)
147
+ return null;
148
+ current = next;
149
+ }
150
+ return current;
151
+ };
152
+ const domPointForSmartPoint = (editor, point) => {
153
+ if (point.path.length === 0)
154
+ return null;
155
+ const leaf = elementAtPath(editor, point.path.slice(0, -1));
156
+ if (!leaf)
157
+ return null;
158
+ const text = textNodesIn(leaf)[point.path[point.path.length - 1]];
159
+ if (!text || point.offset < 0 || point.offset > text.data.length)
160
+ return null;
161
+ return { node: text, offset: point.offset };
162
+ };
163
+ /** Restores a text selection from core paths after the editor DOM is rebuilt. */
164
+ export const restoreSelectionToDom = (editor, smartSelection) => {
165
+ if (smartSelection.type !== "text")
166
+ return false;
167
+ const anchor = domPointForSmartPoint(editor, smartSelection.anchor);
168
+ const focus = domPointForSmartPoint(editor, smartSelection.focus);
169
+ if (!anchor || !focus)
170
+ return false;
171
+ const range = editor.ownerDocument.createRange();
172
+ try {
173
+ range.setStart(anchor.node, anchor.offset);
174
+ range.setEnd(focus.node, focus.offset);
175
+ const selection = editor.ownerDocument.defaultView?.getSelection();
176
+ if (!selection)
177
+ return false;
178
+ selection.removeAllRanges();
179
+ selection.addRange(range);
180
+ return true;
181
+ }
182
+ catch {
183
+ return false;
184
+ }
185
+ };
@@ -0,0 +1,10 @@
1
+ import { type SmartDocument } from "smartrte-core";
2
+ /** Removes editor UI before handing the document to the core parser boundary. */
3
+ export declare const cleanEditorHtml: (root: HTMLElement) => string;
4
+ export declare const smartDocumentFromHtml: (html: string, ownerDocument: Document) => SmartDocument;
5
+ export declare const smartDocumentFromEditorRoot: (root: HTMLElement) => {
6
+ document: SmartDocument;
7
+ html: string;
8
+ };
9
+ /** Serializes the shadow-only model. It is not used for persisted editor HTML. */
10
+ export declare const serializeSmartDocument: (document: SmartDocument) => string;
@@ -0,0 +1,216 @@
1
+ import { normalizeCompatibilityHtml, sanitizeLinkAttrs, } from "smartrte-core";
2
+ import { isEditorOnlyElement } from "./domSelectionBridge.js";
3
+ const blockTags = new Set(["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "blockquote", "pre", "table"]);
4
+ const unwrap = (element) => {
5
+ const parent = element.parentNode;
6
+ if (!parent)
7
+ return;
8
+ while (element.firstChild)
9
+ parent.insertBefore(element.firstChild, element);
10
+ parent.removeChild(element);
11
+ };
12
+ /** Removes editor UI before handing the document to the core parser boundary. */
13
+ export const cleanEditorHtml = (root) => {
14
+ const clone = root.cloneNode(true);
15
+ Array.from(clone.querySelectorAll("*")).forEach((element) => {
16
+ if (element.getAttribute("data-table-wrapper") === "true")
17
+ unwrap(element);
18
+ });
19
+ Array.from(clone.querySelectorAll("*")).forEach((element) => {
20
+ if (isEditorOnlyElement(element))
21
+ element.remove();
22
+ });
23
+ return normalizeCompatibilityHtml(clone.innerHTML);
24
+ };
25
+ const addMark = (marks, mark) => marks.some((candidate) => candidate.type === mark.type) ? marks : [...marks, mark];
26
+ const marksFor = (element, inherited) => {
27
+ const tag = element.tagName.toLowerCase();
28
+ let marks = inherited;
29
+ if (tag === "strong" || tag === "b")
30
+ marks = addMark(marks, { type: "bold" });
31
+ if (tag === "em" || tag === "i")
32
+ marks = addMark(marks, { type: "italic" });
33
+ if (tag === "u")
34
+ marks = addMark(marks, { type: "underline" });
35
+ if (tag === "s" || tag === "strike" || tag === "del")
36
+ marks = addMark(marks, { type: "strike" });
37
+ if (tag === "sup")
38
+ marks = addMark(marks, { type: "superscript" });
39
+ if (tag === "sub")
40
+ marks = addMark(marks, { type: "subscript" });
41
+ if (tag === "code")
42
+ marks = addMark(marks, { type: "code" });
43
+ if (tag === "a") {
44
+ const safeLink = sanitizeLinkAttrs({
45
+ href: element.getAttribute("href") || "",
46
+ target: element.getAttribute("target") || undefined,
47
+ });
48
+ if (safeLink)
49
+ marks = addMark(marks, { type: "link", ...safeLink });
50
+ }
51
+ const color = element.getAttribute("color") || element.style.color;
52
+ const backgroundColor = element.style.backgroundColor;
53
+ const fontSize = element.style.fontSize;
54
+ if (color)
55
+ marks = addMark(marks, { type: "textColor", value: color });
56
+ if (backgroundColor)
57
+ marks = addMark(marks, { type: "backgroundColor", value: backgroundColor });
58
+ if (fontSize) {
59
+ const match = /^([\d.]+)(px|pt)?$/i.exec(fontSize.trim());
60
+ if (match) {
61
+ const numeric = Number(match[1]);
62
+ const valuePx = match[2]?.toLowerCase() === "pt" ? numeric * 4 / 3 : numeric;
63
+ if (Number.isFinite(valuePx) && valuePx > 0)
64
+ marks = addMark(marks, { type: "fontSize", valuePx });
65
+ }
66
+ }
67
+ return marks;
68
+ };
69
+ const inlineNodes = (nodes, inherited = []) => {
70
+ const result = [];
71
+ Array.from(nodes).forEach((node) => {
72
+ if (node.nodeType === Node.TEXT_NODE) {
73
+ if (node.textContent)
74
+ result.push({ type: "text", text: node.textContent, marks: inherited.length ? inherited : undefined });
75
+ return;
76
+ }
77
+ if (!(node instanceof Element))
78
+ return;
79
+ if (node.tagName.toLowerCase() === "br") {
80
+ result.push({ type: "text", text: "\n", marks: inherited.length ? inherited : undefined });
81
+ return;
82
+ }
83
+ result.push(...inlineNodes(node.childNodes, marksFor(node, inherited)));
84
+ });
85
+ return result;
86
+ };
87
+ const directBlockChildren = (parent) => {
88
+ const children = [];
89
+ Array.from(parent.children).forEach((child) => {
90
+ const tag = child.tagName.toLowerCase();
91
+ if (blockTags.has(tag))
92
+ children.push(child);
93
+ else
94
+ children.push(...directBlockChildren(child));
95
+ });
96
+ return children;
97
+ };
98
+ const paragraphFromDirectContent = (element) => {
99
+ const inline = inlineNodes(Array.from(element.childNodes).filter((node) => !(node instanceof Element && blockTags.has(node.tagName.toLowerCase()))));
100
+ return inline.length ? { type: "paragraph", alignment: alignmentFor(element), children: inline } : null;
101
+ };
102
+ const alignmentFor = (element) => {
103
+ const value = (element.style.textAlign || element.getAttribute("align") || "").toLowerCase();
104
+ return value === "center" || value === "right" || value === "justify" ? value : undefined;
105
+ };
106
+ const parseBlocks = (parent) => directBlockChildren(parent).flatMap(parseBlock);
107
+ const parseList = (element) => {
108
+ const style = element.tagName.toLowerCase() === "ol" ? "decimal" : "disc";
109
+ const items = Array.from(element.children)
110
+ .filter((child) => child.tagName.toLowerCase() === "li")
111
+ .map((item) => {
112
+ const content = paragraphFromDirectContent(item);
113
+ const nested = Array.from(item.children)
114
+ .filter((child) => ["ul", "ol", "blockquote", "pre", "table", "p", "h1", "h2", "h3", "h4", "h5", "h6"].includes(child.tagName.toLowerCase()))
115
+ .flatMap(parseBlock);
116
+ return { type: "listItem", alignment: alignmentFor(item), children: [...(content ? [content] : []), ...nested] };
117
+ });
118
+ return { type: "list", style, children: items };
119
+ };
120
+ const tableRows = (table) => {
121
+ const rows = [];
122
+ Array.from(table.children).forEach((child) => {
123
+ if (child.tagName.toLowerCase() === "tr")
124
+ rows.push(child);
125
+ else if (["thead", "tbody", "tfoot"].includes(child.tagName.toLowerCase())) {
126
+ rows.push(...Array.from(child.children).filter((row) => row.tagName.toLowerCase() === "tr"));
127
+ }
128
+ });
129
+ return rows;
130
+ };
131
+ const parseTable = (element) => ({
132
+ type: "table",
133
+ children: tableRows(element).map((row) => ({
134
+ type: "tableRow",
135
+ children: Array.from(row.children)
136
+ .filter((cell) => ["td", "th"].includes(cell.tagName.toLowerCase()))
137
+ .map((cell) => ({
138
+ type: cell.tagName.toLowerCase() === "th" ? "tableHeaderCell" : "tableCell",
139
+ colspan: Number(cell.getAttribute("colspan")) || undefined,
140
+ rowspan: Number(cell.getAttribute("rowspan")) || undefined,
141
+ children: parseBlocks(cell).length ? parseBlocks(cell) : [{ type: "paragraph", alignment: alignmentFor(cell), children: inlineNodes(cell.childNodes) }],
142
+ })),
143
+ })),
144
+ });
145
+ const parseBlock = (element) => {
146
+ const tag = element.tagName.toLowerCase();
147
+ if (tag === "p")
148
+ return [{ type: "paragraph", alignment: alignmentFor(element), children: inlineNodes(element.childNodes) }];
149
+ if (/^h[1-6]$/.test(tag))
150
+ return [{ type: "heading", level: Number(tag.slice(1)), alignment: alignmentFor(element), children: inlineNodes(element.childNodes) }];
151
+ if (tag === "ul" || tag === "ol")
152
+ return [parseList(element)];
153
+ if (tag === "blockquote")
154
+ return [{ type: "blockquote", alignment: alignmentFor(element), children: parseBlocks(element) }];
155
+ if (tag === "pre")
156
+ return [{ type: "codeBlock", alignment: alignmentFor(element), text: element.textContent || "", language: element.querySelector("code")?.className.replace(/^language-/, "") || undefined }];
157
+ if (tag === "table")
158
+ return [parseTable(element)];
159
+ return [];
160
+ };
161
+ export const smartDocumentFromHtml = (html, ownerDocument) => {
162
+ const container = ownerDocument.createElement("div");
163
+ container.innerHTML = normalizeCompatibilityHtml(html);
164
+ return { type: "doc", children: parseBlocks(container) };
165
+ };
166
+ export const smartDocumentFromEditorRoot = (root) => {
167
+ const html = cleanEditorHtml(root);
168
+ return { document: smartDocumentFromHtml(html, root.ownerDocument), html };
169
+ };
170
+ const escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;");
171
+ const serializeText = (node) => {
172
+ let html = escapeHtml(node.text).replace(/\n/g, "<br>");
173
+ (node.marks || []).forEach((mark) => {
174
+ if (mark.type === "bold")
175
+ html = `<strong>${html}</strong>`;
176
+ if (mark.type === "italic")
177
+ html = `<em>${html}</em>`;
178
+ if (mark.type === "underline")
179
+ html = `<u>${html}</u>`;
180
+ if (mark.type === "strike")
181
+ html = `<s>${html}</s>`;
182
+ if (mark.type === "superscript")
183
+ html = `<sup>${html}</sup>`;
184
+ if (mark.type === "subscript")
185
+ html = `<sub>${html}</sub>`;
186
+ if (mark.type === "code")
187
+ html = `<code>${html}</code>`;
188
+ if (mark.type === "textColor")
189
+ html = `<span style="color:${escapeHtml(mark.value)}">${html}</span>`;
190
+ if (mark.type === "backgroundColor")
191
+ html = `<span style="background-color:${escapeHtml(mark.value)}">${html}</span>`;
192
+ if (mark.type === "fontSize")
193
+ html = `<span style="font-size:${mark.valuePx}px">${html}</span>`;
194
+ if (mark.type === "link")
195
+ html = `<a href="${escapeHtml(mark.href)}"${mark.target ? ` target="${escapeHtml(mark.target)}"` : ""}>${html}</a>`;
196
+ });
197
+ return html;
198
+ };
199
+ const alignmentAttribute = (alignment) => alignment ? ` style="text-align:${alignment}"` : "";
200
+ const serializeBlock = (block) => {
201
+ if (block.type === "paragraph")
202
+ return `<p${alignmentAttribute(block.alignment)}>${block.children.map(serializeText).join("")}</p>`;
203
+ if (block.type === "heading")
204
+ return `<h${block.level}${alignmentAttribute(block.alignment)}>${block.children.map(serializeText).join("")}</h${block.level}>`;
205
+ if (block.type === "blockquote")
206
+ return `<blockquote${alignmentAttribute(block.alignment)}>${block.children.map(serializeBlock).join("")}</blockquote>`;
207
+ if (block.type === "codeBlock")
208
+ return `<pre${alignmentAttribute(block.alignment)}><code${block.language ? ` class="language-${escapeHtml(block.language)}"` : ""}>${escapeHtml(block.text)}</code></pre>`;
209
+ if (block.type === "list") {
210
+ const tag = block.style === "decimal" ? "ol" : "ul";
211
+ return `<${tag}>${block.children.map((item) => `<li${alignmentAttribute(item.alignment)}>${item.children.map(serializeBlock).join("")}</li>`).join("")}</${tag}>`;
212
+ }
213
+ return `<table><tbody>${block.children.map((row) => `<tr>${row.children.map((cell) => `<${cell.type === "tableHeaderCell" ? "th" : "td"}>${cell.children.map(serializeBlock).join("")}</${cell.type === "tableHeaderCell" ? "th" : "td"}>`).join("")}</tr>`).join("")}</tbody></table>`;
214
+ };
215
+ /** Serializes the shadow-only model. It is not used for persisted editor HTML. */
216
+ export const serializeSmartDocument = (document) => document.children.map(serializeBlock).join("");
@@ -0,0 +1,3 @@
1
+ export declare const isElement: (target: EventTarget | null) => target is Element;
2
+ export declare const isNode: (target: EventTarget | null) => target is Node;
3
+ export declare const closestFromTarget: (target: EventTarget | null, selector: string) => Element | null;
@@ -0,0 +1,3 @@
1
+ export const isElement = (target) => typeof Element !== "undefined" && target instanceof Element;
2
+ export const isNode = (target) => typeof Node !== "undefined" && target instanceof Node;
3
+ export const closestFromTarget = (target, selector) => isElement(target) ? target.closest(selector) : null;
@@ -0,0 +1,9 @@
1
+ import { type SmartSelection } from "smartrte-core";
2
+ export type CoreInlineMark = "bold" | "italic" | "underline" | "superscript" | "subscript";
3
+ export interface CoreInlineMarkResult {
4
+ html: string;
5
+ selectionBefore: SmartSelection;
6
+ selectionAfter: SmartSelection;
7
+ }
8
+ export declare const isCoreInlineMarkEnabled: (mark: CoreInlineMark) => boolean;
9
+ export declare const getCoreInlineMarkResult: (root: HTMLElement, mark: CoreInlineMark) => CoreInlineMarkResult | null;
@@ -0,0 +1,25 @@
1
+ import { applyTransaction, toggleBold, toggleItalic, toggleSubscript, toggleSuperscript, toggleUnderline, } from "smartrte-core";
2
+ import { selectionFromDom } from "./domSelectionBridge.js";
3
+ import { serializeSmartDocument, smartDocumentFromEditorRoot } from "./domSmartDocument.js";
4
+ import { isCoreInlineMarkFlagEnabled } from "./internalFlags.js";
5
+ const commands = {
6
+ bold: toggleBold,
7
+ italic: toggleItalic,
8
+ underline: toggleUnderline,
9
+ superscript: toggleSuperscript,
10
+ subscript: toggleSubscript,
11
+ };
12
+ export const isCoreInlineMarkEnabled = (mark) => isCoreInlineMarkFlagEnabled(mark);
13
+ export const getCoreInlineMarkResult = (root, mark) => {
14
+ const selection = selectionFromDom(root, window.getSelection());
15
+ if (!selection)
16
+ return null;
17
+ const { document } = smartDocumentFromEditorRoot(root);
18
+ const state = { document, selection };
19
+ const command = commands[mark];
20
+ if (!command.isEnabled(state))
21
+ return null;
22
+ const transaction = command.execute(state);
23
+ const next = applyTransaction(state, transaction);
24
+ return { html: serializeSmartDocument(next.document), selectionBefore: transaction.selectionBefore, selectionAfter: transaction.selectionAfter };
25
+ };
@@ -0,0 +1,13 @@
1
+ import type { CoreInlineMark } from "./inlineMarkCoreExecution.js";
2
+ export interface SmartRteInternalFlags {
3
+ coreBold?: boolean;
4
+ coreItalic?: boolean;
5
+ coreUnderline?: boolean;
6
+ coreSuperscript?: boolean;
7
+ coreSubscript?: boolean;
8
+ coreInlineMarks?: boolean;
9
+ shadowMode?: boolean;
10
+ }
11
+ export declare const getSmartRteInternalFlags: () => SmartRteInternalFlags;
12
+ export declare const isCoreInlineMarkFlagEnabled: (mark: CoreInlineMark) => boolean;
13
+ export declare const isShadowModeFlagEnabled: () => boolean;
@@ -0,0 +1,36 @@
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
+ const getGlobal = () => globalThis;
16
+ const readBoolean = (value) => typeof value === "boolean" ? value : undefined;
17
+ 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
+ export const isShadowModeFlagEnabled = () => {
29
+ const configured = readBoolean(getSmartRteInternalFlags().shadowMode);
30
+ if (configured !== undefined)
31
+ return configured;
32
+ const global = getGlobal();
33
+ if (global.process?.env)
34
+ return global.process.env.NODE_ENV !== "production";
35
+ return global.__SMART_RTE_SHADOW_MODE__ === true;
36
+ };
@@ -0,0 +1,23 @@
1
+ import { type CommandContext, type SmartCommand, type SmartEditorState, type SmartTransaction } from "smartrte-core";
2
+ export interface ShadowComparison {
3
+ commandId: string;
4
+ matches: boolean;
5
+ legacyHtml: string;
6
+ coreHtml: string;
7
+ }
8
+ export declare const isShadowModeEnabled: () => boolean;
9
+ /** Compares canonicalized HTML only; it never changes editor state or output. */
10
+ export declare const compareShadowHtml: (commandId: string, legacyHtml: string, coreHtml: string) => ShadowComparison;
11
+ export declare const reportShadowDifference: (comparison: ShadowComparison) => void;
12
+ /**
13
+ * Executes a core command in memory for diagnostics. The caller owns the
14
+ * legacy output; this helper never persists or applies the core result.
15
+ */
16
+ export declare const runShadowCommand: <Input>(args: {
17
+ command: SmartCommand<Input>;
18
+ context: CommandContext;
19
+ input?: Input;
20
+ state: SmartEditorState;
21
+ legacyHtml: string;
22
+ serialize: (state: SmartEditorState) => string;
23
+ }) => SmartTransaction | null;
@@ -0,0 +1,33 @@
1
+ import { applyTransaction, normalizeCompatibilityHtml, } from "smartrte-core";
2
+ import { isShadowModeFlagEnabled } from "./internalFlags.js";
3
+ const semanticHtml = (html) => normalizeCompatibilityHtml(html)
4
+ .replace(/<(\/?)b(?=[\s>])/g, "<$1strong")
5
+ .replace(/<(\/?)i(?=[\s>])/g, "<$1em")
6
+ .replace(/<(\/?)strike(?=[\s>])/g, "<$1s");
7
+ export const isShadowModeEnabled = () => {
8
+ return isShadowModeFlagEnabled();
9
+ };
10
+ /** Compares canonicalized HTML only; it never changes editor state or output. */
11
+ export const compareShadowHtml = (commandId, legacyHtml, coreHtml) => ({
12
+ commandId,
13
+ matches: semanticHtml(legacyHtml) === semanticHtml(coreHtml),
14
+ legacyHtml: semanticHtml(legacyHtml),
15
+ coreHtml: semanticHtml(coreHtml),
16
+ });
17
+ export const reportShadowDifference = (comparison) => {
18
+ if (!isShadowModeEnabled() || comparison.matches)
19
+ return;
20
+ console.warn(`[Smart RTE shadow mode] ${comparison.commandId} produced a semantic HTML mismatch.`, comparison);
21
+ };
22
+ /**
23
+ * Executes a core command in memory for diagnostics. The caller owns the
24
+ * legacy output; this helper never persists or applies the core result.
25
+ */
26
+ export const runShadowCommand = (args) => {
27
+ if (!isShadowModeEnabled() || !args.command.isEnabled(args.context, args.input))
28
+ return null;
29
+ const transaction = args.command.execute(args.context, args.input);
30
+ const coreHtml = args.serialize(applyTransaction(args.state, transaction));
31
+ reportShadowDifference(compareShadowHtml(args.command.id, args.legacyHtml, coreHtml));
32
+ return transaction;
33
+ };