smartrte-react 0.2.2 → 0.2.3
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/dist/components/ClassicEditor.js +315 -33
- package/package.json +1 -1
|
@@ -669,6 +669,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
669
669
|
let fullHtml = '';
|
|
670
670
|
for (let i = 1; i <= pdf.numPages; i++) {
|
|
671
671
|
const page = await pdf.getPage(i);
|
|
672
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
672
673
|
const textContent = await page.getTextContent();
|
|
673
674
|
const styles = textContent.styles;
|
|
674
675
|
// 1. Group items into lines
|
|
@@ -740,8 +741,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
740
741
|
const width = item.width;
|
|
741
742
|
const fontName = item.fontName;
|
|
742
743
|
const fontObj = styles[fontName];
|
|
743
|
-
const
|
|
744
|
-
|
|
744
|
+
const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
|
|
745
|
+
const isBold = fontFamily.includes('bold') || false;
|
|
746
|
+
const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
|
|
747
|
+
const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
|
|
745
748
|
if (lastX > 0) {
|
|
746
749
|
const gap = x - lastX;
|
|
747
750
|
if (gap > 2) { // Minimal space threshold
|
|
@@ -760,10 +763,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
760
763
|
itemXs.push(x);
|
|
761
764
|
}
|
|
762
765
|
// Append text style
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
766
|
+
const chunkStyle = cssRules([
|
|
767
|
+
['font-size', `${fontSize}px`],
|
|
768
|
+
['font-weight', isBold ? '700' : ''],
|
|
769
|
+
['font-style', isItalic ? 'italic' : ''],
|
|
770
|
+
]);
|
|
771
|
+
let chunk = `<span${styleAttr(chunkStyle)}>${escapeHtml(item.str)}</span>`;
|
|
767
772
|
lineText += item.str;
|
|
768
773
|
lineHtmlContent += chunk;
|
|
769
774
|
lastX = x + width;
|
|
@@ -834,8 +839,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
834
839
|
const w = item.width;
|
|
835
840
|
const txt = item.str;
|
|
836
841
|
const fontObj = styles[item.fontName];
|
|
837
|
-
const
|
|
838
|
-
const
|
|
842
|
+
const fontFamily = fontObj?.fontFamily?.toLowerCase() || '';
|
|
843
|
+
const isBold = fontFamily.includes('bold');
|
|
844
|
+
const isItalic = fontFamily.includes('italic') || fontFamily.includes('oblique');
|
|
845
|
+
const fontSize = Math.max(8, Math.round(Math.abs(item.transform[3])));
|
|
846
|
+
const styledTxt = `<span${styleAttr(cssRules([
|
|
847
|
+
['font-size', `${fontSize}px`],
|
|
848
|
+
['font-weight', isBold ? '700' : ''],
|
|
849
|
+
['font-style', isItalic ? 'italic' : ''],
|
|
850
|
+
]))}>${escapeHtml(txt)}</span>`;
|
|
839
851
|
// Decide which column this belongs to
|
|
840
852
|
// Find closest column to the left (or close enough)
|
|
841
853
|
let colIdx = 0;
|
|
@@ -877,10 +889,23 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
877
889
|
closeList();
|
|
878
890
|
if (isHeader) {
|
|
879
891
|
const tag = maxH > medianHeight * 1.5 ? 'h2' : 'h3';
|
|
880
|
-
|
|
892
|
+
const firstX = line.items[0]?.transform?.[4] || 0;
|
|
893
|
+
const lastItem = line.items[line.items.length - 1];
|
|
894
|
+
const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
|
|
895
|
+
const center = (firstX + lastRight) / 2;
|
|
896
|
+
const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
|
|
897
|
+
html += `<${tag}${styleAttr(cssRules([['text-align', align]]))}>${lineHtmlContent}</${tag}>`;
|
|
881
898
|
}
|
|
882
899
|
else {
|
|
883
|
-
|
|
900
|
+
const firstX = line.items[0]?.transform?.[4] || 0;
|
|
901
|
+
const lastItem = line.items[line.items.length - 1];
|
|
902
|
+
const lastRight = lastItem ? lastItem.transform[4] + lastItem.width : firstX;
|
|
903
|
+
const center = (firstX + lastRight) / 2;
|
|
904
|
+
const align = Math.abs(center - viewport.width / 2) < viewport.width * 0.12 ? 'center' : firstX > viewport.width * 0.55 ? 'right' : '';
|
|
905
|
+
html += `<p${styleAttr(cssRules([
|
|
906
|
+
['text-align', align],
|
|
907
|
+
['margin-left', firstX > 40 && !align ? `${Math.round(firstX)}px` : ''],
|
|
908
|
+
]))}>${lineHtmlContent}</p>`;
|
|
884
909
|
}
|
|
885
910
|
}
|
|
886
911
|
}
|
|
@@ -889,7 +914,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
889
914
|
closeTable();
|
|
890
915
|
fullHtml += html;
|
|
891
916
|
}
|
|
892
|
-
insertImportedHtml(fullHtml, mode);
|
|
917
|
+
insertImportedHtml(fullHtml, mode, { preserveColors: true, preserveDocumentLayout: true });
|
|
893
918
|
}
|
|
894
919
|
catch (error) {
|
|
895
920
|
console.error('Error reading PDF:', error);
|
|
@@ -1146,63 +1171,163 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1146
1171
|
.replace(/&/g, "&")
|
|
1147
1172
|
.replace(/</g, "<")
|
|
1148
1173
|
.replace(/>/g, ">");
|
|
1174
|
+
const escapeHtmlAttribute = (value) => escapeHtml(value).replace(/"/g, """);
|
|
1149
1175
|
const markdownToHtml = (markdown) => {
|
|
1150
1176
|
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
1151
1177
|
let html = "";
|
|
1152
1178
|
let listType = null;
|
|
1179
|
+
let paragraph = [];
|
|
1180
|
+
let codeFence = null;
|
|
1153
1181
|
const closeList = () => {
|
|
1154
1182
|
if (listType) {
|
|
1155
1183
|
html += `</${listType}>`;
|
|
1156
1184
|
listType = null;
|
|
1157
1185
|
}
|
|
1158
1186
|
};
|
|
1159
|
-
const inline = (text) =>
|
|
1160
|
-
|
|
1161
|
-
.replace(
|
|
1162
|
-
|
|
1163
|
-
|
|
1187
|
+
const inline = (text) => {
|
|
1188
|
+
const codeTokens = [];
|
|
1189
|
+
let value = text.replace(/`([^`]+)`/g, (_match, code) => {
|
|
1190
|
+
const token = `@@SRTE_CODE_${codeTokens.length}@@`;
|
|
1191
|
+
codeTokens.push(`<code>${escapeHtml(code)}</code>`);
|
|
1192
|
+
return token;
|
|
1193
|
+
});
|
|
1194
|
+
value = escapeHtml(value)
|
|
1195
|
+
.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, alt, src, title) => {
|
|
1196
|
+
const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
|
|
1197
|
+
return `<img src="${escapeHtmlAttribute(src)}" alt="${escapeHtmlAttribute(alt)}"${titleAttr}>`;
|
|
1198
|
+
})
|
|
1199
|
+
.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/g, (_match, label, href, title) => {
|
|
1200
|
+
const titleAttr = title ? ` title="${escapeHtmlAttribute(title)}"` : "";
|
|
1201
|
+
return `<a href="${escapeHtmlAttribute(href)}"${titleAttr}>${label}</a>`;
|
|
1202
|
+
})
|
|
1203
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
1204
|
+
.replace(/__([^_]+)__/g, "<strong>$1</strong>")
|
|
1205
|
+
.replace(/~~([^~]+)~~/g, "<s>$1</s>")
|
|
1206
|
+
.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>")
|
|
1207
|
+
.replace(/(^|[^_])_([^_\n]+)_/g, "$1<em>$2</em>");
|
|
1208
|
+
codeTokens.forEach((replacement, index) => {
|
|
1209
|
+
value = value.replace(`@@SRTE_CODE_${index}@@`, replacement);
|
|
1210
|
+
});
|
|
1211
|
+
return value;
|
|
1212
|
+
};
|
|
1213
|
+
const closeParagraph = () => {
|
|
1214
|
+
if (!paragraph.length)
|
|
1215
|
+
return;
|
|
1216
|
+
html += `<p>${inline(paragraph.join(" "))}</p>`;
|
|
1217
|
+
paragraph = [];
|
|
1218
|
+
};
|
|
1219
|
+
const isTableSeparator = (line) => /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
|
|
1220
|
+
const parseTableRow = (line) => {
|
|
1221
|
+
let value = line.trim();
|
|
1222
|
+
if (value.startsWith("|"))
|
|
1223
|
+
value = value.slice(1);
|
|
1224
|
+
if (value.endsWith("|"))
|
|
1225
|
+
value = value.slice(0, -1);
|
|
1226
|
+
return value.split("|").map((cell) => cell.trim());
|
|
1227
|
+
};
|
|
1228
|
+
const renderTable = (startIndex) => {
|
|
1229
|
+
const header = parseTableRow(lines[startIndex]);
|
|
1230
|
+
let index = startIndex + 2;
|
|
1231
|
+
const rows = [];
|
|
1232
|
+
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
1233
|
+
rows.push(parseTableRow(lines[index]));
|
|
1234
|
+
index += 1;
|
|
1235
|
+
}
|
|
1236
|
+
const headHtml = `<thead><tr>${header.map((cell) => `<th>${inline(cell)}</th>`).join("")}</tr></thead>`;
|
|
1237
|
+
const bodyHtml = rows.length
|
|
1238
|
+
? `<tbody>${rows.map((row) => `<tr>${header.map((_cell, cellIndex) => `<td>${inline(row[cellIndex] || "")}</td>`).join("")}</tr>`).join("")}</tbody>`
|
|
1239
|
+
: "";
|
|
1240
|
+
html += `<table style="border-collapse: collapse; width: 100%; margin: 12px 0;">${headHtml}${bodyHtml}</table>`;
|
|
1241
|
+
return index;
|
|
1242
|
+
};
|
|
1243
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1244
|
+
const line = lines[i];
|
|
1164
1245
|
const trimmed = line.trim();
|
|
1246
|
+
const fence = /^```([A-Za-z0-9_-]+)?\s*$/.exec(trimmed);
|
|
1247
|
+
if (fence) {
|
|
1248
|
+
closeParagraph();
|
|
1249
|
+
closeList();
|
|
1250
|
+
if (codeFence) {
|
|
1251
|
+
const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
|
|
1252
|
+
html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
|
|
1253
|
+
codeFence = null;
|
|
1254
|
+
}
|
|
1255
|
+
else {
|
|
1256
|
+
codeFence = { lang: fence[1] || "", lines: [] };
|
|
1257
|
+
}
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (codeFence) {
|
|
1261
|
+
codeFence.lines.push(line);
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1165
1264
|
if (!trimmed) {
|
|
1265
|
+
closeParagraph();
|
|
1166
1266
|
closeList();
|
|
1167
|
-
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
1270
|
+
closeParagraph();
|
|
1271
|
+
closeList();
|
|
1272
|
+
html += "<hr>";
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
if (i + 1 < lines.length && trimmed.includes("|") && isTableSeparator(lines[i + 1])) {
|
|
1276
|
+
closeParagraph();
|
|
1277
|
+
closeList();
|
|
1278
|
+
i = renderTable(i) - 1;
|
|
1279
|
+
continue;
|
|
1168
1280
|
}
|
|
1169
1281
|
const heading = /^(#{1,6})\s+(.+)$/.exec(trimmed);
|
|
1170
1282
|
if (heading) {
|
|
1283
|
+
closeParagraph();
|
|
1171
1284
|
closeList();
|
|
1172
1285
|
const level = heading[1].length;
|
|
1173
1286
|
html += `<h${level}>${inline(heading[2])}</h${level}>`;
|
|
1174
|
-
|
|
1287
|
+
continue;
|
|
1175
1288
|
}
|
|
1176
|
-
const bullet = /^[
|
|
1289
|
+
const bullet = /^[-*+]\s+(.+)$/.exec(trimmed);
|
|
1177
1290
|
if (bullet) {
|
|
1291
|
+
closeParagraph();
|
|
1178
1292
|
if (listType !== "ul") {
|
|
1179
1293
|
closeList();
|
|
1180
1294
|
html += "<ul>";
|
|
1181
1295
|
listType = "ul";
|
|
1182
1296
|
}
|
|
1183
1297
|
html += `<li>${inline(bullet[1])}</li>`;
|
|
1184
|
-
|
|
1298
|
+
continue;
|
|
1185
1299
|
}
|
|
1186
1300
|
const numbered = /^\d+[.)]\s+(.+)$/.exec(trimmed);
|
|
1187
1301
|
if (numbered) {
|
|
1302
|
+
closeParagraph();
|
|
1188
1303
|
if (listType !== "ol") {
|
|
1189
1304
|
closeList();
|
|
1190
1305
|
html += "<ol>";
|
|
1191
1306
|
listType = "ol";
|
|
1192
1307
|
}
|
|
1193
1308
|
html += `<li>${inline(numbered[1])}</li>`;
|
|
1194
|
-
|
|
1309
|
+
continue;
|
|
1195
1310
|
}
|
|
1196
|
-
|
|
1311
|
+
const quote = /^>\s?(.*)$/.exec(trimmed);
|
|
1312
|
+
if (quote) {
|
|
1313
|
+
closeParagraph();
|
|
1197
1314
|
closeList();
|
|
1198
|
-
html += `<blockquote>${inline(
|
|
1199
|
-
|
|
1315
|
+
html += `<blockquote>${inline(quote[1]) || "<br>"}</blockquote>`;
|
|
1316
|
+
continue;
|
|
1200
1317
|
}
|
|
1201
1318
|
closeList();
|
|
1202
|
-
|
|
1203
|
-
}
|
|
1319
|
+
paragraph.push(trimmed);
|
|
1320
|
+
}
|
|
1321
|
+
if (codeFence) {
|
|
1322
|
+
const langClass = codeFence.lang ? ` class="language-${escapeHtmlAttribute(codeFence.lang)}"` : "";
|
|
1323
|
+
html += `<pre><code${langClass}>${escapeHtml(codeFence.lines.join("\n"))}</code></pre>`;
|
|
1324
|
+
}
|
|
1325
|
+
closeParagraph();
|
|
1204
1326
|
closeList();
|
|
1205
|
-
|
|
1327
|
+
const root = document.createElement("div");
|
|
1328
|
+
root.innerHTML = html;
|
|
1329
|
+
enhanceImportedTables(root);
|
|
1330
|
+
return root.innerHTML;
|
|
1206
1331
|
};
|
|
1207
1332
|
const htmlToMarkdown = (html) => {
|
|
1208
1333
|
const root = document.createElement("div");
|
|
@@ -1250,7 +1375,10 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1250
1375
|
const html = type === "html" ? text : markdownToHtml(text);
|
|
1251
1376
|
const el = editableRef.current;
|
|
1252
1377
|
const hasContent = el && el.textContent && el.textContent.trim().length > 0;
|
|
1253
|
-
insertImportedHtml(html, hasContent ? "append" : "replace"
|
|
1378
|
+
insertImportedHtml(html, hasContent ? "append" : "replace", {
|
|
1379
|
+
preserveColors: true,
|
|
1380
|
+
preserveDocumentLayout: true,
|
|
1381
|
+
});
|
|
1254
1382
|
};
|
|
1255
1383
|
const downloadText = (filename, content, mimeType) => {
|
|
1256
1384
|
const blob = new Blob([content], { type: mimeType });
|
|
@@ -1271,6 +1399,140 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1271
1399
|
const html = editableRef.current?.innerHTML || "";
|
|
1272
1400
|
downloadText("smart-rte-export.md", htmlToMarkdown(html), "text/markdown");
|
|
1273
1401
|
};
|
|
1402
|
+
const htmlToDocxXml = (html) => {
|
|
1403
|
+
const root = document.createElement("div");
|
|
1404
|
+
root.innerHTML = html;
|
|
1405
|
+
const xmlEscape = (value) => value
|
|
1406
|
+
.replace(/&/g, "&")
|
|
1407
|
+
.replace(/</g, "<")
|
|
1408
|
+
.replace(/>/g, ">")
|
|
1409
|
+
.replace(/"/g, """);
|
|
1410
|
+
const colorValue = (value) => {
|
|
1411
|
+
const hex = /^#([0-9a-f]{6})$/i.exec(value.trim());
|
|
1412
|
+
if (hex)
|
|
1413
|
+
return hex[1].toUpperCase();
|
|
1414
|
+
const rgb = /^rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)$/i.exec(value.trim());
|
|
1415
|
+
if (!rgb)
|
|
1416
|
+
return "";
|
|
1417
|
+
return [rgb[1], rgb[2], rgb[3]]
|
|
1418
|
+
.map((part) => Math.max(0, Math.min(255, Number(part))).toString(16).padStart(2, "0"))
|
|
1419
|
+
.join("")
|
|
1420
|
+
.toUpperCase();
|
|
1421
|
+
};
|
|
1422
|
+
const sizeToHalfPoints = (value) => {
|
|
1423
|
+
const trimmed = value.trim();
|
|
1424
|
+
const match = /^([\d.]+)(px|pt)$/i.exec(trimmed);
|
|
1425
|
+
if (!match)
|
|
1426
|
+
return "";
|
|
1427
|
+
const raw = Number(match[1]);
|
|
1428
|
+
const pt = match[2].toLowerCase() === "px" ? raw * 0.75 : raw;
|
|
1429
|
+
return String(Math.max(2, Math.round(pt * 2)));
|
|
1430
|
+
};
|
|
1431
|
+
const runProperties = (el) => {
|
|
1432
|
+
const style = el.style;
|
|
1433
|
+
const color = colorValue(style.color);
|
|
1434
|
+
const size = sizeToHalfPoints(style.fontSize);
|
|
1435
|
+
const isBold = el.tagName === "B" || el.tagName === "STRONG" || /bold|700|800|900/.test(style.fontWeight);
|
|
1436
|
+
const isItalic = el.tagName === "I" || el.tagName === "EM" || style.fontStyle === "italic";
|
|
1437
|
+
const isUnderline = el.tagName === "U" || style.textDecoration.includes("underline");
|
|
1438
|
+
return [
|
|
1439
|
+
isBold ? "<w:b/>" : "",
|
|
1440
|
+
isItalic ? "<w:i/>" : "",
|
|
1441
|
+
isUnderline ? '<w:u w:val="single"/>' : "",
|
|
1442
|
+
color ? `<w:color w:val="${color}"/>` : "",
|
|
1443
|
+
size ? `<w:sz w:val="${size}"/>` : "",
|
|
1444
|
+
].join("");
|
|
1445
|
+
};
|
|
1446
|
+
const runs = (node, inheritedProps = "") => {
|
|
1447
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1448
|
+
const text = node.textContent || "";
|
|
1449
|
+
return text ? `<w:r>${inheritedProps ? `<w:rPr>${inheritedProps}</w:rPr>` : ""}<w:t xml:space="preserve">${xmlEscape(text)}</w:t></w:r>` : "";
|
|
1450
|
+
}
|
|
1451
|
+
if (!(node instanceof HTMLElement))
|
|
1452
|
+
return "";
|
|
1453
|
+
if (node.tagName === "BR")
|
|
1454
|
+
return "<w:r><w:br/></w:r>";
|
|
1455
|
+
if (node.tagName === "IMG") {
|
|
1456
|
+
const alt = node.getAttribute("alt") || node.getAttribute("title") || "Image";
|
|
1457
|
+
return `<w:r><w:t>[Image: ${xmlEscape(alt)}]</w:t></w:r>`;
|
|
1458
|
+
}
|
|
1459
|
+
const props = `${inheritedProps}${runProperties(node)}`;
|
|
1460
|
+
return Array.from(node.childNodes).map((child) => runs(child, props)).join("");
|
|
1461
|
+
};
|
|
1462
|
+
const paragraph = (el, fallbackTag = "p") => {
|
|
1463
|
+
const tag = el.tagName.toLowerCase();
|
|
1464
|
+
const headingMatch = /^h([1-6])$/.exec(tag);
|
|
1465
|
+
const style = el.style;
|
|
1466
|
+
const align = style.textAlign ? `<w:jc w:val="${xmlEscape(style.textAlign)}"/>` : "";
|
|
1467
|
+
const headingSize = headingMatch ? `<w:rPr><w:b/><w:sz w:val="${Math.max(24, 40 - Number(headingMatch[1]) * 4)}"/></w:rPr>` : "";
|
|
1468
|
+
const body = runs(el);
|
|
1469
|
+
return `<w:p><w:pPr>${align}${headingSize}</w:pPr>${body || "<w:r><w:t></w:t></w:r>"}</w:p>`;
|
|
1470
|
+
};
|
|
1471
|
+
const tableCell = (cell) => {
|
|
1472
|
+
const fill = colorValue(cell.style.backgroundColor);
|
|
1473
|
+
const shading = fill ? `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>` : "";
|
|
1474
|
+
const cellContent = Array.from(cell.childNodes)
|
|
1475
|
+
.map((child) => child instanceof HTMLElement && ["P", "DIV", "H1", "H2", "H3", "H4", "H5", "H6"].includes(child.tagName)
|
|
1476
|
+
? paragraph(child)
|
|
1477
|
+
: `<w:p>${runs(child)}</w:p>`)
|
|
1478
|
+
.join("");
|
|
1479
|
+
return `<w:tc><w:tcPr>${shading}<w:tcBorders><w:top w:val="single" w:sz="4" w:color="D1D5DB"/><w:left w:val="single" w:sz="4" w:color="D1D5DB"/><w:bottom w:val="single" w:sz="4" w:color="D1D5DB"/><w:right w:val="single" w:sz="4" w:color="D1D5DB"/></w:tcBorders></w:tcPr>${cellContent || "<w:p/>"}</w:tc>`;
|
|
1480
|
+
};
|
|
1481
|
+
const tableXml = (table) => {
|
|
1482
|
+
const rows = Array.from(table.querySelectorAll("tr"));
|
|
1483
|
+
return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:color="D1D5DB"/><w:left w:val="single" w:sz="4" w:color="D1D5DB"/><w:bottom w:val="single" w:sz="4" w:color="D1D5DB"/><w:right w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideH w:val="single" w:sz="4" w:color="D1D5DB"/><w:insideV w:val="single" w:sz="4" w:color="D1D5DB"/></w:tblBorders></w:tblPr>${rows.map((row) => `<w:tr>${Array.from(row.children).map((cell) => tableCell(cell)).join("")}</w:tr>`).join("")}</w:tbl>`;
|
|
1484
|
+
};
|
|
1485
|
+
const blockXml = (node) => {
|
|
1486
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1487
|
+
const text = node.textContent?.trim();
|
|
1488
|
+
return text ? `<w:p>${runs(node)}</w:p>` : "";
|
|
1489
|
+
}
|
|
1490
|
+
if (!(node instanceof HTMLElement))
|
|
1491
|
+
return "";
|
|
1492
|
+
if (node.tagName === "TABLE")
|
|
1493
|
+
return tableXml(node);
|
|
1494
|
+
if (node.tagName === "UL" || node.tagName === "OL") {
|
|
1495
|
+
return Array.from(node.children).map((li) => `<w:p><w:r><w:t>• </w:t></w:r>${runs(li)}</w:p>`).join("");
|
|
1496
|
+
}
|
|
1497
|
+
if (node.tagName === "BLOCKQUOTE") {
|
|
1498
|
+
return `<w:p><w:pPr><w:ind w:left="720"/></w:pPr>${runs(node)}</w:p>`;
|
|
1499
|
+
}
|
|
1500
|
+
if (node.tagName === "HR")
|
|
1501
|
+
return '<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:color="D1D5DB"/></w:pBdr></w:pPr></w:p>';
|
|
1502
|
+
if (["P", "DIV", "PRE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(node.tagName))
|
|
1503
|
+
return paragraph(node);
|
|
1504
|
+
return Array.from(node.childNodes).map(blockXml).join("");
|
|
1505
|
+
};
|
|
1506
|
+
const body = Array.from(root.childNodes).map(blockXml).join("");
|
|
1507
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${body}<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="720" w:right="720" w:bottom="720" w:left="720"/></w:sectPr></w:body></w:document>`;
|
|
1508
|
+
};
|
|
1509
|
+
const exportDocx = async () => {
|
|
1510
|
+
const html = editableRef.current?.innerHTML || "";
|
|
1511
|
+
const zip = new JSZip();
|
|
1512
|
+
zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`);
|
|
1513
|
+
zip.folder("_rels")?.file(".rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`);
|
|
1514
|
+
zip.folder("word")?.file("document.xml", htmlToDocxXml(html));
|
|
1515
|
+
zip.folder("word")?.folder("_rels")?.file("document.xml.rels", `<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>`);
|
|
1516
|
+
const blob = await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" });
|
|
1517
|
+
const url = URL.createObjectURL(blob);
|
|
1518
|
+
const link = document.createElement("a");
|
|
1519
|
+
link.href = url;
|
|
1520
|
+
link.download = "smart-rte-export.docx";
|
|
1521
|
+
document.body.appendChild(link);
|
|
1522
|
+
link.click();
|
|
1523
|
+
link.remove();
|
|
1524
|
+
URL.revokeObjectURL(url);
|
|
1525
|
+
};
|
|
1526
|
+
const exportPdf = () => {
|
|
1527
|
+
const html = editableRef.current?.innerHTML || "";
|
|
1528
|
+
const printWindow = window.open("", "_blank", "noopener,noreferrer,width=900,height=700");
|
|
1529
|
+
if (!printWindow)
|
|
1530
|
+
return;
|
|
1531
|
+
printWindow.document.write(`<!doctype html><html><head><title>Export PDF</title><style>body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.6;padding:32px;color:#111}table{border-collapse:collapse;width:100%;margin:12px 0}td,th{border:1px solid #d1d5db;padding:8px;vertical-align:top}img{max-width:100%;height:auto}blockquote{border-left:4px solid #d1d5db;padding-left:12px;color:#374151}pre,code{background:#f3f4f6}pre{padding:12px;white-space:pre-wrap}@media print{body{padding:0}}</style></head><body>${html}</body></html>`);
|
|
1532
|
+
printWindow.document.close();
|
|
1533
|
+
printWindow.focus();
|
|
1534
|
+
setTimeout(() => printWindow.print(), 250);
|
|
1535
|
+
};
|
|
1274
1536
|
const fixNegativeMargins = (root) => {
|
|
1275
1537
|
try {
|
|
1276
1538
|
const nodes = root.querySelectorAll('*');
|
|
@@ -1438,10 +1700,12 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
1438
1700
|
const wrapper = document.createElement('div');
|
|
1439
1701
|
wrapper.setAttribute('data-table-wrapper', 'true');
|
|
1440
1702
|
wrapper.style.overflowX = 'auto';
|
|
1703
|
+
wrapper.style.overflowY = 'visible';
|
|
1441
1704
|
wrapper.style.webkitOverflowScrolling = 'touch';
|
|
1442
1705
|
wrapper.style.width = '100%';
|
|
1443
1706
|
wrapper.style.maxWidth = '100%';
|
|
1444
1707
|
wrapper.style.display = 'block';
|
|
1708
|
+
wrapper.style.paddingBottom = '8px';
|
|
1445
1709
|
// Use insertBefore + appendChild to move element without losing too much state
|
|
1446
1710
|
// simpler than replaceChild for wrapping
|
|
1447
1711
|
parent.insertBefore(wrapper, table);
|
|
@@ -2024,7 +2288,7 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2024
2288
|
borderRadius: 6,
|
|
2025
2289
|
width: "100%",
|
|
2026
2290
|
maxWidth: "100vw",
|
|
2027
|
-
overflow: "
|
|
2291
|
+
overflow: "visible",
|
|
2028
2292
|
display: "flex",
|
|
2029
2293
|
flexDirection: "column",
|
|
2030
2294
|
background: "var(--srte-bg)",
|
|
@@ -2323,7 +2587,21 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2323
2587
|
borderRadius: 6,
|
|
2324
2588
|
background: "var(--srte-input-bg)",
|
|
2325
2589
|
color: "var(--srte-input-text)",
|
|
2326
|
-
}, children: "Export MD" }),
|
|
2590
|
+
}, children: "Export MD" }), _jsx("button", { title: "Export DOCX", onClick: exportDocx, style: {
|
|
2591
|
+
height: 32,
|
|
2592
|
+
padding: "0 10px",
|
|
2593
|
+
border: "1px solid var(--srte-input-border)",
|
|
2594
|
+
borderRadius: 6,
|
|
2595
|
+
background: "var(--srte-input-bg)",
|
|
2596
|
+
color: "var(--srte-input-text)",
|
|
2597
|
+
}, children: "Export DOCX" }), _jsx("button", { title: "Export PDF", onClick: exportPdf, style: {
|
|
2598
|
+
height: 32,
|
|
2599
|
+
padding: "0 10px",
|
|
2600
|
+
border: "1px solid var(--srte-input-border)",
|
|
2601
|
+
borderRadius: 6,
|
|
2602
|
+
background: "var(--srte-input-bg)",
|
|
2603
|
+
color: "var(--srte-input-text)",
|
|
2604
|
+
}, children: "Export PDF" }), _jsxs("div", { style: {
|
|
2327
2605
|
display: "inline-flex",
|
|
2328
2606
|
gap: 4,
|
|
2329
2607
|
alignItems: "center",
|
|
@@ -2716,12 +2994,15 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2716
2994
|
width: "100%",
|
|
2717
2995
|
maxWidth: "100%",
|
|
2718
2996
|
flex: "1 1 auto",
|
|
2997
|
+
minWidth: 0,
|
|
2719
2998
|
minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
|
|
2720
2999
|
maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
|
|
2721
3000
|
overflowY: "auto",
|
|
2722
|
-
overflowX: "
|
|
3001
|
+
overflowX: "auto",
|
|
3002
|
+
overscrollBehavior: "contain",
|
|
2723
3003
|
boxSizing: "border-box",
|
|
2724
3004
|
position: "relative",
|
|
3005
|
+
scrollPaddingBottom: 24,
|
|
2725
3006
|
}, children: _jsx("div", { ref: editableRef, contentEditable: !readOnly, suppressContentEditableWarning: true, onInput: handleInput, onCompositionStart: () => (isComposingRef.current = true), onCompositionEnd: () => {
|
|
2726
3007
|
isComposingRef.current = false;
|
|
2727
3008
|
handleInput();
|
|
@@ -2860,10 +3141,11 @@ export function ClassicEditor({ value, onChange, placeholder = "Type here…", m
|
|
|
2860
3141
|
}, onDragEnd: () => {
|
|
2861
3142
|
draggedImageRef.current = null;
|
|
2862
3143
|
}, style: {
|
|
2863
|
-
minHeight: "
|
|
3144
|
+
minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
|
|
2864
3145
|
maxWidth: "100%",
|
|
2865
|
-
overflowX: "
|
|
3146
|
+
overflowX: "visible",
|
|
2866
3147
|
padding: "16px",
|
|
3148
|
+
paddingBottom: "32px",
|
|
2867
3149
|
outline: "none",
|
|
2868
3150
|
lineHeight: 1.6,
|
|
2869
3151
|
boxSizing: "border-box",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smartrte-react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "A powerful, feature-rich Rich Text Editor for React with support for tables, mathematical formulas (LaTeX/KaTeX), and media management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|