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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/angular.cjs CHANGED
@@ -18,6 +18,7 @@ var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
18
18
  var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
19
19
  var RTFJS = require('rtf.js/dist/RTFJS.bundle.js');
20
20
 
21
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
21
22
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
22
23
 
23
24
  function _interopNamespace(e) {
@@ -41,6 +42,7 @@ function _interopNamespace(e) {
41
42
  var DOMPurify6__default = /*#__PURE__*/_interopDefault(DOMPurify6);
42
43
  var pdfjsLib__namespace = /*#__PURE__*/_interopNamespace(pdfjsLib);
43
44
  var docx__namespace = /*#__PURE__*/_interopNamespace(docx);
45
+ var fflate__namespace = /*#__PURE__*/_interopNamespace(fflate);
44
46
  var XLSX__namespace = /*#__PURE__*/_interopNamespace(XLSX);
45
47
  var hljs__default = /*#__PURE__*/_interopDefault(hljs);
46
48
  var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
@@ -417,16 +419,21 @@ async function sourceToArrayBuffer(source, signal) {
417
419
  metadata.mimeType = source.type || void 0;
418
420
  metadata.extension = extractExtension(source.name);
419
421
  buffer = await source.arrayBuffer();
420
- } else if (source instanceof Blob) {
422
+ } else if (source instanceof Blob || source && typeof source.arrayBuffer === "function" && typeof source.size === "number") {
421
423
  metadata.size = source.size;
422
424
  metadata.mimeType = source.type || void 0;
425
+ if (source.name) {
426
+ metadata.name = source.name;
427
+ metadata.extension = extractExtension(source.name);
428
+ }
423
429
  buffer = await source.arrayBuffer();
424
- } else if (source instanceof ArrayBuffer) {
430
+ } else if (source instanceof ArrayBuffer || Object.prototype.toString.call(source) === "[object ArrayBuffer]" || source && typeof source.byteLength === "number" && typeof source.slice === "function") {
425
431
  buffer = source;
426
- } else if (source instanceof Uint8Array) {
427
- buffer = source.buffer.slice(
428
- source.byteOffset,
429
- source.byteOffset + source.byteLength
432
+ } else if (source instanceof Uint8Array || ArrayBuffer.isView(source)) {
433
+ const view = source;
434
+ buffer = view.buffer.slice(
435
+ view.byteOffset,
436
+ view.byteOffset + view.byteLength
430
437
  );
431
438
  } else {
432
439
  throw new Error("Unsupported file source type");
@@ -511,6 +518,45 @@ function createElement(tag, attrs, ...children) {
511
518
  }
512
519
  return el;
513
520
  }
521
+ var DB_NAME = "PreviewFileTransferDB";
522
+ var DB_STORE = "transfers";
523
+ function openDB() {
524
+ return new Promise((resolve, reject) => {
525
+ if (typeof indexedDB === "undefined") {
526
+ return reject(new Error("IndexedDB is not available"));
527
+ }
528
+ const req = indexedDB.open(DB_NAME, 1);
529
+ req.onupgradeneeded = () => {
530
+ const db = req.result;
531
+ if (!db.objectStoreNames.contains(DB_STORE)) {
532
+ db.createObjectStore(DB_STORE, { keyPath: "id" });
533
+ }
534
+ };
535
+ req.onsuccess = () => resolve(req.result);
536
+ req.onerror = () => reject(req.error);
537
+ });
538
+ }
539
+ async function saveTransferPayload(id, payload) {
540
+ if (typeof window !== "undefined") {
541
+ try {
542
+ window[id] = payload;
543
+ window.__lastTransfer = payload;
544
+ } catch {
545
+ }
546
+ }
547
+ try {
548
+ const db = await openDB();
549
+ return new Promise((resolve, reject) => {
550
+ const tx = db.transaction(DB_STORE, "readwrite");
551
+ const store = tx.objectStore(DB_STORE);
552
+ store.put({ id, ...payload, timestamp: Date.now() });
553
+ tx.oncomplete = () => resolve();
554
+ tx.onerror = () => reject(tx.error);
555
+ });
556
+ } catch (e) {
557
+ console.warn("[saveTransferPayload] IndexedDB store warning:", e);
558
+ }
559
+ }
514
560
  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>`;
515
561
  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>`;
516
562
  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>`;
@@ -530,6 +576,7 @@ var ICON_COPY = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" str
530
576
  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>`;
531
577
  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>`;
532
578
  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>`;
579
+ 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>`;
533
580
  var ICON_MAP = {
534
581
  "zoom-in": ICON_ZOOM_IN,
535
582
  "zoom-out": ICON_ZOOM_OUT,
@@ -556,7 +603,9 @@ var ICON_MAP = {
556
603
  "forward-10": ICON_FAST_FORWARD,
557
604
  "rewind": ICON_REWIND,
558
605
  "replay-10": ICON_REWIND,
559
- "speed": ICON_SPEED
606
+ "speed": ICON_SPEED,
607
+ "open-window": ICON_EXTERNAL_WINDOW,
608
+ "external-window": ICON_EXTERNAL_WINDOW
560
609
  };
561
610
  var ToolbarController = class {
562
611
  el;
@@ -699,9 +748,11 @@ var ToolbarController = class {
699
748
  type: "button",
700
749
  "data-action-id": id
701
750
  });
702
- const svg = ICON_MAP[iconHtml] || ICON_MAP[id] || (iconHtml && iconHtml.startsWith("<svg") ? iconHtml : null);
703
- if (svg) {
704
- btn.innerHTML = sanitizeSVG(svg);
751
+ const internalSvg = ICON_MAP[iconHtml] || ICON_MAP[id];
752
+ if (internalSvg) {
753
+ btn.innerHTML = internalSvg;
754
+ } else if (iconHtml && iconHtml.startsWith("<svg")) {
755
+ btn.innerHTML = sanitizeSVG(iconHtml);
705
756
  } else {
706
757
  btn.textContent = title || id;
707
758
  }
@@ -773,7 +824,7 @@ var ThumbnailPanel = class {
773
824
  }
774
825
  }
775
826
  };
776
- var FilePreviewViewer = class {
827
+ var FilePreviewViewer = class _FilePreviewViewer {
777
828
  plugins = [];
778
829
  activeInstance = null;
779
830
  abortController = null;
@@ -810,6 +861,7 @@ var FilePreviewViewer = class {
810
861
  * Preview a file in the given container element.
811
862
  */
812
863
  async preview(container, source, options = {}) {
864
+ this.currentOptions = options;
813
865
  this.abort();
814
866
  this.abortController = new AbortController();
815
867
  const { signal } = this.abortController;
@@ -819,7 +871,10 @@ var FilePreviewViewer = class {
819
871
  this.showLoading();
820
872
  try {
821
873
  const { buffer, metadata } = await sourceToArrayBuffer(source, signal);
822
- this.currentBuffer = buffer;
874
+ if (options.metadata) {
875
+ Object.assign(metadata, options.metadata);
876
+ }
877
+ this.currentBuffer = buffer.slice(0);
823
878
  this.currentMetadata = metadata;
824
879
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
825
880
  const fileInfo = { metadata, buffer };
@@ -849,6 +904,7 @@ var FilePreviewViewer = class {
849
904
  }
850
905
  });
851
906
  this.activeInstance = instance;
907
+ instance.openInSeparateWindow = () => this.openInSeparateWindow();
852
908
  this.hideLoading();
853
909
  this.eventEmitter.emit("loaded", { metadata, plugin: matchedPlugin.id });
854
910
  if (options.showToolbar !== false && this.toolbar) {
@@ -858,26 +914,16 @@ var FilePreviewViewer = class {
858
914
  actions.push({
859
915
  id: "fullscreen",
860
916
  icon: "fullscreen",
861
- label: "Toggle Fullscreen",
917
+ label: "Fullscreen",
862
918
  type: "button",
863
919
  group: "view",
864
- execute: async () => {
920
+ execute: () => {
865
921
  try {
866
- const isNativeFs = !!document.fullscreenElement;
867
- const isCssFs = this.wrapperEl?.classList.contains("fp-fullscreen-active");
868
- if (!isNativeFs && !isCssFs) {
869
- if (this.wrapperEl?.requestFullscreen) {
870
- await this.wrapperEl.requestFullscreen().catch(() => {
871
- this.wrapperEl?.classList.add("fp-fullscreen-active");
872
- });
873
- } else {
874
- this.wrapperEl?.classList.add("fp-fullscreen-active");
875
- }
922
+ if (!document.fullscreenElement) {
923
+ this.wrapperEl?.requestFullscreen?.();
924
+ this.wrapperEl?.classList.add("fp-fullscreen-active");
876
925
  } else {
877
- if (document.fullscreenElement) {
878
- await document.exitFullscreen().catch(() => {
879
- });
880
- }
926
+ document.exitFullscreen?.();
881
927
  this.wrapperEl?.classList.remove("fp-fullscreen-active");
882
928
  }
883
929
  } catch {
@@ -889,6 +935,28 @@ var FilePreviewViewer = class {
889
935
  }
890
936
  });
891
937
  }
938
+ const openWinAction = actions.find((a) => a.id === "open-window");
939
+ if (openWinAction) {
940
+ if (options?._isSeparateWindow) {
941
+ const idx = actions.indexOf(openWinAction);
942
+ if (idx !== -1) actions.splice(idx, 1);
943
+ } else {
944
+ openWinAction.execute = () => {
945
+ this.openInSeparateWindow();
946
+ };
947
+ }
948
+ } else if (!options?._isSeparateWindow) {
949
+ actions.push({
950
+ id: "open-window",
951
+ icon: "open-window",
952
+ label: "Open in Separate Full Window",
953
+ type: "button",
954
+ group: "actions",
955
+ execute: () => {
956
+ this.openInSeparateWindow();
957
+ }
958
+ });
959
+ }
892
960
  this.toolbar.update(actions);
893
961
  this.toolbar.show();
894
962
  }
@@ -921,6 +989,104 @@ var FilePreviewViewer = class {
921
989
  throw error;
922
990
  }
923
991
  }
992
+ /**
993
+ * Opens the current file preview in a separate full browser window.
994
+ */
995
+ openInSeparateWindow() {
996
+ if (!this.currentBuffer) {
997
+ console.warn("[FilePreviewViewer] No active file buffer to open in separate window");
998
+ return null;
999
+ }
1000
+ if (this.currentOptions.onOpenSeparateWindow) {
1001
+ return this.currentOptions.onOpenSeparateWindow({
1002
+ buffer: this.currentBuffer,
1003
+ metadata: this.currentMetadata || { name: "Document" },
1004
+ options: this.currentOptions
1005
+ });
1006
+ }
1007
+ const transferId = "fp_win_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
1008
+ let clonedBuffer;
1009
+ try {
1010
+ clonedBuffer = this.currentBuffer.slice(0);
1011
+ } catch {
1012
+ clonedBuffer = this.currentBuffer;
1013
+ }
1014
+ const payload = {
1015
+ buffer: clonedBuffer,
1016
+ metadata: this.currentMetadata ? { ...this.currentMetadata } : void 0,
1017
+ options: { ...this.currentOptions, _isSeparateWindow: true }
1018
+ };
1019
+ if (typeof window !== "undefined") {
1020
+ try {
1021
+ window[transferId] = payload;
1022
+ window.__lastTransfer = payload;
1023
+ } catch {
1024
+ }
1025
+ }
1026
+ saveTransferPayload(transferId, payload).catch((err) => {
1027
+ console.warn("[FilePreviewViewer] Transfer payload save warning:", err);
1028
+ });
1029
+ let targetUrl = null;
1030
+ if (this.currentOptions.standaloneViewerUrl) {
1031
+ const u = new URL(this.currentOptions.standaloneViewerUrl, window.location.href);
1032
+ u.searchParams.set("mode", "fullscreen");
1033
+ u.searchParams.set("transferId", transferId);
1034
+ targetUrl = u.toString();
1035
+ } else if (typeof window !== "undefined" && window.location?.href && !window.location.href.startsWith("about:")) {
1036
+ const u = new URL(window.location.href);
1037
+ u.searchParams.set("mode", "fullscreen");
1038
+ u.searchParams.set("transferId", transferId);
1039
+ targetUrl = u.toString();
1040
+ }
1041
+ if (targetUrl) {
1042
+ const newWin2 = window.open(targetUrl, "_blank");
1043
+ if (!newWin2) {
1044
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1045
+ return null;
1046
+ }
1047
+ return newWin2;
1048
+ }
1049
+ const title = (this.currentMetadata?.name || "Document Preview") + " - Full Preview";
1050
+ const newWin = window.open("", "_blank");
1051
+ if (!newWin) {
1052
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
1053
+ return null;
1054
+ }
1055
+ newWin.document.title = title;
1056
+ newWin.document.body.style.margin = "0";
1057
+ newWin.document.body.style.padding = "0";
1058
+ newWin.document.body.style.width = "100vw";
1059
+ newWin.document.body.style.height = "100vh";
1060
+ newWin.document.body.style.overflow = "hidden";
1061
+ newWin.document.body.style.backgroundColor = "#f8fafc";
1062
+ const headNodes = document.querySelectorAll('link[rel="stylesheet"], style');
1063
+ headNodes.forEach((node) => {
1064
+ newWin.document.head.appendChild(node.cloneNode(true));
1065
+ });
1066
+ const root = newWin.document.createElement("div");
1067
+ root.id = "full-window-preview-root";
1068
+ root.style.width = "100%";
1069
+ root.style.height = "100%";
1070
+ root.style.overflow = "hidden";
1071
+ newWin.document.body.appendChild(root);
1072
+ const separateViewer = new _FilePreviewViewer();
1073
+ for (const plugin of this.plugins) {
1074
+ separateViewer.registerPlugin(plugin);
1075
+ }
1076
+ separateViewer.preview(root, this.currentBuffer.slice(0), {
1077
+ ...this.currentOptions,
1078
+ showToolbar: true,
1079
+ toolbarPosition: "top",
1080
+ metadata: this.currentMetadata || void 0,
1081
+ _isSeparateWindow: true
1082
+ }).catch((err) => {
1083
+ console.error("[FilePreviewViewer] Error rendering in separate window:", err);
1084
+ });
1085
+ newWin.addEventListener("beforeunload", () => {
1086
+ separateViewer.destroy();
1087
+ });
1088
+ return newWin;
1089
+ }
924
1090
  /**
925
1091
  * Subscribe to viewer events.
926
1092
  */
@@ -1326,8 +1492,20 @@ var CfbfReader = class {
1326
1492
  }
1327
1493
  };
1328
1494
  if (typeof window !== "undefined" && pdfjsLib__namespace.GlobalWorkerOptions) {
1329
- if (!pdfjsLib__namespace.GlobalWorkerOptions.workerSrc) {
1330
- pdfjsLib__namespace.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1495
+ if (!pdfjsLib__namespace.GlobalWorkerOptions.workerPort && !pdfjsLib__namespace.GlobalWorkerOptions.workerSrc) {
1496
+ const customWorker = window.__PDF_WORKER_SRC__;
1497
+ if (customWorker) {
1498
+ pdfjsLib__namespace.GlobalWorkerOptions.workerSrc = customWorker;
1499
+ } else {
1500
+ try {
1501
+ pdfjsLib__namespace.GlobalWorkerOptions.workerPort = new Worker(
1502
+ 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('angular.cjs', document.baseURI).href))),
1503
+ { type: "module" }
1504
+ );
1505
+ } catch {
1506
+ pdfjsLib__namespace.GlobalWorkerOptions.workerSrc = "./pdf.worker.min.mjs";
1507
+ }
1508
+ }
1331
1509
  }
1332
1510
  }
1333
1511
  var PdfPlugin = class {
@@ -1421,6 +1599,14 @@ var PdfPlugin = class {
1421
1599
  type: "button",
1422
1600
  group: "actions",
1423
1601
  execute: () => instance.print?.()
1602
+ },
1603
+ {
1604
+ id: "open-window",
1605
+ icon: "open-window",
1606
+ label: "Open in Separate Full Window",
1607
+ type: "button",
1608
+ group: "actions",
1609
+ execute: () => instance.openInSeparateWindow?.()
1424
1610
  }
1425
1611
  ];
1426
1612
  }
@@ -1470,18 +1656,21 @@ var PdfPlugin = class {
1470
1656
  indicator.style.pointerEvents = "none";
1471
1657
  container.appendChild(indicator);
1472
1658
  ctx.container.appendChild(container);
1659
+ const standardFontsUrl = typeof window !== "undefined" && window.__PDF_STANDARD_FONTS_URL__ || "./standard_fonts/";
1660
+ const cmapsUrl = typeof window !== "undefined" && window.__PDF_CMAPS_URL__ || "./cmaps/";
1473
1661
  const loadingTask = pdfjsLib__namespace.getDocument({
1474
- data: new Uint8Array(ctx.buffer),
1475
- cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/cmaps/`,
1662
+ data: new Uint8Array(ctx.buffer.slice(0)),
1663
+ cMapUrl: cmapsUrl,
1476
1664
  cMapPacked: true,
1477
- standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/standard_fonts/`
1665
+ standardFontDataUrl: standardFontsUrl,
1666
+ verbosity: 0
1478
1667
  });
1479
1668
  const pdfDoc = await loadingTask.promise;
1480
1669
  const totalPages = Math.max(1, pdfDoc.numPages);
1481
1670
  let currentPage = 1;
1482
1671
  let zoomScale = 1;
1483
1672
  let rotation = 0;
1484
- let fitMode = "width";
1673
+ let fitMode = "page";
1485
1674
  let currentRenderTask = null;
1486
1675
  const renderPage = async (pageNum) => {
1487
1676
  if (currentRenderTask) {
@@ -1501,15 +1690,15 @@ var PdfPlugin = class {
1501
1690
  const containerWidth = container.clientWidth || 900;
1502
1691
  const containerHeight = container.clientHeight || 700;
1503
1692
  const unscaledVp = page.getViewport({ scale: 1, rotation });
1504
- const availWidth = Math.max(280, containerWidth - 48);
1505
- const availHeight = Math.max(280, containerHeight - 88);
1693
+ const availWidth = Math.max(320, containerWidth - 48);
1694
+ const availHeight = Math.max(550, containerHeight - 88);
1506
1695
  const scaleW = availWidth / unscaledVp.width;
1507
1696
  const scaleH = availHeight / unscaledVp.height;
1508
1697
  let fitScale;
1509
1698
  if (fitMode === "page") {
1510
- fitScale = Math.max(0.5, Math.min(scaleW, scaleH));
1699
+ fitScale = Math.max(0.4, Math.min(scaleW, scaleH));
1511
1700
  } else {
1512
- fitScale = Math.max(0.65, Math.min(1.15, scaleW));
1701
+ fitScale = Math.max(0.65, Math.min(1.25, scaleW));
1513
1702
  }
1514
1703
  const effectiveScale = (fitScale > 0 ? fitScale : 1) * zoomScale;
1515
1704
  const pixelRatio = window.devicePixelRatio || 1;
@@ -1535,7 +1724,7 @@ var PdfPlugin = class {
1535
1724
  currentRenderTask = null;
1536
1725
  }
1537
1726
  };
1538
- await renderPage(1);
1727
+ renderPage(1);
1539
1728
  let resizeTimer = null;
1540
1729
  const resizeObserver = new ResizeObserver(() => {
1541
1730
  if (resizeTimer) clearTimeout(resizeTimer);
@@ -1579,7 +1768,7 @@ var PdfPlugin = class {
1579
1768
  renderPage(currentPage);
1580
1769
  },
1581
1770
  fitToPage: () => {
1582
- fitMode = fitMode === "width" ? "page" : "width";
1771
+ fitMode = fitMode === "page" ? "width" : "page";
1583
1772
  zoomScale = 1;
1584
1773
  rotation = 0;
1585
1774
  renderPage(currentPage);
@@ -2056,6 +2245,14 @@ var DocxPlugin = class {
2056
2245
  type: "button",
2057
2246
  group: "actions",
2058
2247
  execute: () => instance.print?.()
2248
+ },
2249
+ {
2250
+ id: "open-window",
2251
+ icon: "open-window",
2252
+ label: "Open in Separate Full Window",
2253
+ type: "button",
2254
+ group: "actions",
2255
+ execute: () => instance.openInSeparateWindow?.()
2059
2256
  }
2060
2257
  );
2061
2258
  return actions;
@@ -2115,6 +2312,31 @@ var DocxPlugin = class {
2115
2312
  }
2116
2313
  if (wrapper.children.length > 0 && (wrapper.textContent?.trim().length ?? 0) > 0) {
2117
2314
  renderedSuccessfully = true;
2315
+ try {
2316
+ const unzipped = fflate.unzipSync(new Uint8Array(ctx.buffer));
2317
+ const chartKeys = Object.keys(unzipped).filter((k) => k.replace(/^[./\\]+/, "").toLowerCase().startsWith("word/charts/chart") && k.endsWith(".xml")).sort();
2318
+ if (chartKeys.length > 0) {
2319
+ const allDivs = Array.from(wrapper.querySelectorAll("div"));
2320
+ const emptyContainers = allDivs.filter((div) => {
2321
+ const st = div.getAttribute("style") || "";
2322
+ return st.includes("width:") && st.includes("height:") && div.children.length === 0 && (div.textContent?.trim().length ?? 0) === 0;
2323
+ });
2324
+ chartKeys.forEach((cKey, idx) => {
2325
+ const target = emptyContainers[idx];
2326
+ if (target) {
2327
+ const xmlStr = fflate.strFromU8(unzipped[cKey]);
2328
+ const svg = this.parseAndRenderChartSvg(xmlStr);
2329
+ if (svg) {
2330
+ target.innerHTML = svg;
2331
+ target.style.display = "block";
2332
+ target.style.margin = "12px auto";
2333
+ }
2334
+ }
2335
+ });
2336
+ }
2337
+ } catch (chartErr) {
2338
+ console.warn("[DocxPlugin] Non-critical error rendering DrawingML charts:", chartErr);
2339
+ }
2118
2340
  }
2119
2341
  } catch (err) {
2120
2342
  console.warn("[DocxPlugin] docx-preview failed, triggering native fallback:", err);
@@ -2146,64 +2368,74 @@ var DocxPlugin = class {
2146
2368
  }
2147
2369
  let sections = Array.from(wrapper.querySelectorAll("section.docx"));
2148
2370
  const cards = Array.from(wrapper.querySelectorAll(".fp-docx-page-card"));
2149
- if (sections.length === 1 && cards.length === 0) {
2150
- const singleSec = sections[0];
2151
- const contentContainer = singleSec.querySelector("article") || singleSec;
2152
- const children = Array.from(contentContainer.children);
2153
- const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2154
- const secH = singleSec.offsetHeight || singleSec.scrollHeight;
2155
- if (secH > pageH * 1.25 && children.length > 1) {
2156
- const childHeights = children.map((c) => {
2157
- const rectH = c.getBoundingClientRect().height;
2158
- const offH = c.offsetHeight;
2159
- const textLen = c.textContent?.trim().length || 0;
2160
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2161
- return Math.max(rectH, offH, estH);
2162
- });
2163
- const parent = singleSec.parentElement || wrapper;
2164
- const newSections = [singleSec];
2165
- const headerEl = singleSec.querySelector("header");
2166
- const footerEl = singleSec.querySelector("footer");
2167
- contentContainer.innerHTML = "";
2168
- singleSec.style.minHeight = `${pageH}px`;
2169
- singleSec.style.boxSizing = "border-box";
2170
- let curContent = contentContainer;
2171
- let curH = 0;
2172
- const maxH = pageH - 140;
2173
- for (let i = 0; i < children.length; i++) {
2174
- const child = children[i];
2175
- const chH = childHeights[i];
2176
- curContent.appendChild(child);
2177
- curH += chH;
2178
- if (curH >= maxH && i < children.length - 1) {
2179
- const nextSec = document.createElement("section");
2180
- nextSec.className = singleSec.className;
2181
- nextSec.style.cssText = singleSec.style.cssText;
2182
- nextSec.style.minHeight = `${pageH}px`;
2183
- nextSec.style.boxSizing = "border-box";
2184
- nextSec.style.backgroundColor = "#ffffff";
2185
- nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2186
- nextSec.style.borderRadius = "4px";
2187
- nextSec.style.marginBottom = "24px";
2188
- if (headerEl) {
2189
- nextSec.appendChild(headerEl.cloneNode(true));
2190
- }
2191
- const nextArticle = document.createElement("article");
2192
- if (contentContainer.tagName.toLowerCase() === "article") {
2193
- nextArticle.style.cssText = contentContainer.style.cssText;
2194
- }
2195
- nextSec.appendChild(nextArticle);
2196
- if (footerEl) {
2197
- nextSec.appendChild(footerEl.cloneNode(true));
2371
+ if (sections.length > 0 && cards.length === 0) {
2372
+ const finalSections = [];
2373
+ for (const singleSec of sections) {
2374
+ const contentContainer = singleSec.querySelector("article") || singleSec;
2375
+ const children = Array.from(contentContainer.children);
2376
+ const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2377
+ const secH = singleSec.scrollHeight || singleSec.offsetHeight;
2378
+ if (secH > pageH * 1.25 && children.length > 1) {
2379
+ const childHeights = children.map((c) => {
2380
+ const rectH = c.getBoundingClientRect().height;
2381
+ const offH = c.offsetHeight;
2382
+ const textLen = c.textContent?.trim().length || 0;
2383
+ const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2384
+ return Math.max(rectH, offH, estH);
2385
+ });
2386
+ const parent = singleSec.parentElement || wrapper;
2387
+ const headerEl = singleSec.querySelector("header");
2388
+ const footerEl = singleSec.querySelector("footer");
2389
+ contentContainer.innerHTML = "";
2390
+ singleSec.style.minHeight = `${pageH}px`;
2391
+ singleSec.style.boxSizing = "border-box";
2392
+ let curContent = contentContainer;
2393
+ let curSec = singleSec;
2394
+ let curH = 0;
2395
+ const maxH = pageH - 140;
2396
+ finalSections.push(singleSec);
2397
+ for (let i = 0; i < children.length; i++) {
2398
+ const child = children[i];
2399
+ const chH = childHeights[i];
2400
+ curContent.appendChild(child);
2401
+ curH += chH;
2402
+ if (curH >= maxH && i < children.length - 1) {
2403
+ const nextSec = document.createElement("section");
2404
+ nextSec.className = singleSec.className;
2405
+ nextSec.style.cssText = singleSec.style.cssText;
2406
+ nextSec.style.minHeight = `${pageH}px`;
2407
+ nextSec.style.boxSizing = "border-box";
2408
+ nextSec.style.backgroundColor = "#ffffff";
2409
+ nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2410
+ nextSec.style.borderRadius = "4px";
2411
+ nextSec.style.marginBottom = "24px";
2412
+ if (headerEl) {
2413
+ nextSec.appendChild(headerEl.cloneNode(true));
2414
+ }
2415
+ const nextArticle = document.createElement("article");
2416
+ if (contentContainer.tagName.toLowerCase() === "article") {
2417
+ nextArticle.style.cssText = contentContainer.style.cssText;
2418
+ }
2419
+ nextSec.appendChild(nextArticle);
2420
+ if (footerEl) {
2421
+ nextSec.appendChild(footerEl.cloneNode(true));
2422
+ }
2423
+ if (curSec.nextSibling) {
2424
+ parent.insertBefore(nextSec, curSec.nextSibling);
2425
+ } else {
2426
+ parent.appendChild(nextSec);
2427
+ }
2428
+ finalSections.push(nextSec);
2429
+ curSec = nextSec;
2430
+ curContent = nextArticle;
2431
+ curH = 0;
2198
2432
  }
2199
- parent.appendChild(nextSec);
2200
- newSections.push(nextSec);
2201
- curContent = nextArticle;
2202
- curH = 0;
2203
2433
  }
2434
+ } else {
2435
+ finalSections.push(singleSec);
2204
2436
  }
2205
- sections = newSections;
2206
2437
  }
2438
+ sections = finalSections;
2207
2439
  }
2208
2440
  const pageElements = sections.length > 0 ? sections : cards;
2209
2441
  const totalPages = Math.max(1, pageElements.length);
@@ -2595,6 +2827,88 @@ var DocxPlugin = class {
2595
2827
  }
2596
2828
  return result;
2597
2829
  }
2830
+ parseAndRenderChartSvg(xmlStr, width = 500, height = 260) {
2831
+ const catMatches = [...xmlStr.matchAll(/<c:cat>[\s\S]*?<c:strCache>([\s\S]*?)<\/c:strCache>/g)];
2832
+ let categories = [];
2833
+ if (catMatches.length > 0) {
2834
+ categories = [...catMatches[0][1].matchAll(/<c:v>([^<]+)<\/c:v>/g)].map((m) => m[1]);
2835
+ }
2836
+ if (categories.length === 0) {
2837
+ categories = ["Category 1", "Category 2", "Category 3", "Category 4"];
2838
+ }
2839
+ const defaultColors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021", "#83caff"];
2840
+ const sers = [...xmlStr.matchAll(/<c:ser>([\s\S]*?)<\/c:ser>/g)];
2841
+ const series = [];
2842
+ sers.forEach((s, sIdx) => {
2843
+ const titleMatch = s[1].match(/<c:tx>[\s\S]*?<c:v>([^<]+)<\/c:v>/);
2844
+ const title = titleMatch ? titleMatch[1] : `Series ${sIdx + 1}`;
2845
+ const clrMatch = s[1].match(/<a:srgbClr\s+val="([^"]+)"/);
2846
+ const color = clrMatch ? "#" + clrMatch[1] : defaultColors[sIdx % defaultColors.length];
2847
+ const valMatch = s[1].match(/<c:val>[\s\S]*?<c:numCache>([\s\S]*?)<\/c:numCache>/);
2848
+ let values = [];
2849
+ if (valMatch) {
2850
+ 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);
2851
+ }
2852
+ series.push({ title, color, values });
2853
+ });
2854
+ if (series.length === 0) return "";
2855
+ let maxVal = 10;
2856
+ series.forEach((s) => s.values.forEach((v) => {
2857
+ if (v > maxVal) maxVal = v;
2858
+ }));
2859
+ maxVal = Math.ceil(maxVal * 1.15);
2860
+ if (maxVal % 2 !== 0) maxVal++;
2861
+ const padLeft = 45;
2862
+ const padBottom = 55;
2863
+ const padTop = 20;
2864
+ const padRight = 20;
2865
+ const plotW = width - padLeft - padRight;
2866
+ const plotH = height - padTop - padBottom;
2867
+ const yTicks = 5;
2868
+ let gridLines = "";
2869
+ for (let i = 0; i <= yTicks; i++) {
2870
+ const val = maxVal / yTicks * i;
2871
+ const y = padTop + plotH - val / maxVal * plotH;
2872
+ gridLines += `<line x1="${padLeft}" y1="${y}" x2="${padLeft + plotW}" y2="${y}" stroke="#e2e8f0" stroke-width="1" />`;
2873
+ 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>`;
2874
+ }
2875
+ const numCats = categories.length;
2876
+ const numSers = series.length;
2877
+ const groupW = plotW / numCats;
2878
+ const barW = Math.max(8, Math.min(28, groupW * 0.7 / numSers));
2879
+ const groupPad = (groupW - barW * numSers) / 2;
2880
+ let bars = "";
2881
+ let catLabels = "";
2882
+ for (let c = 0; c < numCats; c++) {
2883
+ const catX = padLeft + c * groupW;
2884
+ 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>`;
2885
+ for (let s = 0; s < numSers; s++) {
2886
+ const val = series[s].values[c] ?? 0;
2887
+ const bH = Math.max(0, val / maxVal * plotH);
2888
+ const bX = catX + groupPad + s * barW;
2889
+ const bY = padTop + plotH - bH;
2890
+ bars += `<rect x="${bX}" y="${bY}" width="${barW - 2}" height="${bH}" fill="${series[s].color}" rx="1" />`;
2891
+ }
2892
+ }
2893
+ let legend = "";
2894
+ const legY = height - 12;
2895
+ let legX = padLeft + (plotW - numSers * 100) / 2;
2896
+ series.forEach((s) => {
2897
+ legend += `<rect x="${legX}" y="${legY - 9}" width="10" height="10" fill="${s.color}" rx="2" />`;
2898
+ legend += `<text x="${legX + 15}" y="${legY}" font-size="11" fill="#475569" font-family="Calibri, sans-serif">${s.title}</text>`;
2899
+ legX += 95;
2900
+ });
2901
+ return `
2902
+ <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">
2903
+ ${gridLines}
2904
+ <line x1="${padLeft}" y1="${padTop + plotH}" x2="${padLeft + plotW}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2905
+ <line x1="${padLeft}" y1="${padTop}" x2="${padLeft}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2906
+ ${bars}
2907
+ ${catLabels}
2908
+ ${legend}
2909
+ </svg>
2910
+ `.trim();
2911
+ }
2598
2912
  };
2599
2913
  function docxPlugin() {
2600
2914
  return new DocxPlugin();
@@ -3161,6 +3475,16 @@ var CodePlugin = class {
3161
3475
  execute: () => {
3162
3476
  instance.print?.();
3163
3477
  }
3478
+ },
3479
+ {
3480
+ id: "open-window",
3481
+ icon: "open-window",
3482
+ label: "Open in Separate Full Window",
3483
+ type: "button",
3484
+ group: "actions",
3485
+ execute: () => {
3486
+ instance.openInSeparateWindow?.();
3487
+ }
3164
3488
  }
3165
3489
  );
3166
3490
  return actions;
@@ -4329,6 +4653,14 @@ var RtfPlugin = class {
4329
4653
  type: "button",
4330
4654
  group: "actions",
4331
4655
  execute: () => instance.print?.()
4656
+ },
4657
+ {
4658
+ id: "open-window",
4659
+ icon: "open-window",
4660
+ label: "Open in Separate Full Window",
4661
+ type: "button",
4662
+ group: "actions",
4663
+ execute: () => instance.openInSeparateWindow?.()
4332
4664
  }
4333
4665
  );
4334
4666
  return actions;
@@ -4381,59 +4713,54 @@ var RtfPlugin = class {
4381
4713
  }
4382
4714
  const doc = new RTFJS__namespace.Document(ctx.buffer, {});
4383
4715
  const htmlElements = await doc.render();
4384
- if (htmlElements.length === 1) {
4385
- const singleEl = htmlElements[0];
4386
- wrapper.appendChild(singleEl);
4387
- const children = Array.from(singleEl.children);
4388
- const secH = singleEl.offsetHeight || singleEl.scrollHeight;
4389
- if (secH > 1300 && children.length > 1) {
4390
- const childHeights = children.map((c) => {
4391
- const rectH = c.getBoundingClientRect().height;
4392
- const offH = c.offsetHeight;
4393
- const textLen = c.textContent?.trim().length || 0;
4394
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
4395
- return Math.max(rectH, offH, estH);
4396
- });
4397
- wrapper.innerHTML = "";
4398
- const createRtfCard = () => {
4399
- const card = document.createElement("div");
4400
- card.className = "fp-rtf-page-card";
4401
- card.style.backgroundColor = "#ffffff";
4402
- card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4403
- card.style.borderRadius = "4px";
4404
- card.style.padding = "72px 56px";
4405
- card.style.width = "816px";
4406
- card.style.minHeight = "1056px";
4407
- card.style.boxSizing = "border-box";
4408
- card.style.marginBottom = "24px";
4409
- return card;
4410
- };
4411
- let curCard = createRtfCard();
4412
- wrapper.appendChild(curCard);
4413
- pageElements = [curCard];
4414
- let curH = 0;
4415
- const maxH = 920;
4416
- for (let i = 0; i < children.length; i++) {
4417
- const child = children[i];
4418
- const chH = childHeights[i];
4419
- curCard.appendChild(child);
4420
- curH += chH;
4421
- if (curH >= maxH && i < children.length - 1) {
4422
- curCard = createRtfCard();
4423
- wrapper.appendChild(curCard);
4424
- pageElements.push(curCard);
4425
- curH = 0;
4426
- }
4427
- }
4716
+ const contentNodes = [];
4717
+ for (const item of htmlElements) {
4718
+ if (item.children && item.children.length > 0 && !item.tagName.toLowerCase().startsWith("table")) {
4719
+ contentNodes.push(...Array.from(item.children));
4428
4720
  } else {
4429
- pageElements = [singleEl];
4721
+ contentNodes.push(item);
4430
4722
  }
4431
- } else {
4432
- pageElements = htmlElements;
4433
- for (let i = 0; i < htmlElements.length; i++) {
4434
- const el = htmlElements[i];
4435
- el.style.display = i === 0 ? "block" : "none";
4436
- wrapper.appendChild(el);
4723
+ }
4724
+ wrapper.innerHTML = "";
4725
+ contentNodes.forEach((node) => wrapper.appendChild(node));
4726
+ const childHeights = contentNodes.map((c) => {
4727
+ const rectH = c.getBoundingClientRect ? c.getBoundingClientRect().height : 0;
4728
+ const offH = c.offsetHeight || 0;
4729
+ const textLen = c.textContent?.trim().length || 0;
4730
+ const estH = Math.max(24, Math.ceil(textLen / 75) * 22 + 14);
4731
+ return Math.max(rectH, offH, estH);
4732
+ });
4733
+ wrapper.innerHTML = "";
4734
+ const createRtfCard = () => {
4735
+ const card = document.createElement("div");
4736
+ card.className = "fp-rtf-page-card";
4737
+ card.style.backgroundColor = "#ffffff";
4738
+ card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4739
+ card.style.borderRadius = "4px";
4740
+ card.style.padding = "72px 56px";
4741
+ card.style.width = "816px";
4742
+ card.style.minHeight = "1056px";
4743
+ card.style.boxSizing = "border-box";
4744
+ card.style.marginBottom = "24px";
4745
+ card.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4746
+ card.style.lineHeight = "1.6";
4747
+ return card;
4748
+ };
4749
+ let curCard = createRtfCard();
4750
+ wrapper.appendChild(curCard);
4751
+ pageElements = [curCard];
4752
+ let curH = 0;
4753
+ const maxH = 912;
4754
+ for (let i = 0; i < contentNodes.length; i++) {
4755
+ const child = contentNodes[i];
4756
+ const chH = childHeights[i];
4757
+ curCard.appendChild(child);
4758
+ curH += chH;
4759
+ if (curH >= maxH && i < contentNodes.length - 1) {
4760
+ curCard = createRtfCard();
4761
+ wrapper.appendChild(curCard);
4762
+ pageElements.push(curCard);
4763
+ curH = 0;
4437
4764
  }
4438
4765
  }
4439
4766
  } catch (err) {
@@ -4802,6 +5129,14 @@ var OpenDocumentPlugin = class {
4802
5129
  type: "button",
4803
5130
  group: "actions",
4804
5131
  execute: () => instance.print?.()
5132
+ },
5133
+ {
5134
+ id: "open-window",
5135
+ icon: "open-window",
5136
+ label: "Open in Separate Full Window",
5137
+ type: "button",
5138
+ group: "actions",
5139
+ execute: () => instance.openInSeparateWindow?.()
4805
5140
  }
4806
5141
  );
4807
5142
  return actions;
@@ -5390,6 +5725,14 @@ var DocPlugin = class {
5390
5725
  type: "button",
5391
5726
  group: "actions",
5392
5727
  execute: () => instance.print?.()
5728
+ },
5729
+ {
5730
+ id: "open-window",
5731
+ icon: "open-window",
5732
+ label: "Open in Separate Full Window",
5733
+ type: "button",
5734
+ group: "actions",
5735
+ execute: () => instance.openInSeparateWindow?.()
5393
5736
  }
5394
5737
  );
5395
5738
  return actions;
@@ -5409,8 +5752,17 @@ var DocPlugin = class {
5409
5752
  let scale = 1;
5410
5753
  let extractedRawText = "";
5411
5754
  let isFallback = false;
5755
+ let chartSvg = "";
5412
5756
  try {
5413
5757
  const cfbf = new CfbfReader(ctx.buffer);
5758
+ try {
5759
+ const pkg = cfbf.readStream("package_stream");
5760
+ if (pkg && pkg.length > 100) {
5761
+ chartSvg = this.parseOdfChartToSvg(pkg);
5762
+ }
5763
+ } catch (chartErr) {
5764
+ console.warn("[DocPlugin] Chart stream parsing info:", chartErr);
5765
+ }
5414
5766
  const wordDocStream = cfbf.readStream("WordDocument");
5415
5767
  if (!wordDocStream || wordDocStream.length < 512) {
5416
5768
  throw new Error("WordDocument stream not found or invalid in CFBF archive");
@@ -5428,7 +5780,7 @@ var DocPlugin = class {
5428
5780
  extractedRawText = fallback;
5429
5781
  isFallback = true;
5430
5782
  }
5431
- const rawPages = this.splitIntoPages(extractedRawText);
5783
+ const rawPages = this.splitIntoPages(extractedRawText, chartSvg);
5432
5784
  const totalPages = Math.max(1, rawPages.length);
5433
5785
  let currentPage = 1;
5434
5786
  const pageCards = [];
@@ -5670,7 +6022,7 @@ var DocPlugin = class {
5670
6022
  for (const run of [...ansiRuns, ...utf16Runs]) {
5671
6023
  const trimmed = run.trim();
5672
6024
  if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
5673
- 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)) {
6025
+ 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)) {
5674
6026
  seen.add(trimmed);
5675
6027
  candidateLines.push(trimmed);
5676
6028
  }
@@ -5681,12 +6033,138 @@ var DocPlugin = class {
5681
6033
  heuristicTextExtraction(buffer) {
5682
6034
  return this.extractStringsFromBytes(new Uint8Array(buffer));
5683
6035
  }
5684
- splitIntoPages(text) {
6036
+ /**
6037
+ * Parses an embedded OpenDocument Chart package into a vector SVG bar/column chart
6038
+ */
6039
+ parseOdfChartToSvg(zipBytes) {
6040
+ try {
6041
+ const unzipped = fflate__namespace.unzipSync(zipBytes);
6042
+ const contentXml = unzipped["content.xml"] ? new TextDecoder("utf-8").decode(unzipped["content.xml"]) : "";
6043
+ if (!contentXml) return "";
6044
+ const rowsMatch = contentXml.match(/<table:table-row[\s\S]*?<\/table:table-row>/g) || [];
6045
+ if (rowsMatch.length < 2) return "";
6046
+ const headers = [];
6047
+ const firstRow = rowsMatch[0];
6048
+ const headerCells = firstRow ? firstRow.match(/<text:p>([^<]+)<\/text:p>/g) || [] : [];
6049
+ for (const h of headerCells) {
6050
+ headers.push(h.replace(/<\/?text:p>/g, "").trim());
6051
+ }
6052
+ const categories = [];
6053
+ const seriesValues = headers.map(() => []);
6054
+ for (let r = 1; r < rowsMatch.length; r++) {
6055
+ const rowStr = rowsMatch[r];
6056
+ if (!rowStr) continue;
6057
+ const cells = rowStr.match(/<table:table-cell[\s\S]*?<\/table:table-cell>/g) || [];
6058
+ if (cells.length > 0 && cells[0]) {
6059
+ const catMatch = cells[0].match(/<text:p>([^<]+)<\/text:p>/);
6060
+ categories.push(catMatch ? catMatch[1] : "Row " + r);
6061
+ for (let c = 1; c < cells.length && c - 1 < headers.length; c++) {
6062
+ const cellStr = cells[c];
6063
+ if (!cellStr) continue;
6064
+ const valMatch = cellStr.match(/office:value="([0-9.]+)"/) || cellStr.match(/<text:p>([0-9.]+)<\/text:p>/);
6065
+ const series = seriesValues[c - 1];
6066
+ if (series) {
6067
+ series.push(valMatch ? parseFloat(valMatch[1]) : 0);
6068
+ }
6069
+ }
6070
+ }
6071
+ }
6072
+ const colors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021"];
6073
+ const colorMatches = contentXml.matchAll(/draw:fill-color="(#[0-9a-fA-F]{6})"/g);
6074
+ let cIdx = 0;
6075
+ for (const cm of colorMatches) {
6076
+ if (cIdx < colors.length) colors[cIdx] = cm[1];
6077
+ cIdx++;
6078
+ }
6079
+ let maxVal = 10;
6080
+ for (const s of seriesValues) {
6081
+ for (const v of s) {
6082
+ if (v > maxVal) maxVal = v;
6083
+ }
6084
+ }
6085
+ maxVal = Math.ceil(maxVal * 1.15);
6086
+ const width = 560;
6087
+ const height = 280;
6088
+ const padLeft = 45;
6089
+ const padRight = 100;
6090
+ const padTop = 20;
6091
+ const padBottom = 40;
6092
+ const chartW = width - padLeft - padRight;
6093
+ const chartH = height - padTop - padBottom;
6094
+ 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);">`;
6095
+ for (let step = 0; step <= 4; step++) {
6096
+ const yVal = (maxVal / 4 * step).toFixed(1);
6097
+ const yPos = padTop + chartH - step / 4 * chartH;
6098
+ svg += `<line x1="${padLeft}" y1="${yPos}" x2="${padLeft + chartW}" y2="${yPos}" stroke="#e2e8f0" stroke-dasharray="2,2" />`;
6099
+ svg += `<text x="${padLeft - 8}" y="${yPos + 4}" font-size="11" fill="#64748b" text-anchor="end">${yVal}</text>`;
6100
+ }
6101
+ const numCats = categories.length;
6102
+ const numSeries = headers.length;
6103
+ const groupW = chartW / numCats;
6104
+ const barW = Math.max(8, groupW * 0.7 / numSeries);
6105
+ const groupPad = (groupW - barW * numSeries) / 2;
6106
+ for (let catIdx = 0; catIdx < numCats; catIdx++) {
6107
+ const groupX = padLeft + catIdx * groupW + groupPad;
6108
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6109
+ const val = seriesValues[sIdx][catIdx] || 0;
6110
+ const barH = val / maxVal * chartH;
6111
+ const barX = groupX + sIdx * barW;
6112
+ const barY = padTop + chartH - barH;
6113
+ const col = colors[sIdx % colors.length];
6114
+ svg += `<rect x="${barX}" y="${barY}" width="${barW - 2}" height="${barH}" fill="${col}" rx="2"><title>${headers[sIdx]}: ${val}</title></rect>`;
6115
+ }
6116
+ const catX = padLeft + catIdx * groupW + groupW / 2;
6117
+ svg += `<text x="${catX}" y="${padTop + chartH + 18}" font-size="11" fill="#475569" text-anchor="middle">${categories[catIdx]}</text>`;
6118
+ }
6119
+ let legendY = padTop + 20;
6120
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6121
+ const col = colors[sIdx % colors.length];
6122
+ svg += `<rect x="${padLeft + chartW + 15}" y="${legendY}" width="12" height="12" fill="${col}" rx="2" />`;
6123
+ svg += `<text x="${padLeft + chartW + 32}" y="${legendY + 10}" font-size="11" fill="#334155">${headers[sIdx]}</text>`;
6124
+ legendY += 20;
6125
+ }
6126
+ svg += "</svg>";
6127
+ return svg;
6128
+ } catch (e) {
6129
+ console.warn("[DocPlugin] Error generating chart SVG:", e);
6130
+ return "";
6131
+ }
6132
+ }
6133
+ cleanWordDocFields(text, chartSvg = "") {
6134
+ if (!text) return "";
6135
+ let cleaned = text.replace(
6136
+ /\x13\s*EMBED\b[\s\S]*?\x15/gi,
6137
+ () => chartSvg ? `
6138
+
6139
+ ${chartSvg}
6140
+
6141
+ ` : ""
6142
+ );
6143
+ cleaned = cleaned.replace(
6144
+ /\x13\s*HYPERLINK\s*"?([^"\x14]+)"?\s*\x14([\s\S]*?)\x15/gi,
6145
+ (_match, url, label) => {
6146
+ const cleanUrl = url.trim();
6147
+ const cleanLabel = label.trim() || cleanUrl;
6148
+ return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${cleanLabel}</a>`;
6149
+ }
6150
+ );
6151
+ cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, (_m, res) => {
6152
+ if (/[\x00-\x1F]/.test(res)) return "";
6153
+ return res.trim();
6154
+ });
6155
+ cleaned = cleaned.replace(/\x13[^\x15]*\x15/g, "");
6156
+ cleaned = cleaned.replace(/[\x13\x14\x15]/g, "");
6157
+ cleaned = cleaned.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
6158
+ cleaned = cleaned.replace(/EMBED\s+LibreOffice\.ChartDocument\.[0-9]+/gi, chartSvg || "");
6159
+ return cleaned;
6160
+ }
6161
+ splitIntoPages(text, chartSvg = "") {
5685
6162
  if (!text) return [""];
5686
- const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
6163
+ const cleanedText = this.cleanWordDocFields(text, chartSvg);
6164
+ const normalized = cleanedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
5687
6165
  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);
5688
6166
  if (explicitParts.length === 0) explicitParts.push(normalized);
5689
- const maxLinesPerPage = 32;
6167
+ const maxLinesPerPage = 34;
5690
6168
  const charsPerLine = 80;
5691
6169
  const finalPages = [];
5692
6170
  for (const part of explicitParts) {
@@ -5694,7 +6172,8 @@ var DocPlugin = class {
5694
6172
  let currentLines = [];
5695
6173
  let count = 0;
5696
6174
  for (const line of lines) {
5697
- const vLines = Math.max(1, Math.ceil((line.length || 1) / charsPerLine));
6175
+ const isSvg = line.includes("<svg");
6176
+ const vLines = isSvg ? 12 : Math.max(1, Math.ceil((line.replace(/<[^>]+>/g, "").length || 1) / charsPerLine));
5698
6177
  if (count + vLines > maxLinesPerPage && currentLines.length > 0) {
5699
6178
  finalPages.push(currentLines.join("\n"));
5700
6179
  currentLines = [];
@@ -5734,9 +6213,44 @@ var DocPlugin = class {
5734
6213
  tableLines = [];
5735
6214
  }
5736
6215
  };
6216
+ const sanitizeOptions = {
6217
+ ADD_TAGS: ["a", "svg", "g", "path", "line", "rect", "circle", "text", "title"],
6218
+ ADD_ATTR: [
6219
+ "href",
6220
+ "target",
6221
+ "rel",
6222
+ "style",
6223
+ "viewBox",
6224
+ "width",
6225
+ "height",
6226
+ "x",
6227
+ "y",
6228
+ "x1",
6229
+ "y1",
6230
+ "x2",
6231
+ "y2",
6232
+ "fill",
6233
+ "stroke",
6234
+ "stroke-width",
6235
+ "stroke-dasharray",
6236
+ "rx",
6237
+ "font-size",
6238
+ "text-anchor"
6239
+ ]
6240
+ };
5737
6241
  let i = 0;
5738
6242
  while (i < lines.length) {
5739
6243
  let line = lines[i];
6244
+ if (line.includes("<svg")) {
6245
+ if (inList) {
6246
+ html += "</ul>";
6247
+ inList = false;
6248
+ }
6249
+ flushTable();
6250
+ html += line;
6251
+ i++;
6252
+ continue;
6253
+ }
5740
6254
  let tabCount = (line.match(/\t/g) || []).length;
5741
6255
  if (tabCount > 0) {
5742
6256
  let j = i;
@@ -5775,20 +6289,20 @@ var DocPlugin = class {
5775
6289
  html += "</ul>";
5776
6290
  inList = false;
5777
6291
  }
5778
- 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>`;
6292
+ 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>`;
5779
6293
  } else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
5780
6294
  if (!inList) {
5781
6295
  html += '<ul style="margin: 8px 0; padding-left: 24px;">';
5782
6296
  inList = true;
5783
6297
  }
5784
6298
  const bulletText = line.replace(/^[•\-\*]\s*/, "");
5785
- html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6__default.default.sanitize(bulletText)}</li>`;
6299
+ html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6__default.default.sanitize(bulletText, sanitizeOptions)}</li>`;
5786
6300
  } else {
5787
6301
  if (inList) {
5788
6302
  html += "</ul>";
5789
6303
  inList = false;
5790
6304
  }
5791
- html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6__default.default.sanitize(line)}</p>`;
6305
+ html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6__default.default.sanitize(line, sanitizeOptions)}</p>`;
5792
6306
  }
5793
6307
  i++;
5794
6308
  }