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.
@@ -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 isBold = fontObj?.fontFamily?.toLowerCase().includes('bold') || false;
744
- // const isItalic = fontObj?.fontFamily?.toLowerCase().includes('italic') || false;
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
- let chunk = item.str;
764
- if (isBold)
765
- chunk = `<strong>${chunk}</strong>`;
766
- // if (isItalic) chunk = `<em>${chunk}</em>`;
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 isBold = fontObj?.fontFamily?.toLowerCase().includes('bold');
838
- const styledTxt = isBold ? `<strong>${txt}</strong>` : txt;
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
- html += `<${tag}>${lineHtmlContent}</${tag}>`;
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
- html += `<p>${lineHtmlContent}</p>`;
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, "&amp;")
1147
1172
  .replace(/</g, "&lt;")
1148
1173
  .replace(/>/g, "&gt;");
1174
+ const escapeHtmlAttribute = (value) => escapeHtml(value).replace(/"/g, "&quot;");
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) => escapeHtml(text)
1160
- .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
1161
- .replace(/\*([^*]+)\*/g, "<em>$1</em>")
1162
- .replace(/`([^`]+)`/g, "<code>$1</code>");
1163
- lines.forEach((line) => {
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
- return;
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
- return;
1287
+ continue;
1175
1288
  }
1176
- const bullet = /^[-*]\s+(.+)$/.exec(trimmed);
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
- return;
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
- return;
1309
+ continue;
1195
1310
  }
1196
- if (trimmed.startsWith("> ")) {
1311
+ const quote = /^>\s?(.*)$/.exec(trimmed);
1312
+ if (quote) {
1313
+ closeParagraph();
1197
1314
  closeList();
1198
- html += `<blockquote>${inline(trimmed.slice(2))}</blockquote>`;
1199
- return;
1315
+ html += `<blockquote>${inline(quote[1]) || "<br>"}</blockquote>`;
1316
+ continue;
1200
1317
  }
1201
1318
  closeList();
1202
- html += `<p>${inline(trimmed)}</p>`;
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
- return html;
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, "&amp;")
1407
+ .replace(/</g, "&lt;")
1408
+ .replace(/>/g, "&gt;")
1409
+ .replace(/"/g, "&quot;");
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: "hidden",
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" }), _jsxs("div", { style: {
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: "hidden",
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: "100%",
3144
+ minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
2864
3145
  maxWidth: "100%",
2865
- overflowX: "hidden",
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.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",