@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/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import DOMPurify6 from 'dompurify';
2
2
  import * as pdfjsLib from 'pdfjs-dist';
3
3
  import * as docx from 'docx-preview';
4
+ import * as fflate from 'fflate';
4
5
  import { unzipSync, strFromU8, unzip } from 'fflate';
5
6
  import * as XLSX from 'xlsx';
6
7
  import hljs from 'highlight.js';
@@ -336,16 +337,21 @@ async function sourceToArrayBuffer(source, signal) {
336
337
  metadata.mimeType = source.type || void 0;
337
338
  metadata.extension = extractExtension(source.name);
338
339
  buffer = await source.arrayBuffer();
339
- } else if (source instanceof Blob) {
340
+ } else if (source instanceof Blob || source && typeof source.arrayBuffer === "function" && typeof source.size === "number") {
340
341
  metadata.size = source.size;
341
342
  metadata.mimeType = source.type || void 0;
343
+ if (source.name) {
344
+ metadata.name = source.name;
345
+ metadata.extension = extractExtension(source.name);
346
+ }
342
347
  buffer = await source.arrayBuffer();
343
- } else if (source instanceof ArrayBuffer) {
348
+ } else if (source instanceof ArrayBuffer || Object.prototype.toString.call(source) === "[object ArrayBuffer]" || source && typeof source.byteLength === "number" && typeof source.slice === "function") {
344
349
  buffer = source;
345
- } else if (source instanceof Uint8Array) {
346
- buffer = source.buffer.slice(
347
- source.byteOffset,
348
- source.byteOffset + source.byteLength
350
+ } else if (source instanceof Uint8Array || ArrayBuffer.isView(source)) {
351
+ const view = source;
352
+ buffer = view.buffer.slice(
353
+ view.byteOffset,
354
+ view.byteOffset + view.byteLength
349
355
  );
350
356
  } else {
351
357
  throw new Error("Unsupported file source type");
@@ -474,6 +480,86 @@ function createElement(tag, attrs, ...children) {
474
480
  }
475
481
  return el;
476
482
  }
483
+ var DB_NAME = "PreviewFileTransferDB";
484
+ var DB_STORE = "transfers";
485
+ function openDB() {
486
+ return new Promise((resolve, reject) => {
487
+ if (typeof indexedDB === "undefined") {
488
+ return reject(new Error("IndexedDB is not available"));
489
+ }
490
+ const req = indexedDB.open(DB_NAME, 1);
491
+ req.onupgradeneeded = () => {
492
+ const db = req.result;
493
+ if (!db.objectStoreNames.contains(DB_STORE)) {
494
+ db.createObjectStore(DB_STORE, { keyPath: "id" });
495
+ }
496
+ };
497
+ req.onsuccess = () => resolve(req.result);
498
+ req.onerror = () => reject(req.error);
499
+ });
500
+ }
501
+ async function saveTransferPayload(id, payload) {
502
+ if (typeof window !== "undefined") {
503
+ try {
504
+ window[id] = payload;
505
+ window.__lastTransfer = payload;
506
+ } catch {
507
+ }
508
+ }
509
+ try {
510
+ const db = await openDB();
511
+ return new Promise((resolve, reject) => {
512
+ const tx = db.transaction(DB_STORE, "readwrite");
513
+ const store = tx.objectStore(DB_STORE);
514
+ store.put({ id, ...payload, timestamp: Date.now() });
515
+ tx.oncomplete = () => resolve();
516
+ tx.onerror = () => reject(tx.error);
517
+ });
518
+ } catch (e) {
519
+ console.warn("[saveTransferPayload] IndexedDB store warning:", e);
520
+ }
521
+ }
522
+ async function getTransferPayload(id) {
523
+ if (typeof window !== "undefined") {
524
+ try {
525
+ if (window.opener && window.opener[id]) {
526
+ return window.opener[id];
527
+ }
528
+ if (window[id]) {
529
+ return window[id];
530
+ }
531
+ if (window.opener && window.opener.__lastTransfer) {
532
+ return window.opener.__lastTransfer;
533
+ }
534
+ if (window.__lastTransfer) {
535
+ return window.__lastTransfer;
536
+ }
537
+ } catch {
538
+ }
539
+ }
540
+ try {
541
+ const db = await openDB();
542
+ return new Promise((resolve) => {
543
+ const tx = db.transaction(DB_STORE, "readonly");
544
+ const store = tx.objectStore(DB_STORE);
545
+ const req = store.get(id);
546
+ req.onsuccess = () => {
547
+ if (req.result && req.result.buffer) {
548
+ resolve({
549
+ buffer: req.result.buffer,
550
+ metadata: req.result.metadata,
551
+ options: req.result.options
552
+ });
553
+ } else {
554
+ resolve(null);
555
+ }
556
+ };
557
+ req.onerror = () => resolve(null);
558
+ });
559
+ } catch {
560
+ return null;
561
+ }
562
+ }
477
563
  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>`;
478
564
  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>`;
479
565
  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>`;
@@ -493,6 +579,7 @@ var ICON_COPY = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" str
493
579
  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>`;
494
580
  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>`;
495
581
  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>`;
582
+ 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>`;
496
583
  var ICON_MAP = {
497
584
  "zoom-in": ICON_ZOOM_IN,
498
585
  "zoom-out": ICON_ZOOM_OUT,
@@ -519,7 +606,9 @@ var ICON_MAP = {
519
606
  "forward-10": ICON_FAST_FORWARD,
520
607
  "rewind": ICON_REWIND,
521
608
  "replay-10": ICON_REWIND,
522
- "speed": ICON_SPEED
609
+ "speed": ICON_SPEED,
610
+ "open-window": ICON_EXTERNAL_WINDOW,
611
+ "external-window": ICON_EXTERNAL_WINDOW
523
612
  };
524
613
  var ToolbarController = class {
525
614
  el;
@@ -662,9 +751,11 @@ var ToolbarController = class {
662
751
  type: "button",
663
752
  "data-action-id": id
664
753
  });
665
- const svg = ICON_MAP[iconHtml] || ICON_MAP[id] || (iconHtml && iconHtml.startsWith("<svg") ? iconHtml : null);
666
- if (svg) {
667
- btn.innerHTML = sanitizeSVG(svg);
754
+ const internalSvg = ICON_MAP[iconHtml] || ICON_MAP[id];
755
+ if (internalSvg) {
756
+ btn.innerHTML = internalSvg;
757
+ } else if (iconHtml && iconHtml.startsWith("<svg")) {
758
+ btn.innerHTML = sanitizeSVG(iconHtml);
668
759
  } else {
669
760
  btn.textContent = title || id;
670
761
  }
@@ -736,7 +827,7 @@ var ThumbnailPanel = class {
736
827
  }
737
828
  }
738
829
  };
739
- var FilePreviewViewer = class {
830
+ var FilePreviewViewer = class _FilePreviewViewer {
740
831
  plugins = [];
741
832
  activeInstance = null;
742
833
  abortController = null;
@@ -773,6 +864,7 @@ var FilePreviewViewer = class {
773
864
  * Preview a file in the given container element.
774
865
  */
775
866
  async preview(container, source, options = {}) {
867
+ this.currentOptions = options;
776
868
  this.abort();
777
869
  this.abortController = new AbortController();
778
870
  const { signal } = this.abortController;
@@ -782,7 +874,10 @@ var FilePreviewViewer = class {
782
874
  this.showLoading();
783
875
  try {
784
876
  const { buffer, metadata } = await sourceToArrayBuffer(source, signal);
785
- this.currentBuffer = buffer;
877
+ if (options.metadata) {
878
+ Object.assign(metadata, options.metadata);
879
+ }
880
+ this.currentBuffer = buffer.slice(0);
786
881
  this.currentMetadata = metadata;
787
882
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
788
883
  const fileInfo = { metadata, buffer };
@@ -812,6 +907,7 @@ var FilePreviewViewer = class {
812
907
  }
813
908
  });
814
909
  this.activeInstance = instance;
910
+ instance.openInSeparateWindow = () => this.openInSeparateWindow();
815
911
  this.hideLoading();
816
912
  this.eventEmitter.emit("loaded", { metadata, plugin: matchedPlugin.id });
817
913
  if (options.showToolbar !== false && this.toolbar) {
@@ -821,26 +917,16 @@ var FilePreviewViewer = class {
821
917
  actions.push({
822
918
  id: "fullscreen",
823
919
  icon: "fullscreen",
824
- label: "Toggle Fullscreen",
920
+ label: "Fullscreen",
825
921
  type: "button",
826
922
  group: "view",
827
- execute: async () => {
923
+ execute: () => {
828
924
  try {
829
- const isNativeFs = !!document.fullscreenElement;
830
- const isCssFs = this.wrapperEl?.classList.contains("fp-fullscreen-active");
831
- if (!isNativeFs && !isCssFs) {
832
- if (this.wrapperEl?.requestFullscreen) {
833
- await this.wrapperEl.requestFullscreen().catch(() => {
834
- this.wrapperEl?.classList.add("fp-fullscreen-active");
835
- });
836
- } else {
837
- this.wrapperEl?.classList.add("fp-fullscreen-active");
838
- }
925
+ if (!document.fullscreenElement) {
926
+ this.wrapperEl?.requestFullscreen?.();
927
+ this.wrapperEl?.classList.add("fp-fullscreen-active");
839
928
  } else {
840
- if (document.fullscreenElement) {
841
- await document.exitFullscreen().catch(() => {
842
- });
843
- }
929
+ document.exitFullscreen?.();
844
930
  this.wrapperEl?.classList.remove("fp-fullscreen-active");
845
931
  }
846
932
  } catch {
@@ -852,6 +938,28 @@ var FilePreviewViewer = class {
852
938
  }
853
939
  });
854
940
  }
941
+ const openWinAction = actions.find((a) => a.id === "open-window");
942
+ if (openWinAction) {
943
+ if (options?._isSeparateWindow) {
944
+ const idx = actions.indexOf(openWinAction);
945
+ if (idx !== -1) actions.splice(idx, 1);
946
+ } else {
947
+ openWinAction.execute = () => {
948
+ this.openInSeparateWindow();
949
+ };
950
+ }
951
+ } else if (!options?._isSeparateWindow) {
952
+ actions.push({
953
+ id: "open-window",
954
+ icon: "open-window",
955
+ label: "Open in Separate Full Window",
956
+ type: "button",
957
+ group: "actions",
958
+ execute: () => {
959
+ this.openInSeparateWindow();
960
+ }
961
+ });
962
+ }
855
963
  this.toolbar.update(actions);
856
964
  this.toolbar.show();
857
965
  }
@@ -884,6 +992,104 @@ var FilePreviewViewer = class {
884
992
  throw error;
885
993
  }
886
994
  }
995
+ /**
996
+ * Opens the current file preview in a separate full browser window.
997
+ */
998
+ openInSeparateWindow() {
999
+ if (!this.currentBuffer) {
1000
+ console.warn("[FilePreviewViewer] No active file buffer to open in separate window");
1001
+ return null;
1002
+ }
1003
+ if (this.currentOptions.onOpenSeparateWindow) {
1004
+ return this.currentOptions.onOpenSeparateWindow({
1005
+ buffer: this.currentBuffer,
1006
+ metadata: this.currentMetadata || { name: "Document" },
1007
+ options: this.currentOptions
1008
+ });
1009
+ }
1010
+ const transferId = "fp_win_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
1011
+ let clonedBuffer;
1012
+ try {
1013
+ clonedBuffer = this.currentBuffer.slice(0);
1014
+ } catch {
1015
+ clonedBuffer = this.currentBuffer;
1016
+ }
1017
+ const payload = {
1018
+ buffer: clonedBuffer,
1019
+ metadata: this.currentMetadata ? { ...this.currentMetadata } : void 0,
1020
+ options: { ...this.currentOptions, _isSeparateWindow: true }
1021
+ };
1022
+ if (typeof window !== "undefined") {
1023
+ try {
1024
+ window[transferId] = payload;
1025
+ window.__lastTransfer = payload;
1026
+ } catch {
1027
+ }
1028
+ }
1029
+ saveTransferPayload(transferId, payload).catch((err) => {
1030
+ console.warn("[FilePreviewViewer] Transfer payload save warning:", err);
1031
+ });
1032
+ let targetUrl = null;
1033
+ if (this.currentOptions.standaloneViewerUrl) {
1034
+ const u = new URL(this.currentOptions.standaloneViewerUrl, window.location.href);
1035
+ u.searchParams.set("mode", "fullscreen");
1036
+ u.searchParams.set("transferId", transferId);
1037
+ targetUrl = u.toString();
1038
+ } else if (typeof window !== "undefined" && window.location?.href && !window.location.href.startsWith("about:")) {
1039
+ const u = new URL(window.location.href);
1040
+ u.searchParams.set("mode", "fullscreen");
1041
+ u.searchParams.set("transferId", transferId);
1042
+ targetUrl = u.toString();
1043
+ }
1044
+ if (targetUrl) {
1045
+ const newWin2 = window.open(targetUrl, "_blank");
1046
+ if (!newWin2) {
1047
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1048
+ return null;
1049
+ }
1050
+ return newWin2;
1051
+ }
1052
+ const title = (this.currentMetadata?.name || "Document Preview") + " - Full Preview";
1053
+ const newWin = window.open("", "_blank");
1054
+ if (!newWin) {
1055
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1056
+ return null;
1057
+ }
1058
+ newWin.document.title = title;
1059
+ newWin.document.body.style.margin = "0";
1060
+ newWin.document.body.style.padding = "0";
1061
+ newWin.document.body.style.width = "100vw";
1062
+ newWin.document.body.style.height = "100vh";
1063
+ newWin.document.body.style.overflow = "hidden";
1064
+ newWin.document.body.style.backgroundColor = "#f8fafc";
1065
+ const headNodes = document.querySelectorAll('link[rel="stylesheet"], style');
1066
+ headNodes.forEach((node) => {
1067
+ newWin.document.head.appendChild(node.cloneNode(true));
1068
+ });
1069
+ const root = newWin.document.createElement("div");
1070
+ root.id = "full-window-preview-root";
1071
+ root.style.width = "100%";
1072
+ root.style.height = "100%";
1073
+ root.style.overflow = "hidden";
1074
+ newWin.document.body.appendChild(root);
1075
+ const separateViewer = new _FilePreviewViewer();
1076
+ for (const plugin of this.plugins) {
1077
+ separateViewer.registerPlugin(plugin);
1078
+ }
1079
+ separateViewer.preview(root, this.currentBuffer.slice(0), {
1080
+ ...this.currentOptions,
1081
+ showToolbar: true,
1082
+ toolbarPosition: "top",
1083
+ metadata: this.currentMetadata || void 0,
1084
+ _isSeparateWindow: true
1085
+ }).catch((err) => {
1086
+ console.error("[FilePreviewViewer] Error rendering in separate window:", err);
1087
+ });
1088
+ newWin.addEventListener("beforeunload", () => {
1089
+ separateViewer.destroy();
1090
+ });
1091
+ return newWin;
1092
+ }
887
1093
  /**
888
1094
  * Subscribe to viewer events.
889
1095
  */
@@ -1289,8 +1495,20 @@ var CfbfReader = class {
1289
1495
  }
1290
1496
  };
1291
1497
  if (typeof window !== "undefined" && pdfjsLib.GlobalWorkerOptions) {
1292
- if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
1293
- pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1498
+ if (!pdfjsLib.GlobalWorkerOptions.workerPort && !pdfjsLib.GlobalWorkerOptions.workerSrc) {
1499
+ const customWorker = window.__PDF_WORKER_SRC__;
1500
+ if (customWorker) {
1501
+ pdfjsLib.GlobalWorkerOptions.workerSrc = customWorker;
1502
+ } else {
1503
+ try {
1504
+ pdfjsLib.GlobalWorkerOptions.workerPort = new Worker(
1505
+ new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url),
1506
+ { type: "module" }
1507
+ );
1508
+ } catch {
1509
+ pdfjsLib.GlobalWorkerOptions.workerSrc = "./pdf.worker.min.mjs";
1510
+ }
1511
+ }
1294
1512
  }
1295
1513
  }
1296
1514
  var PdfPlugin = class {
@@ -1384,6 +1602,14 @@ var PdfPlugin = class {
1384
1602
  type: "button",
1385
1603
  group: "actions",
1386
1604
  execute: () => instance.print?.()
1605
+ },
1606
+ {
1607
+ id: "open-window",
1608
+ icon: "open-window",
1609
+ label: "Open in Separate Full Window",
1610
+ type: "button",
1611
+ group: "actions",
1612
+ execute: () => instance.openInSeparateWindow?.()
1387
1613
  }
1388
1614
  ];
1389
1615
  }
@@ -1433,18 +1659,21 @@ var PdfPlugin = class {
1433
1659
  indicator.style.pointerEvents = "none";
1434
1660
  container.appendChild(indicator);
1435
1661
  ctx.container.appendChild(container);
1662
+ const standardFontsUrl = typeof window !== "undefined" && window.__PDF_STANDARD_FONTS_URL__ || "./standard_fonts/";
1663
+ const cmapsUrl = typeof window !== "undefined" && window.__PDF_CMAPS_URL__ || "./cmaps/";
1436
1664
  const loadingTask = pdfjsLib.getDocument({
1437
- data: new Uint8Array(ctx.buffer),
1438
- cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/cmaps/`,
1665
+ data: new Uint8Array(ctx.buffer.slice(0)),
1666
+ cMapUrl: cmapsUrl,
1439
1667
  cMapPacked: true,
1440
- standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/standard_fonts/`
1668
+ standardFontDataUrl: standardFontsUrl,
1669
+ verbosity: 0
1441
1670
  });
1442
1671
  const pdfDoc = await loadingTask.promise;
1443
1672
  const totalPages = Math.max(1, pdfDoc.numPages);
1444
1673
  let currentPage = 1;
1445
1674
  let zoomScale = 1;
1446
1675
  let rotation = 0;
1447
- let fitMode = "width";
1676
+ let fitMode = "page";
1448
1677
  let currentRenderTask = null;
1449
1678
  const renderPage = async (pageNum) => {
1450
1679
  if (currentRenderTask) {
@@ -1464,15 +1693,15 @@ var PdfPlugin = class {
1464
1693
  const containerWidth = container.clientWidth || 900;
1465
1694
  const containerHeight = container.clientHeight || 700;
1466
1695
  const unscaledVp = page.getViewport({ scale: 1, rotation });
1467
- const availWidth = Math.max(280, containerWidth - 48);
1468
- const availHeight = Math.max(280, containerHeight - 88);
1696
+ const availWidth = Math.max(320, containerWidth - 48);
1697
+ const availHeight = Math.max(550, containerHeight - 88);
1469
1698
  const scaleW = availWidth / unscaledVp.width;
1470
1699
  const scaleH = availHeight / unscaledVp.height;
1471
1700
  let fitScale;
1472
1701
  if (fitMode === "page") {
1473
- fitScale = Math.max(0.5, Math.min(scaleW, scaleH));
1702
+ fitScale = Math.max(0.4, Math.min(scaleW, scaleH));
1474
1703
  } else {
1475
- fitScale = Math.max(0.65, Math.min(1.15, scaleW));
1704
+ fitScale = Math.max(0.65, Math.min(1.25, scaleW));
1476
1705
  }
1477
1706
  const effectiveScale = (fitScale > 0 ? fitScale : 1) * zoomScale;
1478
1707
  const pixelRatio = window.devicePixelRatio || 1;
@@ -1498,7 +1727,7 @@ var PdfPlugin = class {
1498
1727
  currentRenderTask = null;
1499
1728
  }
1500
1729
  };
1501
- await renderPage(1);
1730
+ renderPage(1);
1502
1731
  let resizeTimer = null;
1503
1732
  const resizeObserver = new ResizeObserver(() => {
1504
1733
  if (resizeTimer) clearTimeout(resizeTimer);
@@ -1542,7 +1771,7 @@ var PdfPlugin = class {
1542
1771
  renderPage(currentPage);
1543
1772
  },
1544
1773
  fitToPage: () => {
1545
- fitMode = fitMode === "width" ? "page" : "width";
1774
+ fitMode = fitMode === "page" ? "width" : "page";
1546
1775
  zoomScale = 1;
1547
1776
  rotation = 0;
1548
1777
  renderPage(currentPage);
@@ -2019,6 +2248,14 @@ var DocxPlugin = class {
2019
2248
  type: "button",
2020
2249
  group: "actions",
2021
2250
  execute: () => instance.print?.()
2251
+ },
2252
+ {
2253
+ id: "open-window",
2254
+ icon: "open-window",
2255
+ label: "Open in Separate Full Window",
2256
+ type: "button",
2257
+ group: "actions",
2258
+ execute: () => instance.openInSeparateWindow?.()
2022
2259
  }
2023
2260
  );
2024
2261
  return actions;
@@ -2078,6 +2315,31 @@ var DocxPlugin = class {
2078
2315
  }
2079
2316
  if (wrapper.children.length > 0 && (wrapper.textContent?.trim().length ?? 0) > 0) {
2080
2317
  renderedSuccessfully = true;
2318
+ try {
2319
+ const unzipped = unzipSync(new Uint8Array(ctx.buffer));
2320
+ const chartKeys = Object.keys(unzipped).filter((k) => k.replace(/^[./\\]+/, "").toLowerCase().startsWith("word/charts/chart") && k.endsWith(".xml")).sort();
2321
+ if (chartKeys.length > 0) {
2322
+ const allDivs = Array.from(wrapper.querySelectorAll("div"));
2323
+ const emptyContainers = allDivs.filter((div) => {
2324
+ const st = div.getAttribute("style") || "";
2325
+ return st.includes("width:") && st.includes("height:") && div.children.length === 0 && (div.textContent?.trim().length ?? 0) === 0;
2326
+ });
2327
+ chartKeys.forEach((cKey, idx) => {
2328
+ const target = emptyContainers[idx];
2329
+ if (target) {
2330
+ const xmlStr = strFromU8(unzipped[cKey]);
2331
+ const svg = this.parseAndRenderChartSvg(xmlStr);
2332
+ if (svg) {
2333
+ target.innerHTML = svg;
2334
+ target.style.display = "block";
2335
+ target.style.margin = "12px auto";
2336
+ }
2337
+ }
2338
+ });
2339
+ }
2340
+ } catch (chartErr) {
2341
+ console.warn("[DocxPlugin] Non-critical error rendering DrawingML charts:", chartErr);
2342
+ }
2081
2343
  }
2082
2344
  } catch (err) {
2083
2345
  console.warn("[DocxPlugin] docx-preview failed, triggering native fallback:", err);
@@ -2109,64 +2371,74 @@ var DocxPlugin = class {
2109
2371
  }
2110
2372
  let sections = Array.from(wrapper.querySelectorAll("section.docx"));
2111
2373
  const cards = Array.from(wrapper.querySelectorAll(".fp-docx-page-card"));
2112
- if (sections.length === 1 && cards.length === 0) {
2113
- const singleSec = sections[0];
2114
- const contentContainer = singleSec.querySelector("article") || singleSec;
2115
- const children = Array.from(contentContainer.children);
2116
- const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2117
- const secH = singleSec.offsetHeight || singleSec.scrollHeight;
2118
- if (secH > pageH * 1.25 && children.length > 1) {
2119
- const childHeights = children.map((c) => {
2120
- const rectH = c.getBoundingClientRect().height;
2121
- const offH = c.offsetHeight;
2122
- const textLen = c.textContent?.trim().length || 0;
2123
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2124
- return Math.max(rectH, offH, estH);
2125
- });
2126
- const parent = singleSec.parentElement || wrapper;
2127
- const newSections = [singleSec];
2128
- const headerEl = singleSec.querySelector("header");
2129
- const footerEl = singleSec.querySelector("footer");
2130
- contentContainer.innerHTML = "";
2131
- singleSec.style.minHeight = `${pageH}px`;
2132
- singleSec.style.boxSizing = "border-box";
2133
- let curContent = contentContainer;
2134
- let curH = 0;
2135
- const maxH = pageH - 140;
2136
- for (let i = 0; i < children.length; i++) {
2137
- const child = children[i];
2138
- const chH = childHeights[i];
2139
- curContent.appendChild(child);
2140
- curH += chH;
2141
- if (curH >= maxH && i < children.length - 1) {
2142
- const nextSec = document.createElement("section");
2143
- nextSec.className = singleSec.className;
2144
- nextSec.style.cssText = singleSec.style.cssText;
2145
- nextSec.style.minHeight = `${pageH}px`;
2146
- nextSec.style.boxSizing = "border-box";
2147
- nextSec.style.backgroundColor = "#ffffff";
2148
- nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2149
- nextSec.style.borderRadius = "4px";
2150
- nextSec.style.marginBottom = "24px";
2151
- if (headerEl) {
2152
- nextSec.appendChild(headerEl.cloneNode(true));
2153
- }
2154
- const nextArticle = document.createElement("article");
2155
- if (contentContainer.tagName.toLowerCase() === "article") {
2156
- nextArticle.style.cssText = contentContainer.style.cssText;
2157
- }
2158
- nextSec.appendChild(nextArticle);
2159
- if (footerEl) {
2160
- nextSec.appendChild(footerEl.cloneNode(true));
2374
+ if (sections.length > 0 && cards.length === 0) {
2375
+ const finalSections = [];
2376
+ for (const singleSec of sections) {
2377
+ const contentContainer = singleSec.querySelector("article") || singleSec;
2378
+ const children = Array.from(contentContainer.children);
2379
+ const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2380
+ const secH = singleSec.scrollHeight || singleSec.offsetHeight;
2381
+ if (secH > pageH * 1.25 && children.length > 1) {
2382
+ const childHeights = children.map((c) => {
2383
+ const rectH = c.getBoundingClientRect().height;
2384
+ const offH = c.offsetHeight;
2385
+ const textLen = c.textContent?.trim().length || 0;
2386
+ const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2387
+ return Math.max(rectH, offH, estH);
2388
+ });
2389
+ const parent = singleSec.parentElement || wrapper;
2390
+ const headerEl = singleSec.querySelector("header");
2391
+ const footerEl = singleSec.querySelector("footer");
2392
+ contentContainer.innerHTML = "";
2393
+ singleSec.style.minHeight = `${pageH}px`;
2394
+ singleSec.style.boxSizing = "border-box";
2395
+ let curContent = contentContainer;
2396
+ let curSec = singleSec;
2397
+ let curH = 0;
2398
+ const maxH = pageH - 140;
2399
+ finalSections.push(singleSec);
2400
+ for (let i = 0; i < children.length; i++) {
2401
+ const child = children[i];
2402
+ const chH = childHeights[i];
2403
+ curContent.appendChild(child);
2404
+ curH += chH;
2405
+ if (curH >= maxH && i < children.length - 1) {
2406
+ const nextSec = document.createElement("section");
2407
+ nextSec.className = singleSec.className;
2408
+ nextSec.style.cssText = singleSec.style.cssText;
2409
+ nextSec.style.minHeight = `${pageH}px`;
2410
+ nextSec.style.boxSizing = "border-box";
2411
+ nextSec.style.backgroundColor = "#ffffff";
2412
+ nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2413
+ nextSec.style.borderRadius = "4px";
2414
+ nextSec.style.marginBottom = "24px";
2415
+ if (headerEl) {
2416
+ nextSec.appendChild(headerEl.cloneNode(true));
2417
+ }
2418
+ const nextArticle = document.createElement("article");
2419
+ if (contentContainer.tagName.toLowerCase() === "article") {
2420
+ nextArticle.style.cssText = contentContainer.style.cssText;
2421
+ }
2422
+ nextSec.appendChild(nextArticle);
2423
+ if (footerEl) {
2424
+ nextSec.appendChild(footerEl.cloneNode(true));
2425
+ }
2426
+ if (curSec.nextSibling) {
2427
+ parent.insertBefore(nextSec, curSec.nextSibling);
2428
+ } else {
2429
+ parent.appendChild(nextSec);
2430
+ }
2431
+ finalSections.push(nextSec);
2432
+ curSec = nextSec;
2433
+ curContent = nextArticle;
2434
+ curH = 0;
2161
2435
  }
2162
- parent.appendChild(nextSec);
2163
- newSections.push(nextSec);
2164
- curContent = nextArticle;
2165
- curH = 0;
2166
2436
  }
2437
+ } else {
2438
+ finalSections.push(singleSec);
2167
2439
  }
2168
- sections = newSections;
2169
2440
  }
2441
+ sections = finalSections;
2170
2442
  }
2171
2443
  const pageElements = sections.length > 0 ? sections : cards;
2172
2444
  const totalPages = Math.max(1, pageElements.length);
@@ -2558,6 +2830,88 @@ var DocxPlugin = class {
2558
2830
  }
2559
2831
  return result;
2560
2832
  }
2833
+ parseAndRenderChartSvg(xmlStr, width = 500, height = 260) {
2834
+ const catMatches = [...xmlStr.matchAll(/<c:cat>[\s\S]*?<c:strCache>([\s\S]*?)<\/c:strCache>/g)];
2835
+ let categories = [];
2836
+ if (catMatches.length > 0) {
2837
+ categories = [...catMatches[0][1].matchAll(/<c:v>([^<]+)<\/c:v>/g)].map((m) => m[1]);
2838
+ }
2839
+ if (categories.length === 0) {
2840
+ categories = ["Category 1", "Category 2", "Category 3", "Category 4"];
2841
+ }
2842
+ const defaultColors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021", "#83caff"];
2843
+ const sers = [...xmlStr.matchAll(/<c:ser>([\s\S]*?)<\/c:ser>/g)];
2844
+ const series = [];
2845
+ sers.forEach((s, sIdx) => {
2846
+ const titleMatch = s[1].match(/<c:tx>[\s\S]*?<c:v>([^<]+)<\/c:v>/);
2847
+ const title = titleMatch ? titleMatch[1] : `Series ${sIdx + 1}`;
2848
+ const clrMatch = s[1].match(/<a:srgbClr\s+val="([^"]+)"/);
2849
+ const color = clrMatch ? "#" + clrMatch[1] : defaultColors[sIdx % defaultColors.length];
2850
+ const valMatch = s[1].match(/<c:val>[\s\S]*?<c:numCache>([\s\S]*?)<\/c:numCache>/);
2851
+ let values = [];
2852
+ if (valMatch) {
2853
+ 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);
2854
+ }
2855
+ series.push({ title, color, values });
2856
+ });
2857
+ if (series.length === 0) return "";
2858
+ let maxVal = 10;
2859
+ series.forEach((s) => s.values.forEach((v) => {
2860
+ if (v > maxVal) maxVal = v;
2861
+ }));
2862
+ maxVal = Math.ceil(maxVal * 1.15);
2863
+ if (maxVal % 2 !== 0) maxVal++;
2864
+ const padLeft = 45;
2865
+ const padBottom = 55;
2866
+ const padTop = 20;
2867
+ const padRight = 20;
2868
+ const plotW = width - padLeft - padRight;
2869
+ const plotH = height - padTop - padBottom;
2870
+ const yTicks = 5;
2871
+ let gridLines = "";
2872
+ for (let i = 0; i <= yTicks; i++) {
2873
+ const val = maxVal / yTicks * i;
2874
+ const y = padTop + plotH - val / maxVal * plotH;
2875
+ gridLines += `<line x1="${padLeft}" y1="${y}" x2="${padLeft + plotW}" y2="${y}" stroke="#e2e8f0" stroke-width="1" />`;
2876
+ 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>`;
2877
+ }
2878
+ const numCats = categories.length;
2879
+ const numSers = series.length;
2880
+ const groupW = plotW / numCats;
2881
+ const barW = Math.max(8, Math.min(28, groupW * 0.7 / numSers));
2882
+ const groupPad = (groupW - barW * numSers) / 2;
2883
+ let bars = "";
2884
+ let catLabels = "";
2885
+ for (let c = 0; c < numCats; c++) {
2886
+ const catX = padLeft + c * groupW;
2887
+ 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>`;
2888
+ for (let s = 0; s < numSers; s++) {
2889
+ const val = series[s].values[c] ?? 0;
2890
+ const bH = Math.max(0, val / maxVal * plotH);
2891
+ const bX = catX + groupPad + s * barW;
2892
+ const bY = padTop + plotH - bH;
2893
+ bars += `<rect x="${bX}" y="${bY}" width="${barW - 2}" height="${bH}" fill="${series[s].color}" rx="1" />`;
2894
+ }
2895
+ }
2896
+ let legend = "";
2897
+ const legY = height - 12;
2898
+ let legX = padLeft + (plotW - numSers * 100) / 2;
2899
+ series.forEach((s) => {
2900
+ legend += `<rect x="${legX}" y="${legY - 9}" width="10" height="10" fill="${s.color}" rx="2" />`;
2901
+ legend += `<text x="${legX + 15}" y="${legY}" font-size="11" fill="#475569" font-family="Calibri, sans-serif">${s.title}</text>`;
2902
+ legX += 95;
2903
+ });
2904
+ return `
2905
+ <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">
2906
+ ${gridLines}
2907
+ <line x1="${padLeft}" y1="${padTop + plotH}" x2="${padLeft + plotW}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2908
+ <line x1="${padLeft}" y1="${padTop}" x2="${padLeft}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2909
+ ${bars}
2910
+ ${catLabels}
2911
+ ${legend}
2912
+ </svg>
2913
+ `.trim();
2914
+ }
2561
2915
  };
2562
2916
  function docxPlugin() {
2563
2917
  return new DocxPlugin();
@@ -3124,6 +3478,16 @@ var CodePlugin = class {
3124
3478
  execute: () => {
3125
3479
  instance.print?.();
3126
3480
  }
3481
+ },
3482
+ {
3483
+ id: "open-window",
3484
+ icon: "open-window",
3485
+ label: "Open in Separate Full Window",
3486
+ type: "button",
3487
+ group: "actions",
3488
+ execute: () => {
3489
+ instance.openInSeparateWindow?.();
3490
+ }
3127
3491
  }
3128
3492
  );
3129
3493
  return actions;
@@ -4292,6 +4656,14 @@ var RtfPlugin = class {
4292
4656
  type: "button",
4293
4657
  group: "actions",
4294
4658
  execute: () => instance.print?.()
4659
+ },
4660
+ {
4661
+ id: "open-window",
4662
+ icon: "open-window",
4663
+ label: "Open in Separate Full Window",
4664
+ type: "button",
4665
+ group: "actions",
4666
+ execute: () => instance.openInSeparateWindow?.()
4295
4667
  }
4296
4668
  );
4297
4669
  return actions;
@@ -4344,59 +4716,54 @@ var RtfPlugin = class {
4344
4716
  }
4345
4717
  const doc = new RTFJS.Document(ctx.buffer, {});
4346
4718
  const htmlElements = await doc.render();
4347
- if (htmlElements.length === 1) {
4348
- const singleEl = htmlElements[0];
4349
- wrapper.appendChild(singleEl);
4350
- const children = Array.from(singleEl.children);
4351
- const secH = singleEl.offsetHeight || singleEl.scrollHeight;
4352
- if (secH > 1300 && children.length > 1) {
4353
- const childHeights = children.map((c) => {
4354
- const rectH = c.getBoundingClientRect().height;
4355
- const offH = c.offsetHeight;
4356
- const textLen = c.textContent?.trim().length || 0;
4357
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
4358
- return Math.max(rectH, offH, estH);
4359
- });
4360
- wrapper.innerHTML = "";
4361
- const createRtfCard = () => {
4362
- const card = document.createElement("div");
4363
- card.className = "fp-rtf-page-card";
4364
- card.style.backgroundColor = "#ffffff";
4365
- card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4366
- card.style.borderRadius = "4px";
4367
- card.style.padding = "72px 56px";
4368
- card.style.width = "816px";
4369
- card.style.minHeight = "1056px";
4370
- card.style.boxSizing = "border-box";
4371
- card.style.marginBottom = "24px";
4372
- return card;
4373
- };
4374
- let curCard = createRtfCard();
4375
- wrapper.appendChild(curCard);
4376
- pageElements = [curCard];
4377
- let curH = 0;
4378
- const maxH = 920;
4379
- for (let i = 0; i < children.length; i++) {
4380
- const child = children[i];
4381
- const chH = childHeights[i];
4382
- curCard.appendChild(child);
4383
- curH += chH;
4384
- if (curH >= maxH && i < children.length - 1) {
4385
- curCard = createRtfCard();
4386
- wrapper.appendChild(curCard);
4387
- pageElements.push(curCard);
4388
- curH = 0;
4389
- }
4390
- }
4719
+ const contentNodes = [];
4720
+ for (const item of htmlElements) {
4721
+ if (item.children && item.children.length > 0 && !item.tagName.toLowerCase().startsWith("table")) {
4722
+ contentNodes.push(...Array.from(item.children));
4391
4723
  } else {
4392
- pageElements = [singleEl];
4724
+ contentNodes.push(item);
4393
4725
  }
4394
- } else {
4395
- pageElements = htmlElements;
4396
- for (let i = 0; i < htmlElements.length; i++) {
4397
- const el = htmlElements[i];
4398
- el.style.display = i === 0 ? "block" : "none";
4399
- wrapper.appendChild(el);
4726
+ }
4727
+ wrapper.innerHTML = "";
4728
+ contentNodes.forEach((node) => wrapper.appendChild(node));
4729
+ const childHeights = contentNodes.map((c) => {
4730
+ const rectH = c.getBoundingClientRect ? c.getBoundingClientRect().height : 0;
4731
+ const offH = c.offsetHeight || 0;
4732
+ const textLen = c.textContent?.trim().length || 0;
4733
+ const estH = Math.max(24, Math.ceil(textLen / 75) * 22 + 14);
4734
+ return Math.max(rectH, offH, estH);
4735
+ });
4736
+ wrapper.innerHTML = "";
4737
+ const createRtfCard = () => {
4738
+ const card = document.createElement("div");
4739
+ card.className = "fp-rtf-page-card";
4740
+ card.style.backgroundColor = "#ffffff";
4741
+ card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4742
+ card.style.borderRadius = "4px";
4743
+ card.style.padding = "72px 56px";
4744
+ card.style.width = "816px";
4745
+ card.style.minHeight = "1056px";
4746
+ card.style.boxSizing = "border-box";
4747
+ card.style.marginBottom = "24px";
4748
+ card.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4749
+ card.style.lineHeight = "1.6";
4750
+ return card;
4751
+ };
4752
+ let curCard = createRtfCard();
4753
+ wrapper.appendChild(curCard);
4754
+ pageElements = [curCard];
4755
+ let curH = 0;
4756
+ const maxH = 912;
4757
+ for (let i = 0; i < contentNodes.length; i++) {
4758
+ const child = contentNodes[i];
4759
+ const chH = childHeights[i];
4760
+ curCard.appendChild(child);
4761
+ curH += chH;
4762
+ if (curH >= maxH && i < contentNodes.length - 1) {
4763
+ curCard = createRtfCard();
4764
+ wrapper.appendChild(curCard);
4765
+ pageElements.push(curCard);
4766
+ curH = 0;
4400
4767
  }
4401
4768
  }
4402
4769
  } catch (err) {
@@ -4765,6 +5132,14 @@ var OpenDocumentPlugin = class {
4765
5132
  type: "button",
4766
5133
  group: "actions",
4767
5134
  execute: () => instance.print?.()
5135
+ },
5136
+ {
5137
+ id: "open-window",
5138
+ icon: "open-window",
5139
+ label: "Open in Separate Full Window",
5140
+ type: "button",
5141
+ group: "actions",
5142
+ execute: () => instance.openInSeparateWindow?.()
4768
5143
  }
4769
5144
  );
4770
5145
  return actions;
@@ -5353,6 +5728,14 @@ var DocPlugin = class {
5353
5728
  type: "button",
5354
5729
  group: "actions",
5355
5730
  execute: () => instance.print?.()
5731
+ },
5732
+ {
5733
+ id: "open-window",
5734
+ icon: "open-window",
5735
+ label: "Open in Separate Full Window",
5736
+ type: "button",
5737
+ group: "actions",
5738
+ execute: () => instance.openInSeparateWindow?.()
5356
5739
  }
5357
5740
  );
5358
5741
  return actions;
@@ -5372,8 +5755,17 @@ var DocPlugin = class {
5372
5755
  let scale = 1;
5373
5756
  let extractedRawText = "";
5374
5757
  let isFallback = false;
5758
+ let chartSvg = "";
5375
5759
  try {
5376
5760
  const cfbf = new CfbfReader(ctx.buffer);
5761
+ try {
5762
+ const pkg = cfbf.readStream("package_stream");
5763
+ if (pkg && pkg.length > 100) {
5764
+ chartSvg = this.parseOdfChartToSvg(pkg);
5765
+ }
5766
+ } catch (chartErr) {
5767
+ console.warn("[DocPlugin] Chart stream parsing info:", chartErr);
5768
+ }
5377
5769
  const wordDocStream = cfbf.readStream("WordDocument");
5378
5770
  if (!wordDocStream || wordDocStream.length < 512) {
5379
5771
  throw new Error("WordDocument stream not found or invalid in CFBF archive");
@@ -5391,7 +5783,7 @@ var DocPlugin = class {
5391
5783
  extractedRawText = fallback;
5392
5784
  isFallback = true;
5393
5785
  }
5394
- const rawPages = this.splitIntoPages(extractedRawText);
5786
+ const rawPages = this.splitIntoPages(extractedRawText, chartSvg);
5395
5787
  const totalPages = Math.max(1, rawPages.length);
5396
5788
  let currentPage = 1;
5397
5789
  const pageCards = [];
@@ -5633,7 +6025,7 @@ var DocPlugin = class {
5633
6025
  for (const run of [...ansiRuns, ...utf16Runs]) {
5634
6026
  const trimmed = run.trim();
5635
6027
  if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
5636
- 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)) {
6028
+ 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)) {
5637
6029
  seen.add(trimmed);
5638
6030
  candidateLines.push(trimmed);
5639
6031
  }
@@ -5644,12 +6036,138 @@ var DocPlugin = class {
5644
6036
  heuristicTextExtraction(buffer) {
5645
6037
  return this.extractStringsFromBytes(new Uint8Array(buffer));
5646
6038
  }
5647
- splitIntoPages(text) {
6039
+ /**
6040
+ * Parses an embedded OpenDocument Chart package into a vector SVG bar/column chart
6041
+ */
6042
+ parseOdfChartToSvg(zipBytes) {
6043
+ try {
6044
+ const unzipped = fflate.unzipSync(zipBytes);
6045
+ const contentXml = unzipped["content.xml"] ? new TextDecoder("utf-8").decode(unzipped["content.xml"]) : "";
6046
+ if (!contentXml) return "";
6047
+ const rowsMatch = contentXml.match(/<table:table-row[\s\S]*?<\/table:table-row>/g) || [];
6048
+ if (rowsMatch.length < 2) return "";
6049
+ const headers = [];
6050
+ const firstRow = rowsMatch[0];
6051
+ const headerCells = firstRow ? firstRow.match(/<text:p>([^<]+)<\/text:p>/g) || [] : [];
6052
+ for (const h of headerCells) {
6053
+ headers.push(h.replace(/<\/?text:p>/g, "").trim());
6054
+ }
6055
+ const categories = [];
6056
+ const seriesValues = headers.map(() => []);
6057
+ for (let r = 1; r < rowsMatch.length; r++) {
6058
+ const rowStr = rowsMatch[r];
6059
+ if (!rowStr) continue;
6060
+ const cells = rowStr.match(/<table:table-cell[\s\S]*?<\/table:table-cell>/g) || [];
6061
+ if (cells.length > 0 && cells[0]) {
6062
+ const catMatch = cells[0].match(/<text:p>([^<]+)<\/text:p>/);
6063
+ categories.push(catMatch ? catMatch[1] : "Row " + r);
6064
+ for (let c = 1; c < cells.length && c - 1 < headers.length; c++) {
6065
+ const cellStr = cells[c];
6066
+ if (!cellStr) continue;
6067
+ const valMatch = cellStr.match(/office:value="([0-9.]+)"/) || cellStr.match(/<text:p>([0-9.]+)<\/text:p>/);
6068
+ const series = seriesValues[c - 1];
6069
+ if (series) {
6070
+ series.push(valMatch ? parseFloat(valMatch[1]) : 0);
6071
+ }
6072
+ }
6073
+ }
6074
+ }
6075
+ const colors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021"];
6076
+ const colorMatches = contentXml.matchAll(/draw:fill-color="(#[0-9a-fA-F]{6})"/g);
6077
+ let cIdx = 0;
6078
+ for (const cm of colorMatches) {
6079
+ if (cIdx < colors.length) colors[cIdx] = cm[1];
6080
+ cIdx++;
6081
+ }
6082
+ let maxVal = 10;
6083
+ for (const s of seriesValues) {
6084
+ for (const v of s) {
6085
+ if (v > maxVal) maxVal = v;
6086
+ }
6087
+ }
6088
+ maxVal = Math.ceil(maxVal * 1.15);
6089
+ const width = 560;
6090
+ const height = 280;
6091
+ const padLeft = 45;
6092
+ const padRight = 100;
6093
+ const padTop = 20;
6094
+ const padBottom = 40;
6095
+ const chartW = width - padLeft - padRight;
6096
+ const chartH = height - padTop - padBottom;
6097
+ 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);">`;
6098
+ for (let step = 0; step <= 4; step++) {
6099
+ const yVal = (maxVal / 4 * step).toFixed(1);
6100
+ const yPos = padTop + chartH - step / 4 * chartH;
6101
+ svg += `<line x1="${padLeft}" y1="${yPos}" x2="${padLeft + chartW}" y2="${yPos}" stroke="#e2e8f0" stroke-dasharray="2,2" />`;
6102
+ svg += `<text x="${padLeft - 8}" y="${yPos + 4}" font-size="11" fill="#64748b" text-anchor="end">${yVal}</text>`;
6103
+ }
6104
+ const numCats = categories.length;
6105
+ const numSeries = headers.length;
6106
+ const groupW = chartW / numCats;
6107
+ const barW = Math.max(8, groupW * 0.7 / numSeries);
6108
+ const groupPad = (groupW - barW * numSeries) / 2;
6109
+ for (let catIdx = 0; catIdx < numCats; catIdx++) {
6110
+ const groupX = padLeft + catIdx * groupW + groupPad;
6111
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6112
+ const val = seriesValues[sIdx][catIdx] || 0;
6113
+ const barH = val / maxVal * chartH;
6114
+ const barX = groupX + sIdx * barW;
6115
+ const barY = padTop + chartH - barH;
6116
+ const col = colors[sIdx % colors.length];
6117
+ svg += `<rect x="${barX}" y="${barY}" width="${barW - 2}" height="${barH}" fill="${col}" rx="2"><title>${headers[sIdx]}: ${val}</title></rect>`;
6118
+ }
6119
+ const catX = padLeft + catIdx * groupW + groupW / 2;
6120
+ svg += `<text x="${catX}" y="${padTop + chartH + 18}" font-size="11" fill="#475569" text-anchor="middle">${categories[catIdx]}</text>`;
6121
+ }
6122
+ let legendY = padTop + 20;
6123
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6124
+ const col = colors[sIdx % colors.length];
6125
+ svg += `<rect x="${padLeft + chartW + 15}" y="${legendY}" width="12" height="12" fill="${col}" rx="2" />`;
6126
+ svg += `<text x="${padLeft + chartW + 32}" y="${legendY + 10}" font-size="11" fill="#334155">${headers[sIdx]}</text>`;
6127
+ legendY += 20;
6128
+ }
6129
+ svg += "</svg>";
6130
+ return svg;
6131
+ } catch (e) {
6132
+ console.warn("[DocPlugin] Error generating chart SVG:", e);
6133
+ return "";
6134
+ }
6135
+ }
6136
+ cleanWordDocFields(text, chartSvg = "") {
6137
+ if (!text) return "";
6138
+ let cleaned = text.replace(
6139
+ /\x13\s*EMBED\b[\s\S]*?\x15/gi,
6140
+ () => chartSvg ? `
6141
+
6142
+ ${chartSvg}
6143
+
6144
+ ` : ""
6145
+ );
6146
+ cleaned = cleaned.replace(
6147
+ /\x13\s*HYPERLINK\s*"?([^"\x14]+)"?\s*\x14([\s\S]*?)\x15/gi,
6148
+ (_match, url, label) => {
6149
+ const cleanUrl = url.trim();
6150
+ const cleanLabel = label.trim() || cleanUrl;
6151
+ return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${cleanLabel}</a>`;
6152
+ }
6153
+ );
6154
+ cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, (_m, res) => {
6155
+ if (/[\x00-\x1F]/.test(res)) return "";
6156
+ return res.trim();
6157
+ });
6158
+ cleaned = cleaned.replace(/\x13[^\x15]*\x15/g, "");
6159
+ cleaned = cleaned.replace(/[\x13\x14\x15]/g, "");
6160
+ cleaned = cleaned.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
6161
+ cleaned = cleaned.replace(/EMBED\s+LibreOffice\.ChartDocument\.[0-9]+/gi, chartSvg || "");
6162
+ return cleaned;
6163
+ }
6164
+ splitIntoPages(text, chartSvg = "") {
5648
6165
  if (!text) return [""];
5649
- const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
6166
+ const cleanedText = this.cleanWordDocFields(text, chartSvg);
6167
+ const normalized = cleanedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
5650
6168
  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);
5651
6169
  if (explicitParts.length === 0) explicitParts.push(normalized);
5652
- const maxLinesPerPage = 32;
6170
+ const maxLinesPerPage = 34;
5653
6171
  const charsPerLine = 80;
5654
6172
  const finalPages = [];
5655
6173
  for (const part of explicitParts) {
@@ -5657,7 +6175,8 @@ var DocPlugin = class {
5657
6175
  let currentLines = [];
5658
6176
  let count = 0;
5659
6177
  for (const line of lines) {
5660
- const vLines = Math.max(1, Math.ceil((line.length || 1) / charsPerLine));
6178
+ const isSvg = line.includes("<svg");
6179
+ const vLines = isSvg ? 12 : Math.max(1, Math.ceil((line.replace(/<[^>]+>/g, "").length || 1) / charsPerLine));
5661
6180
  if (count + vLines > maxLinesPerPage && currentLines.length > 0) {
5662
6181
  finalPages.push(currentLines.join("\n"));
5663
6182
  currentLines = [];
@@ -5697,9 +6216,44 @@ var DocPlugin = class {
5697
6216
  tableLines = [];
5698
6217
  }
5699
6218
  };
6219
+ const sanitizeOptions = {
6220
+ ADD_TAGS: ["a", "svg", "g", "path", "line", "rect", "circle", "text", "title"],
6221
+ ADD_ATTR: [
6222
+ "href",
6223
+ "target",
6224
+ "rel",
6225
+ "style",
6226
+ "viewBox",
6227
+ "width",
6228
+ "height",
6229
+ "x",
6230
+ "y",
6231
+ "x1",
6232
+ "y1",
6233
+ "x2",
6234
+ "y2",
6235
+ "fill",
6236
+ "stroke",
6237
+ "stroke-width",
6238
+ "stroke-dasharray",
6239
+ "rx",
6240
+ "font-size",
6241
+ "text-anchor"
6242
+ ]
6243
+ };
5700
6244
  let i = 0;
5701
6245
  while (i < lines.length) {
5702
6246
  let line = lines[i];
6247
+ if (line.includes("<svg")) {
6248
+ if (inList) {
6249
+ html += "</ul>";
6250
+ inList = false;
6251
+ }
6252
+ flushTable();
6253
+ html += line;
6254
+ i++;
6255
+ continue;
6256
+ }
5703
6257
  let tabCount = (line.match(/\t/g) || []).length;
5704
6258
  if (tabCount > 0) {
5705
6259
  let j = i;
@@ -5738,20 +6292,20 @@ var DocPlugin = class {
5738
6292
  html += "</ul>";
5739
6293
  inList = false;
5740
6294
  }
5741
- html += `<h2 style="font-size: 16px; font-weight: 700; color: #1e3a8a; margin: 16px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify6.sanitize(line)}</h2>`;
6295
+ html += `<h2 style="font-size: 16px; font-weight: 700; color: #1e3a8a; margin: 16px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify6.sanitize(line, sanitizeOptions)}</h2>`;
5742
6296
  } else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
