@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/react.cjs CHANGED
@@ -18,6 +18,7 @@ var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
18
18
  var RTFJS = require('rtf.js/dist/RTFJS.bundle.js');
19
19
  var jsxRuntime = require('react/jsx-runtime');
20
20
 
21
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
21
22
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
22
23
 
23
24
  function _interopNamespace(e) {
@@ -41,6 +42,7 @@ function _interopNamespace(e) {
41
42
  var DOMPurify6__default = /*#__PURE__*/_interopDefault(DOMPurify6);
42
43
  var pdfjsLib__namespace = /*#__PURE__*/_interopNamespace(pdfjsLib);
43
44
  var docx__namespace = /*#__PURE__*/_interopNamespace(docx);
45
+ var fflate__namespace = /*#__PURE__*/_interopNamespace(fflate);
44
46
  var XLSX__namespace = /*#__PURE__*/_interopNamespace(XLSX);
45
47
  var hljs__default = /*#__PURE__*/_interopDefault(hljs);
46
48
  var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
@@ -370,16 +372,21 @@ async function sourceToArrayBuffer(source, signal) {
370
372
  metadata.mimeType = source.type || void 0;
371
373
  metadata.extension = extractExtension(source.name);
372
374
  buffer = await source.arrayBuffer();
373
- } else if (source instanceof Blob) {
375
+ } else if (source instanceof Blob || source && typeof source.arrayBuffer === "function" && typeof source.size === "number") {
374
376
  metadata.size = source.size;
375
377
  metadata.mimeType = source.type || void 0;
378
+ if (source.name) {
379
+ metadata.name = source.name;
380
+ metadata.extension = extractExtension(source.name);
381
+ }
376
382
  buffer = await source.arrayBuffer();
377
- } else if (source instanceof ArrayBuffer) {
383
+ } else if (source instanceof ArrayBuffer || Object.prototype.toString.call(source) === "[object ArrayBuffer]" || source && typeof source.byteLength === "number" && typeof source.slice === "function") {
378
384
  buffer = source;
379
- } else if (source instanceof Uint8Array) {
380
- buffer = source.buffer.slice(
381
- source.byteOffset,
382
- source.byteOffset + source.byteLength
385
+ } else if (source instanceof Uint8Array || ArrayBuffer.isView(source)) {
386
+ const view = source;
387
+ buffer = view.buffer.slice(
388
+ view.byteOffset,
389
+ view.byteOffset + view.byteLength
383
390
  );
384
391
  } else {
385
392
  throw new Error("Unsupported file source type");
@@ -464,6 +471,45 @@ function createElement(tag, attrs, ...children) {
464
471
  }
465
472
  return el;
466
473
  }
474
+ var DB_NAME = "PreviewFileTransferDB";
475
+ var DB_STORE = "transfers";
476
+ function openDB() {
477
+ return new Promise((resolve, reject) => {
478
+ if (typeof indexedDB === "undefined") {
479
+ return reject(new Error("IndexedDB is not available"));
480
+ }
481
+ const req = indexedDB.open(DB_NAME, 1);
482
+ req.onupgradeneeded = () => {
483
+ const db = req.result;
484
+ if (!db.objectStoreNames.contains(DB_STORE)) {
485
+ db.createObjectStore(DB_STORE, { keyPath: "id" });
486
+ }
487
+ };
488
+ req.onsuccess = () => resolve(req.result);
489
+ req.onerror = () => reject(req.error);
490
+ });
491
+ }
492
+ async function saveTransferPayload(id, payload) {
493
+ if (typeof window !== "undefined") {
494
+ try {
495
+ window[id] = payload;
496
+ window.__lastTransfer = payload;
497
+ } catch {
498
+ }
499
+ }
500
+ try {
501
+ const db = await openDB();
502
+ return new Promise((resolve, reject) => {
503
+ const tx = db.transaction(DB_STORE, "readwrite");
504
+ const store = tx.objectStore(DB_STORE);
505
+ store.put({ id, ...payload, timestamp: Date.now() });
506
+ tx.oncomplete = () => resolve();
507
+ tx.onerror = () => reject(tx.error);
508
+ });
509
+ } catch (e) {
510
+ console.warn("[saveTransferPayload] IndexedDB store warning:", e);
511
+ }
512
+ }
467
513
  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>`;
468
514
  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>`;
469
515
  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>`;
@@ -483,6 +529,7 @@ var ICON_COPY = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" str
483
529
  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>`;
484
530
  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>`;
485
531
  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>`;
532
+ 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>`;
486
533
  var ICON_MAP = {
487
534
  "zoom-in": ICON_ZOOM_IN,
488
535
  "zoom-out": ICON_ZOOM_OUT,
@@ -509,7 +556,9 @@ var ICON_MAP = {
509
556
  "forward-10": ICON_FAST_FORWARD,
510
557
  "rewind": ICON_REWIND,
511
558
  "replay-10": ICON_REWIND,
512
- "speed": ICON_SPEED
559
+ "speed": ICON_SPEED,
560
+ "open-window": ICON_EXTERNAL_WINDOW,
561
+ "external-window": ICON_EXTERNAL_WINDOW
513
562
  };
514
563
  var ToolbarController = class {
515
564
  el;
@@ -652,9 +701,11 @@ var ToolbarController = class {
652
701
  type: "button",
653
702
  "data-action-id": id
654
703
  });
655
- const svg = ICON_MAP[iconHtml] || ICON_MAP[id] || (iconHtml && iconHtml.startsWith("<svg") ? iconHtml : null);
656
- if (svg) {
657
- btn.innerHTML = sanitizeSVG(svg);
704
+ const internalSvg = ICON_MAP[iconHtml] || ICON_MAP[id];
705
+ if (internalSvg) {
706
+ btn.innerHTML = internalSvg;
707
+ } else if (iconHtml && iconHtml.startsWith("<svg")) {
708
+ btn.innerHTML = sanitizeSVG(iconHtml);
658
709
  } else {
659
710
  btn.textContent = title || id;
660
711
  }
@@ -726,7 +777,7 @@ var ThumbnailPanel = class {
726
777
  }
727
778
  }
728
779
  };
729
- var FilePreviewViewer = class {
780
+ var FilePreviewViewer = class _FilePreviewViewer {
730
781
  plugins = [];
731
782
  activeInstance = null;
732
783
  abortController = null;
@@ -763,6 +814,7 @@ var FilePreviewViewer = class {
763
814
  * Preview a file in the given container element.
764
815
  */
765
816
  async preview(container, source, options = {}) {
817
+ this.currentOptions = options;
766
818
  this.abort();
767
819
  this.abortController = new AbortController();
768
820
  const { signal } = this.abortController;
@@ -772,7 +824,10 @@ var FilePreviewViewer = class {
772
824
  this.showLoading();
773
825
  try {
774
826
  const { buffer, metadata } = await sourceToArrayBuffer(source, signal);
775
- this.currentBuffer = buffer;
827
+ if (options.metadata) {
828
+ Object.assign(metadata, options.metadata);
829
+ }
830
+ this.currentBuffer = buffer.slice(0);
776
831
  this.currentMetadata = metadata;
777
832
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
778
833
  const fileInfo = { metadata, buffer };
@@ -802,6 +857,7 @@ var FilePreviewViewer = class {
802
857
  }
803
858
  });
804
859
  this.activeInstance = instance;
860
+ instance.openInSeparateWindow = () => this.openInSeparateWindow();
805
861
  this.hideLoading();
806
862
  this.eventEmitter.emit("loaded", { metadata, plugin: matchedPlugin.id });
807
863
  if (options.showToolbar !== false && this.toolbar) {
@@ -811,26 +867,16 @@ var FilePreviewViewer = class {
811
867
  actions.push({
812
868
  id: "fullscreen",
813
869
  icon: "fullscreen",
814
- label: "Toggle Fullscreen",
870
+ label: "Fullscreen",
815
871
  type: "button",
816
872
  group: "view",
817
- execute: async () => {
873
+ execute: () => {
818
874
  try {
819
- const isNativeFs = !!document.fullscreenElement;
820
- const isCssFs = this.wrapperEl?.classList.contains("fp-fullscreen-active");
821
- if (!isNativeFs && !isCssFs) {
822
- if (this.wrapperEl?.requestFullscreen) {
823
- await this.wrapperEl.requestFullscreen().catch(() => {
824
- this.wrapperEl?.classList.add("fp-fullscreen-active");
825
- });
826
- } else {
827
- this.wrapperEl?.classList.add("fp-fullscreen-active");
828
- }
875
+ if (!document.fullscreenElement) {
876
+ this.wrapperEl?.requestFullscreen?.();
877
+ this.wrapperEl?.classList.add("fp-fullscreen-active");
829
878
  } else {
830
- if (document.fullscreenElement) {
831
- await document.exitFullscreen().catch(() => {
832
- });
833
- }
879
+ document.exitFullscreen?.();
834
880
  this.wrapperEl?.classList.remove("fp-fullscreen-active");
835
881
  }
836
882
  } catch {
@@ -842,6 +888,28 @@ var FilePreviewViewer = class {
842
888
  }
843
889
  });
844
890
  }
891
+ const openWinAction = actions.find((a) => a.id === "open-window");
892
+ if (openWinAction) {
893
+ if (options?._isSeparateWindow) {
894
+ const idx = actions.indexOf(openWinAction);
895
+ if (idx !== -1) actions.splice(idx, 1);
896
+ } else {
897
+ openWinAction.execute = () => {
898
+ this.openInSeparateWindow();
899
+ };
900
+ }
901
+ } else if (!options?._isSeparateWindow) {
902
+ actions.push({
903
+ id: "open-window",
904
+ icon: "open-window",
905
+ label: "Open in Separate Full Window",
906
+ type: "button",
907
+ group: "actions",
908
+ execute: () => {
909
+ this.openInSeparateWindow();
910
+ }
911
+ });
912
+ }
845
913
  this.toolbar.update(actions);
846
914
  this.toolbar.show();
847
915
  }
@@ -874,6 +942,104 @@ var FilePreviewViewer = class {
874
942
  throw error;
875
943
  }
876
944
  }
945
+ /**
946
+ * Opens the current file preview in a separate full browser window.
947
+ */
948
+ openInSeparateWindow() {
949
+ if (!this.currentBuffer) {
950
+ console.warn("[FilePreviewViewer] No active file buffer to open in separate window");
951
+ return null;
952
+ }
953
+ if (this.currentOptions.onOpenSeparateWindow) {
954
+ return this.currentOptions.onOpenSeparateWindow({
955
+ buffer: this.currentBuffer,
956
+ metadata: this.currentMetadata || { name: "Document" },
957
+ options: this.currentOptions
958
+ });
959
+ }
960
+ const transferId = "fp_win_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
961
+ let clonedBuffer;
962
+ try {
963
+ clonedBuffer = this.currentBuffer.slice(0);
964
+ } catch {
965
+ clonedBuffer = this.currentBuffer;
966
+ }
967
+ const payload = {
968
+ buffer: clonedBuffer,
969
+ metadata: this.currentMetadata ? { ...this.currentMetadata } : void 0,
970
+ options: { ...this.currentOptions, _isSeparateWindow: true }
971
+ };
972
+ if (typeof window !== "undefined") {
973
+ try {
974
+ window[transferId] = payload;
975
+ window.__lastTransfer = payload;
976
+ } catch {
977
+ }
978
+ }
979
+ saveTransferPayload(transferId, payload).catch((err) => {
980
+ console.warn("[FilePreviewViewer] Transfer payload save warning:", err);
981
+ });
982
+ let targetUrl = null;
983
+ if (this.currentOptions.standaloneViewerUrl) {
984
+ const u = new URL(this.currentOptions.standaloneViewerUrl, window.location.href);
985
+ u.searchParams.set("mode", "fullscreen");
986
+ u.searchParams.set("transferId", transferId);
987
+ targetUrl = u.toString();
988
+ } else if (typeof window !== "undefined" && window.location?.href && !window.location.href.startsWith("about:")) {
989
+ const u = new URL(window.location.href);
990
+ u.searchParams.set("mode", "fullscreen");
991
+ u.searchParams.set("transferId", transferId);
992
+ targetUrl = u.toString();
993
+ }
994
+ if (targetUrl) {
995
+ const newWin2 = window.open(targetUrl, "_blank");
996
+ if (!newWin2) {
997
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
998
+ return null;
999
+ }
1000
+ return newWin2;
1001
+ }
1002
+ const title = (this.currentMetadata?.name || "Document Preview") + " - Full Preview";
1003
+ const newWin = window.open("", "_blank");
1004
+ if (!newWin) {
1005
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1006
+ return null;
1007
+ }
1008
+ newWin.document.title = title;
1009
+ newWin.document.body.style.margin = "0";
1010
+ newWin.document.body.style.padding = "0";
1011
+ newWin.document.body.style.width = "100vw";
1012
+ newWin.document.body.style.height = "100vh";
1013
+ newWin.document.body.style.overflow = "hidden";
1014
+ newWin.document.body.style.backgroundColor = "#f8fafc";
1015
+ const headNodes = document.querySelectorAll('link[rel="stylesheet"], style');
1016
+ headNodes.forEach((node) => {
1017
+ newWin.document.head.appendChild(node.cloneNode(true));
1018
+ });
1019
+ const root = newWin.document.createElement("div");
1020
+ root.id = "full-window-preview-root";
1021
+ root.style.width = "100%";
1022
+ root.style.height = "100%";
1023
+ root.style.overflow = "hidden";
1024
+ newWin.document.body.appendChild(root);
1025
+ const separateViewer = new _FilePreviewViewer();
1026
+ for (const plugin of this.plugins) {
1027
+ separateViewer.registerPlugin(plugin);
1028
+ }
1029
+ separateViewer.preview(root, this.currentBuffer.slice(0), {
1030
+ ...this.currentOptions,
1031
+ showToolbar: true,
1032
+ toolbarPosition: "top",
1033
+ metadata: this.currentMetadata || void 0,
1034
+ _isSeparateWindow: true
1035
+ }).catch((err) => {
1036
+ console.error("[FilePreviewViewer] Error rendering in separate window:", err);
1037
+ });
1038
+ newWin.addEventListener("beforeunload", () => {
1039
+ separateViewer.destroy();
1040
+ });
1041
+ return newWin;
1042
+ }
877
1043
  /**
878
1044
  * Subscribe to viewer events.
879
1045
  */
@@ -1279,8 +1445,20 @@ var CfbfReader = class {
1279
1445
  }
1280
1446
  };
1281
1447
  if (typeof window !== "undefined" && pdfjsLib__namespace.GlobalWorkerOptions) {
1282
- if (!pdfjsLib__namespace.GlobalWorkerOptions.workerSrc) {
1283
- pdfjsLib__namespace.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1448
+ if (!pdfjsLib__namespace.GlobalWorkerOptions.workerPort && !pdfjsLib__namespace.GlobalWorkerOptions.workerSrc) {
1449
+ const customWorker = window.__PDF_WORKER_SRC__;
1450
+ if (customWorker) {
1451
+ pdfjsLib__namespace.GlobalWorkerOptions.workerSrc = customWorker;
1452
+ } else {
1453
+ try {
1454
+ pdfjsLib__namespace.GlobalWorkerOptions.workerPort = new Worker(
1455
+ new URL("pdfjs-dist/build/pdf.worker.min.mjs", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('react.cjs', document.baseURI).href))),
1456
+ { type: "module" }
1457
+ );
1458
+ } catch {
1459
+ pdfjsLib__namespace.GlobalWorkerOptions.workerSrc = "./pdf.worker.min.mjs";
1460
+ }
1461
+ }
1284
1462
  }
1285
1463
  }
1286
1464
  var PdfPlugin = class {
@@ -1374,6 +1552,14 @@ var PdfPlugin = class {
1374
1552
  type: "button",
1375
1553
  group: "actions",
1376
1554
  execute: () => instance.print?.()
1555
+ },
1556
+ {
1557
+ id: "open-window",
1558
+ icon: "open-window",
1559
+ label: "Open in Separate Full Window",
1560
+ type: "button",
1561
+ group: "actions",
1562
+ execute: () => instance.openInSeparateWindow?.()
1377
1563
  }
1378
1564
  ];
1379
1565
  }
@@ -1423,18 +1609,21 @@ var PdfPlugin = class {
1423
1609
  indicator.style.pointerEvents = "none";
1424
1610
  container.appendChild(indicator);
1425
1611
  ctx.container.appendChild(container);
1612
+ const standardFontsUrl = typeof window !== "undefined" && window.__PDF_STANDARD_FONTS_URL__ || "./standard_fonts/";
1613
+ const cmapsUrl = typeof window !== "undefined" && window.__PDF_CMAPS_URL__ || "./cmaps/";
1426
1614
  const loadingTask = pdfjsLib__namespace.getDocument({
1427
- data: new Uint8Array(ctx.buffer),
1428
- cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/cmaps/`,
1615
+ data: new Uint8Array(ctx.buffer.slice(0)),
1616
+ cMapUrl: cmapsUrl,
1429
1617
  cMapPacked: true,
1430
- standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/standard_fonts/`
1618
+ standardFontDataUrl: standardFontsUrl,
1619
+ verbosity: 0
1431
1620
  });
1432
1621
  const pdfDoc = await loadingTask.promise;
1433
1622
  const totalPages = Math.max(1, pdfDoc.numPages);
1434
1623
  let currentPage = 1;
1435
1624
  let zoomScale = 1;
1436
1625
  let rotation = 0;
1437
- let fitMode = "width";
1626
+ let fitMode = "page";
1438
1627
  let currentRenderTask = null;
1439
1628
  const renderPage = async (pageNum) => {
1440
1629
  if (currentRenderTask) {
@@ -1454,15 +1643,15 @@ var PdfPlugin = class {
1454
1643
  const containerWidth = container.clientWidth || 900;
1455
1644
  const containerHeight = container.clientHeight || 700;
1456
1645
  const unscaledVp = page.getViewport({ scale: 1, rotation });
1457
- const availWidth = Math.max(280, containerWidth - 48);
1458
- const availHeight = Math.max(280, containerHeight - 88);
1646
+ const availWidth = Math.max(320, containerWidth - 48);
1647
+ const availHeight = Math.max(550, containerHeight - 88);
1459
1648
  const scaleW = availWidth / unscaledVp.width;
1460
1649
  const scaleH = availHeight / unscaledVp.height;
1461
1650
  let fitScale;
1462
1651
  if (fitMode === "page") {
1463
- fitScale = Math.max(0.5, Math.min(scaleW, scaleH));
1652
+ fitScale = Math.max(0.4, Math.min(scaleW, scaleH));
1464
1653
  } else {
1465
- fitScale = Math.max(0.65, Math.min(1.15, scaleW));
1654
+ fitScale = Math.max(0.65, Math.min(1.25, scaleW));
1466
1655
  }
1467
1656
  const effectiveScale = (fitScale > 0 ? fitScale : 1) * zoomScale;
1468
1657
  const pixelRatio = window.devicePixelRatio || 1;
@@ -1488,7 +1677,7 @@ var PdfPlugin = class {
1488
1677
  currentRenderTask = null;
1489
1678
  }
1490
1679
  };
1491
- await renderPage(1);
1680
+ renderPage(1);
1492
1681
  let resizeTimer = null;
1493
1682
  const resizeObserver = new ResizeObserver(() => {
1494
1683
  if (resizeTimer) clearTimeout(resizeTimer);
@@ -1532,7 +1721,7 @@ var PdfPlugin = class {
1532
1721
  renderPage(currentPage);
1533
1722
  },
1534
1723
  fitToPage: () => {
1535
- fitMode = fitMode === "width" ? "page" : "width";
1724
+ fitMode = fitMode === "page" ? "width" : "page";
1536
1725
  zoomScale = 1;
1537
1726
  rotation = 0;
1538
1727
  renderPage(currentPage);
@@ -2009,6 +2198,14 @@ var DocxPlugin = class {
2009
2198
  type: "button",
2010
2199
  group: "actions",
2011
2200
  execute: () => instance.print?.()
2201
+ },
2202
+ {
2203
+ id: "open-window",
2204
+ icon: "open-window",
2205
+ label: "Open in Separate Full Window",
2206
+ type: "button",
2207
+ group: "actions",
2208
+ execute: () => instance.openInSeparateWindow?.()
2012
2209
  }
2013
2210
  );
2014
2211
  return actions;
@@ -2068,6 +2265,31 @@ var DocxPlugin = class {
2068
2265
  }
2069
2266
  if (wrapper.children.length > 0 && (wrapper.textContent?.trim().length ?? 0) > 0) {
2070
2267
  renderedSuccessfully = true;
2268
+ try {
2269
+ const unzipped = fflate.unzipSync(new Uint8Array(ctx.buffer));
2270
+ const chartKeys = Object.keys(unzipped).filter((k) => k.replace(/^[./\\]+/, "").toLowerCase().startsWith("word/charts/chart") && k.endsWith(".xml")).sort();
2271
+ if (chartKeys.length > 0) {
2272
+ const allDivs = Array.from(wrapper.querySelectorAll("div"));
2273
+ const emptyContainers = allDivs.filter((div) => {
2274
+ const st = div.getAttribute("style") || "";
2275
+ return st.includes("width:") && st.includes("height:") && div.children.length === 0 && (div.textContent?.trim().length ?? 0) === 0;
2276
+ });
2277
+ chartKeys.forEach((cKey, idx) => {
2278
+ const target = emptyContainers[idx];
2279
+ if (target) {
2280
+ const xmlStr = fflate.strFromU8(unzipped[cKey]);
2281
+ const svg = this.parseAndRenderChartSvg(xmlStr);
2282
+ if (svg) {
2283
+ target.innerHTML = svg;
2284
+ target.style.display = "block";
2285
+ target.style.margin = "12px auto";
2286
+ }
2287
+ }
2288
+ });
2289
+ }
2290
+ } catch (chartErr) {
2291
+ console.warn("[DocxPlugin] Non-critical error rendering DrawingML charts:", chartErr);
2292
+ }
2071
2293
  }
2072
2294
  } catch (err) {
2073
2295
  console.warn("[DocxPlugin] docx-preview failed, triggering native fallback:", err);
@@ -2099,64 +2321,74 @@ var DocxPlugin = class {
2099
2321
  }
2100
2322
  let sections = Array.from(wrapper.querySelectorAll("section.docx"));
2101
2323
  const cards = Array.from(wrapper.querySelectorAll(".fp-docx-page-card"));
2102
- if (sections.length === 1 && cards.length === 0) {
2103
- const singleSec = sections[0];
2104
- const contentContainer = singleSec.querySelector("article") || singleSec;
2105
- const children = Array.from(contentContainer.children);
2106
- const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2107
- const secH = singleSec.offsetHeight || singleSec.scrollHeight;
2108
- if (secH > pageH * 1.25 && children.length > 1) {
2109
- const childHeights = children.map((c) => {
2110
- const rectH = c.getBoundingClientRect().height;
2111
- const offH = c.offsetHeight;
2112
- const textLen = c.textContent?.trim().length || 0;
2113
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2114
- return Math.max(rectH, offH, estH);
2115
- });
2116
- const parent = singleSec.parentElement || wrapper;
2117
- const newSections = [singleSec];
2118
- const headerEl = singleSec.querySelector("header");
2119
- const footerEl = singleSec.querySelector("footer");
2120
- contentContainer.innerHTML = "";
2121
- singleSec.style.minHeight = `${pageH}px`;
2122
- singleSec.style.boxSizing = "border-box";
2123
- let curContent = contentContainer;
2124
- let curH = 0;
2125
- const maxH = pageH - 140;
2126
- for (let i = 0; i < children.length; i++) {
2127
- const child = children[i];
2128
- const chH = childHeights[i];
2129
- curContent.appendChild(child);
2130
- curH += chH;
2131
- if (curH >= maxH && i < children.length - 1) {
2132
- const nextSec = document.createElement("section");
2133
- nextSec.className = singleSec.className;
2134
- nextSec.style.cssText = singleSec.style.cssText;
2135
- nextSec.style.minHeight = `${pageH}px`;
2136
- nextSec.style.boxSizing = "border-box";
2137
- nextSec.style.backgroundColor = "#ffffff";
2138
- nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2139
- nextSec.style.borderRadius = "4px";
2140
- nextSec.style.marginBottom = "24px";
2141
- if (headerEl) {
2142
- nextSec.appendChild(headerEl.cloneNode(true));
2143
- }
2144
- const nextArticle = document.createElement("article");
2145
- if (contentContainer.tagName.toLowerCase() === "article") {
2146
- nextArticle.style.cssText = contentContainer.style.cssText;
2147
- }
2148
- nextSec.appendChild(nextArticle);
2149
- if (footerEl) {
2150
- nextSec.appendChild(footerEl.cloneNode(true));
2324
+ if (sections.length > 0 && cards.length === 0) {
2325
+ const finalSections = [];
2326
+ for (const singleSec of sections) {
2327
+ const contentContainer = singleSec.querySelector("article") || singleSec;
2328
+ const children = Array.from(contentContainer.children);
2329
+ const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2330
+ const secH = singleSec.scrollHeight || singleSec.offsetHeight;
2331
+ if (secH > pageH * 1.25 && children.length > 1) {
2332
+ const childHeights = children.map((c) => {
2333
+ const rectH = c.getBoundingClientRect().height;
2334
+ const offH = c.offsetHeight;
2335
+ const textLen = c.textContent?.trim().length || 0;
2336
+ const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2337
+ return Math.max(rectH, offH, estH);
2338
+ });
2339
+ const parent = singleSec.parentElement || wrapper;
2340
+ const headerEl = singleSec.querySelector("header");
2341
+ const footerEl = singleSec.querySelector("footer");
2342
+ contentContainer.innerHTML = "";
2343
+ singleSec.style.minHeight = `${pageH}px`;
2344
+ singleSec.style.boxSizing = "border-box";
2345
+ let curContent = contentContainer;
2346
+ let curSec = singleSec;
2347
+ let curH = 0;
2348
+ const maxH = pageH - 140;
2349
+ finalSections.push(singleSec);
2350
+ for (let i = 0; i < children.length; i++) {
2351
+ const child = children[i];
2352
+ const chH = childHeights[i];
2353
+ curContent.appendChild(child);
2354
+ curH += chH;
2355
+ if (curH >= maxH && i < children.length - 1) {
2356
+ const nextSec = document.createElement("section");
2357
+ nextSec.className = singleSec.className;
2358
+ nextSec.style.cssText = singleSec.style.cssText;
2359
+ nextSec.style.minHeight = `${pageH}px`;
2360
+ nextSec.style.boxSizing = "border-box";
2361
+ nextSec.style.backgroundColor = "#ffffff";
2362
+ nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2363
+ nextSec.style.borderRadius = "4px";
2364
+ nextSec.style.marginBottom = "24px";
2365
+ if (headerEl) {
2366
+ nextSec.appendChild(headerEl.cloneNode(true));
2367
+ }
2368
+ const nextArticle = document.createElement("article");
2369
+ if (contentContainer.tagName.toLowerCase() === "article") {
2370
+ nextArticle.style.cssText = contentContainer.style.cssText;
2371
+ }
2372
+ nextSec.appendChild(nextArticle);
2373
+ if (footerEl) {
2374
+ nextSec.appendChild(footerEl.cloneNode(true));
2375
+ }
2376
+ if (curSec.nextSibling) {
2377
+ parent.insertBefore(nextSec, curSec.nextSibling);
2378
+ } else {
2379
+ parent.appendChild(nextSec);
2380
+ }
2381
+ finalSections.push(nextSec);
2382
+ curSec = nextSec;
2383
+ curContent = nextArticle;
2384
+ curH = 0;
2151
2385
  }
2152
- parent.appendChild(nextSec);
2153
- newSections.push(nextSec);
2154
- curContent = nextArticle;
2155
- curH = 0;
2156
2386
  }
2387
+ } else {
2388
+ finalSections.push(singleSec);
2157
2389
  }
2158
- sections = newSections;
2159
2390
  }
2391
+ sections = finalSections;
2160
2392
  }
2161
2393
  const pageElements = sections.length > 0 ? sections : cards;
2162
2394
  const totalPages = Math.max(1, pageElements.length);
@@ -2548,6 +2780,88 @@ var DocxPlugin = class {
2548
2780
  }
2549
2781
  return result;
2550
2782
  }
2783
+ parseAndRenderChartSvg(xmlStr, width = 500, height = 260) {
2784
+ const catMatches = [...xmlStr.matchAll(/<c:cat>[\s\S]*?<c:strCache>([\s\S]*?)<\/c:strCache>/g)];
2785
+ let categories = [];
2786
+ if (catMatches.length > 0) {
2787
+ categories = [...catMatches[0][1].matchAll(/<c:v>([^<]+)<\/c:v>/g)].map((m) => m[1]);
2788
+ }
2789
+ if (categories.length === 0) {
2790
+ categories = ["Category 1", "Category 2", "Category 3", "Category 4"];
2791
+ }
2792
+ const defaultColors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021", "#83caff"];
2793
+ const sers = [...xmlStr.matchAll(/<c:ser>([\s\S]*?)<\/c:ser>/g)];
2794
+ const series = [];
2795
+ sers.forEach((s, sIdx) => {
2796
+ const titleMatch = s[1].match(/<c:tx>[\s\S]*?<c:v>([^<]+)<\/c:v>/);
2797
+ const title = titleMatch ? titleMatch[1] : `Series ${sIdx + 1}`;
2798
+ const clrMatch = s[1].match(/<a:srgbClr\s+val="([^"]+)"/);
2799
+ const color = clrMatch ? "#" + clrMatch[1] : defaultColors[sIdx % defaultColors.length];
2800
+ const valMatch = s[1].match(/<c:val>[\s\S]*?<c:numCache>([\s\S]*?)<\/c:numCache>/);
2801
+ let values = [];
2802
+ if (valMatch) {
2803
+ 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);
2804
+ }
2805
+ series.push({ title, color, values });
2806
+ });
2807
+ if (series.length === 0) return "";
2808
+ let maxVal = 10;
2809
+ series.forEach((s) => s.values.forEach((v) => {
2810
+ if (v > maxVal) maxVal = v;
2811
+ }));
2812
+ maxVal = Math.ceil(maxVal * 1.15);
2813
+ if (maxVal % 2 !== 0) maxVal++;
2814
+ const padLeft = 45;
2815
+ const padBottom = 55;
2816
+ const padTop = 20;
2817
+ const padRight = 20;
2818
+ const plotW = width - padLeft - padRight;
2819
+ const plotH = height - padTop - padBottom;
2820
+ const yTicks = 5;
2821
+ let gridLines = "";
2822
+ for (let i = 0; i <= yTicks; i++) {
2823
+ const val = maxVal / yTicks * i;
2824
+ const y = padTop + plotH - val / maxVal * plotH;
2825
+ gridLines += `<line x1="${padLeft}" y1="${y}" x2="${padLeft + plotW}" y2="${y}" stroke="#e2e8f0" stroke-width="1" />`;
2826
+ 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>`;
2827
+ }
2828
+ const numCats = categories.length;
2829
+ const numSers = series.length;
2830
+ const groupW = plotW / numCats;
2831
+ const barW = Math.max(8, Math.min(28, groupW * 0.7 / numSers));
2832
+ const groupPad = (groupW - barW * numSers) / 2;
2833
+ let bars = "";
2834
+ let catLabels = "";
2835
+ for (let c = 0; c < numCats; c++) {
2836
+ const catX = padLeft + c * groupW;
2837
+ 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>`;
2838
+ for (let s = 0; s < numSers; s++) {
2839
+ const val = series[s].values[c] ?? 0;
2840
+ const bH = Math.max(0, val / maxVal * plotH);
2841
+ const bX = catX + groupPad + s * barW;
2842
+ const bY = padTop + plotH - bH;
2843
+ bars += `<rect x="${bX}" y="${bY}" width="${barW - 2}" height="${bH}" fill="${series[s].color}" rx="1" />`;
2844
+ }
2845
+ }
2846
+ let legend = "";
2847
+ const legY = height - 12;
2848
+ let legX = padLeft + (plotW - numSers * 100) / 2;
2849
+ series.forEach((s) => {
2850
+ legend += `<rect x="${legX}" y="${legY - 9}" width="10" height="10" fill="${s.color}" rx="2" />`;
2851
+ legend += `<text x="${legX + 15}" y="${legY}" font-size="11" fill="#475569" font-family="Calibri, sans-serif">${s.title}</text>`;
2852
+ legX += 95;
2853
+ });
2854
+ return `
2855
+ <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">
2856
+ ${gridLines}
2857
+ <line x1="${padLeft}" y1="${padTop + plotH}" x2="${padLeft + plotW}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2858
+ <line x1="${padLeft}" y1="${padTop}" x2="${padLeft}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2859
+ ${bars}
2860
+ ${catLabels}
2861
+ ${legend}
2862
+ </svg>
2863
+ `.trim();
2864
+ }
2551
2865
  };
2552
2866
  function docxPlugin() {
2553
2867
  return new DocxPlugin();
@@ -3114,6 +3428,16 @@ var CodePlugin = class {
3114
3428
  execute: () => {
3115
3429
  instance.print?.();
3116
3430
  }
3431
+ },
3432
+ {
3433
+ id: "open-window",
3434
+ icon: "open-window",
3435
+ label: "Open in Separate Full Window",
3436
+ type: "button",
3437
+ group: "actions",
3438
+ execute: () => {
3439
+ instance.openInSeparateWindow?.();
3440
+ }
3117
3441
  }
3118
3442
  );
3119
3443
  return actions;
@@ -4282,6 +4606,14 @@ var RtfPlugin = class {
4282
4606
  type: "button",
4283
4607
  group: "actions",
4284
4608
  execute: () => instance.print?.()
4609
+ },
4610
+ {
4611
+ id: "open-window",
4612
+ icon: "open-window",
4613
+ label: "Open in Separate Full Window",
4614
+ type: "button",
4615
+ group: "actions",
4616
+ execute: () => instance.openInSeparateWindow?.()
4285
4617
  }
4286
4618
  );
4287
4619
  return actions;
@@ -4334,59 +4666,54 @@ var RtfPlugin = class {
4334
4666
  }
4335
4667
  const doc = new RTFJS__namespace.Document(ctx.buffer, {});
4336
4668
  const htmlElements = await doc.render();
4337
- if (htmlElements.length === 1) {
4338
- const singleEl = htmlElements[0];
4339
- wrapper.appendChild(singleEl);
4340
- const children = Array.from(singleEl.children);
4341
- const secH = singleEl.offsetHeight || singleEl.scrollHeight;
4342
- if (secH > 1300 && children.length > 1) {
4343
- const childHeights = children.map((c) => {
4344
- const rectH = c.getBoundingClientRect().height;
4345
- const offH = c.offsetHeight;
4346
- const textLen = c.textContent?.trim().length || 0;
4347
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
4348
- return Math.max(rectH, offH, estH);
4349
- });
4350
- wrapper.innerHTML = "";
4351
- const createRtfCard = () => {
4352
- const card = document.createElement("div");
4353
- card.className = "fp-rtf-page-card";
4354
- card.style.backgroundColor = "#ffffff";
4355
- card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4356
- card.style.borderRadius = "4px";
4357
- card.style.padding = "72px 56px";
4358
- card.style.width = "816px";
4359
- card.style.minHeight = "1056px";
4360
- card.style.boxSizing = "border-box";
4361
- card.style.marginBottom = "24px";
4362
- return card;
4363
- };
4364
- let curCard = createRtfCard();
4365
- wrapper.appendChild(curCard);
4366
- pageElements = [curCard];
4367
- let curH = 0;
4368
- const maxH = 920;
4369
- for (let i = 0; i < children.length; i++) {
4370
- const child = children[i];
4371
- const chH = childHeights[i];
4372
- curCard.appendChild(child);
4373
- curH += chH;
4374
- if (curH >= maxH && i < children.length - 1) {
4375
- curCard = createRtfCard();
4376
- wrapper.appendChild(curCard);
4377
- pageElements.push(curCard);
4378
- curH = 0;
4379
- }
4380
- }
4669
+ const contentNodes = [];
4670
+ for (const item of htmlElements) {
4671
+ if (item.children && item.children.length > 0 && !item.tagName.toLowerCase().startsWith("table")) {
4672
+ contentNodes.push(...Array.from(item.children));
4381
4673
  } else {
4382
- pageElements = [singleEl];
4674
+ contentNodes.push(item);
4383
4675
  }
4384
- } else {
4385
- pageElements = htmlElements;
4386
- for (let i = 0; i < htmlElements.length; i++) {
4387
- const el = htmlElements[i];
4388
- el.style.display = i === 0 ? "block" : "none";
4389
- wrapper.appendChild(el);
4676
+ }
4677
+ wrapper.innerHTML = "";
4678
+ contentNodes.forEach((node) => wrapper.appendChild(node));
4679
+ const childHeights = contentNodes.map((c) => {
4680
+ const rectH = c.getBoundingClientRect ? c.getBoundingClientRect().height : 0;
4681
+ const offH = c.offsetHeight || 0;
4682
+ const textLen = c.textContent?.trim().length || 0;
4683
+ const estH = Math.max(24, Math.ceil(textLen / 75) * 22 + 14);
4684
+ return Math.max(rectH, offH, estH);
4685
+ });
4686
+ wrapper.innerHTML = "";
4687
+ const createRtfCard = () => {
4688
+ const card = document.createElement("div");
4689
+ card.className = "fp-rtf-page-card";
4690
+ card.style.backgroundColor = "#ffffff";
4691
+ card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4692
+ card.style.borderRadius = "4px";
4693
+ card.style.padding = "72px 56px";
4694
+ card.style.width = "816px";
4695
+ card.style.minHeight = "1056px";
4696
+ card.style.boxSizing = "border-box";
4697
+ card.style.marginBottom = "24px";
4698
+ card.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4699
+ card.style.lineHeight = "1.6";
4700
+ return card;
4701
+ };
4702
+ let curCard = createRtfCard();
4703
+ wrapper.appendChild(curCard);
4704
+ pageElements = [curCard];
4705
+ let curH = 0;
4706
+ const maxH = 912;
4707
+ for (let i = 0; i < contentNodes.length; i++) {
4708
+ const child = contentNodes[i];
4709
+ const chH = childHeights[i];
4710
+ curCard.appendChild(child);
4711
+ curH += chH;
4712
+ if (curH >= maxH && i < contentNodes.length - 1) {
4713
+ curCard = createRtfCard();
4714
+ wrapper.appendChild(curCard);
4715
+ pageElements.push(curCard);
4716
+ curH = 0;
4390
4717
  }
4391
4718
  }
4392
4719
  } catch (err) {
@@ -4755,6 +5082,14 @@ var OpenDocumentPlugin = class {
4755
5082
  type: "button",
4756
5083
  group: "actions",
4757
5084
  execute: () => instance.print?.()
5085
+ },
5086
+ {
5087
+ id: "open-window",
5088
+ icon: "open-window",
5089
+ label: "Open in Separate Full Window",
5090
+ type: "button",
5091
+ group: "actions",
5092
+ execute: () => instance.openInSeparateWindow?.()
4758
5093
  }
4759
5094
  );
4760
5095
  return actions;
@@ -5343,6 +5678,14 @@ var DocPlugin = class {
5343
5678
  type: "button",
5344
5679
  group: "actions",
5345
5680
  execute: () => instance.print?.()
5681
+ },
5682
+ {
5683
+ id: "open-window",
5684
+ icon: "open-window",
5685
+ label: "Open in Separate Full Window",
5686
+ type: "button",
5687
+ group: "actions",
5688
+ execute: () => instance.openInSeparateWindow?.()
5346
5689
  }
5347
5690
  );
5348
5691
  return actions;
@@ -5362,8 +5705,17 @@ var DocPlugin = class {
5362
5705
  let scale = 1;
5363
5706
  let extractedRawText = "";
5364
5707
  let isFallback = false;
5708
+ let chartSvg = "";
5365
5709
  try {
5366
5710
  const cfbf = new CfbfReader(ctx.buffer);
5711
+ try {
5712
+ const pkg = cfbf.readStream("package_stream");
5713
+ if (pkg && pkg.length > 100) {
5714
+ chartSvg = this.parseOdfChartToSvg(pkg);
5715
+ }
5716
+ } catch (chartErr) {
5717
+ console.warn("[DocPlugin] Chart stream parsing info:", chartErr);
5718
+ }
5367
5719
  const wordDocStream = cfbf.readStream("WordDocument");
5368
5720
  if (!wordDocStream || wordDocStream.length < 512) {
5369
5721
  throw new Error("WordDocument stream not found or invalid in CFBF archive");
@@ -5381,7 +5733,7 @@ var DocPlugin = class {
5381
5733
  extractedRawText = fallback;
5382
5734
  isFallback = true;
5383
5735
  }
5384
- const rawPages = this.splitIntoPages(extractedRawText);
5736
+ const rawPages = this.splitIntoPages(extractedRawText, chartSvg);
5385
5737
  const totalPages = Math.max(1, rawPages.length);
5386
5738
  let currentPage = 1;
5387
5739
  const pageCards = [];
@@ -5623,7 +5975,7 @@ var DocPlugin = class {
5623
5975
  for (const run of [...ansiRuns, ...utf16Runs]) {
5624
5976
  const trimmed = run.trim();
5625
5977
  if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
5626
- 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)) {
5978
+ 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)) {
5627
5979
  seen.add(trimmed);
5628
5980
  candidateLines.push(trimmed);
5629
5981
  }
@@ -5634,12 +5986,138 @@ var DocPlugin = class {
5634
5986
  heuristicTextExtraction(buffer) {
5635
5987
  return this.extractStringsFromBytes(new Uint8Array(buffer));
5636
5988
  }
5637
- splitIntoPages(text) {
5989
+ /**
5990
+ * Parses an embedded OpenDocument Chart package into a vector SVG bar/column chart
5991
+ */
5992
+ parseOdfChartToSvg(zipBytes) {
5993
+ try {
5994
+ const unzipped = fflate__namespace.unzipSync(zipBytes);
5995
+ const contentXml = unzipped["content.xml"] ? new TextDecoder("utf-8").decode(unzipped["content.xml"]) : "";
5996
+ if (!contentXml) return "";
5997
+ const rowsMatch = contentXml.match(/<table:table-row[\s\S]*?<\/table:table-row>/g) || [];
5998
+ if (rowsMatch.length < 2) return "";
5999
+ const headers = [];
6000
+ const firstRow = rowsMatch[0];
6001
+ const headerCells = firstRow ? firstRow.match(/<text:p>([^<]+)<\/text:p>/g) || [] : [];
6002
+ for (const h of headerCells) {
6003
+ headers.push(h.replace(/<\/?text:p>/g, "").trim());
6004
+ }
6005
+ const categories = [];
6006
+ const seriesValues = headers.map(() => []);
6007
+ for (let r = 1; r < rowsMatch.length; r++) {
6008
+ const rowStr = rowsMatch[r];
6009
+ if (!rowStr) continue;
6010
+ const cells = rowStr.match(/<table:table-cell[\s\S]*?<\/table:table-cell>/g) || [];
6011
+ if (cells.length > 0 && cells[0]) {
6012
+ const catMatch = cells[0].match(/<text:p>([^<]+)<\/text:p>/);
6013
+ categories.push(catMatch ? catMatch[1] : "Row " + r);
6014
+ for (let c = 1; c < cells.length && c - 1 < headers.length; c++) {
6015
+ const cellStr = cells[c];
6016
+ if (!cellStr) continue;
6017
+ const valMatch = cellStr.match(/office:value="([0-9.]+)"/) || cellStr.match(/<text:p>([0-9.]+)<\/text:p>/);
6018
+ const series = seriesValues[c - 1];
6019
+ if (series) {
6020
+ series.push(valMatch ? parseFloat(valMatch[1]) : 0);
6021
+ }
6022
+ }
6023
+ }
6024
+ }
6025
+ const colors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021"];
6026
+ const colorMatches = contentXml.matchAll(/draw:fill-color="(#[0-9a-fA-F]{6})"/g);
6027
+ let cIdx = 0;
6028
+ for (const cm of colorMatches) {
6029
+ if (cIdx < colors.length) colors[cIdx] = cm[1];
6030
+ cIdx++;
6031
+ }
6032
+ let maxVal = 10;
6033
+ for (const s of seriesValues) {
6034
+ for (const v of s) {
6035
+ if (v > maxVal) maxVal = v;
6036
+ }
6037
+ }
6038
+ maxVal = Math.ceil(maxVal * 1.15);
6039
+ const width = 560;
6040
+ const height = 280;
6041
+ const padLeft = 45;
6042
+ const padRight = 100;
6043
+ const padTop = 20;
6044
+ const padBottom = 40;
6045
+ const chartW = width - padLeft - padRight;
6046
+ const chartH = height - padTop - padBottom;
6047
+ 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);">`;
6048
+ for (let step = 0; step <= 4; step++) {
6049
+ const yVal = (maxVal / 4 * step).toFixed(1);
6050
+ const yPos = padTop + chartH - step / 4 * chartH;
6051
+ svg += `<line x1="${padLeft}" y1="${yPos}" x2="${padLeft + chartW}" y2="${yPos}" stroke="#e2e8f0" stroke-dasharray="2,2" />`;
6052
+ svg += `<text x="${padLeft - 8}" y="${yPos + 4}" font-size="11" fill="#64748b" text-anchor="end">${yVal}</text>`;
6053
+ }
6054
+ const numCats = categories.length;
6055
+ const numSeries = headers.length;
6056
+ const groupW = chartW / numCats;
6057
+ const barW = Math.max(8, groupW * 0.7 / numSeries);
6058
+ const groupPad = (groupW - barW * numSeries) / 2;
6059
+ for (let catIdx = 0; catIdx < numCats; catIdx++) {
6060
+ const groupX = padLeft + catIdx * groupW + groupPad;
6061
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6062
+ const val = seriesValues[sIdx][catIdx] || 0;
6063
+ const barH = val / maxVal * chartH;
6064
+ const barX = groupX + sIdx * barW;
6065
+ const barY = padTop + chartH - barH;
6066
+ const col = colors[sIdx % colors.length];
6067
+ svg += `<rect x="${barX}" y="${barY}" width="${barW - 2}" height="${barH}" fill="${col}" rx="2"><title>${headers[sIdx]}: ${val}</title></rect>`;
6068
+ }
6069
+ const catX = padLeft + catIdx * groupW + groupW / 2;
6070
+ svg += `<text x="${catX}" y="${padTop + chartH + 18}" font-size="11" fill="#475569" text-anchor="middle">${categories[catIdx]}</text>`;
6071
+ }
6072
+ let legendY = padTop + 20;
6073
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6074
+ const col = colors[sIdx % colors.length];
6075
+ svg += `<rect x="${padLeft + chartW + 15}" y="${legendY}" width="12" height="12" fill="${col}" rx="2" />`;
6076
+ svg += `<text x="${padLeft + chartW + 32}" y="${legendY + 10}" font-size="11" fill="#334155">${headers[sIdx]}</text>`;
6077
+ legendY += 20;
6078
+ }
6079
+ svg += "</svg>";
6080
+ return svg;
6081
+ } catch (e) {
6082
+ console.warn("[DocPlugin] Error generating chart SVG:", e);
6083
+ return "";
6084
+ }
6085
+ }
6086
+ cleanWordDocFields(text, chartSvg = "") {
6087
+ if (!text) return "";
6088
+ let cleaned = text.replace(
6089
+ /\x13\s*EMBED\b[\s\S]*?\x15/gi,
6090
+ () => chartSvg ? `
6091
+
6092
+ ${chartSvg}
6093
+
6094
+ ` : ""
6095
+ );
6096
+ cleaned = cleaned.replace(
6097
+ /\x13\s*HYPERLINK\s*"?([^"\x14]+)"?\s*\x14([\s\S]*?)\x15/gi,
6098
+ (_match, url, label) => {
6099
+ const cleanUrl = url.trim();
6100
+ const cleanLabel = label.trim() || cleanUrl;
6101
+ return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${cleanLabel}</a>`;
6102
+ }
6103
+ );
6104
+ cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, (_m, res) => {
6105
+ if (/[\x00-\x1F]/.test(res)) return "";
6106
+ return res.trim();
6107
+ });
6108
+ cleaned = cleaned.replace(/\x13[^\x15]*\x15/g, "");
6109
+ cleaned = cleaned.replace(/[\x13\x14\x15]/g, "");
6110
+ cleaned = cleaned.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
6111
+ cleaned = cleaned.replace(/EMBED\s+LibreOffice\.ChartDocument\.[0-9]+/gi, chartSvg || "");
6112
+ return cleaned;
6113
+ }
6114
+ splitIntoPages(text, chartSvg = "") {
5638
6115
  if (!text) return [""];
5639
- const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
6116
+ const cleanedText = this.cleanWordDocFields(text, chartSvg);
6117
+ const normalized = cleanedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
5640
6118
  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);
5641
6119
  if (explicitParts.length === 0) explicitParts.push(normalized);
5642
- const maxLinesPerPage = 32;
6120
+ const maxLinesPerPage = 34;
5643
6121
  const charsPerLine = 80;
5644
6122
  const finalPages = [];
5645
6123
  for (const part of explicitParts) {
@@ -5647,7 +6125,8 @@ var DocPlugin = class {
5647
6125
  let currentLines = [];
5648
6126
  let count = 0;
5649
6127
  for (const line of lines) {
5650
- const vLines = Math.max(1, Math.ceil((line.length || 1) / charsPerLine));
6128
+ const isSvg = line.includes("<svg");
6129
+ const vLines = isSvg ? 12 : Math.max(1, Math.ceil((line.replace(/<[^>]+>/g, "").length || 1) / charsPerLine));
5651
6130
  if (count + vLines > maxLinesPerPage && currentLines.length > 0) {
5652
6131
  finalPages.push(currentLines.join("\n"));
5653
6132
  currentLines = [];
@@ -5687,9 +6166,44 @@ var DocPlugin = class {
5687
6166
  tableLines = [];
5688
6167
  }
5689
6168
  };
6169
+ const sanitizeOptions = {
6170
+ ADD_TAGS: ["a", "svg", "g", "path", "line", "rect", "circle", "text", "title"],
6171
+ ADD_ATTR: [
6172
+ "href",
6173
+ "target",
6174
+ "rel",
6175
+ "style",
6176
+ "viewBox",
6177
+ "width",
6178
+ "height",
6179
+ "x",
6180
+ "y",
6181
+ "x1",
6182
+ "y1",
6183
+ "x2",
6184
+ "y2",
6185
+ "fill",
6186
+ "stroke",
6187
+ "stroke-width",
6188
+ "stroke-dasharray",
6189
+ "rx",
6190
+ "font-size",
6191
+ "text-anchor"
6192
+ ]
6193
+ };
5690
6194
  let i = 0;
5691
6195
  while (i < lines.length) {
5692
6196
  let line = lines[i];
6197
+ if (line.includes("<svg")) {
6198
+ if (inList) {
6199
+ html += "</ul>";
6200
+ inList = false;
6201
+ }
6202
+ flushTable();
6203
+ html += line;
6204
+ i++;
6205
+ continue;
6206
+ }
5693
6207
  let tabCount = (line.match(/\t/g) || []).length;
5694
6208
  if (tabCount > 0) {
5695
6209
  let j = i;
@@ -5728,20 +6242,20 @@ var DocPlugin = class {
5728
6242
  html += "</ul>";
5729
6243
  inList = false;
5730
6244
  }
5731
- html += `<h2 style="font-size: 16px; font-weight: 700; color: #1e3a8a; margin: 16px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify6__default.default.sanitize(line)}</h2>`;
6245
+ html += `<h2 style="font-size: 16px; font-weight: 700; color: #1e3a8a; margin: 16px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify6__default.default.sanitize(line, sanitizeOptions)}</h2>`;
5732
6246
  } else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
5733
6247
  if (!inList) {
5734
6248
  html += '<ul style="margin: 8px 0; padding-left: 24px;">';
5735
6249
  inList = true;
5736
6250
  }
5737
6251
  const bulletText = line.replace(/^[•\-\*]\s*/, "");
5738
- html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6__default.default.sanitize(bulletText)}</li>`;
6252
+ html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6__default.default.sanitize(bulletText, sanitizeOptions)}</li>`;
5739
6253
  } else {
5740
6254
  if (inList) {
5741
6255
  html += "</ul>";
5742
6256
  inList = false;
5743
6257
  }
5744
- html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6__default.default.sanitize(line)}</p>`;
6258
+ html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6__default.default.sanitize(line, sanitizeOptions)}</p>`;
5745
6259
  }
5746
6260
  i++;
5747
6261
  }