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

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