smartrte-react 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +14 -83
- package/dist/adapters/canonicalInlineCommandBridge.d.ts +15 -0
- package/dist/adapters/canonicalInlineCommandBridge.js +269 -0
- package/dist/adapters/canonicalListCommandBridge.d.ts +26 -0
- package/dist/adapters/canonicalListCommandBridge.js +430 -0
- package/dist/adapters/documentFormats.d.ts +26 -0
- package/dist/adapters/documentFormats.js +49 -0
- package/dist/adapters/docxFormat.d.ts +5 -0
- package/dist/adapters/docxFormat.js +272 -0
- package/dist/adapters/domBlockCommandBridge.d.ts +19 -0
- package/dist/adapters/domBlockCommandBridge.js +79 -0
- package/dist/adapters/domChecklistCommandBridge.d.ts +6 -0
- package/dist/adapters/domChecklistCommandBridge.js +33 -0
- package/dist/adapters/domCommandBridge.d.ts +19 -0
- package/dist/adapters/domCommandBridge.js +33 -0
- package/dist/adapters/domInlineAtomCommandBridge.d.ts +14 -0
- package/dist/adapters/domInlineAtomCommandBridge.js +101 -0
- package/dist/adapters/domInlineImageCommandBridge.d.ts +14 -0
- package/dist/adapters/domInlineImageCommandBridge.js +72 -0
- package/dist/adapters/domMoveCommandBridge.d.ts +7 -0
- package/dist/adapters/domMoveCommandBridge.js +54 -0
- package/dist/adapters/domSelectionBridge.d.ts +3 -1
- package/dist/adapters/domSelectionBridge.js +96 -44
- package/dist/adapters/domSmartDocument.js +158 -16
- package/dist/adapters/domTableCommandBridge.d.ts +35 -0
- package/dist/adapters/domTableCommandBridge.js +201 -0
- package/dist/adapters/legacyListShadowComparator.d.ts +26 -0
- package/dist/adapters/legacyListShadowComparator.js +316 -0
- package/dist/adapters/pdfFormat.d.ts +23 -0
- package/dist/adapters/pdfFormat.js +204 -0
- package/dist/adapters/portableDocxAtoms.d.ts +4 -0
- package/dist/adapters/portableDocxAtoms.js +58 -0
- package/dist/adapters/styledDocxFormat.d.ts +8 -0
- package/dist/adapters/styledDocxFormat.js +190 -0
- package/dist/builtInFormatDefinitions.d.ts +5 -0
- package/dist/builtInFormatDefinitions.js +82 -0
- package/dist/canonicalAuthorityFlag.d.ts +32 -0
- package/dist/canonicalAuthorityFlag.js +49 -0
- package/dist/canonicalClipboardRuntime.d.ts +15 -0
- package/dist/canonicalClipboardRuntime.js +76 -0
- package/dist/canonicalEditorRuntime.d.ts +77 -0
- package/dist/canonicalEditorRuntime.js +274 -0
- package/dist/components/CanonicalAuthorityEditor.d.ts +25 -0
- package/dist/components/CanonicalAuthorityEditor.js +518 -0
- package/dist/components/ClassicEditor.d.ts +13 -3
- package/dist/components/ClassicEditor.js +1309 -1159
- package/dist/components/ClassicEditorAuthority.d.ts +38 -0
- package/dist/components/ClassicEditorAuthority.js +49 -0
- package/dist/components/MediaPicker.d.ts +10 -0
- package/dist/components/MediaPicker.js +16 -0
- package/dist/editorController.d.ts +78 -0
- package/dist/editorController.js +298 -0
- package/dist/formatFidelity.d.ts +14 -0
- package/dist/formatFidelity.js +107 -0
- package/dist/formatRuntime.d.ts +50 -0
- package/dist/formatRuntime.js +20 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +10 -0
- package/dist/mediaProvider.d.ts +39 -0
- package/dist/mediaProvider.js +1 -0
- package/dist/pluginRuntime.d.ts +68 -0
- package/dist/pluginRuntime.js +78 -0
- package/dist/standalone/editor.js +722 -495
- package/dist/test-harness/atomShadowComparator.d.ts +14 -0
- package/dist/test-harness/atomShadowComparator.js +63 -0
- package/dist/test-harness/blockShadowComparator.d.ts +30 -0
- package/dist/test-harness/blockShadowComparator.js +140 -0
- package/dist/test-harness/clipboardShadowComparator.d.ts +12 -0
- package/dist/test-harness/clipboardShadowComparator.js +46 -0
- package/dist/test-harness/inlineShadowComparator.d.ts +32 -0
- package/dist/test-harness/inlineShadowComparator.js +152 -0
- package/dist/test-harness/legacyAtomEngine.d.ts +10 -0
- package/dist/test-harness/legacyAtomEngine.js +15 -0
- package/dist/test-harness/legacyBlockEngine.d.ts +31 -0
- package/dist/test-harness/legacyBlockEngine.js +31 -0
- package/dist/test-harness/legacyClipboardEngine.d.ts +14 -0
- package/dist/test-harness/legacyClipboardEngine.js +67 -0
- package/dist/test-harness/legacyInlineEngine.d.ts +19 -0
- package/dist/test-harness/legacyInlineEngine.js +49 -0
- package/dist/test-harness/legacyTableEngine.d.ts +12 -0
- package/dist/test-harness/legacyTableEngine.js +25 -0
- package/dist/test-harness/tableShadowComparator.d.ts +29 -0
- package/dist/test-harness/tableShadowComparator.js +101 -0
- package/dist/theme.d.ts +1 -1
- package/dist/theme.js +28 -6
- package/package.json +1 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { normalizeSmartDocument } from "smartrte-core";
|
|
2
|
+
import { exportTextDocument } from "./documentFormats.js";
|
|
3
|
+
import { smartDocumentFromHtml } from "./domSmartDocument.js";
|
|
4
|
+
export const PDF_MEDIA_TYPE = "application/pdf";
|
|
5
|
+
const escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6
|
+
const spanFor = (item) => {
|
|
7
|
+
const family = item.fontFamily?.toLowerCase() || "";
|
|
8
|
+
const styles = [
|
|
9
|
+
`font-size:${Math.max(8, Math.round(item.height))}px`,
|
|
10
|
+
family.includes("bold") ? "font-weight:700" : "",
|
|
11
|
+
family.includes("italic") || family.includes("oblique") ? "font-style:italic" : "",
|
|
12
|
+
].filter(Boolean).join(";");
|
|
13
|
+
return `<span style="${styles}">${escapeHtml(item.text)}</span>`;
|
|
14
|
+
};
|
|
15
|
+
const lineContent = (items) => {
|
|
16
|
+
let previousRight;
|
|
17
|
+
let text = "";
|
|
18
|
+
let html = "";
|
|
19
|
+
const starts = [];
|
|
20
|
+
const gaps = [];
|
|
21
|
+
items.forEach((item, index) => {
|
|
22
|
+
const gap = previousRight === undefined ? 0 : item.x - previousRight;
|
|
23
|
+
if (index === 0 || gap > 20)
|
|
24
|
+
starts.push(item.x);
|
|
25
|
+
if (gap > 2) {
|
|
26
|
+
text += " ";
|
|
27
|
+
html += " ";
|
|
28
|
+
if (gap > 20)
|
|
29
|
+
gaps.push(gap);
|
|
30
|
+
}
|
|
31
|
+
text += item.text;
|
|
32
|
+
html += spanFor(item);
|
|
33
|
+
previousRight = item.x + item.width;
|
|
34
|
+
});
|
|
35
|
+
return { text, html, starts, gaps };
|
|
36
|
+
};
|
|
37
|
+
const alignmentForLine = (items, pageWidth) => {
|
|
38
|
+
const first = items[0]?.x || 0;
|
|
39
|
+
const last = items[items.length - 1];
|
|
40
|
+
const right = last ? last.x + last.width : first;
|
|
41
|
+
const center = (first + right) / 2;
|
|
42
|
+
if (Math.abs(center - pageWidth / 2) < pageWidth * 0.12)
|
|
43
|
+
return "center";
|
|
44
|
+
if (first > pageWidth * 0.55)
|
|
45
|
+
return "right";
|
|
46
|
+
return "";
|
|
47
|
+
};
|
|
48
|
+
export const reconstructPdfPages = (pages, ownerDocument) => {
|
|
49
|
+
let layoutHtml = "";
|
|
50
|
+
pages.forEach((page) => {
|
|
51
|
+
const heights = page.items.map((item) => item.height).filter((height) => height > 0).sort((a, b) => a - b);
|
|
52
|
+
const medianHeight = heights[Math.floor(heights.length / 2)] || 12;
|
|
53
|
+
const lines = [];
|
|
54
|
+
page.items.filter((item) => item.text.trim()).forEach((item) => {
|
|
55
|
+
const line = lines.find((candidate) => Math.abs(candidate.y - item.y) < medianHeight * 0.5);
|
|
56
|
+
if (line)
|
|
57
|
+
line.items.push(item);
|
|
58
|
+
else
|
|
59
|
+
lines.push({ y: item.y, items: [item] });
|
|
60
|
+
});
|
|
61
|
+
lines.sort((left, right) => right.y - left.y);
|
|
62
|
+
lines.forEach((line) => line.items.sort((left, right) => left.x - right.x));
|
|
63
|
+
let pageHtml = "";
|
|
64
|
+
let activeList;
|
|
65
|
+
let tableColumns = [];
|
|
66
|
+
let tableRows = [];
|
|
67
|
+
const closeList = () => {
|
|
68
|
+
if (activeList)
|
|
69
|
+
pageHtml += `</${activeList}>`;
|
|
70
|
+
activeList = undefined;
|
|
71
|
+
};
|
|
72
|
+
const closeTable = () => {
|
|
73
|
+
if (tableRows.length) {
|
|
74
|
+
pageHtml += `<table style="border-collapse:collapse;width:100%"><tbody>${tableRows.join("")}</tbody></table>`;
|
|
75
|
+
}
|
|
76
|
+
tableRows = [];
|
|
77
|
+
tableColumns = [];
|
|
78
|
+
};
|
|
79
|
+
lines.forEach((line) => {
|
|
80
|
+
const content = lineContent(line.items);
|
|
81
|
+
const alignsWithTable = tableColumns.length > 0
|
|
82
|
+
&& content.starts.some((x) => tableColumns.some((column) => Math.abs(column - x) < 20));
|
|
83
|
+
const beginsTable = content.starts.length >= 2 && content.gaps.some((gap) => gap > 30);
|
|
84
|
+
if (beginsTable || alignsWithTable) {
|
|
85
|
+
closeList();
|
|
86
|
+
if (!tableColumns.length)
|
|
87
|
+
tableColumns = [...content.starts];
|
|
88
|
+
const cells = new Array(tableColumns.length).fill("");
|
|
89
|
+
line.items.forEach((item) => {
|
|
90
|
+
let column = 0;
|
|
91
|
+
tableColumns.forEach((start, index) => {
|
|
92
|
+
if (item.x >= start - 10)
|
|
93
|
+
column = index;
|
|
94
|
+
});
|
|
95
|
+
cells[column] += `${cells[column] ? " " : ""}${spanFor(item)}`;
|
|
96
|
+
});
|
|
97
|
+
tableRows.push(`<tr>${cells.map((cell) => `<td style="border:1px solid #d1d5db;padding:8px;vertical-align:top">${cell || " "}</td>`).join("")}</tr>`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
closeTable();
|
|
101
|
+
const bullet = /^[•\-*]\s/.test(content.text);
|
|
102
|
+
const numbered = /^\d+[.)]\s/.test(content.text);
|
|
103
|
+
if (bullet || numbered) {
|
|
104
|
+
const type = bullet ? "ul" : "ol";
|
|
105
|
+
if (activeList !== type) {
|
|
106
|
+
closeList();
|
|
107
|
+
activeList = type;
|
|
108
|
+
pageHtml += `<${type}>`;
|
|
109
|
+
}
|
|
110
|
+
const listItems = line.items.map((item, index) => index === 0
|
|
111
|
+
? { ...item, text: item.text.replace(bullet ? /^[•\-*]\s*/ : /^\d+[.)]\s*/, "") }
|
|
112
|
+
: item);
|
|
113
|
+
pageHtml += `<li>${lineContent(listItems).html.trim()}</li>`;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
closeList();
|
|
117
|
+
const largest = Math.max(...line.items.map((item) => item.height));
|
|
118
|
+
const heading = largest > medianHeight * 1.2;
|
|
119
|
+
const tag = heading ? (largest > medianHeight * 1.5 ? "h2" : "h3") : "p";
|
|
120
|
+
const alignment = alignmentForLine(line.items, page.width);
|
|
121
|
+
const indent = !alignment && line.items[0]?.x > 40 ? `margin-left:${Math.round(line.items[0].x)}px` : "";
|
|
122
|
+
const style = [alignment ? `text-align:${alignment}` : "", indent].filter(Boolean).join(";");
|
|
123
|
+
pageHtml += `<${tag}${style ? ` style="${style}"` : ""}>${content.html}</${tag}>`;
|
|
124
|
+
});
|
|
125
|
+
closeList();
|
|
126
|
+
closeTable();
|
|
127
|
+
layoutHtml += pageHtml;
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
document: normalizeSmartDocument(smartDocumentFromHtml(layoutHtml, ownerDocument)),
|
|
131
|
+
layoutHtml,
|
|
132
|
+
pages: pages.length,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
export const importPdfDocument = async (arrayBuffer, ownerDocument) => {
|
|
136
|
+
const pdfjsLib = await import("pdfjs-dist");
|
|
137
|
+
if (typeof window !== "undefined") {
|
|
138
|
+
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).toString();
|
|
139
|
+
}
|
|
140
|
+
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
|
141
|
+
const pages = [];
|
|
142
|
+
for (let index = 1; index <= pdf.numPages; index += 1) {
|
|
143
|
+
const page = await pdf.getPage(index);
|
|
144
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
145
|
+
const content = await page.getTextContent();
|
|
146
|
+
const styles = content.styles;
|
|
147
|
+
pages.push({
|
|
148
|
+
width: viewport.width,
|
|
149
|
+
items: content.items.map((item) => ({
|
|
150
|
+
text: item.str || "",
|
|
151
|
+
x: item.transform[4],
|
|
152
|
+
y: item.transform[5],
|
|
153
|
+
width: item.width || 0,
|
|
154
|
+
height: Math.abs(item.transform[3]) || 0,
|
|
155
|
+
fontFamily: styles[item.fontName]?.fontFamily,
|
|
156
|
+
})),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return reconstructPdfPages(pages, ownerDocument);
|
|
160
|
+
};
|
|
161
|
+
export const buildPdfPrintDocument = (document) => {
|
|
162
|
+
const html = exportTextDocument("html", document);
|
|
163
|
+
return `<!doctype html>
|
|
164
|
+
<html lang="en">
|
|
165
|
+
<head>
|
|
166
|
+
<meta charset="utf-8">
|
|
167
|
+
<title>Smart RTE PDF Export</title>
|
|
168
|
+
<style>
|
|
169
|
+
@page { margin: 18mm; }
|
|
170
|
+
html, body { background: #fff; }
|
|
171
|
+
body {
|
|
172
|
+
color: #111;
|
|
173
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
174
|
+
line-height: 1.6;
|
|
175
|
+
padding: 32px;
|
|
176
|
+
}
|
|
177
|
+
table { border-collapse: collapse; width: 100%; margin: 12px 0; break-inside: auto; }
|
|
178
|
+
tr, img, blockquote, pre { break-inside: avoid; }
|
|
179
|
+
td, th { border: 1px solid #d1d5db; padding: 8px; vertical-align: top; }
|
|
180
|
+
img { max-width: 100%; height: auto; }
|
|
181
|
+
blockquote { border-left: 4px solid #d1d5db; padding-left: 12px; color: #374151; }
|
|
182
|
+
pre, code { background: #f3f4f6; }
|
|
183
|
+
pre { padding: 12px; white-space: pre-wrap; }
|
|
184
|
+
@media print { body { padding: 0; } }
|
|
185
|
+
</style>
|
|
186
|
+
</head>
|
|
187
|
+
<body>${html || "<p></p>"}</body>
|
|
188
|
+
</html>`;
|
|
189
|
+
};
|
|
190
|
+
export const printSmartDocumentAsPdf = (document, hostWindow, delayMs = 150) => {
|
|
191
|
+
const printWindow = hostWindow.open("", "_blank", "width=900,height=700");
|
|
192
|
+
if (!printWindow)
|
|
193
|
+
return false;
|
|
194
|
+
printWindow.addEventListener("load", () => {
|
|
195
|
+
printWindow.setTimeout(() => {
|
|
196
|
+
printWindow.focus();
|
|
197
|
+
printWindow.print();
|
|
198
|
+
}, delayMs);
|
|
199
|
+
}, { once: true });
|
|
200
|
+
printWindow.document.open();
|
|
201
|
+
printWindow.document.write(buildPdfPrintDocument(document));
|
|
202
|
+
printWindow.document.close();
|
|
203
|
+
return true;
|
|
204
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const portableFormulaMarker: (value: string, displayText?: string) => string;
|
|
2
|
+
export declare const portableImageMarker: (src: string, alt?: string, title?: string) => string;
|
|
3
|
+
/** Restores Smart RTE atom fallback markers retained by DOCX text runs. */
|
|
4
|
+
export declare const restorePortableDocxAtoms: (html: string, ownerDocument: Document) => string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const marker = /⟦SRTE_(FORMULA|IMAGE):([^⟧]+)⟧/g;
|
|
2
|
+
export const portableFormulaMarker = (value, displayText) => `⟦SRTE_FORMULA:${encodeURIComponent(JSON.stringify({ value, displayText }))}⟧`;
|
|
3
|
+
export const portableImageMarker = (src, alt, title) => `⟦SRTE_IMAGE:${encodeURIComponent(JSON.stringify({ src, alt, title }))}⟧`;
|
|
4
|
+
/** Restores Smart RTE atom fallback markers retained by DOCX text runs. */
|
|
5
|
+
export const restorePortableDocxAtoms = (html, ownerDocument) => {
|
|
6
|
+
const root = ownerDocument.createElement("div");
|
|
7
|
+
root.innerHTML = html;
|
|
8
|
+
const walker = ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
9
|
+
const textNodes = [];
|
|
10
|
+
let current = walker.nextNode();
|
|
11
|
+
while (current) {
|
|
12
|
+
textNodes.push(current);
|
|
13
|
+
current = walker.nextNode();
|
|
14
|
+
}
|
|
15
|
+
textNodes.forEach((textNode) => {
|
|
16
|
+
const text = textNode.data;
|
|
17
|
+
marker.lastIndex = 0;
|
|
18
|
+
if (!marker.test(text))
|
|
19
|
+
return;
|
|
20
|
+
marker.lastIndex = 0;
|
|
21
|
+
const fragment = ownerDocument.createDocumentFragment();
|
|
22
|
+
let cursor = 0;
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = marker.exec(text))) {
|
|
25
|
+
if (match.index > cursor)
|
|
26
|
+
fragment.append(text.slice(cursor, match.index));
|
|
27
|
+
try {
|
|
28
|
+
const payload = JSON.parse(decodeURIComponent(match[2]));
|
|
29
|
+
if (match[1] === "FORMULA" && payload.value) {
|
|
30
|
+
const formula = ownerDocument.createElement("span");
|
|
31
|
+
formula.dataset.formula = payload.value;
|
|
32
|
+
formula.textContent = payload.displayText || `$${payload.value}$`;
|
|
33
|
+
fragment.append(formula);
|
|
34
|
+
}
|
|
35
|
+
else if (match[1] === "IMAGE" && payload.src) {
|
|
36
|
+
const image = ownerDocument.createElement("img");
|
|
37
|
+
image.src = payload.src;
|
|
38
|
+
image.alt = payload.alt || "";
|
|
39
|
+
if (payload.title)
|
|
40
|
+
image.title = payload.title;
|
|
41
|
+
image.dataset.srteInline = "true";
|
|
42
|
+
fragment.append(image);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
fragment.append(match[0]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
fragment.append(match[0]);
|
|
50
|
+
}
|
|
51
|
+
cursor = marker.lastIndex;
|
|
52
|
+
}
|
|
53
|
+
if (cursor < text.length)
|
|
54
|
+
fragment.append(text.slice(cursor));
|
|
55
|
+
textNode.replaceWith(fragment);
|
|
56
|
+
});
|
|
57
|
+
return root.innerHTML;
|
|
58
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type SmartDocument } from "smartrte-core";
|
|
2
|
+
export interface StyledDocxImportResult {
|
|
3
|
+
document: SmartDocument;
|
|
4
|
+
layoutHtml: string;
|
|
5
|
+
source: "wordprocessingml" | "mammoth";
|
|
6
|
+
}
|
|
7
|
+
export declare const enhanceDocxTables: (root: HTMLElement) => void;
|
|
8
|
+
export declare const importStyledDocxDocument: (arrayBuffer: ArrayBuffer, ownerDocument: Document) => Promise<StyledDocxImportResult>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import JSZip from "jszip";
|
|
2
|
+
import { normalizeSmartDocument } from "smartrte-core";
|
|
3
|
+
import { restorePortableDocxAtoms } from "./portableDocxAtoms.js";
|
|
4
|
+
import { serializeSmartDocument, smartDocumentFromHtml } from "./domSmartDocument.js";
|
|
5
|
+
import { importDocxDocumentWithMammoth } from "./docxFormat.js";
|
|
6
|
+
const directChildren = (node) => Array.from(node.children);
|
|
7
|
+
const firstChildByName = (node, localName) => node ? directChildren(node).find((child) => child.localName === localName) : undefined;
|
|
8
|
+
const childrenByName = (node, localName) => node ? directChildren(node).filter((child) => child.localName === localName) : [];
|
|
9
|
+
const docxAttr = (node, name) => {
|
|
10
|
+
if (!node)
|
|
11
|
+
return "";
|
|
12
|
+
return node.getAttribute(`w:${name}`)
|
|
13
|
+
|| node.getAttribute(name)
|
|
14
|
+
|| node.getAttributeNS("http://schemas.openxmlformats.org/wordprocessingml/2006/main", name)
|
|
15
|
+
|| "";
|
|
16
|
+
};
|
|
17
|
+
const escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
18
|
+
const docxHexColor = (value) => {
|
|
19
|
+
if (!value || value.toLowerCase() === "auto")
|
|
20
|
+
return "";
|
|
21
|
+
const normalized = value.replace(/[^0-9a-f]/gi, "");
|
|
22
|
+
return normalized.length === 6 ? `#${normalized}` : "";
|
|
23
|
+
};
|
|
24
|
+
const twipsToPt = (value) => {
|
|
25
|
+
if (!value)
|
|
26
|
+
return "";
|
|
27
|
+
const numeric = Number(value);
|
|
28
|
+
return Number.isFinite(numeric) ? `${Math.max(numeric / 20, 0)}pt` : "";
|
|
29
|
+
};
|
|
30
|
+
const halfPointsToPt = (value) => {
|
|
31
|
+
if (!value)
|
|
32
|
+
return "";
|
|
33
|
+
const numeric = Number(value);
|
|
34
|
+
return Number.isFinite(numeric) ? `${Math.max(numeric / 2, 1)}pt` : "";
|
|
35
|
+
};
|
|
36
|
+
const cssRules = (rules) => rules.filter(([, value]) => Boolean(value)).map(([name, value]) => `${name}: ${value}`).join("; ");
|
|
37
|
+
const styleAttr = (style) => style ? ` style="${escapeHtml(style)}"` : "";
|
|
38
|
+
const borderCss = (border) => {
|
|
39
|
+
if (!border)
|
|
40
|
+
return "";
|
|
41
|
+
const value = docxAttr(border, "val");
|
|
42
|
+
if (!value || value === "nil" || value === "none")
|
|
43
|
+
return "";
|
|
44
|
+
const size = Number(docxAttr(border, "sz")) || 4;
|
|
45
|
+
const color = docxHexColor(docxAttr(border, "color")) || "#d1d5db";
|
|
46
|
+
return `${Math.max(size / 8, 0.5)}px solid ${color}`;
|
|
47
|
+
};
|
|
48
|
+
const paragraphStyle = (paragraph) => {
|
|
49
|
+
const properties = firstChildByName(paragraph, "pPr");
|
|
50
|
+
if (!properties)
|
|
51
|
+
return "";
|
|
52
|
+
const spacing = firstChildByName(properties, "spacing");
|
|
53
|
+
const justification = firstChildByName(properties, "jc");
|
|
54
|
+
const indent = firstChildByName(properties, "ind");
|
|
55
|
+
const bottomBorder = firstChildByName(firstChildByName(properties, "pBdr"), "bottom");
|
|
56
|
+
const line = docxAttr(spacing, "line");
|
|
57
|
+
return cssRules([
|
|
58
|
+
["text-align", docxAttr(justification, "val")],
|
|
59
|
+
["margin-top", twipsToPt(docxAttr(spacing, "before"))],
|
|
60
|
+
["margin-bottom", twipsToPt(docxAttr(spacing, "after"))],
|
|
61
|
+
["margin-left", twipsToPt(docxAttr(indent, "left"))],
|
|
62
|
+
["text-indent", twipsToPt(docxAttr(indent, "firstLine"))],
|
|
63
|
+
["line-height", line && docxAttr(spacing, "lineRule") === "auto" ? `${Number(line) / 240}` : ""],
|
|
64
|
+
["border-bottom", borderCss(bottomBorder)],
|
|
65
|
+
]);
|
|
66
|
+
};
|
|
67
|
+
const runStyle = (run) => {
|
|
68
|
+
const properties = firstChildByName(run, "rPr");
|
|
69
|
+
if (!properties)
|
|
70
|
+
return "";
|
|
71
|
+
const color = docxHexColor(docxAttr(firstChildByName(properties, "color"), "val"));
|
|
72
|
+
const highlight = docxHexColor(docxAttr(firstChildByName(properties, "highlight"), "val"));
|
|
73
|
+
const shade = docxHexColor(docxAttr(firstChildByName(properties, "shd"), "fill"));
|
|
74
|
+
return cssRules([
|
|
75
|
+
["font-weight", firstChildByName(properties, "b") ? "700" : ""],
|
|
76
|
+
["font-style", firstChildByName(properties, "i") ? "italic" : ""],
|
|
77
|
+
["text-decoration", firstChildByName(properties, "u") ? "underline" : ""],
|
|
78
|
+
["color", color],
|
|
79
|
+
["background-color", highlight || shade],
|
|
80
|
+
["font-size", halfPointsToPt(docxAttr(firstChildByName(properties, "sz"), "val"))],
|
|
81
|
+
]);
|
|
82
|
+
};
|
|
83
|
+
const runHtml = (run) => {
|
|
84
|
+
const properties = firstChildByName(run, "rPr");
|
|
85
|
+
const verticalAlignment = docxAttr(firstChildByName(properties, "vertAlign"), "val");
|
|
86
|
+
const content = directChildren(run).map((child) => {
|
|
87
|
+
if (child.localName === "t")
|
|
88
|
+
return escapeHtml(child.textContent || "");
|
|
89
|
+
if (child.localName === "tab")
|
|
90
|
+
return " ";
|
|
91
|
+
if (child.localName === "br") {
|
|
92
|
+
return docxAttr(child, "type") === "page" ? '<hr class="srte-docx-page-break">' : "<br>";
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}).join("");
|
|
96
|
+
if (!content)
|
|
97
|
+
return "";
|
|
98
|
+
const tag = verticalAlignment === "superscript" ? "sup" : verticalAlignment === "subscript" ? "sub" : "span";
|
|
99
|
+
return `<${tag}${styleAttr(runStyle(run))}>${content}</${tag}>`;
|
|
100
|
+
};
|
|
101
|
+
const paragraphHtml = (paragraph) => {
|
|
102
|
+
const content = childrenByName(paragraph, "r").map(runHtml).join("");
|
|
103
|
+
return `<p${styleAttr(paragraphStyle(paragraph))}>${content || "<br>"}</p>`;
|
|
104
|
+
};
|
|
105
|
+
const cellStyle = (cell) => {
|
|
106
|
+
const properties = firstChildByName(cell, "tcPr");
|
|
107
|
+
const borders = firstChildByName(properties, "tcBorders");
|
|
108
|
+
return cssRules([
|
|
109
|
+
["width", twipsToPt(docxAttr(firstChildByName(properties, "tcW"), "w"))],
|
|
110
|
+
["background-color", docxHexColor(docxAttr(firstChildByName(properties, "shd"), "fill"))],
|
|
111
|
+
["border-top", borderCss(firstChildByName(borders, "top"))],
|
|
112
|
+
["border-right", borderCss(firstChildByName(borders, "right"))],
|
|
113
|
+
["border-bottom", borderCss(firstChildByName(borders, "bottom"))],
|
|
114
|
+
["border-left", borderCss(firstChildByName(borders, "left"))],
|
|
115
|
+
["padding", "8px"],
|
|
116
|
+
["vertical-align", "top"],
|
|
117
|
+
]);
|
|
118
|
+
};
|
|
119
|
+
const tableHtml = (table) => {
|
|
120
|
+
const rows = childrenByName(table, "tr").map((row) => {
|
|
121
|
+
const cells = childrenByName(row, "tc").map((cell) => {
|
|
122
|
+
const content = directChildren(cell)
|
|
123
|
+
.filter((child) => child.localName === "p" || child.localName === "tbl")
|
|
124
|
+
.map((child) => child.localName === "p" ? paragraphHtml(child) : tableHtml(child))
|
|
125
|
+
.join("");
|
|
126
|
+
return `<td${styleAttr(cellStyle(cell))}>${content || "<p><br></p>"}</td>`;
|
|
127
|
+
}).join("");
|
|
128
|
+
return `<tr>${cells}</tr>`;
|
|
129
|
+
}).join("");
|
|
130
|
+
return `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;"><tbody>${rows}</tbody></table>`;
|
|
131
|
+
};
|
|
132
|
+
export const enhanceDocxTables = (root) => {
|
|
133
|
+
root.querySelectorAll("table").forEach((table) => {
|
|
134
|
+
var _a, _b;
|
|
135
|
+
const styledTable = table;
|
|
136
|
+
(_a = styledTable.style).borderCollapse || (_a.borderCollapse = "collapse");
|
|
137
|
+
(_b = styledTable.style).width || (_b.width = "100%");
|
|
138
|
+
table.querySelectorAll("td, th").forEach((cell) => {
|
|
139
|
+
var _a, _b;
|
|
140
|
+
const styledCell = cell;
|
|
141
|
+
if (!styledCell.style.border && !styledCell.style.borderTop && !styledCell.style.borderRight
|
|
142
|
+
&& !styledCell.style.borderBottom && !styledCell.style.borderLeft) {
|
|
143
|
+
styledCell.style.border = "1px solid #d1d5db";
|
|
144
|
+
}
|
|
145
|
+
(_a = styledCell.style).padding || (_a.padding = "8px");
|
|
146
|
+
(_b = styledCell.style).verticalAlign || (_b.verticalAlign = "top");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
const mammothFallback = async (arrayBuffer, ownerDocument) => {
|
|
151
|
+
const document = await importDocxDocumentWithMammoth(arrayBuffer, ownerDocument);
|
|
152
|
+
const container = ownerDocument.createElement("div");
|
|
153
|
+
container.innerHTML = serializeSmartDocument(document);
|
|
154
|
+
enhanceDocxTables(container);
|
|
155
|
+
return { document, layoutHtml: container.innerHTML, source: "mammoth" };
|
|
156
|
+
};
|
|
157
|
+
export const importStyledDocxDocument = async (arrayBuffer, ownerDocument) => {
|
|
158
|
+
try {
|
|
159
|
+
const zip = await JSZip.loadAsync(arrayBuffer);
|
|
160
|
+
const documentXml = await zip.file("word/document.xml")?.async("text");
|
|
161
|
+
if (!documentXml)
|
|
162
|
+
return mammothFallback(arrayBuffer, ownerDocument);
|
|
163
|
+
const parsed = new ownerDocument.defaultView.DOMParser().parseFromString(documentXml, "application/xml");
|
|
164
|
+
if (parsed.querySelector("parsererror"))
|
|
165
|
+
return mammothFallback(arrayBuffer, ownerDocument);
|
|
166
|
+
const body = Array.from(parsed.getElementsByTagName("*")).find((node) => node.localName === "body");
|
|
167
|
+
if (!body)
|
|
168
|
+
return mammothFallback(arrayBuffer, ownerDocument);
|
|
169
|
+
const rawHtml = directChildren(body).filter((node) => node.localName !== "sectPr").map((node) => {
|
|
170
|
+
if (node.localName === "p")
|
|
171
|
+
return paragraphHtml(node);
|
|
172
|
+
if (node.localName === "tbl")
|
|
173
|
+
return tableHtml(node);
|
|
174
|
+
return "";
|
|
175
|
+
}).join("");
|
|
176
|
+
if (!rawHtml.trim())
|
|
177
|
+
return mammothFallback(arrayBuffer, ownerDocument);
|
|
178
|
+
const container = ownerDocument.createElement("div");
|
|
179
|
+
container.innerHTML = restorePortableDocxAtoms(rawHtml, ownerDocument);
|
|
180
|
+
enhanceDocxTables(container);
|
|
181
|
+
return {
|
|
182
|
+
document: normalizeSmartDocument(smartDocumentFromHtml(container.innerHTML, ownerDocument)),
|
|
183
|
+
layoutHtml: container.innerHTML,
|
|
184
|
+
source: "wordprocessingml",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return mammothFallback(arrayBuffer, ownerDocument);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { EditorFormatDefinition } from "./formatRuntime.js";
|
|
2
|
+
export interface BuiltInFormatDefinitionOptions {
|
|
3
|
+
preserveDocxStyles?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare const createBuiltInFormatDefinitions: (options?: BuiltInFormatDefinitionOptions) => readonly EditorFormatDefinition[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { exportTextDocument, importTextDocument } from "./adapters/documentFormats.js";
|
|
2
|
+
import { exportDocxDocument, importDocxDocumentWithMammoth } from "./adapters/docxFormat.js";
|
|
3
|
+
import { importPdfDocument, printSmartDocumentAsPdf } from "./adapters/pdfFormat.js";
|
|
4
|
+
import { enhanceDocxTables, importStyledDocxDocument } from "./adapters/styledDocxFormat.js";
|
|
5
|
+
import { serializeSmartDocument } from "./adapters/domSmartDocument.js";
|
|
6
|
+
const standaloneHtmlDocument = (html) => `<!doctype html>
|
|
7
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
<title>Smart RTE Export</title><style>
|
|
9
|
+
html,body{margin:0;background:#fff;color:#111;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.6}
|
|
10
|
+
body{padding:32px}.srte-export{max-width:960px;margin:0 auto}h1,h2,h3,h4,h5,h6{line-height:1.3;margin:1.25em 0 .5em}
|
|
11
|
+
p{margin:0 0 .85em}blockquote{border-left:4px solid #1e90ff;margin:.75em 0;padding:.5em 1em;background:#f3f4f6}
|
|
12
|
+
ul,ol{margin:.75em 0;padding-left:1.75em;list-style-position:outside}li{display:list-item;margin:.25em 0;padding-left:.25em}
|
|
13
|
+
table{border-collapse:collapse;width:100%;margin:12px 0}td,th{border:1px solid #d1d5db;padding:8px;vertical-align:top;text-align:left}
|
|
14
|
+
th{background:#f3f4f6;font-weight:700}img{max-width:100%;height:auto}pre,code{background:#f3f4f6;border-radius:4px}
|
|
15
|
+
pre{padding:12px;overflow-x:auto;white-space:pre-wrap}@media print{body{padding:0}.srte-export{max-width:none}}
|
|
16
|
+
</style></head><body><main class="srte-export">${html || "<p></p>"}</main></body></html>`;
|
|
17
|
+
export const createBuiltInFormatDefinitions = (options = {}) => [
|
|
18
|
+
{
|
|
19
|
+
id: "pdf", label: "PDF", extension: "pdf", accept: "application/pdf",
|
|
20
|
+
canImport: true, canExport: true, confirmImportWhenNotEmpty: true,
|
|
21
|
+
importFile: async (file, context) => {
|
|
22
|
+
const result = await importPdfDocument(await file.arrayBuffer(), context.ownerDocument);
|
|
23
|
+
return { ...result, preserveColors: true, preserveDocumentLayout: true };
|
|
24
|
+
},
|
|
25
|
+
exportDocument: (document, context) => {
|
|
26
|
+
printSmartDocumentAsPdf(document, context.hostWindow);
|
|
27
|
+
return { kind: "handled" };
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "docx", label: "Microsoft Word", extension: "docx", accept: ".docx",
|
|
32
|
+
canImport: true, canExport: true, confirmImportWhenNotEmpty: true,
|
|
33
|
+
importFile: async (file, context) => {
|
|
34
|
+
const buffer = await file.arrayBuffer();
|
|
35
|
+
if (options.preserveDocxStyles ?? true) {
|
|
36
|
+
const result = await importStyledDocxDocument(buffer, context.ownerDocument);
|
|
37
|
+
return {
|
|
38
|
+
document: result.document,
|
|
39
|
+
layoutHtml: `<div class="srte-preserve-colors">${result.layoutHtml}</div>`,
|
|
40
|
+
preserveColors: true,
|
|
41
|
+
preserveDocumentLayout: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const document = await importDocxDocumentWithMammoth(buffer, context.ownerDocument);
|
|
45
|
+
const container = context.ownerDocument.createElement("div");
|
|
46
|
+
container.innerHTML = serializeSmartDocument(document);
|
|
47
|
+
enhanceDocxTables(container);
|
|
48
|
+
return {
|
|
49
|
+
document,
|
|
50
|
+
layoutHtml: `<div class="srte-preserve-colors">${container.innerHTML}</div>`,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
exportDocument: async (document) => ({
|
|
54
|
+
kind: "blob", filename: "smart-rte-export.docx",
|
|
55
|
+
content: await exportDocxDocument(document),
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "html", label: "HTML", extension: "html", accept: ".html,.htm,text/html",
|
|
60
|
+
canImport: true, canExport: true,
|
|
61
|
+
importFile: async (file, context) => ({
|
|
62
|
+
document: importTextDocument("html", await file.text(), context),
|
|
63
|
+
preserveColors: true, preserveDocumentLayout: true,
|
|
64
|
+
}),
|
|
65
|
+
exportDocument: (document) => ({
|
|
66
|
+
kind: "text", filename: "smart-rte-export.html", mediaType: "text/html;charset=utf-8",
|
|
67
|
+
content: standaloneHtmlDocument(exportTextDocument("html", document)),
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "markdown", label: "Markdown", extension: "md",
|
|
72
|
+
accept: ".md,.markdown,text/markdown,text/plain", canImport: true, canExport: true,
|
|
73
|
+
importFile: async (file, context) => ({
|
|
74
|
+
document: importTextDocument("markdown", await file.text(), context),
|
|
75
|
+
preserveColors: true, preserveDocumentLayout: true,
|
|
76
|
+
}),
|
|
77
|
+
exportDocument: (document) => ({
|
|
78
|
+
kind: "text", filename: "smart-rte-export.md", mediaType: "text/markdown;charset=utf-8",
|
|
79
|
+
content: exportTextDocument("markdown", document),
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface CanonicalAuthorityContext {
|
|
2
|
+
tenantId?: string;
|
|
3
|
+
documentId?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CanonicalAuthorityFlagSnapshot {
|
|
6
|
+
global: boolean;
|
|
7
|
+
tenants: Readonly<Record<string, boolean>>;
|
|
8
|
+
documents: Readonly<Record<string, boolean>>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Runtime rollback switch. Resolution order is direct component override,
|
|
12
|
+
* document, tenant, then global. Development stays opt-in until the Phase 8b
|
|
13
|
+
* replay and production-surface gates have passed.
|
|
14
|
+
*/
|
|
15
|
+
declare class CanonicalAuthorityFlag {
|
|
16
|
+
private globalEnabled;
|
|
17
|
+
private tenants;
|
|
18
|
+
private documents;
|
|
19
|
+
private listeners;
|
|
20
|
+
private version;
|
|
21
|
+
private changed;
|
|
22
|
+
enabled(context?: CanonicalAuthorityContext, direct?: boolean): boolean;
|
|
23
|
+
setGlobal(enabled: boolean): void;
|
|
24
|
+
setTenant(tenantId: string, enabled: boolean | null): void;
|
|
25
|
+
setDocument(documentId: string, enabled: boolean | null): void;
|
|
26
|
+
snapshot(): CanonicalAuthorityFlagSnapshot;
|
|
27
|
+
subscribe(listener: () => void): () => void;
|
|
28
|
+
getVersion(): number;
|
|
29
|
+
reset(): void;
|
|
30
|
+
}
|
|
31
|
+
export declare const canonicalAuthorityFlag: CanonicalAuthorityFlag;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime rollback switch. Resolution order is direct component override,
|
|
3
|
+
* document, tenant, then global. Development stays opt-in until the Phase 8b
|
|
4
|
+
* replay and production-surface gates have passed.
|
|
5
|
+
*/
|
|
6
|
+
class CanonicalAuthorityFlag {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.globalEnabled = false;
|
|
9
|
+
this.tenants = new Map();
|
|
10
|
+
this.documents = new Map();
|
|
11
|
+
this.listeners = new Set();
|
|
12
|
+
this.version = 0;
|
|
13
|
+
}
|
|
14
|
+
changed() { this.version += 1; this.listeners.forEach((listener) => listener()); }
|
|
15
|
+
enabled(context = {}, direct) {
|
|
16
|
+
if (direct !== undefined)
|
|
17
|
+
return direct;
|
|
18
|
+
if (context.documentId && this.documents.has(context.documentId))
|
|
19
|
+
return this.documents.get(context.documentId);
|
|
20
|
+
if (context.tenantId && this.tenants.has(context.tenantId))
|
|
21
|
+
return this.tenants.get(context.tenantId);
|
|
22
|
+
return this.globalEnabled;
|
|
23
|
+
}
|
|
24
|
+
setGlobal(enabled) { if (this.globalEnabled !== enabled) {
|
|
25
|
+
this.globalEnabled = enabled;
|
|
26
|
+
this.changed();
|
|
27
|
+
} }
|
|
28
|
+
setTenant(tenantId, enabled) {
|
|
29
|
+
if (enabled === null)
|
|
30
|
+
this.tenants.delete(tenantId);
|
|
31
|
+
else
|
|
32
|
+
this.tenants.set(tenantId, enabled);
|
|
33
|
+
this.changed();
|
|
34
|
+
}
|
|
35
|
+
setDocument(documentId, enabled) {
|
|
36
|
+
if (enabled === null)
|
|
37
|
+
this.documents.delete(documentId);
|
|
38
|
+
else
|
|
39
|
+
this.documents.set(documentId, enabled);
|
|
40
|
+
this.changed();
|
|
41
|
+
}
|
|
42
|
+
snapshot() {
|
|
43
|
+
return { global: this.globalEnabled, tenants: Object.fromEntries(this.tenants), documents: Object.fromEntries(this.documents) };
|
|
44
|
+
}
|
|
45
|
+
subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); }
|
|
46
|
+
getVersion() { return this.version; }
|
|
47
|
+
reset() { this.globalEnabled = false; this.tenants.clear(); this.documents.clear(); this.changed(); }
|
|
48
|
+
}
|
|
49
|
+
export const canonicalAuthorityFlag = new CanonicalAuthorityFlag();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ClipboardDiagnosticReport } from "smartrte-core/foundation";
|
|
2
|
+
/**
|
|
3
|
+
* ROLLBACK_ADAPTER: canonical-clipboard-dom-insert; unreachable from canonical authority.
|
|
4
|
+
* Legacy rollback-only DOM insertion. The canonical product path never imports this module.
|
|
5
|
+
*/
|
|
6
|
+
export declare const insertCanonicalClipboardData: (transfer: DataTransfer, ownerDocument: Document, onDiagnostic?: (report: ClipboardDiagnosticReport) => void) => boolean;
|
|
7
|
+
export interface CanonicalClipboardRuntimeOptions {
|
|
8
|
+
ownerDocument: Document;
|
|
9
|
+
onFiles?: (files: readonly File[]) => boolean;
|
|
10
|
+
beforeInsert?: () => void;
|
|
11
|
+
afterInsert?: () => void;
|
|
12
|
+
onDiagnostic?: (report: ClipboardDiagnosticReport) => void;
|
|
13
|
+
}
|
|
14
|
+
/** Owns the product paste listener outside ClassicEditor. */
|
|
15
|
+
export declare const installCanonicalClipboardRuntime: (root: HTMLElement, options: CanonicalClipboardRuntimeOptions) => (() => void);
|