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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vue.js CHANGED
@@ -2,6 +2,7 @@ import { defineComponent, ref, onMounted, watch, onBeforeUnmount, h } from 'vue'
2
2
  import DOMPurify6 from 'dompurify';
3
3
  import * as pdfjsLib from 'pdfjs-dist';
4
4
  import * as docx from 'docx-preview';
5
+ import * as fflate from 'fflate';
5
6
  import { unzipSync, strFromU8, unzip } from 'fflate';
6
7
  import * as XLSX from 'xlsx';
7
8
  import hljs from 'highlight.js';
@@ -337,16 +338,21 @@ async function sourceToArrayBuffer(source, signal) {
337
338
  metadata.mimeType = source.type || void 0;
338
339
  metadata.extension = extractExtension(source.name);
339
340
  buffer = await source.arrayBuffer();
340
- } else if (source instanceof Blob) {
341
+ } else if (source instanceof Blob || source && typeof source.arrayBuffer === "function" && typeof source.size === "number") {
341
342
  metadata.size = source.size;
342
343
  metadata.mimeType = source.type || void 0;
344
+ if (source.name) {
345
+ metadata.name = source.name;
346
+ metadata.extension = extractExtension(source.name);
347
+ }
343
348
  buffer = await source.arrayBuffer();
344
- } else if (source instanceof ArrayBuffer) {
349
+ } else if (source instanceof ArrayBuffer || Object.prototype.toString.call(source) === "[object ArrayBuffer]" || source && typeof source.byteLength === "number" && typeof source.slice === "function") {
345
350
  buffer = source;
346
- } else if (source instanceof Uint8Array) {
347
- buffer = source.buffer.slice(
348
- source.byteOffset,
349
- source.byteOffset + source.byteLength
351
+ } else if (source instanceof Uint8Array || ArrayBuffer.isView(source)) {
352
+ const view = source;
353
+ buffer = view.buffer.slice(
354
+ view.byteOffset,
355
+ view.byteOffset + view.byteLength
350
356
  );
351
357
  } else {
352
358
  throw new Error("Unsupported file source type");
@@ -431,6 +437,45 @@ function createElement(tag, attrs, ...children) {
431
437
  }
432
438
  return el;
433
439
  }
440
+ var DB_NAME = "PreviewFileTransferDB";
441
+ var DB_STORE = "transfers";
442
+ function openDB() {
443
+ return new Promise((resolve, reject) => {
444
+ if (typeof indexedDB === "undefined") {
445
+ return reject(new Error("IndexedDB is not available"));
446
+ }
447
+ const req = indexedDB.open(DB_NAME, 1);
448
+ req.onupgradeneeded = () => {
449
+ const db = req.result;
450
+ if (!db.objectStoreNames.contains(DB_STORE)) {
451
+ db.createObjectStore(DB_STORE, { keyPath: "id" });
452
+ }
453
+ };
454
+ req.onsuccess = () => resolve(req.result);
455
+ req.onerror = () => reject(req.error);
456
+ });
457
+ }
458
+ async function saveTransferPayload(id, payload) {
459
+ if (typeof window !== "undefined") {
460
+ try {
461
+ window[id] = payload;
462
+ window.__lastTransfer = payload;
463
+ } catch {
464
+ }
465
+ }
466
+ try {
467
+ const db = await openDB();
468
+ return new Promise((resolve, reject) => {
469
+ const tx = db.transaction(DB_STORE, "readwrite");
470
+ const store = tx.objectStore(DB_STORE);
471
+ store.put({ id, ...payload, timestamp: Date.now() });
472
+ tx.oncomplete = () => resolve();
473
+ tx.onerror = () => reject(tx.error);
474
+ });
475
+ } catch (e) {
476
+ console.warn("[saveTransferPayload] IndexedDB store warning:", e);
477
+ }
478
+ }
434
479
  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>`;
435
480
  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>`;
436
481
  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>`;
@@ -450,6 +495,7 @@ var ICON_COPY = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" str
450
495
  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>`;
451
496
  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>`;
452
497
  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>`;
498
+ 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>`;
453
499
  var ICON_MAP = {
454
500
  "zoom-in": ICON_ZOOM_IN,
455
501
  "zoom-out": ICON_ZOOM_OUT,
@@ -476,7 +522,9 @@ var ICON_MAP = {
476
522
  "forward-10": ICON_FAST_FORWARD,
477
523
  "rewind": ICON_REWIND,
478
524
  "replay-10": ICON_REWIND,
479
- "speed": ICON_SPEED
525
+ "speed": ICON_SPEED,
526
+ "open-window": ICON_EXTERNAL_WINDOW,
527
+ "external-window": ICON_EXTERNAL_WINDOW
480
528
  };
481
529
  var ToolbarController = class {
482
530
  el;
@@ -619,9 +667,11 @@ var ToolbarController = class {
619
667
  type: "button",
620
668
  "data-action-id": id
621
669
  });
622
- const svg = ICON_MAP[iconHtml] || ICON_MAP[id] || (iconHtml && iconHtml.startsWith("<svg") ? iconHtml : null);
623
- if (svg) {
624
- btn.innerHTML = sanitizeSVG(svg);
670
+ const internalSvg = ICON_MAP[iconHtml] || ICON_MAP[id];
671
+ if (internalSvg) {
672
+ btn.innerHTML = internalSvg;
673
+ } else if (iconHtml && iconHtml.startsWith("<svg")) {
674
+ btn.innerHTML = sanitizeSVG(iconHtml);
625
675
  } else {
626
676
  btn.textContent = title || id;
627
677
  }
@@ -693,7 +743,7 @@ var ThumbnailPanel = class {
693
743
  }
694
744
  }
695
745
  };
696
- var FilePreviewViewer = class {
746
+ var FilePreviewViewer = class _FilePreviewViewer {
697
747
  plugins = [];
698
748
  activeInstance = null;
699
749
  abortController = null;
@@ -730,6 +780,7 @@ var FilePreviewViewer = class {
730
780
  * Preview a file in the given container element.
731
781
  */
732
782
  async preview(container, source, options = {}) {
783
+ this.currentOptions = options;
733
784
  this.abort();
734
785
  this.abortController = new AbortController();
735
786
  const { signal } = this.abortController;
@@ -739,7 +790,10 @@ var FilePreviewViewer = class {
739
790
  this.showLoading();
740
791
  try {
741
792
  const { buffer, metadata } = await sourceToArrayBuffer(source, signal);
742
- this.currentBuffer = buffer;
793
+ if (options.metadata) {
794
+ Object.assign(metadata, options.metadata);
795
+ }
796
+ this.currentBuffer = buffer.slice(0);
743
797
  this.currentMetadata = metadata;
744
798
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
745
799
  const fileInfo = { metadata, buffer };
@@ -769,6 +823,7 @@ var FilePreviewViewer = class {
769
823
  }
770
824
  });
771
825
  this.activeInstance = instance;
826
+ instance.openInSeparateWindow = () => this.openInSeparateWindow();
772
827
  this.hideLoading();
773
828
  this.eventEmitter.emit("loaded", { metadata, plugin: matchedPlugin.id });
774
829
  if (options.showToolbar !== false && this.toolbar) {
@@ -778,26 +833,16 @@ var FilePreviewViewer = class {
778
833
  actions.push({
779
834
  id: "fullscreen",
780
835
  icon: "fullscreen",
781
- label: "Toggle Fullscreen",
836
+ label: "Fullscreen",
782
837
  type: "button",
783
838
  group: "view",
784
- execute: async () => {
839
+ execute: () => {
785
840
  try {
786
- const isNativeFs = !!document.fullscreenElement;
787
- const isCssFs = this.wrapperEl?.classList.contains("fp-fullscreen-active");
788
- if (!isNativeFs && !isCssFs) {
789
- if (this.wrapperEl?.requestFullscreen) {
790
- await this.wrapperEl.requestFullscreen().catch(() => {
791
- this.wrapperEl?.classList.add("fp-fullscreen-active");
792
- });
793
- } else {
794
- this.wrapperEl?.classList.add("fp-fullscreen-active");
795
- }
841
+ if (!document.fullscreenElement) {
842
+ this.wrapperEl?.requestFullscreen?.();
843
+ this.wrapperEl?.classList.add("fp-fullscreen-active");
796
844
  } else {
797
- if (document.fullscreenElement) {
798
- await document.exitFullscreen().catch(() => {
799
- });
800
- }
845
+ document.exitFullscreen?.();
801
846
  this.wrapperEl?.classList.remove("fp-fullscreen-active");
802
847
  }
803
848
  } catch {
@@ -809,6 +854,28 @@ var FilePreviewViewer = class {
809
854
  }
810
855
  });
811
856
  }
857
+ const openWinAction = actions.find((a) => a.id === "open-window");
858
+ if (openWinAction) {
859
+ if (options?._isSeparateWindow) {
860
+ const idx = actions.indexOf(openWinAction);
861
+ if (idx !== -1) actions.splice(idx, 1);
862
+ } else {
863
+ openWinAction.execute = () => {
864
+ this.openInSeparateWindow();
865
+ };
866
+ }
867
+ } else if (!options?._isSeparateWindow) {
868
+ actions.push({
869
+ id: "open-window",
870
+ icon: "open-window",
871
+ label: "Open in Separate Full Window",
872
+ type: "button",
873
+ group: "actions",
874
+ execute: () => {
875
+ this.openInSeparateWindow();
876
+ }
877
+ });
878
+ }
812
879
  this.toolbar.update(actions);
813
880
  this.toolbar.show();
814
881
  }
@@ -841,6 +908,104 @@ var FilePreviewViewer = class {
841
908
  throw error;
842
909
  }
843
910
  }
911
+ /**
912
+ * Opens the current file preview in a separate full browser window.
913
+ */
914
+ openInSeparateWindow() {
915
+ if (!this.currentBuffer) {
916
+ console.warn("[FilePreviewViewer] No active file buffer to open in separate window");
917
+ return null;
918
+ }
919
+ if (this.currentOptions.onOpenSeparateWindow) {
920
+ return this.currentOptions.onOpenSeparateWindow({
921
+ buffer: this.currentBuffer,
922
+ metadata: this.currentMetadata || { name: "Document" },
923
+ options: this.currentOptions
924
+ });
925
+ }
926
+ const transferId = "fp_win_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8);
927
+ let clonedBuffer;
928
+ try {
929
+ clonedBuffer = this.currentBuffer.slice(0);
930
+ } catch {
931
+ clonedBuffer = this.currentBuffer;
932
+ }
933
+ const payload = {
934
+ buffer: clonedBuffer,
935
+ metadata: this.currentMetadata ? { ...this.currentMetadata } : void 0,
936
+ options: { ...this.currentOptions, _isSeparateWindow: true }
937
+ };
938
+ if (typeof window !== "undefined") {
939
+ try {
940
+ window[transferId] = payload;
941
+ window.__lastTransfer = payload;
942
+ } catch {
943
+ }
944
+ }
945
+ saveTransferPayload(transferId, payload).catch((err) => {
946
+ console.warn("[FilePreviewViewer] Transfer payload save warning:", err);
947
+ });
948
+ let targetUrl = null;
949
+ if (this.currentOptions.standaloneViewerUrl) {
950
+ const u = new URL(this.currentOptions.standaloneViewerUrl, window.location.href);
951
+ u.searchParams.set("mode", "fullscreen");
952
+ u.searchParams.set("transferId", transferId);
953
+ targetUrl = u.toString();
954
+ } else if (typeof window !== "undefined" && window.location?.href && !window.location.href.startsWith("about:")) {
955
+ const u = new URL(window.location.href);
956
+ u.searchParams.set("mode", "fullscreen");
957
+ u.searchParams.set("transferId", transferId);
958
+ targetUrl = u.toString();
959
+ }
960
+ if (targetUrl) {
961
+ const newWin2 = window.open(targetUrl, "_blank");
962
+ if (!newWin2) {
963
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
964
+ return null;
965
+ }
966
+ return newWin2;
967
+ }
968
+ const title = (this.currentMetadata?.name || "Document Preview") + " - Full Preview";
969
+ const newWin = window.open("", "_blank");
970
+ if (!newWin) {
971
+ alert("Popup blocker prevented opening the preview in a separate window. Please allow popups for this site.");
972
+ return null;
973
+ }
974
+ newWin.document.title = title;
975
+ newWin.document.body.style.margin = "0";
976
+ newWin.document.body.style.padding = "0";
977
+ newWin.document.body.style.width = "100vw";
978
+ newWin.document.body.style.height = "100vh";
979
+ newWin.document.body.style.overflow = "hidden";
980
+ newWin.document.body.style.backgroundColor = "#f8fafc";
981
+ const headNodes = document.querySelectorAll('link[rel="stylesheet"], style');
982
+ headNodes.forEach((node) => {
983
+ newWin.document.head.appendChild(node.cloneNode(true));
984
+ });
985
+ const root = newWin.document.createElement("div");
986
+ root.id = "full-window-preview-root";
987
+ root.style.width = "100%";
988
+ root.style.height = "100%";
989
+ root.style.overflow = "hidden";
990
+ newWin.document.body.appendChild(root);
991
+ const separateViewer = new _FilePreviewViewer();
992
+ for (const plugin of this.plugins) {
993
+ separateViewer.registerPlugin(plugin);
994
+ }
995
+ separateViewer.preview(root, this.currentBuffer.slice(0), {
996
+ ...this.currentOptions,
997
+ showToolbar: true,
998
+ toolbarPosition: "top",
999
+ metadata: this.currentMetadata || void 0,
1000
+ _isSeparateWindow: true
1001
+ }).catch((err) => {
1002
+ console.error("[FilePreviewViewer] Error rendering in separate window:", err);
1003
+ });
1004
+ newWin.addEventListener("beforeunload", () => {
1005
+ separateViewer.destroy();
1006
+ });
1007
+ return newWin;
1008
+ }
844
1009
  /**
845
1010
  * Subscribe to viewer events.
846
1011
  */
@@ -1246,8 +1411,20 @@ var CfbfReader = class {
1246
1411
  }
1247
1412
  };
1248
1413
  if (typeof window !== "undefined" && pdfjsLib.GlobalWorkerOptions) {
1249
- if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
1250
- pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1414
+ if (!pdfjsLib.GlobalWorkerOptions.workerPort && !pdfjsLib.GlobalWorkerOptions.workerSrc) {
1415
+ const customWorker = window.__PDF_WORKER_SRC__;
1416
+ if (customWorker) {
1417
+ pdfjsLib.GlobalWorkerOptions.workerSrc = customWorker;
1418
+ } else {
1419
+ try {
1420
+ pdfjsLib.GlobalWorkerOptions.workerPort = new Worker(
1421
+ new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url),
1422
+ { type: "module" }
1423
+ );
1424
+ } catch {
1425
+ pdfjsLib.GlobalWorkerOptions.workerSrc = "./pdf.worker.min.mjs";
1426
+ }
1427
+ }
1251
1428
  }
1252
1429
  }
1253
1430
  var PdfPlugin = class {
@@ -1341,6 +1518,14 @@ var PdfPlugin = class {
1341
1518
  type: "button",
1342
1519
  group: "actions",
1343
1520
  execute: () => instance.print?.()
1521
+ },
1522
+ {
1523
+ id: "open-window",
1524
+ icon: "open-window",
1525
+ label: "Open in Separate Full Window",
1526
+ type: "button",
1527
+ group: "actions",
1528
+ execute: () => instance.openInSeparateWindow?.()
1344
1529
  }
1345
1530
  ];
1346
1531
  }
@@ -1390,18 +1575,21 @@ var PdfPlugin = class {
1390
1575
  indicator.style.pointerEvents = "none";
1391
1576
  container.appendChild(indicator);
1392
1577
  ctx.container.appendChild(container);
1578
+ const standardFontsUrl = typeof window !== "undefined" && window.__PDF_STANDARD_FONTS_URL__ || "./standard_fonts/";
1579
+ const cmapsUrl = typeof window !== "undefined" && window.__PDF_CMAPS_URL__ || "./cmaps/";
1393
1580
  const loadingTask = pdfjsLib.getDocument({
1394
- data: new Uint8Array(ctx.buffer),
1395
- cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/cmaps/`,
1581
+ data: new Uint8Array(ctx.buffer.slice(0)),
1582
+ cMapUrl: cmapsUrl,
1396
1583
  cMapPacked: true,
1397
- standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/standard_fonts/`
1584
+ standardFontDataUrl: standardFontsUrl,
1585
+ verbosity: 0
1398
1586
  });
1399
1587
  const pdfDoc = await loadingTask.promise;
1400
1588
  const totalPages = Math.max(1, pdfDoc.numPages);
1401
1589
  let currentPage = 1;
1402
1590
  let zoomScale = 1;
1403
1591
  let rotation = 0;
1404
- let fitMode = "width";
1592
+ let fitMode = "page";
1405
1593
  let currentRenderTask = null;
1406
1594
  const renderPage = async (pageNum) => {
1407
1595
  if (currentRenderTask) {
@@ -1421,15 +1609,15 @@ var PdfPlugin = class {
1421
1609
  const containerWidth = container.clientWidth || 900;
1422
1610
  const containerHeight = container.clientHeight || 700;
1423
1611
  const unscaledVp = page.getViewport({ scale: 1, rotation });
1424
- const availWidth = Math.max(280, containerWidth - 48);
1425
- const availHeight = Math.max(280, containerHeight - 88);
1612
+ const availWidth = Math.max(320, containerWidth - 48);
1613
+ const availHeight = Math.max(550, containerHeight - 88);
1426
1614
  const scaleW = availWidth / unscaledVp.width;
1427
1615
  const scaleH = availHeight / unscaledVp.height;
1428
1616
  let fitScale;
1429
1617
  if (fitMode === "page") {
1430
- fitScale = Math.max(0.5, Math.min(scaleW, scaleH));
1618
+ fitScale = Math.max(0.4, Math.min(scaleW, scaleH));
1431
1619
  } else {
1432
- fitScale = Math.max(0.65, Math.min(1.15, scaleW));
1620
+ fitScale = Math.max(0.65, Math.min(1.25, scaleW));
1433
1621
  }
1434
1622
  const effectiveScale = (fitScale > 0 ? fitScale : 1) * zoomScale;
1435
1623
  const pixelRatio = window.devicePixelRatio || 1;
@@ -1455,7 +1643,7 @@ var PdfPlugin = class {
1455
1643
  currentRenderTask = null;
1456
1644
  }
1457
1645
  };
1458
- await renderPage(1);
1646
+ renderPage(1);
1459
1647
  let resizeTimer = null;
1460
1648
  const resizeObserver = new ResizeObserver(() => {
1461
1649
  if (resizeTimer) clearTimeout(resizeTimer);
@@ -1499,7 +1687,7 @@ var PdfPlugin = class {
1499
1687
  renderPage(currentPage);
1500
1688
  },
1501
1689
  fitToPage: () => {
1502
- fitMode = fitMode === "width" ? "page" : "width";
1690
+ fitMode = fitMode === "page" ? "width" : "page";
1503
1691
  zoomScale = 1;
1504
1692
  rotation = 0;
1505
1693
  renderPage(currentPage);
@@ -1976,6 +2164,14 @@ var DocxPlugin = class {
1976
2164
  type: "button",
1977
2165
  group: "actions",
1978
2166
  execute: () => instance.print?.()
2167
+ },
2168
+ {
2169
+ id: "open-window",
2170
+ icon: "open-window",
2171
+ label: "Open in Separate Full Window",
2172
+ type: "button",
2173
+ group: "actions",
2174
+ execute: () => instance.openInSeparateWindow?.()
1979
2175
  }
1980
2176
  );
1981
2177
  return actions;
@@ -2035,6 +2231,31 @@ var DocxPlugin = class {
2035
2231
  }
2036
2232
  if (wrapper.children.length > 0 && (wrapper.textContent?.trim().length ?? 0) > 0) {
2037
2233
  renderedSuccessfully = true;
2234
+ try {
2235
+ const unzipped = unzipSync(new Uint8Array(ctx.buffer));
2236
+ const chartKeys = Object.keys(unzipped).filter((k) => k.replace(/^[./\\]+/, "").toLowerCase().startsWith("word/charts/chart") && k.endsWith(".xml")).sort();
2237
+ if (chartKeys.length > 0) {
2238
+ const allDivs = Array.from(wrapper.querySelectorAll("div"));
2239
+ const emptyContainers = allDivs.filter((div) => {
2240
+ const st = div.getAttribute("style") || "";
2241
+ return st.includes("width:") && st.includes("height:") && div.children.length === 0 && (div.textContent?.trim().length ?? 0) === 0;
2242
+ });
2243
+ chartKeys.forEach((cKey, idx) => {
2244
+ const target = emptyContainers[idx];
2245
+ if (target) {
2246
+ const xmlStr = strFromU8(unzipped[cKey]);
2247
+ const svg = this.parseAndRenderChartSvg(xmlStr);
2248
+ if (svg) {
2249
+ target.innerHTML = svg;
2250
+ target.style.display = "block";
2251
+ target.style.margin = "12px auto";
2252
+ }
2253
+ }
2254
+ });
2255
+ }
2256
+ } catch (chartErr) {
2257
+ console.warn("[DocxPlugin] Non-critical error rendering DrawingML charts:", chartErr);
2258
+ }
2038
2259
  }
2039
2260
  } catch (err) {
2040
2261
  console.warn("[DocxPlugin] docx-preview failed, triggering native fallback:", err);
@@ -2066,64 +2287,74 @@ var DocxPlugin = class {
2066
2287
  }
2067
2288
  let sections = Array.from(wrapper.querySelectorAll("section.docx"));
2068
2289
  const cards = Array.from(wrapper.querySelectorAll(".fp-docx-page-card"));
2069
- if (sections.length === 1 && cards.length === 0) {
2070
- const singleSec = sections[0];
2071
- const contentContainer = singleSec.querySelector("article") || singleSec;
2072
- const children = Array.from(contentContainer.children);
2073
- const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2074
- const secH = singleSec.offsetHeight || singleSec.scrollHeight;
2075
- if (secH > pageH * 1.25 && children.length > 1) {
2076
- const childHeights = children.map((c) => {
2077
- const rectH = c.getBoundingClientRect().height;
2078
- const offH = c.offsetHeight;
2079
- const textLen = c.textContent?.trim().length || 0;
2080
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2081
- return Math.max(rectH, offH, estH);
2082
- });
2083
- const parent = singleSec.parentElement || wrapper;
2084
- const newSections = [singleSec];
2085
- const headerEl = singleSec.querySelector("header");
2086
- const footerEl = singleSec.querySelector("footer");
2087
- contentContainer.innerHTML = "";
2088
- singleSec.style.minHeight = `${pageH}px`;
2089
- singleSec.style.boxSizing = "border-box";
2090
- let curContent = contentContainer;
2091
- let curH = 0;
2092
- const maxH = pageH - 140;
2093
- for (let i = 0; i < children.length; i++) {
2094
- const child = children[i];
2095
- const chH = childHeights[i];
2096
- curContent.appendChild(child);
2097
- curH += chH;
2098
- if (curH >= maxH && i < children.length - 1) {
2099
- const nextSec = document.createElement("section");
2100
- nextSec.className = singleSec.className;
2101
- nextSec.style.cssText = singleSec.style.cssText;
2102
- nextSec.style.minHeight = `${pageH}px`;
2103
- nextSec.style.boxSizing = "border-box";
2104
- nextSec.style.backgroundColor = "#ffffff";
2105
- nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2106
- nextSec.style.borderRadius = "4px";
2107
- nextSec.style.marginBottom = "24px";
2108
- if (headerEl) {
2109
- nextSec.appendChild(headerEl.cloneNode(true));
2110
- }
2111
- const nextArticle = document.createElement("article");
2112
- if (contentContainer.tagName.toLowerCase() === "article") {
2113
- nextArticle.style.cssText = contentContainer.style.cssText;
2114
- }
2115
- nextSec.appendChild(nextArticle);
2116
- if (footerEl) {
2117
- nextSec.appendChild(footerEl.cloneNode(true));
2290
+ if (sections.length > 0 && cards.length === 0) {
2291
+ const finalSections = [];
2292
+ for (const singleSec of sections) {
2293
+ const contentContainer = singleSec.querySelector("article") || singleSec;
2294
+ const children = Array.from(contentContainer.children);
2295
+ const pageH = singleSec.offsetHeight > 1300 ? 1122 : Math.max(1056, singleSec.offsetHeight);
2296
+ const secH = singleSec.scrollHeight || singleSec.offsetHeight;
2297
+ if (secH > pageH * 1.25 && children.length > 1) {
2298
+ const childHeights = children.map((c) => {
2299
+ const rectH = c.getBoundingClientRect().height;
2300
+ const offH = c.offsetHeight;
2301
+ const textLen = c.textContent?.trim().length || 0;
2302
+ const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
2303
+ return Math.max(rectH, offH, estH);
2304
+ });
2305
+ const parent = singleSec.parentElement || wrapper;
2306
+ const headerEl = singleSec.querySelector("header");
2307
+ const footerEl = singleSec.querySelector("footer");
2308
+ contentContainer.innerHTML = "";
2309
+ singleSec.style.minHeight = `${pageH}px`;
2310
+ singleSec.style.boxSizing = "border-box";
2311
+ let curContent = contentContainer;
2312
+ let curSec = singleSec;
2313
+ let curH = 0;
2314
+ const maxH = pageH - 140;
2315
+ finalSections.push(singleSec);
2316
+ for (let i = 0; i < children.length; i++) {
2317
+ const child = children[i];
2318
+ const chH = childHeights[i];
2319
+ curContent.appendChild(child);
2320
+ curH += chH;
2321
+ if (curH >= maxH && i < children.length - 1) {
2322
+ const nextSec = document.createElement("section");
2323
+ nextSec.className = singleSec.className;
2324
+ nextSec.style.cssText = singleSec.style.cssText;
2325
+ nextSec.style.minHeight = `${pageH}px`;
2326
+ nextSec.style.boxSizing = "border-box";
2327
+ nextSec.style.backgroundColor = "#ffffff";
2328
+ nextSec.style.boxShadow = "0 4px 24px rgba(0, 0, 0, 0.08)";
2329
+ nextSec.style.borderRadius = "4px";
2330
+ nextSec.style.marginBottom = "24px";
2331
+ if (headerEl) {
2332
+ nextSec.appendChild(headerEl.cloneNode(true));
2333
+ }
2334
+ const nextArticle = document.createElement("article");
2335
+ if (contentContainer.tagName.toLowerCase() === "article") {
2336
+ nextArticle.style.cssText = contentContainer.style.cssText;
2337
+ }
2338
+ nextSec.appendChild(nextArticle);
2339
+ if (footerEl) {
2340
+ nextSec.appendChild(footerEl.cloneNode(true));
2341
+ }
2342
+ if (curSec.nextSibling) {
2343
+ parent.insertBefore(nextSec, curSec.nextSibling);
2344
+ } else {
2345
+ parent.appendChild(nextSec);
2346
+ }
2347
+ finalSections.push(nextSec);
2348
+ curSec = nextSec;
2349
+ curContent = nextArticle;
2350
+ curH = 0;
2118
2351
  }
2119
- parent.appendChild(nextSec);
2120
- newSections.push(nextSec);
2121
- curContent = nextArticle;
2122
- curH = 0;
2123
2352
  }
2353
+ } else {
2354
+ finalSections.push(singleSec);
2124
2355
  }
2125
- sections = newSections;
2126
2356
  }
2357
+ sections = finalSections;
2127
2358
  }
2128
2359
  const pageElements = sections.length > 0 ? sections : cards;
2129
2360
  const totalPages = Math.max(1, pageElements.length);
@@ -2515,6 +2746,88 @@ var DocxPlugin = class {
2515
2746
  }
2516
2747
  return result;
2517
2748
  }
2749
+ parseAndRenderChartSvg(xmlStr, width = 500, height = 260) {
2750
+ const catMatches = [...xmlStr.matchAll(/<c:cat>[\s\S]*?<c:strCache>([\s\S]*?)<\/c:strCache>/g)];
2751
+ let categories = [];
2752
+ if (catMatches.length > 0) {
2753
+ categories = [...catMatches[0][1].matchAll(/<c:v>([^<]+)<\/c:v>/g)].map((m) => m[1]);
2754
+ }
2755
+ if (categories.length === 0) {
2756
+ categories = ["Category 1", "Category 2", "Category 3", "Category 4"];
2757
+ }
2758
+ const defaultColors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021", "#83caff"];
2759
+ const sers = [...xmlStr.matchAll(/<c:ser>([\s\S]*?)<\/c:ser>/g)];
2760
+ const series = [];
2761
+ sers.forEach((s, sIdx) => {
2762
+ const titleMatch = s[1].match(/<c:tx>[\s\S]*?<c:v>([^<]+)<\/c:v>/);
2763
+ const title = titleMatch ? titleMatch[1] : `Series ${sIdx + 1}`;
2764
+ const clrMatch = s[1].match(/<a:srgbClr\s+val="([^"]+)"/);
2765
+ const color = clrMatch ? "#" + clrMatch[1] : defaultColors[sIdx % defaultColors.length];
2766
+ const valMatch = s[1].match(/<c:val>[\s\S]*?<c:numCache>([\s\S]*?)<\/c:numCache>/);
2767
+ let values = [];
2768
+ if (valMatch) {
2769
+ 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);
2770
+ }
2771
+ series.push({ title, color, values });
2772
+ });
2773
+ if (series.length === 0) return "";
2774
+ let maxVal = 10;
2775
+ series.forEach((s) => s.values.forEach((v) => {
2776
+ if (v > maxVal) maxVal = v;
2777
+ }));
2778
+ maxVal = Math.ceil(maxVal * 1.15);
2779
+ if (maxVal % 2 !== 0) maxVal++;
2780
+ const padLeft = 45;
2781
+ const padBottom = 55;
2782
+ const padTop = 20;
2783
+ const padRight = 20;
2784
+ const plotW = width - padLeft - padRight;
2785
+ const plotH = height - padTop - padBottom;
2786
+ const yTicks = 5;
2787
+ let gridLines = "";
2788
+ for (let i = 0; i <= yTicks; i++) {
2789
+ const val = maxVal / yTicks * i;
2790
+ const y = padTop + plotH - val / maxVal * plotH;
2791
+ gridLines += `<line x1="${padLeft}" y1="${y}" x2="${padLeft + plotW}" y2="${y}" stroke="#e2e8f0" stroke-width="1" />`;
2792
+ 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>`;
2793
+ }
2794
+ const numCats = categories.length;
2795
+ const numSers = series.length;
2796
+ const groupW = plotW / numCats;
2797
+ const barW = Math.max(8, Math.min(28, groupW * 0.7 / numSers));
2798
+ const groupPad = (groupW - barW * numSers) / 2;
2799
+ let bars = "";
2800
+ let catLabels = "";
2801
+ for (let c = 0; c < numCats; c++) {
2802
+ const catX = padLeft + c * groupW;
2803
+ 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>`;
2804
+ for (let s = 0; s < numSers; s++) {
2805
+ const val = series[s].values[c] ?? 0;
2806
+ const bH = Math.max(0, val / maxVal * plotH);
2807
+ const bX = catX + groupPad + s * barW;
2808
+ const bY = padTop + plotH - bH;
2809
+ bars += `<rect x="${bX}" y="${bY}" width="${barW - 2}" height="${bH}" fill="${series[s].color}" rx="1" />`;
2810
+ }
2811
+ }
2812
+ let legend = "";
2813
+ const legY = height - 12;
2814
+ let legX = padLeft + (plotW - numSers * 100) / 2;
2815
+ series.forEach((s) => {
2816
+ legend += `<rect x="${legX}" y="${legY - 9}" width="10" height="10" fill="${s.color}" rx="2" />`;
2817
+ legend += `<text x="${legX + 15}" y="${legY}" font-size="11" fill="#475569" font-family="Calibri, sans-serif">${s.title}</text>`;
2818
+ legX += 95;
2819
+ });
2820
+ return `
2821
+ <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">
2822
+ ${gridLines}
2823
+ <line x1="${padLeft}" y1="${padTop + plotH}" x2="${padLeft + plotW}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2824
+ <line x1="${padLeft}" y1="${padTop}" x2="${padLeft}" y2="${padTop + plotH}" stroke="#94a3b8" stroke-width="1.5" />
2825
+ ${bars}
2826
+ ${catLabels}
2827
+ ${legend}
2828
+ </svg>
2829
+ `.trim();
2830
+ }
2518
2831
  };
2519
2832
  function docxPlugin() {
2520
2833
  return new DocxPlugin();
@@ -3081,6 +3394,16 @@ var CodePlugin = class {
3081
3394
  execute: () => {
3082
3395
  instance.print?.();
3083
3396
  }
3397
+ },
3398
+ {
3399
+ id: "open-window",
3400
+ icon: "open-window",
3401
+ label: "Open in Separate Full Window",
3402
+ type: "button",
3403
+ group: "actions",
3404
+ execute: () => {
3405
+ instance.openInSeparateWindow?.();
3406
+ }
3084
3407
  }
3085
3408
  );
3086
3409
  return actions;
@@ -4249,6 +4572,14 @@ var RtfPlugin = class {
4249
4572
  type: "button",
4250
4573
  group: "actions",
4251
4574
  execute: () => instance.print?.()
4575
+ },
4576
+ {
4577
+ id: "open-window",
4578
+ icon: "open-window",
4579
+ label: "Open in Separate Full Window",
4580
+ type: "button",
4581
+ group: "actions",
4582
+ execute: () => instance.openInSeparateWindow?.()
4252
4583
  }
4253
4584
  );
4254
4585
  return actions;
@@ -4301,59 +4632,54 @@ var RtfPlugin = class {
4301
4632
  }
4302
4633
  const doc = new RTFJS.Document(ctx.buffer, {});
4303
4634
  const htmlElements = await doc.render();
4304
- if (htmlElements.length === 1) {
4305
- const singleEl = htmlElements[0];
4306
- wrapper.appendChild(singleEl);
4307
- const children = Array.from(singleEl.children);
4308
- const secH = singleEl.offsetHeight || singleEl.scrollHeight;
4309
- if (secH > 1300 && children.length > 1) {
4310
- const childHeights = children.map((c) => {
4311
- const rectH = c.getBoundingClientRect().height;
4312
- const offH = c.offsetHeight;
4313
- const textLen = c.textContent?.trim().length || 0;
4314
- const estH = Math.max(24, Math.ceil(textLen / 80) * 22 + 16);
4315
- return Math.max(rectH, offH, estH);
4316
- });
4317
- wrapper.innerHTML = "";
4318
- const createRtfCard = () => {
4319
- const card = document.createElement("div");
4320
- card.className = "fp-rtf-page-card";
4321
- card.style.backgroundColor = "#ffffff";
4322
- card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4323
- card.style.borderRadius = "4px";
4324
- card.style.padding = "72px 56px";
4325
- card.style.width = "816px";
4326
- card.style.minHeight = "1056px";
4327
- card.style.boxSizing = "border-box";
4328
- card.style.marginBottom = "24px";
4329
- return card;
4330
- };
4331
- let curCard = createRtfCard();
4332
- wrapper.appendChild(curCard);
4333
- pageElements = [curCard];
4334
- let curH = 0;
4335
- const maxH = 920;
4336
- for (let i = 0; i < children.length; i++) {
4337
- const child = children[i];
4338
- const chH = childHeights[i];
4339
- curCard.appendChild(child);
4340
- curH += chH;
4341
- if (curH >= maxH && i < children.length - 1) {
4342
- curCard = createRtfCard();
4343
- wrapper.appendChild(curCard);
4344
- pageElements.push(curCard);
4345
- curH = 0;
4346
- }
4347
- }
4635
+ const contentNodes = [];
4636
+ for (const item of htmlElements) {
4637
+ if (item.children && item.children.length > 0 && !item.tagName.toLowerCase().startsWith("table")) {
4638
+ contentNodes.push(...Array.from(item.children));
4348
4639
  } else {
4349
- pageElements = [singleEl];
4640
+ contentNodes.push(item);
4350
4641
  }
4351
- } else {
4352
- pageElements = htmlElements;
4353
- for (let i = 0; i < htmlElements.length; i++) {
4354
- const el = htmlElements[i];
4355
- el.style.display = i === 0 ? "block" : "none";
4356
- wrapper.appendChild(el);
4642
+ }
4643
+ wrapper.innerHTML = "";
4644
+ contentNodes.forEach((node) => wrapper.appendChild(node));
4645
+ const childHeights = contentNodes.map((c) => {
4646
+ const rectH = c.getBoundingClientRect ? c.getBoundingClientRect().height : 0;
4647
+ const offH = c.offsetHeight || 0;
4648
+ const textLen = c.textContent?.trim().length || 0;
4649
+ const estH = Math.max(24, Math.ceil(textLen / 75) * 22 + 14);
4650
+ return Math.max(rectH, offH, estH);
4651
+ });
4652
+ wrapper.innerHTML = "";
4653
+ const createRtfCard = () => {
4654
+ const card = document.createElement("div");
4655
+ card.className = "fp-rtf-page-card";
4656
+ card.style.backgroundColor = "#ffffff";
4657
+ card.style.boxShadow = "0 4px 24px rgba(0,0,0,0.08)";
4658
+ card.style.borderRadius = "4px";
4659
+ card.style.padding = "72px 56px";
4660
+ card.style.width = "816px";
4661
+ card.style.minHeight = "1056px";
4662
+ card.style.boxSizing = "border-box";
4663
+ card.style.marginBottom = "24px";
4664
+ card.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
4665
+ card.style.lineHeight = "1.6";
4666
+ return card;
4667
+ };
4668
+ let curCard = createRtfCard();
4669
+ wrapper.appendChild(curCard);
4670
+ pageElements = [curCard];
4671
+ let curH = 0;
4672
+ const maxH = 912;
4673
+ for (let i = 0; i < contentNodes.length; i++) {
4674
+ const child = contentNodes[i];
4675
+ const chH = childHeights[i];
4676
+ curCard.appendChild(child);
4677
+ curH += chH;
4678
+ if (curH >= maxH && i < contentNodes.length - 1) {
4679
+ curCard = createRtfCard();
4680
+ wrapper.appendChild(curCard);
4681
+ pageElements.push(curCard);
4682
+ curH = 0;
4357
4683
  }
4358
4684
  }
4359
4685
  } catch (err) {
@@ -4722,6 +5048,14 @@ var OpenDocumentPlugin = class {
4722
5048
  type: "button",
4723
5049
  group: "actions",
4724
5050
  execute: () => instance.print?.()
5051
+ },
5052
+ {
5053
+ id: "open-window",
5054
+ icon: "open-window",
5055
+ label: "Open in Separate Full Window",
5056
+ type: "button",
5057
+ group: "actions",
5058
+ execute: () => instance.openInSeparateWindow?.()
4725
5059
  }
4726
5060
  );
4727
5061
  return actions;
@@ -5310,6 +5644,14 @@ var DocPlugin = class {
5310
5644
  type: "button",
5311
5645
  group: "actions",
5312
5646
  execute: () => instance.print?.()
5647
+ },
5648
+ {
5649
+ id: "open-window",
5650
+ icon: "open-window",
5651
+ label: "Open in Separate Full Window",
5652
+ type: "button",
5653
+ group: "actions",
5654
+ execute: () => instance.openInSeparateWindow?.()
5313
5655
  }
5314
5656
  );
5315
5657
  return actions;
@@ -5329,8 +5671,17 @@ var DocPlugin = class {
5329
5671
  let scale = 1;
5330
5672
  let extractedRawText = "";
5331
5673
  let isFallback = false;
5674
+ let chartSvg = "";
5332
5675
  try {
5333
5676
  const cfbf = new CfbfReader(ctx.buffer);
5677
+ try {
5678
+ const pkg = cfbf.readStream("package_stream");
5679
+ if (pkg && pkg.length > 100) {
5680
+ chartSvg = this.parseOdfChartToSvg(pkg);
5681
+ }
5682
+ } catch (chartErr) {
5683
+ console.warn("[DocPlugin] Chart stream parsing info:", chartErr);
5684
+ }
5334
5685
  const wordDocStream = cfbf.readStream("WordDocument");
5335
5686
  if (!wordDocStream || wordDocStream.length < 512) {
5336
5687
  throw new Error("WordDocument stream not found or invalid in CFBF archive");
@@ -5348,7 +5699,7 @@ var DocPlugin = class {
5348
5699
  extractedRawText = fallback;
5349
5700
  isFallback = true;
5350
5701
  }
5351
- const rawPages = this.splitIntoPages(extractedRawText);
5702
+ const rawPages = this.splitIntoPages(extractedRawText, chartSvg);
5352
5703
  const totalPages = Math.max(1, rawPages.length);
5353
5704
  let currentPage = 1;
5354
5705
  const pageCards = [];
@@ -5590,7 +5941,7 @@ var DocPlugin = class {
5590
5941
  for (const run of [...ansiRuns, ...utf16Runs]) {
5591
5942
  const trimmed = run.trim();
5592
5943
  if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
5593
- 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)) {
5944
+ 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)) {
5594
5945
  seen.add(trimmed);
5595
5946
  candidateLines.push(trimmed);
5596
5947
  }
@@ -5601,12 +5952,138 @@ var DocPlugin = class {
5601
5952
  heuristicTextExtraction(buffer) {
5602
5953
  return this.extractStringsFromBytes(new Uint8Array(buffer));
5603
5954
  }
5604
- splitIntoPages(text) {
5955
+ /**
5956
+ * Parses an embedded OpenDocument Chart package into a vector SVG bar/column chart
5957
+ */
5958
+ parseOdfChartToSvg(zipBytes) {
5959
+ try {
5960
+ const unzipped = fflate.unzipSync(zipBytes);
5961
+ const contentXml = unzipped["content.xml"] ? new TextDecoder("utf-8").decode(unzipped["content.xml"]) : "";
5962
+ if (!contentXml) return "";
5963
+ const rowsMatch = contentXml.match(/<table:table-row[\s\S]*?<\/table:table-row>/g) || [];
5964
+ if (rowsMatch.length < 2) return "";
5965
+ const headers = [];
5966
+ const firstRow = rowsMatch[0];
5967
+ const headerCells = firstRow ? firstRow.match(/<text:p>([^<]+)<\/text:p>/g) || [] : [];
5968
+ for (const h2 of headerCells) {
5969
+ headers.push(h2.replace(/<\/?text:p>/g, "").trim());
5970
+ }
5971
+ const categories = [];
5972
+ const seriesValues = headers.map(() => []);
5973
+ for (let r = 1; r < rowsMatch.length; r++) {
5974
+ const rowStr = rowsMatch[r];
5975
+ if (!rowStr) continue;
5976
+ const cells = rowStr.match(/<table:table-cell[\s\S]*?<\/table:table-cell>/g) || [];
5977
+ if (cells.length > 0 && cells[0]) {
5978
+ const catMatch = cells[0].match(/<text:p>([^<]+)<\/text:p>/);
5979
+ categories.push(catMatch ? catMatch[1] : "Row " + r);
5980
+ for (let c = 1; c < cells.length && c - 1 < headers.length; c++) {
5981
+ const cellStr = cells[c];
5982
+ if (!cellStr) continue;
5983
+ const valMatch = cellStr.match(/office:value="([0-9.]+)"/) || cellStr.match(/<text:p>([0-9.]+)<\/text:p>/);
5984
+ const series = seriesValues[c - 1];
5985
+ if (series) {
5986
+ series.push(valMatch ? parseFloat(valMatch[1]) : 0);
5987
+ }
5988
+ }
5989
+ }
5990
+ }
5991
+ const colors = ["#004586", "#ff420e", "#ffd320", "#579d1c", "#7e0021"];
5992
+ const colorMatches = contentXml.matchAll(/draw:fill-color="(#[0-9a-fA-F]{6})"/g);
5993
+ let cIdx = 0;
5994
+ for (const cm of colorMatches) {
5995
+ if (cIdx < colors.length) colors[cIdx] = cm[1];
5996
+ cIdx++;
5997
+ }
5998
+ let maxVal = 10;
5999
+ for (const s of seriesValues) {
6000
+ for (const v of s) {
6001
+ if (v > maxVal) maxVal = v;
6002
+ }
6003
+ }
6004
+ maxVal = Math.ceil(maxVal * 1.15);
6005
+ const width = 560;
6006
+ const height = 280;
6007
+ const padLeft = 45;
6008
+ const padRight = 100;
6009
+ const padTop = 20;
6010
+ const padBottom = 40;
6011
+ const chartW = width - padLeft - padRight;
6012
+ const chartH = height - padTop - padBottom;
6013
+ 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);">`;
6014
+ for (let step = 0; step <= 4; step++) {
6015
+ const yVal = (maxVal / 4 * step).toFixed(1);
6016
+ const yPos = padTop + chartH - step / 4 * chartH;
6017
+ svg += `<line x1="${padLeft}" y1="${yPos}" x2="${padLeft + chartW}" y2="${yPos}" stroke="#e2e8f0" stroke-dasharray="2,2" />`;
6018
+ svg += `<text x="${padLeft - 8}" y="${yPos + 4}" font-size="11" fill="#64748b" text-anchor="end">${yVal}</text>`;
6019
+ }
6020
+ const numCats = categories.length;
6021
+ const numSeries = headers.length;
6022
+ const groupW = chartW / numCats;
6023
+ const barW = Math.max(8, groupW * 0.7 / numSeries);
6024
+ const groupPad = (groupW - barW * numSeries) / 2;
6025
+ for (let catIdx = 0; catIdx < numCats; catIdx++) {
6026
+ const groupX = padLeft + catIdx * groupW + groupPad;
6027
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6028
+ const val = seriesValues[sIdx][catIdx] || 0;
6029
+ const barH = val / maxVal * chartH;
6030
+ const barX = groupX + sIdx * barW;
6031
+ const barY = padTop + chartH - barH;
6032
+ const col = colors[sIdx % colors.length];
6033
+ svg += `<rect x="${barX}" y="${barY}" width="${barW - 2}" height="${barH}" fill="${col}" rx="2"><title>${headers[sIdx]}: ${val}</title></rect>`;
6034
+ }
6035
+ const catX = padLeft + catIdx * groupW + groupW / 2;
6036
+ svg += `<text x="${catX}" y="${padTop + chartH + 18}" font-size="11" fill="#475569" text-anchor="middle">${categories[catIdx]}</text>`;
6037
+ }
6038
+ let legendY = padTop + 20;
6039
+ for (let sIdx = 0; sIdx < numSeries; sIdx++) {
6040
+ const col = colors[sIdx % colors.length];
6041
+ svg += `<rect x="${padLeft + chartW + 15}" y="${legendY}" width="12" height="12" fill="${col}" rx="2" />`;
6042
+ svg += `<text x="${padLeft + chartW + 32}" y="${legendY + 10}" font-size="11" fill="#334155">${headers[sIdx]}</text>`;
6043
+ legendY += 20;
6044
+ }
6045
+ svg += "</svg>";
6046
+ return svg;
6047
+ } catch (e) {
6048
+ console.warn("[DocPlugin] Error generating chart SVG:", e);
6049
+ return "";
6050
+ }
6051
+ }
6052
+ cleanWordDocFields(text, chartSvg = "") {
6053
+ if (!text) return "";
6054
+ let cleaned = text.replace(
6055
+ /\x13\s*EMBED\b[\s\S]*?\x15/gi,
6056
+ () => chartSvg ? `
6057
+
6058
+ ${chartSvg}
6059
+
6060
+ ` : ""
6061
+ );
6062
+ cleaned = cleaned.replace(
6063
+ /\x13\s*HYPERLINK\s*"?([^"\x14]+)"?\s*\x14([\s\S]*?)\x15/gi,
6064
+ (_match, url, label) => {
6065
+ const cleanUrl = url.trim();
6066
+ const cleanLabel = label.trim() || cleanUrl;
6067
+ return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #2563eb; text-decoration: underline;">${cleanLabel}</a>`;
6068
+ }
6069
+ );
6070
+ cleaned = cleaned.replace(/\x13[^\x14\x15]*\x14([^\x15]*)\x15/g, (_m, res) => {
6071
+ if (/[\x00-\x1F]/.test(res)) return "";
6072
+ return res.trim();
6073
+ });
6074
+ cleaned = cleaned.replace(/\x13[^\x15]*\x15/g, "");
6075
+ cleaned = cleaned.replace(/[\x13\x14\x15]/g, "");
6076
+ cleaned = cleaned.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, "");
6077
+ cleaned = cleaned.replace(/EMBED\s+LibreOffice\.ChartDocument\.[0-9]+/gi, chartSvg || "");
6078
+ return cleaned;
6079
+ }
6080
+ splitIntoPages(text, chartSvg = "") {
5605
6081
  if (!text) return [""];
5606
- const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
6082
+ const cleanedText = this.cleanWordDocFields(text, chartSvg);
6083
+ const normalized = cleanedText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x07\n/g, "\n").replace(/\x07/g, " ");
5607
6084
  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);
