@files-preview-app/preview-file 1.2.9 → 1.3.1

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/angular.js CHANGED
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
3
3
  import DOMPurify6 from 'dompurify';
4
4
  import * as pdfjsLib from 'pdfjs-dist';
5
5
  import * as docx from 'docx-preview';
6
+ import * as fflate from 'fflate';
6
7
  import { unzipSync, strFromU8, unzip } from 'fflate';
7
8
  import * as XLSX from 'xlsx';
8
9
  import hljs from 'highlight.js';
@@ -385,16 +386,21 @@ async function sourceToArrayBuffer(source, signal) {
385
386
  metadata.mimeType = source.type || void 0;
386
387
  metadata.extension = extractExtension(source.name);
387
388
  buffer = await source.arrayBuffer();
388
- } else if (source instanceof Blob) {
389
+ } else if (source instanceof Blob || source && typeof source.arrayBuffer === "function" && typeof source.size === "number") {
389
390
  metadata.size = source.size;
390
391
  metadata.mimeType = source.type || void 0;
392
+ if (source.name) {
393
+ metadata.name = source.name;
394
+ metadata.extension = extractExtension(source.name);
395
+ }
391
396
  buffer = await source.arrayBuffer();
392
- } else if (source instanceof ArrayBuffer) {
397
+ } else if (source instanceof ArrayBuffer || Object.prototype.toString.call(source) === "[object ArrayBuffer]" || source && typeof source.byteLength === "number" && typeof source.slice === "function") {
393
398
  buffer = source;
394
- } else if (source instanceof Uint8Array) {
395
- buffer = source.buffer.slice(
396
- source.byteOffset,
397
- source.byteOffset + source.byteLength
399
+ } else if (source instanceof Uint8Array || ArrayBuffer.isView(source)) {
400
+ const view = source;
401
+ buffer = view.buffer.slice(
402
+ view.byteOffset,
403
+ view.byteOffset + view.byteLength
398
404
  );
399
405
  } else {
400
406
  throw new Error("Unsupported file source type");
@@ -479,6 +485,45 @@ function createElement(tag, attrs, ...children) {
479
485
  }
480
486
  return el;
481
487
  }
488
+ var DB_NAME = "PreviewFileTransferDB";
489
+ var DB_STORE = "transfers";
490
+ function openDB() {
491
+ return new Promise((resolve, reject) => {
492
+ if (typeof indexedDB === "undefined") {
493
+ return reject(new Error("IndexedDB is not available"));
494
+ }
495
+ const req = indexedDB.open(DB_NAME, 1);
496
+ req.onupgradeneeded = () => {
497
+ const db = req.result;
498
+ if (!db.objectStoreNames.contains(DB_STORE)) {
499
+ db.createObjectStore(DB_STORE, { keyPath: "id" });
500
+ }
501
+ };
502
+ req.onsuccess = () => resolve(req.result);
503
+ req.onerror = () => reject(req.error);
504
+ });
505
+ }
506
+ async function saveTransferPayload(id, payload) {
507
+ if (typeof window !== "undefined") {
508
+ try {
509
+ window[id] = payload;
510
+ window.__lastTransfer = payload;
511
+ } catch {
512
+ }
513
+ }
514
+ try {
515
+ const db = await openDB();
516
+ return new Promise((resolve, reject) => {
517
+ const tx = db.transaction(DB_STORE, "readwrite");
518
+ const store = tx.objectStore(DB_STORE);
519
+ store.put({ id, ...payload, timestamp: Date.now() });
520
+ tx.oncomplete = () => resolve();
521
+ tx.onerror = () => reject(tx.error);
522
+ });
523
+ } catch (e) {
524
+ console.warn("[saveTransferPayload] IndexedDB store warning:", e);
525
+ }
526
+ }
482
527
  var ICON_ZOOM_IN = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`;
483
528
  var ICON_ZOOM_OUT = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`;
484
529
  var ICON_FIT_PAGE = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"></rect><line x1="8" y1="12" x2="16" y2="12"></line><polyline points="11 9 8 12 11 15"></polyline><polyline points="13 9 16 12 13 15"></polyline></svg>`;
@@ -498,6 +543,7 @@ var ICON_COPY = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" str
498
543
  var ICON_FAST_FORWARD = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 19 22 12 13 5 13 19"></polygon><polygon points="2 19 11 12 2 5 2 19"></polygon></svg>`;
499
544
  var ICON_REWIND = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 19 2 12 11 5 11 19"></polygon><polygon points="22 19 13 12 22 5 22 19"></polygon></svg>`;
500
545
  var ICON_SPEED = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`;
546
+ var ICON_EXTERNAL_WINDOW = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`;
501
547
  var ICON_MAP = {
502
548
  "zoom-in": ICON_ZOOM_IN,
503
549
  "zoom-out": ICON_ZOOM_OUT,
@@ -524,7 +570,9 @@ var ICON_MAP = {
524
570
  "forward-10": ICON_FAST_FORWARD,
525
571
  "rewind": ICON_REWIND,
526
572
  "replay-10": ICON_REWIND,
527
- "speed": ICON_SPEED
573
+ "speed": ICON_SPEED,
574
+ "open-window": ICON_EXTERNAL_WINDOW,
575
+ "external-window": ICON_EXTERNAL_WINDOW
528
576
  };
529
577
  var ToolbarController = class {
530
578
  el;
@@ -667,9 +715,11 @@ var ToolbarController = class {
667
715
  type: "button",
668
716
  "data-action-id": id
669
717
  });
670
- const svg = ICON_MAP[iconHtml] || ICON_MAP[id] || (iconHtml && iconHtml.startsWith("<svg") ? iconHtml : null);
671
- if (svg) {
672
- btn.innerHTML = sanitizeSVG(svg);
718
+ const internalSvg = ICON_MAP[iconHtml] || ICON_MAP[id];
719
+ if (internalSvg) {
720
+ btn.innerHTML = internalSvg;
721
+ } else if (iconHtml && iconHtml.startsWith("<svg")) {
722
+ btn.innerHTML = sanitizeSVG(iconHtml);
673
723
  } else {
674
724
  btn.textContent = title || id;
675
725
  }
@@ -741,7 +791,7 @@ var ThumbnailPanel = class {
741
791
  }
742
792
  }
743
793
  };
744
- var FilePreviewViewer = class {
794
+ var FilePreviewViewer = class _FilePreviewViewer {
745
795
  plugins = [];
746
796
  activeInstance = null;
747
797
  abortController = null;
@@ -778,6 +828,7 @@ var FilePreviewViewer = class {
778
828
  * Preview a file in the given container element.
779
829
  */
780
830
  async preview(container, source, options = {}) {
831
+ this.currentOptions = options;
781
832
  this.abort();
782
833
  this.abortController = new AbortController();
783
834
  const { signal } = this.abortController;
@@ -787,7 +838,10 @@ var FilePreviewViewer = class {
787
838
  this.showLoading();
788
839
  try {
789
840
  const { buffer, metadata } = await sourceToArrayBuffer(source, signal);
790
- this.currentBuffer = buffer;
841
+ if (options.metadata) {
842
+ Object.assign(metadata, options.metadata);
843
+ }
844
+ this.currentBuffer = buffer.slice(0);
791
845
  this.currentMetadata = metadata;
792
846
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
793
847
  const fileInfo = { metadata, buffer };
@@ -817,6 +871,7 @@ var FilePreviewViewer = class {
817
871
  }
818
872
  });
819
873
  this.activeInstance = instance;
874
+ instance.openInSeparateWindow = () => this.openInSeparateWindow();
820
875
  this.hideLoading();
821
876
  this.eventEmitter.emit("loaded", { metadata, plugin: matchedPlugin.id });
822
877
  if (options.showToolbar !== false && this.toolbar) {
@@ -826,26 +881,16 @@ var FilePreviewViewer = class {
826
881
  actions.push({
827
882
  id: "fullscreen",
828
883
  icon: "fullscreen",
829
- label: "Toggle Fullscreen",
884
+ label: "Fullscreen",
830
885
  type: "button",
831
886
  group: "view",
832
- execute: async () => {
887
+ execute: () => {
833
888
  try {
834
- const isNativeFs = !!document.fullscreenElement;
835
- const isCssFs = this.wrapperEl?.classList.contains("fp-fullscreen-active");
836
- if (!isNativeFs && !isCssFs) {
837
- if (this.wrapperEl?.requestFullscreen) {
838
- await this.wrapperEl.requestFullscreen().catch(() => {
839
- this.wrapperEl?.classList.add("fp-fullscreen-active");
840
- });
841
- } else {
842
- this.wrapperEl?.classList.add("fp-fullscreen-active");
843
- }
889
+ if (!document.fullscreenElement) {
890
+ this.wrapperEl?.requestFullscreen?.();
891
+ this.wrapperEl?.classList.add("fp-fullscreen-active");
844
892
  } else {
845
- if (document.fullscreenElement) {
846
- await document.exitFullscreen().catch(() => {
847
- });
848
- }
893
+ document.exitFullscreen?.();
849
894
  this.wrapperEl?.classList.remove("fp-fullscreen-active");
850
895
  }
851
896
  } catch {
@@ -857,6 +902,28 @@ var FilePreviewViewer = class {
857
902
  }
858
903
  });
859
904
  }
905
+ const openWinAction = actions.find((a) => a.id === "open-window");
906
+ if (openWinAction) {
907
+ if (options?._isSeparateWindow) {
908
+ const idx = actions.indexOf(openWinAction);
909
+ if (idx !== -1) actions.splice(idx, 1);
910
+ } else {
911
+ openWinAction.execute = () => {
912
+ this.openInSeparateWindow();
913
+ };
914
+ }
915
+ } else if (!options?._isSeparateWindow) {
916
+ actions.push({
917
+ id: "open-window",
918
+ icon: "open-window",
919
+ label: "Open in Separate Full Window",
920
+ type: "button",
921
+ group: "actions",
922
+ execute: () => {
923
+ this.openInSeparateWindow();
924
+ }
925
+ });
926
+ }
860
927
  this.toolbar.update(actions);
861
928
  this.toolbar.show();
862
929
  }
@@ -889,6 +956,104 @@ var FilePreviewViewer = class {
889
956
  throw error;
890
957
  }
891
958
  }
959
+ /**
960
+ * Opens the current file preview in a separate full browser window.
961
+ */
962
+ openInSeparateWindow() {
963
+ if (!this.currentBuffer) {
964
+ console.warn("[FilePreviewViewer] No active file buffer to open in separate window");
965
+ return null;
966
+ }
967
+ if (this.currentOptions.onOpenSeparateWindow) {
968
+ return this.currentOptions.onOpenSeparateWindow({
969
+ buffer: this.currentBuffer,
970
+ metadata: this.currentMetadata || { name: "Document" },
971
+ options: this.currentOptions
972
+ });
973
+ }
974
+ const transferId = "fp_win_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
975
+ let clonedBuffer;
976
+ try {
977
+ clonedBuffer = this.currentBuffer.slice(0);
978
+ } catch {
979
+ clonedBuffer = this.currentBuffer;
980
+ }
981
+ const payload = {
982
+ buffer: clonedBuffer,
983
+ metadata: this.currentMetadata ? { ...this.currentMetadata } : void 0,
984
+ options: { ...this.currentOptions, _isSeparateWindow: true }
985
+ };
986
+ if (typeof window !== "undefined") {
987
+ try {
988
+ window[transferId] = payload;
989
+ window.__lastTransfer = payload;
990
+ } catch {
991
+ }
992
+ }
993
+ saveTransferPayload(transferId, payload).catch((err) => {
994
+ console.warn("[FilePreviewViewer] Transfer payload save warning:", err);
995
+ });
996
+ let targetUrl = null;
997
+ if (this.currentOptions.standaloneViewerUrl) {
998
+ const u = new URL(this.currentOptions.standaloneViewerUrl, window.location.href);
999
+ u.searchParams.set("mode", "fullscreen");
1000
+ u.searchParams.set("transferId", transferId);
1001
+ targetUrl = u.toString();
1002
+ } else if (typeof window !== "undefined" && window.location?.href && !window.location.href.startsWith("about:")) {
1003
+ const u = new URL(window.location.href);
1004
+ u.searchParams.set("mode", "fullscreen");
1005
+ u.searchParams.set("transferId", transferId);
1006
+ targetUrl = u.toString();
1007
+ }
1008
+ if (targetUrl) {
1009
+ const newWin2 = window.open(targetUrl, "_blank");
1010
+ if (!newWin2) {
1011
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1012
+ return null;
1013
+ }
1014
+ return newWin2;
1015
+ }
1016
+ const title = (this.currentMetadata?.name || "Document Preview") + " - Full Preview";
1017
+ const newWin = window.open("", "_blank");
1018
+ if (!newWin) {
1019
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1020
+ return null;
1021
+ }
1022
+ newWin.document.title = title;
1023
+ newWin.document.body.style.margin = "0";
1024
+ newWin.document.body.style.padding = "0";
1025
+ newWin.document.body.style.width = "100vw";
1026
+ newWin.document.body.style.height = "100vh";
1027
+ newWin.document.body.style.overflow = "hidden";
1028
+ newWin.document.body.style.backgroundColor = "#f8fafc";
1029
+ const headNodes = document.querySelectorAll('link[rel="stylesheet"], style');
1030
+ headNodes.forEach((node) => {
1031
+ newWin.document.head.appendChild(node.cloneNode(true));
1032
+ });
1033
+ const root = newWin.document.createElement("div");
1034
+ root.id = "full-window-preview-root";
1035
+ root.style.width = "100%";
1036
+ root.style.height = "100%";
1037
+ root.style.overflow = "hidden";
1038
+ newWin.document.body.appendChild(root);
1039
+ const separateViewer = new _FilePreviewViewer();
1040
+ for (const plugin of this.plugins) {
1041
+ separateViewer.registerPlugin(plugin);
1042
+ }
1043
+ separateViewer.preview(root, this.currentBuffer.slice(0), {
1044
+ ...this.currentOptions,
1045
+ showToolbar: true,
1046
+ toolbarPosition: "top",
1047
+ metadata: this.currentMetadata || void 0,
1048
+ _isSeparateWindow: true
1049
+ }).catch((err) => {
1050
+ console.error("[FilePreviewViewer] Error rendering in separate window:", err);
1051
+ });
1052
+ newWin.addEventListener("beforeunload", () => {
1053
+ separateViewer.destroy();
1054
+ });
1055
+ return newWin;
1056
+ }
892
1057
  /**
893
1058
  * Subscribe to viewer events.
894
1059
  */
@@ -1294,8 +1459,20 @@ var CfbfReader = class {
1294
1459
  }
1295
1460
  };
1296
1461
  if (typeof window !== "undefined" && pdfjsLib.GlobalWorkerOptions) {
1297
- if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
1298
- pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1462
+ if (!pdfjsLib.GlobalWorkerOptions.workerPort && !pdfjsLib.GlobalWorkerOptions.workerSrc) {
1463
+ const customWorker = window.__PDF_WORKER_SRC__;
1464
+ if (customWorker) {
1465
+ pdfjsLib.GlobalWorkerOptions.workerSrc = customWorker;
1466
+ } else {
1467
+ try {
1468
+ pdfjsLib.GlobalWorkerOptions.workerPort = new Worker(
1469
+ new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url),
1470
+ { type: "module" }
1471
+ );
1472
+ } catch {
1473
+ pdfjsLib.GlobalWorkerOptions.workerSrc = "./pdf.worker.min.mjs";
1474
+ }
1475
+ }
1299
1476
  }
1300
1477
  }
1301
1478
  var PdfPlugin = class {
@@ -1389,6 +1566,14 @@ var PdfPlugin = class {
1389
1566
  type: "button",
1390
1567
  group: "actions",
1391
1568
  execute: () => instance.print?.()
1569
+ },
1570
+ {
1571
+ id: "open-window",
1572
+ icon: "open-window",
1573
+ label: "Open in Separate Full Window",
1574
+ type: "button",
1575
+ group: "actions",
1576
+ execute: () => instance.openInSeparateWindow?.()
1392
1577
  }
1393
1578
  ];
1394
1579
  }
@@ -1438,18 +1623,21 @@ var PdfPlugin = class {
1438
1623
  indicator.style.pointerEvents = "none";
1439
1624
  container.appendChild(indicator);
1440
1625
  ctx.container.appendChild(container);
1626
+ const standardFontsUrl = typeof window !== "undefined" && window.__PDF_STANDARD_FONTS_URL__ || "./standard_fonts/";
1627
+ const cmapsUrl = typeof window !== "undefined" && window.__PDF_CMAPS_URL__ || "./cmaps/";
1441
1628
  const loadingTask = pdfjsLib.getDocument({
1442
- data: new Uint8Array(ctx.buffer),
1443
- cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/cmaps/`,
1629
+ data: new Uint8Array(ctx.buffer.slice(0)),
1630
+ cMapUrl: cmapsUrl,
1444
1631
  cMapPacked: true,
1445
- standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/standard_fonts/`
1632
+ standardFontDataUrl: standardFontsUrl,
1633
+ verbosity: 0
1446
1634
  });
1447
1635
  const pdfDoc = await loadingTask.promise;
1448
1636
  const totalPages = Math.max(1, pdfDoc.numPages);
1449
1637
  let currentPage = 1;
1450
1638
  let zoomScale = 1;
1451
1639
  let rotation = 0;
1452
- let fitMode = "width";
1640
+ let fitMode = "page";
1453
1641
  let currentRenderTask = null;
1454
1642
  const renderPage = async (pageNum) => {
1455
1643
  if (currentRenderTask) {
@@ -1469,15 +1657,15 @@ var PdfPlugin = class {
1469
1657
  const containerWidth = container.clientWidth || 900;
1470
1658
  const containerHeight = container.clientHeight || 700;
1471
1659
  const unscaledVp = page.getViewport({ scale: 1, rotation });
1472
- const availWidth = Math.max(280, containerWidth - 48);
1473
- const availHeight = Math.max(280, containerHeight - 88);
1660
+ const availWidth = Math.max(320, containerWidth - 48);
1661
+ const availHeight = Math.max(550, containerHeight - 88);
1474
1662
  const scaleW = availWidth / unscaledVp.width;
1475
1663
  const scaleH = availHeight / unscaledVp.height;
1476
1664
  let fitScale;
1477
1665
  if (fitMode === "page") {
1478
- fitScale = Math.max(0.5, Math.min(scaleW, scaleH));
1666
+ fitScale = Math.max(0.4, Math.min(scaleW, scaleH));
1479
1667
  } else {
1480
- fitScale = Math.max(0.65, Math.min(1.15, scaleW));
1668
+ fitScale = Math.max(0.65, Math.min(1.25, scaleW));
1481
1669
  }
1482
1670
  const effectiveScale = (fitScale > 0 ? fitScale : 1) * zoomScale;
1483
1671
  const pixelRatio = window.devicePixelRatio || 1;
@@ -1503,7 +1691,7 @@ var PdfPlugin = class {
1503
1691
  currentRenderTask = null;
1504
1692
  }
1505
1693
  };
1506
- await renderPage(1);
1694
+ renderPage(1);
1507
1695
  let resizeTimer = null;
1508
1696
  const resizeObserver = new ResizeObserver(() => {
1509
1697
  if (resizeTimer) clearTimeout(resizeTimer);
@@ -1547,7 +1735,7 @@ var PdfPlugin = class {
1547
1735
  renderPage(currentPage);
1548
1736
  },
1549
1737
  fitToPage: () => {
1550
- fitMode = fitMode === "width" ? "page" : "width";
1738
+ fitMode = fitMode === "page" ? "width" : "page";
1551
1739
  zoomScale = 1;
1552
1740
  rotation = 0;
1553
1741
  renderPage(currentPage);
@@ -2024,6 +2212,14 @@ var DocxPlugin = class {
2024
2212
  type: "button",
2025
2213
  group: "actions",
2026
2214
  execute: () => instance.print?.()
2215
+ },
2216
+ {
2217
+ id: "open-window",
2218
+ icon: "open-window",
2219
+ label: "Open in Separate Full Window",
2220
+ type: "button",
2221
+ group: "actions",
2222
+ execute: () => instance.openInSeparateWindow?.()
2027
2223
  }
2028
2224
  );
2029
2225
  return actions;
@@ -2083,6 +2279,31 @@ var DocxPlugin = class {
2083
2279
  }
2084
2280
  if (wrapper.children.length > 0 && (wrapper.textContent?.trim().length ?? 0) > 0) {
2085
2281
  renderedSuccessfully = true;
2282
+ try {
2283
+ const unzipped = unzipSync(new Uint8Array(ctx.buffer));
2284
+ const chartKeys = Object.keys(unzipped).filter((k) => k.replace(/^[./\\]+/, "").toLowerCase().startsWith("word/charts/chart") && k.endsWith(".xml")).sort();
2285
+ if (chartKeys.length > 0) {
2286
+ const allDivs = Array.from(wrapper.querySelectorAll("div"));
2287
+ const emptyContainers = allDivs.filter((div) => {
2288
+ const st = div.getAttribute("style") || "";
2289
+ return st.includes("width:") && st.includes("height:") && div.children.length === 0 && (div.textContent?.trim().length ?? 0) === 0;
2290
+ });
2291
+ chartKeys.forEach((cKey, idx) => {
2292
+ const target = emptyContainers[idx];
2293
+ if (target) {
2294
+ const xmlStr = strFromU8(unzipped[cKey]);
2295
+ const svg = this.parseAndRenderChartSvg(xmlStr);
2296
+ if (svg) {
2297
+ target.innerHTML = svg;
2298
+ target.style.display = "block";
2299
+ target.style.margin = "12px auto";
2300
+ }
2301
+ }
2302
+ });
2303
+ }
2304
+ } catch (chartErr) {
2305
+ console.warn("[DocxPlugin] Non-critical error rendering DrawingML charts:", chartErr);
2306
+ }
2086
2307
  }
2087
2308
  } catch (err) {
2088
2309
  console.warn("[DocxPlugin] docx-preview failed, triggering native fallback:", err);
@@ -2114,64 +2335,74 @@ var DocxPlugin = class {
2114
2335
  }
2115
2336
  let sections = Array.from(wrapper.querySelectorAll("section.docx"));
2116
2337
  const cards = Array.from(wrapper.querySelectorAll(".fp-docx-page-card"));
2117
- if (sections.length === 1 && cards.length === 0) {
2118
- const singleSec = sections[0];
2119
- const contentContainer = singleSec.querySelector("article") || singleSec;
2120
- const children = Array.from(contentContainer.children);
2121
- const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2122
- const secH = singleSec.offsetHeight || singleSec.scrollHeight;
2123
- if (secH > pageH * 1.25 && children.length > 1) {
2124
- const childHeights = children.map((c) => {
2125
- const rectH = c.getBoundingClientRect().height;
2126
- const offH = c.offsetHeight;
2127
- const textLen = c.textContent?.trim().length || 0;
2128
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2129
- return Math.max(rectH, offH, estH);
2130
- });
2131
- const parent = singleSec.parentElement || wrapper;
2132
- const newSections = [singleSec];
2133
- const headerEl = singleSec.querySelector("header");
2134
- const footerEl = singleSec.querySelector("footer");
2135
- contentContainer.innerHTML = "";
2136
- singleSec.style.minHeight = `${pageH}px`;
2137
- singleSec.style.boxSizing = "border-box";
2138
- let curContent = contentContainer;
2139
- let curH = 0;
2140
- const maxH = pageH - 140;
2141
- for (let i = 0; i < children.length; i++) {
2142
- const child = children[i];
2143
- const chH = childHeights[i];
2144
- curContent.appendChild(child);
2145
- curH += chH;
2146
- if (curH >= maxH && i < children.length - 1) {
2147
- const nextSec = document.createElement("section");
2148
- nextSec.className = singleSec.className;
2149
- nextSec.style.cssText = singleSec.style.cssText;
2150
- nextSec.style.minHeight = `${pageH}px`;
2151
- nextSec.style.boxSizing = "border-box";
2152
- nextSec.style.backgroundColor = "#ffffff";
2153
- nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2154
- nextSec.style.borderRadius = "4px";
2155
- nextSec.style.marginBottom = "24px";
2156
- if (headerEl) {
2157
- nextSec.appendChild(headerEl.cloneNode(true));
2158
- }
2159
- const nextArticle = document.createElement("article");
2160
- if (contentContainer.tagName.toLowerCase() === "article") {
2161
- nextArticle.style.cssText = contentContainer.style.cssText;
2162
- }
2163
- nextSec.appendChild(nextArticle);
2164
- if (footerEl) {
2165
- nextSec.appendChild(footerEl.cloneNode(true));
2338
+ if (sections.length > 0 && cards.length === 0) {
2339
+ const finalSections = [];
2340
+ for (const singleSec of sections) {
2341
+ const contentContainer = singleSec.querySelector("article") || singleSec;
2342
+ const children = Array.from(contentContainer.children);
2343
+ const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2344
+ const secH = singleSec.scrollHeight || singleSec.offsetHeight;
2345
+ if (secH > pageH * 1.25 && children.length > 1) {
2346
+ const childHeights = children.map((c) => {
2347
+ const rectH = c.getBoundingClientRect().height;
2348
+ const offH = c.offsetHeight;
2349
+ const textLen = c.textContent?.trim().length || 0;
2350
+ const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2351
+ return Math.max(rectH, offH, estH);
2352
+ });
2353
+ const parent = singleSec.parentElement || wrapper;
2354
+ const headerEl = singleSec.querySelector("header");
2355
+ const footerEl = singleSec.querySelector("footer");
2356
+ contentContainer.innerHTML = "";
2357
+ singleSec.style.minHeight = `${pageH}px`;
2358
+ singleSec.style.boxSizing = "border-box";
2359
+ let curContent = contentContainer;
2360
+ let curSec = singleSec;
2361
+ let curH = 0;
2362
+ const maxH = pageH - 140;
2363
+ finalSections.push(singleSec);
2364
+ for (let i = 0; i < children.length; i++) {
2365
+ const child = children[i];
2366
+ const chH = childHeights[i];
2367
+ curContent.appendChild(child);
2368
+ curH += chH;
2369
+ if (curH >= maxH && i < children.length - 1) {
2370
+ const nextSec = document.createElement("section");
2371
+ nextSec.className = singleSec.className;
2372
+ nextSec.style.cssText = singleSec.style.cssText;
2373
+ nextSec.style.minHeight = `${pageH}px`;
2374
+ nextSec.style.boxSizing = "border-box";
2375
+ nextSec.style.backgroundColor = "#ffffff";
2376
+ nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2377
+ nextSec.style.borderRadius = "4px";
2378
+ nextSec.style.marginBottom = "24px";
2379
+ if (headerEl) {
2380
+ nextSec.appendChild(headerEl.cloneNode(true));
2381
+ }
2382
+ const nextArticle = document.createElement("article");
2383
+ if (contentContainer.tagName.toLowerCase() === "article") {
2384
+ nextArticle.style.cssText = contentContainer.style.cssText;
2385
+ }
2386
+ nextSec.appendChild(nextArticle);
2387
+ if (footerEl) {
2388
+ nextSec.appendChild(footerEl.cloneNode(true));
2389
+ }
2390
+ if (curSec.nextSibling) {
2391
+ parent.insertBefore(nextSec, curSec.nextSibling);
2392
+ } else {
2393
+ parent.appendChild(nextSec);
2394
+ }
2395
+ finalSections.push(nextSec);
2396
+ curSec = nextSec;
2397
+ curContent = nextArticle;
2398
+ curH = 0;
2166
2399
  }
2167
- parent.appendChild(nextSec);
2168
- newSections.push(nextSec);
2169
- curContent = nextArticle;
2170
- curH = 0;
2171
2400
  }
2401
+ } else {
2402
+ finalSections.push(singleSec);
2172
2403
  }
2173
- sections = newSections;
2174
2404
  }
2405
+ sections = finalSections;
2175
2406
  }
2176
2407
  const pageElements = sections.length > 0 ? sections : cards;
2177
2408
  const totalPages = Math.max(1, pageElements.length);
@@ -2563,6 +2794,88 @@ var DocxPlugin = class {
2563
2794
  }
2564
2795
  return result;
2565
2796
  }
2797
+ parseAndRenderChartSvg(xmlStr, width = 500, height = 260) {
2798
+ const catMatches = [...xmlStr.matchAll(/<c:cat>[\s\S]*?<c:strCache>([\s\S]*?)<\/c:strCache>/g)];
2799
+ let categories = [];
2800
+ if (catMatches.length > 0) {
2801
+ categories = [...catMatches[0][1].matchAll(/<c:v>([^<]+)<\/c:v>/g)].map((m) => m[1]);
2802
+ }
2803
+ if (categories.length === 0) {
2804
+ categories = ["Category 1", "Category 2", "Category 3", "Category 4"];
2805
+ }
2806
+ const defaultColors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021", "#83caff"];
2807
+ const sers = [...xmlStr.matchAll(/<c:ser>([\s\S]*?)<\/c:ser>/g)];
2808
+ const series = [];
2809
+ sers.forEach((s, sIdx) => {
2810
+ const titleMatch = s[1].match(/<c:tx>[\s\S]*?<c:v>([^<]+)<\/c:v>/);
2811
+ const title = titleMatch ? titleMatch[1] : `Series ${sIdx + 1}`;
2812
+ const clrMatch = s[1].match(/<a:srgbClr\s+val="([^"]+)"/);
2813
+ const color = clrMatch ? "#" + clrMatch[1] : defaultColors[sIdx % defaultColors.length];
2814
+ const valMatch = s[1].match(/<c:val>[\s\S]*?<c:numCache>([\s\S]*?)<\/c:numCache>/);
2815
+ let values = [];
2816
+ if (valMatch) {
2817
+ values = [...valMatch[1].matchAll(/<c:pt\s+idx="(\d+)">\s*<c:v>([^<]+)<\/c:v>/g)].sort((a, b) => parseInt(a[1], 10) - parseInt(b[1], 10)).map((m) => parseFloat(m[2]) || 0);
2818
+ }
2819
+ series.push({ title, color, values });
2820
+ });
2821
+ if (series.length === 0) return "";
2822
+ let maxVal = 10;
2823
+ series.forEach((s) => s.values.forEach((v) => {
2824
+ if (v > maxVal) maxVal = v;
2825
+ }));
2826
+ maxVal = Math.ceil(maxVal * 1.15);
2827
+ if (maxVal % 2 !== 0) maxVal++;
2828
+ const padLeft = 45;
2829
+ const padBottom = 55;
2830
+ const padTop = 20;
2831
+ const padRight = 20;
2832
+ const plotW = width - padLeft - padRight;
2833
+ const plotH = height - padTop - padBottom;
2834
+ const yTicks = 5;
2835
+ let gridLines = "";
2836
+ for (let i = 0; i <= yTicks; i++) {
2837
+ const val = maxVal / yTicks * i;
2838
+ const y = padTop + plotH - val / maxVal * plotH;
2839
+ gridLines += `<line x1="${padLeft}" y1="${y}" x2="${padLeft + plotW}" y2="${y}" stroke="#e2e8f0" stroke-width="1" />`;
2840
+ gridLines += `<text x="${padLeft - 8}" y="${y + 4}" text-anchor="end" font-size="11" fill="#64748b" font-family="Calibri, sans-serif">${Math.round(val)}</text>`;
2841
+ }
2842
+ const numCats = categories.length;
2843
+ const numSers = series.length;
2844
+ const groupW = plotW / numCats;
2845
+ const barW = Math.max(8, Math.min(28, groupW * 0.7 / numSers));
2846
+ const groupPad = (groupW - barW * numSers) / 2;
2847
+ let bars = "";
2848
+ let catLabels = "";
2849
+ for (let c = 0; c < numCats; c++) {
2850
+ const catX = padLeft + c * groupW;
2851
+ catLabels += `<text x="${catX + groupW / 2}" y="${padTop + plotH + 18}" text-anchor="middle" font-size="11" fill="#334155" font-family="Calibri, sans-serif">${categories[c]}</text>`;
2852
+ for (let s = 0; s < numSers; s++) {
2853
+ const val = series[s].values[c] ?? 0;
2854
+ const bH = Math.max(0, val / maxVal * plotH);
2855
+ const bX = catX + groupPad + s * barW;
2856
+ const bY = padTop + plotH - bH;
2857
+ bars += `<rect x="${bX}" y="${bY}" width="${barW - 2}" height="${bH}" fill="${series[s].color}" rx="1" />`;
2858
+ }
2859
+ }
2860
+ let legend = "";
2861
+ const legY = height - 12;
2862
+ let legX = padLeft + (plotW - numSers * 100) / 2;
2863
+ series.forEach((s) => {
2864
+ legend += `<rect x="${legX}" y="${legY - 9}" width="10" height="10" fill="${s.color}" rx="2" />`;
2865
+ legend += `<text x="${legX + 15}" y="${legY}" font-size="11" fill="#475569" font-family="Calibri, sans-serif">${s.title}</text>`;
2866
+ legX += 95;
2867
+ });
2868
+ return `
2869
+ <svg viewBox="0 0 ${width} ${height}" width="100%" height="100%" style="background:#ffffff; border-radius:4px; overflow:visible;" xmlns="http://www.w3.org/2000/svg">
2870
+ ${gridLines}
2871
+ <line x1="${padLeft}" y1="${padTop + plotH}" x2="${padLeft + plotW}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2872
+ <line x1="${padLeft}" y1="${padTop}" x2="${padLeft}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2873
+ ${bars}
2874
+ ${catLabels}
2875
+ ${legend}
2876
+ </svg>
2877
+ `.trim();
2878
+ }
2566
2879
  };
2567
2880
  function docxPlugin() {
2568
2881
  return new DocxPlugin();
@@ -3129,6 +3442,16 @@ var CodePlugin = class {
3129
3442
  execute: () => {
3130
3443
  instance.print?.();
3131
3444
  }
3445
+ },
3446
+ {
3447
+ id: "open-window",
3448
+ icon: "open-window",
3449
+ label: "Open in Separate Full Window",
3450
+ type: "button",
3451
+ group: "actions",
3452
+ execute: () => {
3453
+ instance.openInSeparateWindow?.();
3454
+ }
3132
3455
  }
3133
3456
  );
3134
3457
  return actions;
@@ -4297,6 +4620,14 @@ var RtfPlugin = class {
4297
4620
  type: "button",
4298
4621
  group: "actions",
4299
4622
  execute: () => instance.print?.()
4623
+ },
4624
+ {
4625
+ id: "open-window",
4626
+ icon: "open-window",
4627
+ label: "Open in Separate Full Window",
4628
+ type: "button",
4629
+ group: "actions",
4630
+ execute: () => instance.openInSeparateWindow?.()
4300
4631
  }
4301
4632
  );
4302
4633
  return actions;
@@ -4349,59 +4680,54 @@ var RtfPlugin = class {
4349
4680
  }
4350
4681
  const doc = new RTFJS.Document(ctx.buffer, {});
4351
4682
  const htmlElements = await doc.render();
4352
- if (htmlElements.length === 1) {
4353
- const singleEl = htmlElements[0];
4354
- wrapper.appendChild(singleEl);
4355
- const children = Array.from(singleEl.children);
4356
- const secH = singleEl.offsetHeight || singleEl.scrollHeight;
4357
- if (secH > 1300 && children.length > 1) {
4358
- const childHeights = children.map((c) => {
4359
- const rectH = c.getBoundingClientRect().height;
4360
- const offH = c.offsetHeight;
4361
- const textLen = c.textContent?.trim().length || 0;
4362
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
4363
- return Math.max(rectH, offH, estH);
4364
- });
4365
- wrapper.innerHTML = "";
4366
- const createRtfCard = () => {
4367
- const card = document.createElement("div");
4368
- card.className = "fp-rtf-page-card";
4369
- card.style.backgroundColor = "#ffffff";
4370
- card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4371
- card.style.borderRadius = "4px";
4372
- card.style.padding = "72px 56px";
4373
- card.style.width = "816px";
4374
- card.style.minHeight = "1056px";
4375
- card.style.boxSizing = "border-box";
4376
- card.style.marginBottom = "24px";
4377
- return card;
4378
- };
4379
- let curCard = createRtfCard();
4380
- wrapper.appendChild(curCard);
4381
- pageElements = [curCard];
4382
- let curH = 0;
4383
- const maxH = 920;
4384
- for (let i = 0; i < children.length; i++) {
4385
- const child = children[i];
4386
- const chH = childHeights[i];
4387
- curCard.appendChild(child);
4388
- curH += chH;
4389
- if (curH >= maxH && i < children.length - 1) {
4390
- curCard = createRtfCard();
4391
- wrapper.appendChild(curCard);
4392
- pageElements.push(curCard);
4393
- curH = 0;
4394
- }
4395
- }
4683
+ const contentNodes = [];
4684
+ for (const item of htmlElements) {
4685
+ if (item.children && item.children.length > 0 && !item.tagName.toLowerCase().startsWith("table")) {
4686
+ contentNodes.push(...Array.from(item.children));
4396
4687
  } else {
4397
- pageElements = [singleEl];
4688
+ contentNodes.push(item);
4398
4689
  }
4399
- } else {
4400
- pageElements = htmlElements;
4401
- for (let i = 0; i < htmlElements.length; i++) {
4402
- const el = htmlElements[i];
4403
- el.style.display = i === 0 ? "block" : "none";
4404
- wrapper.appendChild(el);
4690
+ }
4691
+ wrapper.innerHTML = "";
4692
+ contentNodes.forEach((node) => wrapper.appendChild(node));
4693
+ const childHeights = contentNodes.map((c) => {
4694
+ const rectH = c.getBoundingClientRect ? c.getBoundingClientRect().height : 0;
4695
+ const offH = c.offsetHeight || 0;
4696
+ const textLen = c.textContent?.trim().length || 0;
4697
+ const estH = Math.max(24, Math.ceil(textLen / 75) * 22 + 14);
4698
+ return Math.max(rectH, offH, estH);
4699
+ });
4700
+ wrapper.innerHTML = "";
4701
+ const createRtfCard = () => {
4702
+ const card = document.createElement("div");
4703
+ card.className = "fp-rtf-page-card";
4704
+ card.style.backgroundColor = "#ffffff";
4705
+ card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4706
+ card.style.borderRadius = "4px";
4707
+ card.style.padding = "72px 56px";
4708
+ card.style.width = "816px";
4709
+ card.style.minHeight = "1056px";
4710
+ card.style.boxSizing = "border-box";
4711
+ card.style.marginBottom = "24px";
4712
+ card.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4713
+ card.style.lineHeight = "1.6";
4714
+ return card;
4715
+ };
4716
+ let curCard = createRtfCard();
4717
+ wrapper.appendChild(curCard);
4718
+ pageElements = [curCard];
4719
+ let curH = 0;
4720
+ const maxH = 912;
4721
+ for (let i = 0; i < contentNodes.length; i++) {
4722
+ const child = contentNodes[i];
4723
+ const chH = childHeights[i];
4724
+ curCard.appendChild(child);
4725
+ curH += chH;
4726
+ if (curH >= maxH && i < contentNodes.length - 1) {
4727
+ curCard = createRtfCard();
4728
+ wrapper.appendChild(curCard);
4729
+ pageElements.push(curCard);
4730
+ curH = 0;
4405
4731
  }
4406
4732
  }
4407
4733
  } catch (err) {
@@ -4770,6 +5096,14 @@ var OpenDocumentPlugin = class {
4770
5096
  type: "button",
4771
5097
  group: "actions",
4772
5098
  execute: () => instance.print?.()
5099
+ },
5100
+ {
5101
+ id: "open-window",
5102
+ icon: "open-window",
5103
+ label: "Open in Separate Full Window",
5104
+ type: "button",
5105
+ group: "actions",
5106
+ execute: () => instance.openInSeparateWindow?.()
4773
5107
  }
4774
5108
  );
4775
5109
  return actions;
@@ -5358,6 +5692,14 @@ var DocPlugin = class {
5358
5692
  type: "button",
5359
5693
  group: "actions",
5360
5694
  execute: () => instance.print?.()
5695
+ },
5696
+ {
5697
+ id: "open-window",
5698
+ icon: "open-window",
5699
+ label: "Open in Separate Full Window",
5700
+ type: "button",
5701
+ group: "actions",
5702
+ execute: () => instance.openInSeparateWindow?.()
5361
5703
  }
5362
5704
  );
5363
5705
  return actions;
@@ -5377,8 +5719,17 @@ var DocPlugin = class {
5377
5719
  let scale = 1;
5378
5720
  let extractedRawText = "";
5379
5721
  let isFallback = false;
5722
+ let chartSvg = "";
5380
5723
  try {
5381
5724
  const cfbf = new CfbfReader(ctx.buffer);
5725
+ try {
5726
+ const pkg = cfbf.readStream("package_stream");
5727
+ if (pkg && pkg.length > 100) {
5728
+ chartSvg = this.parseOdfChartToSvg(pkg);
5729
+ }
5730
+ } catch (chartErr) {
5731
+ console.warn("[DocPlugin] Chart stream parsing info:", chartErr);
5732
+ }
5382
5733
  const wordDocStream = cfbf.readStream("WordDocument");
5383
5734
  if (!wordDocStream || wordDocStream.length < 512) {
5384
5735
  throw new Error("WordDocument stream not found or invalid in CFBF archive");
@@ -5396,7 +5747,7 @@ var DocPlugin = class {
5396
5747
  extractedRawText = fallback;
5397
5748
  isFallback = true;
5398
5749
  }
5399
- const rawPages = this.splitIntoPages(extractedRawText);
5750
+ const rawPages = this.splitIntoPages(extractedRawText, chartSvg);
5400
5751
  const totalPages = Math.max(1, rawPages.length);
5401
5752
  let currentPage = 1;
5402
5753
  const pageCards = [];
@@ -5638,7 +5989,7 @@ var DocPlugin = class {
5638
5989
  for (const run of [...ansiRuns, ...utf16Runs]) {
5639
5990
  const trimmed = run.trim();
5640
5991
  if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
5641
- if (!trimmed.includes("Normal.dot") && !trimmed.includes("Microsoft Word") && !trimmed.includes("Times New Roman") && !trimmed.startsWith("\xD0\xCF\xE0\xA1\xB1\xE1") && !/^[\W_0-9]+$/.test(trimmed)) {
5992
+ if (!trimmed.includes("Normal.dot") && !trimmed.includes("Microsoft Word") && !trimmed.includes("Times New Roman") && !trimmed.startsWith("\xD0\xCF\xE0\xA1\xB1\xE1") && !/^EMBED\b/i.test(trimmed) && !trimmed.includes("ChartDocument") && !/^[\W_0-9]+$/.test(trimmed)) {
5642
5993
  seen.add(trimmed);
5643
5994
  candidateLines.push(trimmed);
5644
5995
  }
@@ -5649,12 +6000,138 @@ var DocPlugin = class {
5649
6000
  heuristicTextExtraction(buffer) {
5650
6001
  return this.extractStringsFromBytes(new Uint8Array(buffer));
5651
6002
  }
5652
- splitIntoPages(text) {
6003
+ /**
6004
+ * Parses an embedded OpenDocument Chart package into a vector SVG bar/column chart
6005
+ */
6006
+ parseOdfChartToSvg(zipBytes) {
6007
+ try {
6008
+ const unzipped = fflate.unzipSync(zipBytes);
6009
+ const contentXml = unzipped["content.xml"] ? new TextDecoder("utf-8").decode(unzipped["content.xml"]) : "";
6010
+ if (!contentXml) return "";
6011
+ const rowsMatch = contentXml.match(/<table:table-row[\s\S]*?<\/table:table-row>/g) || [];
6012
+ if (rowsMatch.length < 2) return "";
6013
+ const headers = [];
6014
+ const firstRow = rowsMatch[0];
6015
+ const headerCells = firstRow ? firstRow.match(/<text:p>([^<]+)<\/text:p>/g) || [] : [];
6016
+ for (const h of headerCells) {
6017
+ headers.push(h.replace(/<\/?text:p>/g, "").trim());
6018
+ }
6019
+ const categories = [];
6020
+ const seriesValues = headers.map(() => []);
6021
+ for (let r = 1; r < rowsMatch.length; r++) {
6022
+ const rowStr = rowsMatch[r];
6023
+ if (!rowStr) continue;
6024
+ const cells = rowStr.match(/<table:table-cell[\s\S]*?<\/table:table-cell>/g) || [];
6025
+ if (cells.length > 0 && cells[0]) {
6026
+ const catMatch = cells[0].match(/<text:p>([^<]+)<\/text:p>/);
6027
+ categories.push(catMatch ? catMatch[1] : "Row " + r);
6028
+ for (let c = 1; c < cells.length && c - 1 < headers.length; c++) {
6029
+ const cellStr = cells[c];
6030
+ if (!cellStr) continue;
6031
+ const valMatch = cellStr.match(/office:value="([0-9.]+)"/) || cellStr.match(/<text:p>([0-9.]+)<\/text:p>/);
6032
+ const series = seriesValues[c - 1];
6033
+ if (series) {
6034
+ series.push(valMatch ? parseFloat(valMatch[1]) : 0);
6035
+ }
6036
+ }
6037
+ }
6038
+ }
6039
+ const colors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021"];
6040
+ const colorMatches = contentXml.matchAll(/draw:fill-color="(#[0-9a-fA-F]{6})"/g);
6041
+ let cIdx = 0;
6042
+ for (const cm of colorMatches) {
6043
+ if (cIdx < colors.length) colors[cIdx] = cm[1];
6044
+ cIdx++;
6045
+ }
6046
+ let maxVal = 10;
6047
+ for (const s of seriesValues) {
6048
+ for (const v of s) {
6049
+ if (v > maxVal) maxVal = v;
6050
+ }
6051
+ }
6052
+ maxVal = Math.ceil(maxVal * 1.15);
6053
+ const width = 560;
6054
+ const height = 280;
6055
+ const padLeft = 45;
6056
+ const padRight = 100;
6057
+ const padTop = 20;
6058
+ const padBottom = 40;
6059
+ const chartW = width - padLeft - padRight;
6060
+ const chartH = height - padTop - padBottom;
6061
+ let svg = `<svg viewBox="0 0 ${width} ${height}" width="100%" height="auto" style="max-width: 560px; height: 280px; margin: 16px auto; display: block; font-family: Calibri, sans-serif; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 6px; box-shadow: 0 1px 4px rgba(0,0,0,0.05);">`;
6062
+ for (let step = 0; step <= 4; step++) {
6063
+ const yVal = (maxVal / 4 * step).toFixed(1);
6064
+ const yPos = padTop + chartH - step / 4 * chartH;
6065
+ svg += `<line x1="${padLeft}" y1="${yPos}" x2="${padLeft + chartW}" y2="${yPos}" stroke="#e2e8f0" stroke-dasharray="2,2" />`;
6066
+ svg += `<text x="${padLeft - 8}" y="${yPos + 4}" font-size="11" fill="#64748b" text-anchor="end">${yVal}</text>`;
6067
+ }
6068
+ const numCats = categories.length;
6069
+ const numSeries = headers.length;
6070
+ const groupW = chartW / numCats;
6071
+ const barW = Math.max(8, groupW * 0.7 / numSeries);
6072
+ const groupPad = (groupW - barW * numSeries) / 2;
6073
+ for (let catIdx = 0; catIdx < numCats; catIdx++) {
6074
+ const groupX = padLeft + catIdx * groupW + groupPad;
6075
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6076
+ const val = seriesValues[sIdx][catIdx] || 0;
6077
+ const barH = val / maxVal * chartH;
6078
+ const barX = groupX + sIdx * barW;
6079
+ const barY = padTop + chartH - barH;
6080
+ const col = colors[sIdx % colors.length];
6081
+ svg += `<rect x="${barX}" y="${barY}" width="${barW - 2}" height="${barH}" fill="${col}" rx="2"><title>${headers[sIdx]}: ${val}</title></rect>`;
6082
+ }
6083
+ const catX = padLeft + catIdx * groupW + groupW / 2;
6084
+ svg += `<text x="${catX}" y="${padTop + chartH + 18}" font-size="11" fill="#475569" text-anchor="middle">${categories[catIdx]}</text>`;
6085
+ }
6086
+ let legendY = padTop + 20;
6087
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6088
+ const col = colors[sIdx % colors.length];
6089
+ svg += `<rect x="${padLeft + chartW + 15}" y="${legendY}" width="12" height="12" fill="${col}" rx="2" />`;
6090
+ svg += `<text x="${padLeft + chartW + 32}" y="${legendY + 10}" font-size="11" fill="#334155">${headers[sIdx]}</text>`;
6091
+ legendY += 20;
6092
+ }
6093
+ svg += "</svg>";
6094
+ return svg;
6095
+ } catch (e) {
6096
+ console.warn("[DocPlugin] Error generating chart SVG:", e);
6097
+ return "";
6098
+ }
6099
+ }
6100
+ cleanWordDocFields(text, chartSvg = "") {
6101
+ if (!text) return "";
6102
+ let cleaned = text.replace(
6103
+ /\x13\s*EMBED\b[\s\S]*?\x15/gi,
6104
+ () => chartSvg ? `
6105
+
6106
+ ${chartSvg}
6107
+
6108
+ ` : ""
6109
+ );
6110
+ cleaned = cleaned.replace(
6111
+ /\x13\s*HYPERLINK\s*"?([^"\x14]+)"?\s*\x14([\s\S]*?)\x15/gi,
6112
+ (_match, url, label) => {
6113
+ const cleanUrl = url.trim();
6114
+ const cleanLabel = label.trim() || cleanUrl;
6115
+ return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${cleanLabel}</a>`;
6116
+ }
6117
+ );
6118
+ cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, (_m, res) => {
6119
+ if (/[\x00-\x1F]/.test(res)) return "";
6120
+ return res.trim();
6121
+ });
6122
+ cleaned = cleaned.replace(/\x13[^\x15]*\x15/g, "");
6123
+ cleaned = cleaned.replace(/[\x13\x14\x15]/g, "");
6124
+ cleaned = cleaned.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
6125
+ cleaned = cleaned.replace(/EMBED\s+LibreOffice\.ChartDocument\.[0-9]+/gi, chartSvg || "");
6126
+ return cleaned;
6127
+ }
6128
+ splitIntoPages(text, chartSvg = "") {
5653
6129
  if (!text) return [""];
5654
- const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
6130
+ const cleanedText = this.cleanWordDocFields(text, chartSvg);
6131
+ const normalized = cleanedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
5655
6132
  const explicitParts = normalized.split(/[\x0C\f]|\n\s*[-=_]{3,}\s*(?:PAGE|Page|page break)[\s\d\w-]*[-=_]{3,}\s*\n/i).map((p) => p.trim()).filter((p) => p.length > 0);
5656
6133
  if (explicitParts.length === 0) explicitParts.push(normalized);
5657
- const maxLinesPerPage = 32;
6134
+ const maxLinesPerPage = 34;
5658
6135
  const charsPerLine = 80;
5659
6136
  const finalPages = [];
5660
6137
  for (const part of explicitParts) {
@@ -5662,7 +6139,8 @@ var DocPlugin = class {
5662
6139
  let currentLines = [];
5663
6140
  let count = 0;
5664
6141
  for (const line of lines) {
5665
- const vLines = Math.max(1, Math.ceil((line.length || 1) / charsPerLine));
6142
+ const isSvg = line.includes("<svg");
6143
+ const vLines = isSvg ? 12 : Math.max(1, Math.ceil((line.replace(/<[^>]+>/g, "").length || 1) / charsPerLine));
5666
6144
  if (count + vLines > maxLinesPerPage && currentLines.length > 0) {
5667
6145
  finalPages.push(currentLines.join("\n"));
5668
6146
  currentLines = [];
@@ -5702,9 +6180,44 @@ var DocPlugin = class {
5702
6180
  tableLines = [];
5703
6181
  }
5704
6182
  };
6183
+ const sanitizeOptions = {
6184
+ ADD_TAGS: ["a", "svg", "g", "path", "line", "rect", "circle", "text", "title"],
6185
+ ADD_ATTR: [
6186
+ "href",
6187
+ "target",
6188
+ "rel",
6189
+ "style",
6190
+ "viewBox",
6191
+ "width",
6192
+ "height",
6193
+ "x",
6194
+ "y",
6195
+ "x1",
6196
+ "y1",
6197
+ "x2",
6198
+ "y2",
6199
+ "fill",
6200
+ "stroke",
6201
+ "stroke-width",
6202
+ "stroke-dasharray",
6203
+ "rx",
6204
+ "font-size",
6205
+ "text-anchor"
6206
+ ]
6207
+ };
5705
6208
  let i = 0;
5706
6209
  while (i < lines.length) {
5707
6210
  let line = lines[i];
6211
+ if (line.includes("<svg")) {
6212
+ if (inList) {
6213
+ html += "</ul>";
6214
+ inList = false;
6215
+ }
6216
+ flushTable();
6217
+ html += line;
6218
+ i++;
6219
+ continue;
6220
+ }
5708
6221
  let tabCount = (line.match(/\t/g) || []).length;
5709
6222
  if (tabCount > 0) {
5710
6223
  let j = i;
@@ -5743,20 +6256,20 @@ var DocPlugin = class {
5743
6256
  html += "</ul>";
5744
6257
  inList = false;
5745
6258
  }
5746
- html += `<h2 style="font-size: 16px; font-weight: 700; color: #1e3a8a; margin: 16px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify6.sanitize(line)}</h2>`;
6259
+ html += `<h2 style="font-size: 16px; font-weight: 700; color: #1e3a8a; margin: 16px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify6.sanitize(line, sanitizeOptions)}</h2>`;
5747
6260
  } else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
5748
6261
  if (!inList) {
5749
6262
  html += '<ul style="margin: 8px 0; padding-left: 24px;">';
5750
6263
  inList = true;
5751
6264
  }
5752
6265
  const bulletText = line.replace(/^[•\-\*]\s*/, "");
5753
- html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6.sanitize(bulletText)}</li>`;
6266
+ html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6.sanitize(bulletText, sanitizeOptions)}</li>`;
5754
6267
  } else {
5755
6268
  if (inList) {
5756
6269
  html += "</ul>";
5757
6270
  inList = false;
5758
6271
  }
5759
- html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6.sanitize(line)}</p>`;
6272
+ html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6.sanitize(line, sanitizeOptions)}</p>`;
5760
6273
  }
5761
6274
  i++;
5762
6275
  }