5743
6297
  if (!inList) {
5744
6298
  html += '<ul style="margin: 8px 0; padding-left: 24px;">';
5745
6299
  inList = true;
5746
6300
  }
5747
6301
  const bulletText = line.replace(/^[•\-\*]\s*/, "");
5748
- html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6.sanitize(bulletText)}</li>`;
6302
+ html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6.sanitize(bulletText, sanitizeOptions)}</li>`;
5749
6303
  } else {
5750
6304
  if (inList) {
5751
6305
  html += "</ul>";
5752
6306
  inList = false;
5753
6307
  }
5754
- html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6.sanitize(line)}</p>`;
6308
+ html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6.sanitize(line, sanitizeOptions)}</p>`;
5755
6309
  }
5756
6310
  i++;
5757
6311
  }
@@ -6304,6 +6858,6 @@ var FilePreviewViewer2 = class extends FilePreviewViewer {
6304
6858
  }
6305
6859
  };
6306
6860
 
6307
- export { CfbfReader, EventEmitter, FilePreviewViewer2 as FilePreviewViewer, ThumbnailPanel, ToolbarController, archivePlugin, clamp, codePlugin, createElement, csvPlugin, debounce, detectMagicBytes, detectOoxmlType, docPlugin, docxPlugin, downloadFile, excelPlugin, extractExtension, formatFileSize, getDefaultPlugins, htmlPreviewPlugin, markdownPlugin, mediaPlugin, mimeFromExtension, openDocumentPlugin, pdfPlugin, pptPlugin, pptxPlugin, printElement, rtfPlugin, sanitizeHTML, sanitizeSVG, sourceToArrayBuffer, threeDPlugin };
6861
+ export { CfbfReader, EventEmitter, FilePreviewViewer2 as FilePreviewViewer, ThumbnailPanel, ToolbarController, archivePlugin, clamp, codePlugin, createElement, csvPlugin, debounce, detectMagicBytes, detectOoxmlType, docPlugin, docxPlugin, downloadFile, excelPlugin, extractExtension, formatFileSize, getDefaultPlugins, getTransferPayload, htmlPreviewPlugin, markdownPlugin, mediaPlugin, mimeFromExtension, openDocumentPlugin, pdfPlugin, pptPlugin, pptxPlugin, printElement, rtfPlugin, sanitizeHTML, sanitizeSVG, saveTransferPayload, sourceToArrayBuffer, threeDPlugin };
6308
6862
  //# sourceMappingURL=index.js.map
6309
6863
  //# sourceMappingURL=index.js.map