5608
6085
  if (explicitParts.length === 0) explicitParts.push(normalized);
5609
- const maxLinesPerPage = 32;
6086
+ const maxLinesPerPage = 34;
5610
6087
  const charsPerLine = 80;
5611
6088
  const finalPages = [];
5612
6089
  for (const part of explicitParts) {
@@ -5614,7 +6091,8 @@ var DocPlugin = class {
5614
6091
  let currentLines = [];
5615
6092
  let count = 0;
5616
6093
  for (const line of lines) {
5617
- const vLines = Math.max(1, Math.ceil((line.length || 1) / charsPerLine));
6094
+ const isSvg = line.includes("<svg");
6095
+ const vLines = isSvg ? 12 : Math.max(1, Math.ceil((line.replace(/<[^>]+>/g, "").length || 1) / charsPerLine));
5618
6096
  if (count + vLines > maxLinesPerPage && currentLines.length > 0) {
5619
6097
  finalPages.push(currentLines.join("\n"));
5620
6098
  currentLines = [];
@@ -5654,9 +6132,44 @@ var DocPlugin = class {
5654
6132
  tableLines = [];
5655
6133
  }
5656
6134
  };
6135
+ const sanitizeOptions = {
6136
+ ADD_TAGS: ["a", "svg", "g", "path", "line", "rect", "circle", "text", "title"],
6137
+ ADD_ATTR: [
6138
+ "href",
6139
+ "target",
6140
+ "rel",
6141
+ "style",
6142
+ "viewBox",
6143
+ "width",
6144
+ "height",
6145
+ "x",
6146
+ "y",
6147
+ "x1",
6148
+ "y1",
6149
+ "x2",
6150
+ "y2",
6151
+ "fill",
6152
+ "stroke",
6153
+ "stroke-width",
6154
+ "stroke-dasharray",
6155
+ "rx",
6156
+ "font-size",
6157
+ "text-anchor"
6158
+ ]
6159
+ };
5657
6160
  let i = 0;
5658
6161
  while (i < lines.length) {
5659
6162
  let line = lines[i];
6163
+ if (line.includes("<svg")) {
6164
+ if (inList) {
6165
+ html += "</ul>";
6166
+ inList = false;
6167
+ }
6168
+ flushTable();
6169
+ html += line;
6170
+ i++;
6171
+ continue;
6172
+ }
5660
6173
  let tabCount = (line.match(/\t/g) || []).length;
5661
6174
  if (tabCount > 0) {
5662
6175
  let j = i;
@@ -5695,20 +6208,20 @@ var DocPlugin = class {
5695
6208
  html += "</ul>";
5696
6209
  inList = false;
5697
6210
  }
5698
- 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>`;
6211
+ 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>`;
5699
6212
  } else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
5700
6213
  if (!inList) {
5701
6214
  html += '<ul style="margin: 8px 0; padding-left: 24px;">';
5702
6215
  inList = true;
5703
6216
  }
5704
6217
  const bulletText = line.replace(/^[•\-\*]\s*/, "");
5705
- html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6.sanitize(bulletText)}</li>`;
6218
+ html += `<li style="margin: 4px 0; line-height: 1.15; font-size: 12pt;">${DOMPurify6.sanitize(bulletText, sanitizeOptions)}</li>`;
5706
6219
  } else {
5707
6220
  if (inList) {
5708
6221
  html += "</ul>";
5709
6222
  inList = false;
5710
6223
  }
5711
- html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6.sanitize(line)}</p>`;
6224
+ html += `<p style="line-height: 1.15; margin: 0; font-size: 12pt; text-align: justify;">${DOMPurify6.sanitize(line, sanitizeOptions)}</p>`;
5712
6225
  }
5713
6226
  i++;
5714
6227
  }