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