@files-preview-app/preview-file 1.1.5 → 1.2.0

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
@@ -1,15 +1,16 @@
1
1
  import { memo, forwardRef, useRef, useImperativeHandle, useEffect } from 'react';
2
- import DOMPurify from 'dompurify';
2
+ import DOMPurify5 from 'dompurify';
3
3
  import * as docx from 'docx-preview';
4
- import ExcelJS from 'exceljs';
4
+ import * as XLSX from 'xlsx';
5
5
  import hljs from 'highlight.js';
6
- import { unzip } from 'fflate';
6
+ import { unzipSync, strFromU8, unzip } from 'fflate';
7
7
  import { marked } from 'marked';
8
8
  import { PptxRenderer } from 'pptx-browser';
9
9
  import * as THREE from 'three';
10
10
  import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
11
11
  import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
12
12
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
13
+ import { RTFJS } from 'rtf.js';
13
14
  import { jsx } from 'react/jsx-runtime';
14
15
 
15
16
  // src/react.tsx
@@ -74,7 +75,9 @@ var MAGIC_NUMBERS = [
74
75
  { bytes: [37, 80, 68, 70], mime: "application/pdf" },
75
76
  // %PDF
76
77
  { bytes: [80, 75, 3, 4], mime: "application/zip" },
77
- // PK.. (ZIP/DOCX/XLSX/PPTX)
78
+ // PK.. (ZIP/DOCX/XLSX/PPTX/ODT/ODS/ODP)
79
+ { bytes: [208, 207, 17, 224, 161, 177, 26, 225], mime: "application/x-cfbf" },
80
+ // CFBF/OLE2 (DOC/XLS/PPT)
78
81
  { bytes: [137, 80, 78, 71, 13, 10, 26, 10], mime: "image/png" },
79
82
  // PNG
80
83
  { bytes: [255, 216, 255], mime: "image/jpeg" },
@@ -109,14 +112,40 @@ var MAGIC_NUMBERS = [
109
112
  // RTF
110
113
  ];
111
114
  var EXTENSION_MIME_MAP = {
112
- // Documents
115
+ // Documents — Modern OOXML
113
116
  ".pdf": "application/pdf",
114
117
  ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
118
+ ".docm": "application/vnd.ms-word.document.macroEnabled.12",
119
+ ".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
120
+ ".dotm": "application/vnd.ms-word.template.macroEnabled.12",
115
121
  ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
122
+ ".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
123
+ ".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
124
+ ".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
125
+ ".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
116
126
  ".xls": "application/vnd.ms-excel",
117
127
  ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
128
+ ".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
129
+ ".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
130
+ ".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
131
+ ".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
132
+ ".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
118
133
  ".csv": "text/csv",
119
134
  ".tsv": "text/tab-separated-values",
135
+ // Documents — Legacy Binary (CFBF/OLE2)
136
+ ".doc": "application/msword",
137
+ ".dot": "application/msword",
138
+ ".ppt": "application/vnd.ms-powerpoint",
139
+ ".pps": "application/vnd.ms-powerpoint",
140
+ ".pot": "application/vnd.ms-powerpoint",
141
+ // Documents — OpenDocument (ODF)
142
+ ".odt": "application/vnd.oasis.opendocument.text",
143
+ ".ods": "application/vnd.oasis.opendocument.spreadsheet",
144
+ ".odp": "application/vnd.oasis.opendocument.presentation",
145
+ ".odg": "application/vnd.oasis.opendocument.graphics",
146
+ ".odf": "application/vnd.oasis.opendocument.formula",
147
+ // Documents — RTF
148
+ ".rtf": "text/rtf",
120
149
  // Images
121
150
  ".png": "image/png",
122
151
  ".jpg": "image/jpeg",
@@ -210,7 +239,7 @@ function detectMagicBytes(buffer) {
210
239
  }
211
240
  function detectOoxmlType(buffer) {
212
241
  const text = new TextDecoder("ascii", { fatal: false }).decode(
213
- new Uint8Array(buffer.slice(0, 4e3))
242
+ new Uint8Array(buffer.slice(0, 8e3))
214
243
  );
215
244
  if (text.includes("word/")) {
216
245
  return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
@@ -221,6 +250,21 @@ function detectOoxmlType(buffer) {
221
250
  if (text.includes("ppt/")) {
222
251
  return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
223
252
  }
253
+ if (text.includes("application/vnd.oasis.opendocument.text")) {
254
+ return "application/vnd.oasis.opendocument.text";
255
+ }
256
+ if (text.includes("application/vnd.oasis.opendocument.spreadsheet")) {
257
+ return "application/vnd.oasis.opendocument.spreadsheet";
258
+ }
259
+ if (text.includes("application/vnd.oasis.opendocument.presentation")) {
260
+ return "application/vnd.oasis.opendocument.presentation";
261
+ }
262
+ if (text.includes("application/vnd.oasis.opendocument.graphics")) {
263
+ return "application/vnd.oasis.opendocument.graphics";
264
+ }
265
+ if (text.includes("application/vnd.oasis.opendocument.formula")) {
266
+ return "application/vnd.oasis.opendocument.formula";
267
+ }
224
268
  return "application/zip";
225
269
  }
226
270
  function extractExtension(nameOrUrl) {
@@ -297,7 +341,7 @@ async function sourceToArrayBuffer(source, signal) {
297
341
  return { buffer, metadata };
298
342
  }
299
343
  function sanitizeSVG(svg) {
300
- return DOMPurify.sanitize(svg, {
344
+ return DOMPurify5.sanitize(svg, {
301
345
  USE_PROFILES: { svg: true, svgFilters: true }
302
346
  });
303
347
  }
@@ -865,6 +909,192 @@ var FilePreviewViewer = class {
865
909
  this.contentEl.appendChild(errorEl);
866
910
  }
867
911
  };
912
+ var CfbfReader = class {
913
+ view;
914
+ u8;
915
+ sectorSize;
916
+ miniSectorSize;
917
+ miniStreamCutoff;
918
+ fat = [];
919
+ miniFat = [];
920
+ entries = /* @__PURE__ */ new Map();
921
+ miniStream = new Uint8Array(0);
922
+ constructor(buffer) {
923
+ this.u8 = new Uint8Array(buffer);
924
+ this.view = new DataView(buffer);
925
+ if (!this.isValid()) {
926
+ throw new Error("Invalid CFBF / OLE2 file header signature");
927
+ }
928
+ const sectorShift = this.view.getUint16(30, true);
929
+ this.sectorSize = 1 << sectorShift;
930
+ const miniSectorShift = this.view.getUint16(32, true);
931
+ this.miniSectorSize = 1 << miniSectorShift;
932
+ this.miniStreamCutoff = this.view.getUint32(56, true) || 4096;
933
+ this.parseFat();
934
+ this.parseDirectory();
935
+ this.parseMiniFat();
936
+ }
937
+ /** Check if the magic 8-byte signature matches [MS-CFB] */
938
+ isValid() {
939
+ if (this.u8.length < 512) return false;
940
+ const sig = [208, 207, 17, 224, 161, 177, 26, 225];
941
+ return sig.every((b, i) => this.u8[i] === b);
942
+ }
943
+ /** Return all directory stream/storage entries found in the file */
944
+ listEntries() {
945
+ return Array.from(this.entries.values());
946
+ }
947
+ /** Check if a stream exists */
948
+ hasStream(name) {
949
+ return this.entries.has(name) || this.findEntryCaseInsensitive(name) !== null;
950
+ }
951
+ /**
952
+ * Read raw bytes of a named stream (e.g. "WordDocument", "PowerPoint Document", "Workbook")
953
+ */
954
+ readStream(name) {
955
+ const entry = this.entries.get(name) || this.findEntryCaseInsensitive(name);
956
+ if (!entry) return null;
957
+ if (entry.size === 0) return new Uint8Array(0);
958
+ if (entry.size < this.miniStreamCutoff && entry.type !== 5) {
959
+ return this.readMiniStream(entry.startSector, entry.size);
960
+ } else {
961
+ return this.readRegularStream(entry.startSector, entry.size);
962
+ }
963
+ }
964
+ findEntryCaseInsensitive(name) {
965
+ const lower = name.toLowerCase();
966
+ for (const [key, val] of this.entries.entries()) {
967
+ if (key.toLowerCase() === lower) return val;
968
+ }
969
+ return null;
970
+ }
971
+ getSectorOffset(sectId) {
972
+ return (sectId + 1) * this.sectorSize;
973
+ }
974
+ parseFat() {
975
+ this.view.getUint32(44, true);
976
+ const difat = [];
977
+ for (let i = 0; i < 109; i++) {
978
+ const sect = this.view.getUint32(76 + i * 4, true);
979
+ if (sect !== 4294967295 && sect !== 4294967294) {
980
+ difat.push(sect);
981
+ }
982
+ }
983
+ let difatSect = this.view.getUint32(68, true);
984
+ const csectDif = this.view.getUint32(72, true);
985
+ const entriesPerSector = this.sectorSize / 4 - 1;
986
+ for (let i = 0; i < csectDif && difatSect < 4294967294; i++) {
987
+ const offset = this.getSectorOffset(difatSect);
988
+ for (let j = 0; j < entriesPerSector; j++) {
989
+ const sect = this.view.getUint32(offset + j * 4, true);
990
+ if (sect !== 4294967295 && sect !== 4294967294) {
991
+ difat.push(sect);
992
+ }
993
+ }
994
+ difatSect = this.view.getUint32(offset + entriesPerSector * 4, true);
995
+ }
996
+ for (const fatSect of difat) {
997
+ if (fatSect >= 4294967294) continue;
998
+ const offset = this.getSectorOffset(fatSect);
999
+ const count = this.sectorSize / 4;
1000
+ for (let j = 0; j < count; j++) {
1001
+ this.fat.push(this.view.getUint32(offset + j * 4, true));
1002
+ }
1003
+ }
1004
+ }
1005
+ parseDirectory() {
1006
+ const dirStartSect = this.view.getUint32(48, true);
1007
+ const dirBytes = this.readRegularStream(dirStartSect);
1008
+ const dirView = new DataView(dirBytes.buffer, dirBytes.byteOffset, dirBytes.byteLength);
1009
+ const entryCount = dirBytes.length / 128;
1010
+ for (let i = 0; i < entryCount; i++) {
1011
+ const offset = i * 128;
1012
+ const type = dirView.getUint8(offset + 66);
1013
+ if (type === 0) continue;
1014
+ const nameLen = dirView.getUint16(offset + 64, true);
1015
+ let name = "";
1016
+ if (nameLen > 2) {
1017
+ const charCount = nameLen / 2 - 1;
1018
+ const chars = [];
1019
+ for (let c = 0; c < charCount; c++) {
1020
+ const charCode = dirView.getUint16(offset + c * 2, true);
1021
+ chars.push(String.fromCharCode(charCode));
1022
+ }
1023
+ name = chars.join("");
1024
+ }
1025
+ const startSector = dirView.getUint32(offset + 116, true);
1026
+ const size = dirView.getUint32(offset + 120, true);
1027
+ const entry = { name, type, size, startSector };
1028
+ this.entries.set(name, entry);
1029
+ if (type === 5) {
1030
+ this.miniStream = this.readRegularStream(startSector, size);
1031
+ }
1032
+ }
1033
+ }
1034
+ parseMiniFat() {
1035
+ const miniFatStart = this.view.getUint32(60, true);
1036
+ const csectMiniFat = this.view.getUint32(64, true);
1037
+ if (miniFatStart >= 4294967294 || csectMiniFat === 0) return;
1038
+ let sect = miniFatStart;
1039
+ for (let i = 0; i < csectMiniFat && sect < 4294967294; i++) {
1040
+ const offset = this.getSectorOffset(sect);
1041
+ const count = this.sectorSize / 4;
1042
+ for (let j = 0; j < count; j++) {
1043
+ this.miniFat.push(this.view.getUint32(offset + j * 4, true));
1044
+ }
1045
+ sect = this.fat[sect] ?? 4294967294;
1046
+ }
1047
+ }
1048
+ readRegularStream(startSector, targetSize) {
1049
+ if (startSector >= 4294967294) return new Uint8Array(0);
1050
+ const chunks = [];
1051
+ let curr = startSector;
1052
+ let totalBytes = 0;
1053
+ const visited = /* @__PURE__ */ new Set();
1054
+ while (curr < 4294967294 && !visited.has(curr)) {
1055
+ visited.add(curr);
1056
+ const offset = this.getSectorOffset(curr);
1057
+ if (offset + this.sectorSize <= this.u8.length) {
1058
+ chunks.push(this.u8.subarray(offset, offset + this.sectorSize));
1059
+ totalBytes += this.sectorSize;
1060
+ }
1061
+ curr = this.fat[curr] ?? 4294967294;
1062
+ }
1063
+ const result = new Uint8Array(totalBytes);
1064
+ let pos = 0;
1065
+ for (const chunk of chunks) {
1066
+ result.set(chunk, pos);
1067
+ pos += chunk.length;
1068
+ }
1069
+ if (targetSize !== void 0 && targetSize < result.length) {
1070
+ return result.subarray(0, targetSize);
1071
+ }
1072
+ return result;
1073
+ }
1074
+ readMiniStream(startSector, targetSize) {
1075
+ if (this.miniStream.length === 0 || startSector >= 4294967294) return new Uint8Array(0);
1076
+ const chunks = [];
1077
+ let curr = startSector;
1078
+ let totalBytes = 0;
1079
+ const visited = /* @__PURE__ */ new Set();
1080
+ while (curr < 4294967294 && !visited.has(curr)) {
1081
+ visited.add(curr);
1082
+ const offset = curr * this.miniSectorSize;
1083
+ if (offset + this.miniSectorSize <= this.miniStream.length) {
1084
+ chunks.push(this.miniStream.subarray(offset, offset + this.miniSectorSize));
1085
+ totalBytes += this.miniSectorSize;
1086
+ }
1087
+ curr = this.miniFat[curr] ?? 4294967294;
1088
+ }
1089
+ const result = new Uint8Array(totalBytes);
1090
+ let pos = 0;
1091
+ for (const chunk of chunks) {
1092
+ result.set(chunk, pos);
1093
+ pos += chunk.length;
1094
+ }
1095
+ return result.subarray(0, targetSize);
1096
+ }
1097
+ };
868
1098
 
869
1099
  // ../plugins/pdf/dist/index.js
870
1100
  var PdfPlugin = class {
@@ -1275,13 +1505,18 @@ function mediaPlugin() {
1275
1505
  var DocxPlugin = class {
1276
1506
  id = "docx";
1277
1507
  name = "Word Document Preview";
1278
- extensions = [".docx"];
1279
- mimeTypes = ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"];
1508
+ extensions = [".docx", ".docm", ".dotx", ".dotm"];
1509
+ mimeTypes = [
1510
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1511
+ "application/vnd.ms-word.document.macroEnabled.12",
1512
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
1513
+ "application/vnd.ms-word.template.macroEnabled.12"
1514
+ ];
1280
1515
  weight = 80;
1281
1516
  supports(file) {
1282
1517
  const ext = file.metadata.extension?.toLowerCase();
1283
1518
  const mime = file.metadata.mimeType?.toLowerCase();
1284
- return ext === ".docx" || this.mimeTypes.includes(mime || "");
1519
+ return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
1285
1520
  }
1286
1521
  getToolbarActions(instance) {
1287
1522
  return [
@@ -1405,11 +1640,16 @@ function docxPlugin() {
1405
1640
  }
1406
1641
  var ExcelPlugin = class {
1407
1642
  id = "excel";
1408
- name = "Excel Spreadsheet Preview";
1409
- extensions = [".xlsx", ".xls"];
1643
+ name = "Spreadsheet Preview (Excel / OpenDocument)";
1644
+ extensions = [".xlsx", ".xls", ".xlsm", ".xlsb", ".xltx", ".xltm", ".ods"];
1410
1645
  mimeTypes = [
1411
1646
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1412
- "application/vnd.ms-excel"
1647
+ "application/vnd.ms-excel",
1648
+ "application/vnd.ms-excel.sheet.macroEnabled.12",
1649
+ "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
1650
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
1651
+ "application/vnd.ms-excel.template.macroEnabled.12",
1652
+ "application/vnd.oasis.opendocument.spreadsheet"
1413
1653
  ];
1414
1654
  weight = 80;
1415
1655
  supports(file) {
@@ -1473,70 +1713,111 @@ var ExcelPlugin = class {
1473
1713
  }
1474
1714
  async render(ctx) {
1475
1715
  const container = document.createElement("div");
1716
+ container.className = "fp-excel-container";
1476
1717
  container.style.display = "flex";
1477
1718
  container.style.flexDirection = "column";
1478
1719
  container.style.width = "100%";
1479
1720
  container.style.height = "100%";
1480
1721
  container.style.overflow = "hidden";
1481
1722
  const contentArea = document.createElement("div");
1723
+ contentArea.className = "fp-excel-content";
1482
1724
  contentArea.style.flex = "1";
1483
1725
  contentArea.style.overflow = "auto";
1484
1726
  contentArea.style.padding = "16px";
1485
1727
  contentArea.style.transformOrigin = "top left";
1486
1728
  const tabsArea = document.createElement("div");
1729
+ tabsArea.className = "fp-excel-tabs";
1487
1730
  tabsArea.style.display = "flex";
1488
- tabsArea.style.gap = "8px";
1731
+ tabsArea.style.gap = "6px";
1489
1732
  tabsArea.style.padding = "8px 16px";
1490
1733
  tabsArea.style.borderTop = "1px solid #e0e0e0";
1491
1734
  tabsArea.style.backgroundColor = "#fafafa";
1492
1735
  tabsArea.style.overflowX = "auto";
1736
+ tabsArea.style.flexShrink = "0";
1493
1737
  container.appendChild(contentArea);
1494
1738
  container.appendChild(tabsArea);
1495
1739
  ctx.container.appendChild(container);
1496
1740
  let scale = 1;
1497
1741
  let currentSheetIndex = 1;
1498
- const workbook = new ExcelJS.Workbook();
1742
+ let sheetNames = [];
1743
+ let wb = null;
1499
1744
  try {
1500
- await workbook.xlsx.load(ctx.buffer);
1501
- } catch {
1745
+ wb = XLSX.read(new Uint8Array(ctx.buffer), { type: "array", cellDates: true });
1746
+ sheetNames = wb.SheetNames || [];
1747
+ } catch (err) {
1748
+ console.warn("[ExcelPlugin] SheetJS parse failed, rendering fallback:", err);
1502
1749
  }
1750
+ const tabButtons = [];
1503
1751
  const renderSheet = (index) => {
1752
+ if (!wb || index < 1 || index > sheetNames.length) return;
1753
+ currentSheetIndex = index;
1754
+ const sheetName = sheetNames[index - 1];
1755
+ const ws = wb.Sheets[sheetName];
1504
1756
  contentArea.innerHTML = "";
1505
- const sheet = workbook.getWorksheet(index);
1506
- if (!sheet) return;
1507
- const table = document.createElement("table");
1508
- table.style.borderCollapse = "collapse";
1509
- table.style.fontFamily = "sans-serif";
1510
- table.style.fontSize = "13px";
1511
- table.style.minWidth = "100%";
1512
- sheet.eachRow({ includeEmpty: false }, (row) => {
1513
- const tr = document.createElement("tr");
1514
- row.eachCell({ includeEmpty: true }, (cell) => {
1515
- const td = document.createElement("td");
1516
- td.style.border = "1px solid #d0d7de";
1517
- td.style.padding = "6px 12px";
1518
- td.textContent = cell.text || "";
1519
- tr.appendChild(td);
1757
+ if (!ws) {
1758
+ contentArea.innerHTML = '<div style="padding: 24px; color: #888;">Empty sheet</div>';
1759
+ return;
1760
+ }
1761
+ const html = XLSX.utils.sheet_to_html(ws, { id: "fp-sheet-table", editable: false });
1762
+ contentArea.innerHTML = html;
1763
+ const table = contentArea.querySelector("table");
1764
+ if (table) {
1765
+ table.style.borderCollapse = "collapse";
1766
+ table.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1767
+ table.style.fontSize = "13px";
1768
+ table.style.minWidth = "100%";
1769
+ table.style.border = "1px solid #d0d7de";
1770
+ table.style.background = "#fff";
1771
+ table.querySelectorAll("td, th").forEach((cell) => {
1772
+ const el = cell;
1773
+ el.style.border = "1px solid #d0d7de";
1774
+ el.style.padding = "6px 12px";
1775
+ el.style.whiteSpace = "nowrap";
1520
1776
  });
1521
- table.appendChild(tr);
1777
+ table.querySelectorAll("tr:first-child td, th").forEach((cell) => {
1778
+ const el = cell;
1779
+ el.style.fontWeight = "600";
1780
+ el.style.backgroundColor = "#f6f8fa";
1781
+ });
1782
+ }
1783
+ tabButtons.forEach((b, i) => {
1784
+ if (i === index - 1) {
1785
+ b.style.backgroundColor = "#2563eb";
1786
+ b.style.color = "#fff";
1787
+ b.style.borderColor = "#2563eb";
1788
+ b.style.fontWeight = "600";
1789
+ } else {
1790
+ b.style.backgroundColor = "#fff";
1791
+ b.style.color = "#333";
1792
+ b.style.borderColor = "#ccc";
1793
+ b.style.fontWeight = "normal";
1794
+ }
1522
1795
  });
1523
- contentArea.appendChild(table);
1524
- currentSheetIndex = index;
1525
1796
  };
1526
- workbook.worksheets.forEach((sheet, idx) => {
1527
- const btn = document.createElement("button");
1528
- btn.textContent = sheet.name;
1529
- btn.style.padding = "4px 12px";
1530
- btn.style.fontSize = "12px";
1531
- btn.style.cursor = "pointer";
1532
- btn.style.border = "1px solid #ccc";
1533
- btn.style.borderRadius = "4px";
1534
- btn.style.backgroundColor = "#fff";
1535
- btn.onclick = () => renderSheet(idx + 1);
1536
- tabsArea.appendChild(btn);
1537
- });
1538
- if (workbook.worksheets.length > 0) {
1797
+ if (sheetNames.length > 0) {
1798
+ sheetNames.forEach((name, idx) => {
1799
+ const btn = document.createElement("button");
1800
+ btn.textContent = name;
1801
+ btn.style.padding = "4px 12px";
1802
+ btn.style.fontSize = "12px";
1803
+ btn.style.cursor = "pointer";
1804
+ btn.style.border = "1px solid #ccc";
1805
+ btn.style.borderRadius = "4px";
1806
+ btn.style.backgroundColor = "#fff";
1807
+ btn.style.transition = "all 0.15s ease";
1808
+ btn.onclick = () => renderSheet(idx + 1);
1809
+ tabsArea.appendChild(btn);
1810
+ tabButtons.push(btn);
1811
+ });
1539
1812
  renderSheet(1);
1813
+ } else {
1814
+ contentArea.innerHTML = `
1815
+ <div style="text-align:center; padding: 40px; color: #666;">
1816
+ <div style="font-size:48px; margin-bottom: 16px;">\u{1F4CA}</div>
1817
+ <h3>${ctx.metadata.name || "Spreadsheet"}</h3>
1818
+ <p>Unable to load sheet data</p>
1819
+ </div>
1820
+ `;
1540
1821
  }
1541
1822
  const cleanup = () => {
1542
1823
  container.remove();
@@ -1559,11 +1840,11 @@ var ExcelPlugin = class {
1559
1840
  contentArea.style.transform = `scale(${scale})`;
1560
1841
  },
1561
1842
  goToPage: (page) => {
1562
- if (page > 0 && page <= workbook.worksheets.length) {
1843
+ if (page > 0 && page <= sheetNames.length) {
1563
1844
  renderSheet(page);
1564
1845
  }
1565
1846
  },
1566
- getPageCount: () => workbook.worksheets.length,
1847
+ getPageCount: () => sheetNames.length,
1567
1848
  getCurrentPage: () => currentSheetIndex,
1568
1849
  download: () => {
1569
1850
  const blob = new Blob([ctx.buffer], { type: this.mimeTypes[0] });
@@ -2164,7 +2445,7 @@ var MarkdownPlugin = class {
2164
2445
  gfm: true,
2165
2446
  breaks: true
2166
2447
  });
2167
- const cleanHtml = DOMPurify.sanitize(rawHtml, {
2448
+ const cleanHtml = DOMPurify5.sanitize(rawHtml, {
2168
2449
  USE_PROFILES: { html: true }
2169
2450
  });
2170
2451
  const wrapper = document.createElement("div");
@@ -2303,10 +2584,14 @@ function markdownPlugin() {
2303
2584
  var PptxPlugin = class {
2304
2585
  id = "pptx";
2305
2586
  name = "PowerPoint Presentation";
2306
- extensions = [".pptx", ".ppsx"];
2587
+ extensions = [".pptx", ".ppsx", ".pptm", ".ppsm", ".potx", ".potm"];
2307
2588
  mimeTypes = [
2308
2589
  "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2309
- "application/vnd.openxmlformats-officedocument.presentationml.slideshow"
2590
+ "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
2591
+ "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
2592
+ "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
2593
+ "application/vnd.openxmlformats-officedocument.presentationml.template",
2594
+ "application/vnd.ms-powerpoint.template.macroEnabled.12"
2310
2595
  ];
2311
2596
  weight = 80;
2312
2597
  supports(file) {
@@ -2692,6 +2977,1292 @@ var ThreeDPlugin = class {
2692
2977
  function threeDPlugin() {
2693
2978
  return new ThreeDPlugin();
2694
2979
  }
2980
+ var RtfPlugin = class {
2981
+ id = "rtf";
2982
+ name = "Rich Text Format (RTF)";
2983
+ extensions = [".rtf"];
2984
+ mimeTypes = ["text/rtf", "application/rtf"];
2985
+ weight = 80;
2986
+ supports(file) {
2987
+ const ext = file.metadata.extension?.toLowerCase();
2988
+ const mime = file.metadata.mimeType?.toLowerCase();
2989
+ return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
2990
+ }
2991
+ getToolbarActions(instance) {
2992
+ return [
2993
+ {
2994
+ id: "zoom-out",
2995
+ icon: "zoom-out",
2996
+ label: "Zoom Out",
2997
+ type: "button",
2998
+ group: "zoom",
2999
+ execute: () => instance.zoomOut?.()
3000
+ },
3001
+ {
3002
+ id: "zoom-in",
3003
+ icon: "zoom-in",
3004
+ label: "Zoom In",
3005
+ type: "button",
3006
+ group: "zoom",
3007
+ execute: () => instance.zoomIn?.()
3008
+ },
3009
+ {
3010
+ id: "fit-page",
3011
+ icon: "fit-page",
3012
+ label: "Fit to Page",
3013
+ type: "button",
3014
+ group: "zoom",
3015
+ execute: () => instance.fitToPage?.()
3016
+ },
3017
+ {
3018
+ id: "download",
3019
+ icon: "download",
3020
+ label: "Download",
3021
+ type: "button",
3022
+ group: "actions",
3023
+ execute: () => instance.download?.()
3024
+ },
3025
+ {
3026
+ id: "print",
3027
+ icon: "print",
3028
+ label: "Print",
3029
+ type: "button",
3030
+ group: "actions",
3031
+ execute: () => instance.print?.()
3032
+ }
3033
+ ];
3034
+ }
3035
+ async render(ctx) {
3036
+ const wrapper = document.createElement("div");
3037
+ wrapper.className = "fp-rtf-wrapper";
3038
+ wrapper.style.padding = "32px";
3039
+ wrapper.style.maxWidth = "850px";
3040
+ wrapper.style.margin = "0 auto";
3041
+ wrapper.style.backgroundColor = "#fff";
3042
+ wrapper.style.boxShadow = "0 2px 8px rgba(0,0,0,0.08)";
3043
+ wrapper.style.borderRadius = "4px";
3044
+ wrapper.style.minHeight = "100%";
3045
+ wrapper.style.transformOrigin = "top center";
3046
+ wrapper.style.transition = "transform 0.2s ease";
3047
+ ctx.container.style.overflow = "auto";
3048
+ ctx.container.style.padding = "24px";
3049
+ ctx.container.style.backgroundColor = "#f1f5f9";
3050
+ ctx.container.appendChild(wrapper);
3051
+ let scale = 1;
3052
+ try {
3053
+ const doc = new RTFJS.Document(ctx.buffer, {});
3054
+ const htmlElements = await doc.render();
3055
+ for (const el of htmlElements) {
3056
+ wrapper.appendChild(el);
3057
+ }
3058
+ } catch (err) {
3059
+ console.warn("[RtfPlugin] RTF render error, fallback text:", err);
3060
+ const text = new TextDecoder("latin1").decode(ctx.buffer);
3061
+ const clean = text.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "");
3062
+ wrapper.innerHTML = `<pre style="white-space: pre-wrap; font-family: serif; color: #333;">${clean}</pre>`;
3063
+ }
3064
+ const cleanup = () => {
3065
+ wrapper.remove();
3066
+ ctx.container.innerHTML = "";
3067
+ };
3068
+ ctx.signal.addEventListener("abort", cleanup);
3069
+ return {
3070
+ destroy: cleanup,
3071
+ zoomIn: () => {
3072
+ scale += 0.1;
3073
+ wrapper.style.transform = `scale(${scale})`;
3074
+ },
3075
+ zoomOut: () => {
3076
+ scale = Math.max(0.2, scale - 0.1);
3077
+ wrapper.style.transform = `scale(${scale})`;
3078
+ },
3079
+ getZoom: () => scale,
3080
+ setZoom: (level) => {
3081
+ scale = level;
3082
+ wrapper.style.transform = `scale(${scale})`;
3083
+ },
3084
+ fitToPage: () => {
3085
+ scale = 1;
3086
+ wrapper.style.transform = "scale(1)";
3087
+ },
3088
+ download: () => {
3089
+ const blob = new Blob([ctx.buffer], { type: "application/rtf" });
3090
+ const url = URL.createObjectURL(blob);
3091
+ const a = document.createElement("a");
3092
+ a.href = url;
3093
+ a.download = ctx.metadata.name || "document.rtf";
3094
+ a.click();
3095
+ URL.revokeObjectURL(url);
3096
+ },
3097
+ print: () => {
3098
+ window.print();
3099
+ }
3100
+ };
3101
+ }
3102
+ };
3103
+ function rtfPlugin() {
3104
+ return new RtfPlugin();
3105
+ }
3106
+ var HtmlPreviewPlugin = class {
3107
+ id = "html-preview";
3108
+ name = "HTML Document Preview";
3109
+ extensions = [".html", ".htm", ".xhtml"];
3110
+ mimeTypes = ["text/html", "application/xhtml+xml"];
3111
+ weight = 85;
3112
+ supports(file) {
3113
+ const ext = file.metadata.extension?.toLowerCase();
3114
+ const mime = file.metadata.mimeType?.toLowerCase();
3115
+ return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3116
+ }
3117
+ getToolbarActions(instance) {
3118
+ return [
3119
+ {
3120
+ id: "zoom-out",
3121
+ icon: "zoom-out",
3122
+ label: "Zoom Out",
3123
+ type: "button",
3124
+ group: "zoom",
3125
+ execute: () => instance.zoomOut?.()
3126
+ },
3127
+ {
3128
+ id: "zoom-in",
3129
+ icon: "zoom-in",
3130
+ label: "Zoom In",
3131
+ type: "button",
3132
+ group: "zoom",
3133
+ execute: () => instance.zoomIn?.()
3134
+ },
3135
+ {
3136
+ id: "fit-page",
3137
+ icon: "fit-page",
3138
+ label: "Fit to Page",
3139
+ type: "button",
3140
+ group: "zoom",
3141
+ execute: () => instance.fitToPage?.()
3142
+ },
3143
+ {
3144
+ id: "copy",
3145
+ icon: "copy",
3146
+ label: "Copy HTML",
3147
+ type: "button",
3148
+ group: "actions",
3149
+ execute: () => instance.copy?.()
3150
+ },
3151
+ {
3152
+ id: "download",
3153
+ icon: "download",
3154
+ label: "Download",
3155
+ type: "button",
3156
+ group: "actions",
3157
+ execute: () => instance.download?.()
3158
+ },
3159
+ {
3160
+ id: "print",
3161
+ icon: "print",
3162
+ label: "Print",
3163
+ type: "button",
3164
+ group: "actions",
3165
+ execute: () => instance.print?.()
3166
+ }
3167
+ ];
3168
+ }
3169
+ async render(ctx) {
3170
+ const rawHtml = new TextDecoder("utf-8").decode(ctx.buffer);
3171
+ const sanitized = DOMPurify5.sanitize(rawHtml, {
3172
+ WHOLE_DOCUMENT: true,
3173
+ ADD_TAGS: ["style", "link"],
3174
+ ADD_ATTR: ["target", "rel"]
3175
+ });
3176
+ const iframe = document.createElement("iframe");
3177
+ iframe.className = "fp-html-iframe";
3178
+ iframe.style.width = "100%";
3179
+ iframe.style.height = "100%";
3180
+ iframe.style.border = "none";
3181
+ iframe.style.backgroundColor = "#ffffff";
3182
+ iframe.style.transformOrigin = "top left";
3183
+ iframe.style.transition = "transform 0.2s ease";
3184
+ iframe.sandbox.add("allow-same-origin");
3185
+ ctx.container.style.overflow = "auto";
3186
+ ctx.container.style.width = "100%";
3187
+ ctx.container.style.height = "100%";
3188
+ ctx.container.appendChild(iframe);
3189
+ iframe.srcdoc = sanitized;
3190
+ let scale = 1;
3191
+ const cleanup = () => {
3192
+ iframe.remove();
3193
+ ctx.container.innerHTML = "";
3194
+ };
3195
+ ctx.signal.addEventListener("abort", cleanup);
3196
+ return {
3197
+ destroy: cleanup,
3198
+ zoomIn: () => {
3199
+ scale += 0.1;
3200
+ iframe.style.transform = `scale(${scale})`;
3201
+ },
3202
+ zoomOut: () => {
3203
+ scale = Math.max(0.2, scale - 0.1);
3204
+ iframe.style.transform = `scale(${scale})`;
3205
+ },
3206
+ getZoom: () => scale,
3207
+ setZoom: (level) => {
3208
+ scale = level;
3209
+ iframe.style.transform = `scale(${scale})`;
3210
+ },
3211
+ fitToPage: () => {
3212
+ scale = 1;
3213
+ iframe.style.transform = "scale(1)";
3214
+ },
3215
+ copy: () => {
3216
+ navigator.clipboard.writeText(rawHtml);
3217
+ },
3218
+ download: () => {
3219
+ const blob = new Blob([ctx.buffer], { type: "text/html" });
3220
+ const url = URL.createObjectURL(blob);
3221
+ const a = document.createElement("a");
3222
+ a.href = url;
3223
+ a.download = ctx.metadata.name || "document.html";
3224
+ a.click();
3225
+ URL.revokeObjectURL(url);
3226
+ },
3227
+ print: () => {
3228
+ iframe.contentWindow?.print();
3229
+ }
3230
+ };
3231
+ }
3232
+ };
3233
+ function htmlPreviewPlugin() {
3234
+ return new HtmlPreviewPlugin();
3235
+ }
3236
+ var OpenDocumentPlugin = class {
3237
+ id = "opendocument";
3238
+ name = "OpenDocument Preview (ODT, ODP, ODS, ODG, ODF)";
3239
+ extensions = [".odt", ".odp", ".ods", ".odg", ".odf"];
3240
+ mimeTypes = [
3241
+ "application/vnd.oasis.opendocument.text",
3242
+ "application/vnd.oasis.opendocument.presentation",
3243
+ "application/vnd.oasis.opendocument.spreadsheet",
3244
+ "application/vnd.oasis.opendocument.graphics",
3245
+ "application/vnd.oasis.opendocument.formula"
3246
+ ];
3247
+ weight = 80;
3248
+ supports(file) {
3249
+ const ext = file.metadata.extension?.toLowerCase();
3250
+ const mime = file.metadata.mimeType?.toLowerCase();
3251
+ return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3252
+ }
3253
+ getToolbarActions(instance) {
3254
+ const isPresentation = instance.isPresentation;
3255
+ const actions = [];
3256
+ if (isPresentation) {
3257
+ actions.push(
3258
+ {
3259
+ id: "thumbnails",
3260
+ icon: "thumbnails",
3261
+ label: "Slide Thumbnails",
3262
+ type: "button",
3263
+ group: "navigation",
3264
+ execute: () => instance.toggleThumbnails?.()
3265
+ },
3266
+ {
3267
+ id: "page-prev",
3268
+ icon: "page-prev",
3269
+ label: "Previous Slide",
3270
+ type: "button",
3271
+ group: "navigation",
3272
+ execute: () => {
3273
+ const cur = instance.getCurrentPage?.() ?? 1;
3274
+ if (cur > 1) instance.goToPage?.(cur - 1);
3275
+ }
3276
+ },
3277
+ {
3278
+ id: "page-nav",
3279
+ icon: "",
3280
+ label: "Slide Number",
3281
+ type: "page-nav",
3282
+ group: "navigation",
3283
+ execute: (p) => instance.goToPage?.(Number(p))
3284
+ },
3285
+ {
3286
+ id: "page-next",
3287
+ icon: "page-next",
3288
+ label: "Next Slide",
3289
+ type: "button",
3290
+ group: "navigation",
3291
+ execute: () => {
3292
+ const cur = instance.getCurrentPage?.() ?? 1;
3293
+ const total = instance.getPageCount?.() ?? 1;
3294
+ if (cur < total) instance.goToPage?.(cur + 1);
3295
+ }
3296
+ }
3297
+ );
3298
+ }
3299
+ actions.push(
3300
+ {
3301
+ id: "zoom-out",
3302
+ icon: "zoom-out",
3303
+ label: "Zoom Out",
3304
+ type: "button",
3305
+ group: "zoom",
3306
+ execute: () => instance.zoomOut?.()
3307
+ },
3308
+ {
3309
+ id: "zoom-in",
3310
+ icon: "zoom-in",
3311
+ label: "Zoom In",
3312
+ type: "button",
3313
+ group: "zoom",
3314
+ execute: () => instance.zoomIn?.()
3315
+ },
3316
+ {
3317
+ id: "fit-page",
3318
+ icon: "fit-page",
3319
+ label: "Fit to Page",
3320
+ type: "button",
3321
+ group: "zoom",
3322
+ execute: () => instance.fitToPage?.()
3323
+ },
3324
+ {
3325
+ id: "download",
3326
+ icon: "download",
3327
+ label: "Download",
3328
+ type: "button",
3329
+ group: "actions",
3330
+ execute: () => instance.download?.()
3331
+ },
3332
+ {
3333
+ id: "print",
3334
+ icon: "print",
3335
+ label: "Print",
3336
+ type: "button",
3337
+ group: "actions",
3338
+ execute: () => instance.print?.()
3339
+ }
3340
+ );
3341
+ return actions;
3342
+ }
3343
+ async render(ctx) {
3344
+ const ext = (ctx.metadata.extension || "").toLowerCase();
3345
+ const mime = (ctx.metadata.mimeType || "").toLowerCase();
3346
+ const isPresentation = ext === ".odp" || mime.includes("presentation");
3347
+ const isFormula = ext === ".odf" || mime.includes("formula");
3348
+ ext === ".odg" || mime.includes("graphics");
3349
+ let unzipped;
3350
+ try {
3351
+ unzipped = unzipSync(new Uint8Array(ctx.buffer));
3352
+ } catch {
3353
+ throw new Error("Failed to decompress OpenDocument package (invalid ZIP format)");
3354
+ }
3355
+ const imageUrls = /* @__PURE__ */ new Map();
3356
+ for (const [filePath, fileBytes] of Object.entries(unzipped)) {
3357
+ if (filePath.startsWith("Pictures/")) {
3358
+ const imageMime = filePath.endsWith(".png") ? "image/png" : filePath.endsWith(".jpg") || filePath.endsWith(".jpeg") ? "image/jpeg" : filePath.endsWith(".svg") ? "image/svg+xml" : "application/octet-stream";
3359
+ const blob = new Blob([fileBytes], { type: imageMime });
3360
+ imageUrls.set(filePath, URL.createObjectURL(blob));
3361
+ }
3362
+ }
3363
+ const contentXmlStr = unzipped["content.xml"] ? strFromU8(unzipped["content.xml"]) : "";
3364
+ const stylesXmlStr = unzipped["styles.xml"] ? strFromU8(unzipped["styles.xml"]) : "";
3365
+ const parser = new DOMParser();
3366
+ const contentDoc = parser.parseFromString(contentXmlStr, "application/xml");
3367
+ const stylesDoc = stylesXmlStr ? parser.parseFromString(stylesXmlStr, "application/xml") : null;
3368
+ const styleMap = this.extractStyles(stylesDoc, contentDoc);
3369
+ const container = document.createElement("div");
3370
+ container.className = "fp-odf-container";
3371
+ container.style.width = "100%";
3372
+ container.style.height = "100%";
3373
+ container.style.overflow = "auto";
3374
+ container.style.padding = "24px";
3375
+ container.style.backgroundColor = "#f1f5f9";
3376
+ const wrapper = document.createElement("div");
3377
+ wrapper.className = "fp-odf-wrapper";
3378
+ wrapper.style.margin = "0 auto";
3379
+ wrapper.style.backgroundColor = "#ffffff";
3380
+ wrapper.style.boxShadow = "0 2px 10px rgba(0,0,0,0.08)";
3381
+ wrapper.style.borderRadius = "4px";
3382
+ wrapper.style.transformOrigin = "top center";
3383
+ wrapper.style.transition = "transform 0.2s ease";
3384
+ container.appendChild(wrapper);
3385
+ ctx.container.appendChild(container);
3386
+ let scale = 1;
3387
+ let currentPage = 1;
3388
+ let totalPages = 1;
3389
+ let slides = [];
3390
+ if (isPresentation) {
3391
+ wrapper.style.maxWidth = "960px";
3392
+ wrapper.style.aspectRatio = "16 / 9";
3393
+ wrapper.style.position = "relative";
3394
+ wrapper.style.overflow = "hidden";
3395
+ slides = this.renderSlides(contentDoc, styleMap, imageUrls);
3396
+ totalPages = Math.max(1, slides.length);
3397
+ slides.forEach((slide, idx) => {
3398
+ slide.style.display = idx === 0 ? "block" : "none";
3399
+ slide.style.width = "100%";
3400
+ slide.style.height = "100%";
3401
+ slide.style.position = "absolute";
3402
+ slide.style.top = "0";
3403
+ slide.style.left = "0";
3404
+ wrapper.appendChild(slide);
3405
+ });
3406
+ } else if (isFormula) {
3407
+ wrapper.style.maxWidth = "800px";
3408
+ wrapper.style.padding = "48px";
3409
+ wrapper.style.textAlign = "center";
3410
+ wrapper.style.fontSize = "24px";
3411
+ const mathEl = contentDoc.querySelector("math");
3412
+ if (mathEl) {
3413
+ wrapper.innerHTML = mathEl.outerHTML;
3414
+ } else {
3415
+ wrapper.textContent = contentDoc.documentElement.textContent || "Formula content";
3416
+ }
3417
+ } else {
3418
+ wrapper.style.maxWidth = "850px";
3419
+ wrapper.style.padding = "48px";
3420
+ wrapper.style.minHeight = "100%";
3421
+ const bodyHtml = this.renderOdfBody(contentDoc, styleMap, imageUrls);
3422
+ wrapper.innerHTML = DOMPurify5.sanitize(bodyHtml, {
3423
+ ADD_TAGS: ["math", "semantics", "mrow", "mi", "mo", "mn", "msup", "msub"],
3424
+ ADD_ATTR: ["style", "colspan", "rowspan"]
3425
+ });
3426
+ }
3427
+ const cleanup = () => {
3428
+ imageUrls.forEach((url) => URL.revokeObjectURL(url));
3429
+ container.remove();
3430
+ ctx.container.innerHTML = "";
3431
+ };
3432
+ ctx.signal.addEventListener("abort", cleanup);
3433
+ const goToPage = (page) => {
3434
+ if (!isPresentation || page < 1 || page > totalPages) return;
3435
+ currentPage = page;
3436
+ slides.forEach((s, idx) => {
3437
+ s.style.display = idx === page - 1 ? "block" : "none";
3438
+ });
3439
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
3440
+ };
3441
+ return {
3442
+ destroy: cleanup,
3443
+ isPresentation,
3444
+ zoomIn: () => {
3445
+ scale += 0.1;
3446
+ wrapper.style.transform = `scale(${scale})`;
3447
+ },
3448
+ zoomOut: () => {
3449
+ scale = Math.max(0.2, scale - 0.1);
3450
+ wrapper.style.transform = `scale(${scale})`;
3451
+ },
3452
+ getZoom: () => scale,
3453
+ setZoom: (level) => {
3454
+ scale = level;
3455
+ wrapper.style.transform = `scale(${scale})`;
3456
+ },
3457
+ fitToPage: () => {
3458
+ scale = 1;
3459
+ wrapper.style.transform = "scale(1)";
3460
+ },
3461
+ goToPage,
3462
+ getPageCount: () => totalPages,
3463
+ getCurrentPage: () => currentPage,
3464
+ getThumbnails: async () => {
3465
+ if (!isPresentation) return [];
3466
+ return slides.map((_, idx) => ({
3467
+ index: idx,
3468
+ label: `Slide ${idx + 1}`,
3469
+ render: async (canvas) => {
3470
+ const ctx2d = canvas.getContext("2d");
3471
+ if (!ctx2d) return;
3472
+ canvas.width = 160;
3473
+ canvas.height = 90;
3474
+ ctx2d.fillStyle = "#f8fafc";
3475
+ ctx2d.fillRect(0, 0, 160, 90);
3476
+ ctx2d.fillStyle = "#334155";
3477
+ ctx2d.font = "bold 12px sans-serif";
3478
+ ctx2d.textAlign = "center";
3479
+ ctx2d.fillText(`Slide ${idx + 1}`, 80, 50);
3480
+ }
3481
+ }));
3482
+ },
3483
+ download: () => {
3484
+ const mimeType = this.mimeTypes.find((m) => m.includes(ext.replace(".", ""))) || "application/octet-stream";
3485
+ const blob = new Blob([ctx.buffer], { type: mimeType });
3486
+ const url = URL.createObjectURL(blob);
3487
+ const a = document.createElement("a");
3488
+ a.href = url;
3489
+ a.download = ctx.metadata.name || `document${ext}`;
3490
+ a.click();
3491
+ URL.revokeObjectURL(url);
3492
+ },
3493
+ print: () => {
3494
+ window.print();
3495
+ }
3496
+ };
3497
+ }
3498
+ extractStyles(stylesDoc, contentDoc) {
3499
+ const map = /* @__PURE__ */ new Map();
3500
+ const styleNodes = [];
3501
+ if (stylesDoc) {
3502
+ styleNodes.push(...Array.from(stylesDoc.querySelectorAll("style, [name]")));
3503
+ }
3504
+ styleNodes.push(...Array.from(contentDoc.querySelectorAll("style, [name]")));
3505
+ for (const node of styleNodes) {
3506
+ const name = node.getAttribute("style:name") || node.getAttribute("name");
3507
+ if (!name) continue;
3508
+ let css = "";
3509
+ const textProp = node.querySelector("text-properties, [font-weight], [font-style], [color]");
3510
+ if (textProp) {
3511
+ const weight = textProp.getAttribute("fo:font-weight") || textProp.getAttribute("font-weight");
3512
+ const style = textProp.getAttribute("fo:font-style") || textProp.getAttribute("font-style");
3513
+ const color = textProp.getAttribute("fo:color") || textProp.getAttribute("color");
3514
+ const size = textProp.getAttribute("fo:font-size") || textProp.getAttribute("font-size");
3515
+ if (weight === "bold") css += "font-weight: bold; ";
3516
+ if (style === "italic") css += "font-style: italic; ";
3517
+ if (color) css += `color: ${color}; `;
3518
+ if (size) css += `font-size: ${size}; `;
3519
+ }
3520
+ const paraProp = node.querySelector("paragraph-properties, [text-align]");
3521
+ if (paraProp) {
3522
+ const align = paraProp.getAttribute("fo:text-align") || paraProp.getAttribute("text-align");
3523
+ const mt = paraProp.getAttribute("fo:margin-top") || paraProp.getAttribute("margin-top");
3524
+ const mb = paraProp.getAttribute("fo:margin-bottom") || paraProp.getAttribute("margin-bottom");
3525
+ if (align) css += `text-align: ${align}; `;
3526
+ if (mt) css += `margin-top: ${mt}; `;
3527
+ if (mb) css += `margin-bottom: ${mb}; `;
3528
+ }
3529
+ if (css) map.set(name, css);
3530
+ }
3531
+ return map;
3532
+ }
3533
+ renderOdfBody(contentDoc, styles, images) {
3534
+ const body = contentDoc.querySelector("body") || contentDoc.documentElement;
3535
+ let html = "";
3536
+ const walk = (node) => {
3537
+ const tag = node.localName || node.tagName.toLowerCase();
3538
+ switch (tag) {
3539
+ case "h": {
3540
+ const level = Math.min(6, Math.max(1, Number(node.getAttribute("text:outline-level") || 1)));
3541
+ const style = styles.get(node.getAttribute("text:style-name") || "") || "";
3542
+ html += `<h${level} style="${style}; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${node.textContent || ""}</h${level}>`;
3543
+ break;
3544
+ }
3545
+ case "p": {
3546
+ const style = styles.get(node.getAttribute("text:style-name") || "") || "";
3547
+ const inner = this.renderSpans(node, styles, images);
3548
+ html += `<p style="${style}; line-height: 1.6; margin: 8px 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${inner}</p>`;
3549
+ break;
3550
+ }
3551
+ case "list": {
3552
+ html += '<ul style="margin: 8px 0; padding-left: 24px;">';
3553
+ Array.from(node.children).forEach((c) => walk(c));
3554
+ html += "</ul>";
3555
+ break;
3556
+ }
3557
+ case "list-item": {
3558
+ html += "<li>";
3559
+ Array.from(node.children).forEach((c) => walk(c));
3560
+ html += "</li>";
3561
+ break;
3562
+ }
3563
+ case "table": {
3564
+ html += '<table style="border-collapse: collapse; width: 100%; margin: 16px 0; border: 1px solid #d0d7de;">';
3565
+ Array.from(node.children).forEach((c) => walk(c));
3566
+ html += "</table>";
3567
+ break;
3568
+ }
3569
+ case "table-row": {
3570
+ html += "<tr>";
3571
+ Array.from(node.children).forEach((c) => walk(c));
3572
+ html += "</tr>";
3573
+ break;
3574
+ }
3575
+ case "table-cell": {
3576
+ html += '<td style="border: 1px solid #d0d7de; padding: 8px 12px; font-size: 13px;">';
3577
+ Array.from(node.children).forEach((c) => walk(c));
3578
+ html += "</td>";
3579
+ break;
3580
+ }
3581
+ default:
3582
+ Array.from(node.children).forEach((c) => walk(c));
3583
+ }
3584
+ };
3585
+ Array.from(body.children).forEach((c) => walk(c));
3586
+ return html || '<p style="color: #666; font-style: italic;">(Document is empty)</p>';
3587
+ }
3588
+ renderSpans(node, styles, images) {
3589
+ let result = "";
3590
+ for (const child of Array.from(node.childNodes)) {
3591
+ if (child.nodeType === Node.TEXT_NODE) {
3592
+ result += child.textContent || "";
3593
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
3594
+ const el = child;
3595
+ const tag = el.localName || el.tagName.toLowerCase();
3596
+ if (tag === "span") {
3597
+ const style = styles.get(el.getAttribute("text:style-name") || "") || "";
3598
+ result += `<span style="${style}">${el.textContent || ""}</span>`;
3599
+ } else if (tag === "image") {
3600
+ const href = el.getAttribute("xlink:href") || el.getAttribute("href") || "";
3601
+ const blobUrl = images.get(href);
3602
+ if (blobUrl) {
3603
+ result += `<img src="${blobUrl}" style="max-width: 100%; height: auto; margin: 8px 0;" alt="Embedded image" />`;
3604
+ }
3605
+ } else if (tag === "s") {
3606
+ const count = Number(el.getAttribute("text:c") || 1);
3607
+ result += "&nbsp;".repeat(count);
3608
+ } else if (tag === "tab") {
3609
+ result += "&emsp;";
3610
+ } else if (tag === "line-break") {
3611
+ result += "<br/>";
3612
+ } else {
3613
+ result += el.textContent || "";
3614
+ }
3615
+ }
3616
+ }
3617
+ return result;
3618
+ }
3619
+ renderSlides(contentDoc, styles, images) {
3620
+ const pages = Array.from(contentDoc.querySelectorAll("page, [draw\\:name]"));
3621
+ const slides = [];
3622
+ const pageNodes = pages.length > 0 ? pages : Array.from(contentDoc.getElementsByTagNameNS("*", "page"));
3623
+ if (pageNodes.length === 0) {
3624
+ const fallbackSlide = document.createElement("div");
3625
+ fallbackSlide.style.padding = "40px";
3626
+ fallbackSlide.style.textAlign = "center";
3627
+ fallbackSlide.innerHTML = "<h2>Presentation</h2><p>No slides found</p>";
3628
+ return [fallbackSlide];
3629
+ }
3630
+ pageNodes.forEach((pageEl, idx) => {
3631
+ const slide = document.createElement("div");
3632
+ slide.className = `fp-odp-slide fp-odp-slide-${idx + 1}`;
3633
+ slide.style.padding = "40px";
3634
+ slide.style.boxSizing = "border-box";
3635
+ slide.style.display = "flex";
3636
+ slide.style.flexDirection = "column";
3637
+ slide.style.justifyContent = "center";
3638
+ slide.style.alignItems = "center";
3639
+ slide.style.background = "#ffffff";
3640
+ const textFrames = Array.from(pageEl.querySelectorAll("frame, text-box, [draw\\:text-style-name]"));
3641
+ if (textFrames.length > 0) {
3642
+ textFrames.forEach((frame) => {
3643
+ const text = frame.textContent?.trim();
3644
+ if (text) {
3645
+ const p = document.createElement("div");
3646
+ p.style.margin = "12px 0";
3647
+ p.style.fontSize = idx === 0 ? "24px" : "16px";
3648
+ p.style.fontWeight = idx === 0 ? "bold" : "normal";
3649
+ p.style.color = "#1e293b";
3650
+ p.textContent = text;
3651
+ slide.appendChild(p);
3652
+ }
3653
+ });
3654
+ } else {
3655
+ slide.innerHTML = `<h3>Slide ${idx + 1}</h3><p>${pageEl.textContent?.trim() || ""}</p>`;
3656
+ }
3657
+ slides.push(slide);
3658
+ });
3659
+ return slides;
3660
+ }
3661
+ };
3662
+ function openDocumentPlugin() {
3663
+ return new OpenDocumentPlugin();
3664
+ }
3665
+ var DocPlugin = class {
3666
+ id = "doc";
3667
+ name = "Legacy Word Document Preview (.doc, .dot)";
3668
+ extensions = [".doc", ".dot"];
3669
+ mimeTypes = ["application/msword", "application/vnd.ms-word"];
3670
+ weight = 75;
3671
+ supports(file) {
3672
+ const ext = file.metadata.extension?.toLowerCase();
3673
+ const mime = file.metadata.mimeType?.toLowerCase();
3674
+ return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3675
+ }
3676
+ getToolbarActions(instance) {
3677
+ return [
3678
+ {
3679
+ id: "zoom-out",
3680
+ icon: "zoom-out",
3681
+ label: "Zoom Out",
3682
+ type: "button",
3683
+ group: "zoom",
3684
+ execute: () => instance.zoomOut?.()
3685
+ },
3686
+ {
3687
+ id: "zoom-in",
3688
+ icon: "zoom-in",
3689
+ label: "Zoom In",
3690
+ type: "button",
3691
+ group: "zoom",
3692
+ execute: () => instance.zoomIn?.()
3693
+ },
3694
+ {
3695
+ id: "fit-page",
3696
+ icon: "fit-page",
3697
+ label: "Fit to Page",
3698
+ type: "button",
3699
+ group: "zoom",
3700
+ execute: () => instance.fitToPage?.()
3701
+ },
3702
+ {
3703
+ id: "copy",
3704
+ icon: "copy",
3705
+ label: "Copy Text",
3706
+ type: "button",
3707
+ group: "actions",
3708
+ execute: () => instance.copy?.()
3709
+ },
3710
+ {
3711
+ id: "download",
3712
+ icon: "download",
3713
+ label: "Download",
3714
+ type: "button",
3715
+ group: "actions",
3716
+ execute: () => instance.download?.()
3717
+ },
3718
+ {
3719
+ id: "print",
3720
+ icon: "print",
3721
+ label: "Print",
3722
+ type: "button",
3723
+ group: "actions",
3724
+ execute: () => instance.print?.()
3725
+ }
3726
+ ];
3727
+ }
3728
+ async render(ctx) {
3729
+ const container = document.createElement("div");
3730
+ container.className = "fp-doc-container";
3731
+ container.style.width = "100%";
3732
+ container.style.height = "100%";
3733
+ container.style.overflow = "auto";
3734
+ container.style.padding = "32px 16px";
3735
+ container.style.backgroundColor = "#f1f5f9";
3736
+ const wrapper = document.createElement("div");
3737
+ wrapper.className = "fp-doc-wrapper";
3738
+ wrapper.style.maxWidth = "850px";
3739
+ wrapper.style.margin = "0 auto";
3740
+ wrapper.style.backgroundColor = "#ffffff";
3741
+ wrapper.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
3742
+ wrapper.style.borderRadius = "4px";
3743
+ wrapper.style.padding = "56px 48px";
3744
+ wrapper.style.minHeight = "100%";
3745
+ wrapper.style.transformOrigin = "top center";
3746
+ wrapper.style.transition = "transform 0.2s ease";
3747
+ wrapper.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
3748
+ wrapper.style.color = "#1e293b";
3749
+ container.appendChild(wrapper);
3750
+ ctx.container.appendChild(container);
3751
+ let scale = 1;
3752
+ let extractedRawText = "";
3753
+ try {
3754
+ const cfbf = new CfbfReader(ctx.buffer);
3755
+ const wordDocStream = cfbf.readStream("WordDocument");
3756
+ if (!wordDocStream || wordDocStream.length < 512) {
3757
+ throw new Error("WordDocument stream not found or invalid in CFBF archive");
3758
+ }
3759
+ const view = new DataView(wordDocStream.buffer, wordDocStream.byteOffset, wordDocStream.byteLength);
3760
+ const flags = view.getUint16(10, true);
3761
+ const is1Table = (flags & 512) !== 0;
3762
+ const tableName = is1Table ? "1Table" : "0Table";
3763
+ const tableStream = cfbf.readStream(tableName);
3764
+ const text = this.extractDocText(wordDocStream, tableStream);
3765
+ extractedRawText = text;
3766
+ wrapper.innerHTML = this.formatDocToHtml(text, ctx.metadata.name || "Document");
3767
+ } catch (err) {
3768
+ console.warn("[DocPlugin] Binary parsing error, fallback text:", err);
3769
+ const fallback = this.heuristicTextExtraction(ctx.buffer);
3770
+ extractedRawText = fallback;
3771
+ wrapper.innerHTML = `
3772
+ <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
3773
+ <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${ctx.metadata.name || "Word Document (.doc)"}</h2>
3774
+ <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
3775
+ </div>
3776
+ ${this.formatDocToHtml(fallback, ctx.metadata.name || "Document")}
3777
+ `;
3778
+ }
3779
+ const cleanup = () => {
3780
+ container.remove();
3781
+ ctx.container.innerHTML = "";
3782
+ };
3783
+ ctx.signal.addEventListener("abort", cleanup);
3784
+ return {
3785
+ destroy: cleanup,
3786
+ zoomIn: () => {
3787
+ scale += 0.1;
3788
+ wrapper.style.transform = `scale(${scale})`;
3789
+ },
3790
+ zoomOut: () => {
3791
+ scale = Math.max(0.2, scale - 0.1);
3792
+ wrapper.style.transform = `scale(${scale})`;
3793
+ },
3794
+ getZoom: () => scale,
3795
+ setZoom: (level) => {
3796
+ scale = level;
3797
+ wrapper.style.transform = `scale(${scale})`;
3798
+ },
3799
+ fitToPage: () => {
3800
+ scale = 1;
3801
+ wrapper.style.transform = "scale(1)";
3802
+ },
3803
+ copy: () => {
3804
+ navigator.clipboard.writeText(extractedRawText);
3805
+ },
3806
+ download: () => {
3807
+ const blob = new Blob([ctx.buffer], { type: "application/msword" });
3808
+ const url = URL.createObjectURL(blob);
3809
+ const a = document.createElement("a");
3810
+ a.href = url;
3811
+ a.download = ctx.metadata.name || "document.doc";
3812
+ a.click();
3813
+ URL.revokeObjectURL(url);
3814
+ },
3815
+ print: () => {
3816
+ window.print();
3817
+ }
3818
+ };
3819
+ }
3820
+ /**
3821
+ * Extract document text from WordDocument stream and Table stream
3822
+ */
3823
+ extractDocText(wordDoc, tableStream) {
3824
+ const view = new DataView(wordDoc.buffer, wordDoc.byteOffset, wordDoc.byteLength);
3825
+ let fcClx = 0;
3826
+ let lcbClx = 0;
3827
+ if (wordDoc.length >= 426) {
3828
+ fcClx = view.getUint32(418, true);
3829
+ lcbClx = view.getUint32(422, true);
3830
+ }
3831
+ if (tableStream && fcClx > 0 && lcbClx > 0 && fcClx + lcbClx <= tableStream.length) {
3832
+ try {
3833
+ const text = this.parsePieceTable(wordDoc, tableStream, fcClx, lcbClx);
3834
+ if (text && text.trim().length > 0) return text;
3835
+ } catch (err) {
3836
+ console.warn("[DocPlugin] Error in piece table parsing:", err);
3837
+ }
3838
+ }
3839
+ if (wordDoc.length > 2560) {
3840
+ const textChunk = wordDoc.subarray(2560);
3841
+ const extracted = this.extractStringsFromBytes(textChunk);
3842
+ if (extracted.trim().length > 0) return extracted;
3843
+ }
3844
+ return this.extractStringsFromBytes(wordDoc);
3845
+ }
3846
+ /**
3847
+ * Parse the CLX and Piece Table (Plcfpcd) according to [MS-DOC]
3848
+ */
3849
+ parsePieceTable(wordDoc, table, fcClx, lcbClx) {
3850
+ let offset = fcClx;
3851
+ const end = fcClx + lcbClx;
3852
+ while (offset < end) {
3853
+ const clxt = table[offset];
3854
+ if (clxt === 1) {
3855
+ const cb = new DataView(table.buffer, table.byteOffset + offset + 1).getUint16(0, true);
3856
+ offset += 3 + cb;
3857
+ } else if (clxt === 2) {
3858
+ offset += 1;
3859
+ const lcb = new DataView(table.buffer, table.byteOffset + offset).getUint32(0, true);
3860
+ offset += 4;
3861
+ return this.readPlcfpcd(wordDoc, table, offset, lcb);
3862
+ } else {
3863
+ break;
3864
+ }
3865
+ }
3866
+ return "";
3867
+ }
3868
+ readPlcfpcd(wordDoc, table, offset, lcb) {
3869
+ const view = new DataView(table.buffer, table.byteOffset + offset);
3870
+ const n = Math.floor((lcb - 4) / 12);
3871
+ if (n <= 0) return "";
3872
+ const cpOffsets = [];
3873
+ for (let i = 0; i <= n; i++) {
3874
+ cpOffsets.push(view.getUint32(i * 4, true));
3875
+ }
3876
+ const pcdOffset = (n + 1) * 4;
3877
+ let fullText = "";
3878
+ for (let i = 0; i < n; i++) {
3879
+ const fc = view.getUint32(pcdOffset + i * 8 + 2, true);
3880
+ const isCompressed = (fc & 1073741824) !== 0;
3881
+ const byteOffset = (fc & 1073741823) >> (isCompressed ? 1 : 0);
3882
+ const charCount = cpOffsets[i + 1] - cpOffsets[i];
3883
+ if (isCompressed) {
3884
+ const slice = wordDoc.subarray(byteOffset, byteOffset + charCount);
3885
+ fullText += new TextDecoder("latin1").decode(slice);
3886
+ } else {
3887
+ const slice = wordDoc.subarray(byteOffset, byteOffset + charCount * 2);
3888
+ fullText += new TextDecoder("utf-16le").decode(slice);
3889
+ }
3890
+ }
3891
+ return fullText;
3892
+ }
3893
+ /**
3894
+ * Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
3895
+ */
3896
+ extractStringsFromBytes(bytes) {
3897
+ const chars = [];
3898
+ const len = bytes.length;
3899
+ for (let i = 0; i < len; i++) {
3900
+ const b = bytes[i];
3901
+ if (b === 13 || b === 10 || b === 9 || b >= 32 && b <= 126 || b >= 160 && b <= 255) {
3902
+ chars.push(String.fromCharCode(b));
3903
+ } else if (b === 0 && i + 1 < len && bytes[i + 1] >= 32 && bytes[i + 1] <= 126) {
3904
+ chars.push(String.fromCharCode(bytes[i + 1]));
3905
+ i++;
3906
+ } else if (b === 7) {
3907
+ chars.push(" ");
3908
+ } else if (b === 12) {
3909
+ chars.push("\n\n---PAGE---\n\n");
3910
+ }
3911
+ }
3912
+ return chars.join("");
3913
+ }
3914
+ heuristicTextExtraction(buffer) {
3915
+ return this.extractStringsFromBytes(new Uint8Array(buffer));
3916
+ }
3917
+ /**
3918
+ * Format cleaned extracted text into attractive HTML paragraphs, headings, and lists
3919
+ */
3920
+ formatDocToHtml(text, filename) {
3921
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x0C/g, "\n\n").split("\n");
3922
+ let html = "";
3923
+ let inList = false;
3924
+ for (const rawLine of lines) {
3925
+ const line = rawLine.trim();
3926
+ if (!line) {
3927
+ if (inList) {
3928
+ html += "</ul>";
3929
+ inList = false;
3930
+ }
3931
+ continue;
3932
+ }
3933
+ if (/^[\x00-\x1F\x7F-\x9F]+$/.test(line)) continue;
3934
+ if (line.includes("Normal.dot") || line.includes("Microsoft Word") || line.includes("Times New Roman") && line.length < 30) {
3935
+ continue;
3936
+ }
3937
+ if (line.length < 60 && !line.endsWith(".") && (/^[A-Z0-9\s:_-]+$/.test(line) || line.startsWith("#"))) {
3938
+ if (inList) {
3939
+ html += "</ul>";
3940
+ inList = false;
3941
+ }
3942
+ html += `<h2 style="font-size: 18px; font-weight: 700; color: #1e3a8a; margin: 20px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify5.sanitize(line)}</h2>`;
3943
+ } else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
3944
+ if (!inList) {
3945
+ html += '<ul style="margin: 8px 0; padding-left: 24px;">';
3946
+ inList = true;
3947
+ }
3948
+ const bulletText = line.replace(/^[•\-\*]\s*/, "");
3949
+ html += `<li style="margin: 4px 0; line-height: 1.6;">${DOMPurify5.sanitize(bulletText)}</li>`;
3950
+ } else {
3951
+ if (inList) {
3952
+ html += "</ul>";
3953
+ inList = false;
3954
+ }
3955
+ html += `<p style="line-height: 1.7; margin: 10px 0; font-size: 14px; text-align: justify;">${DOMPurify5.sanitize(line)}</p>`;
3956
+ }
3957
+ }
3958
+ if (inList) html += "</ul>";
3959
+ return html || `<p style="color: #64748b; font-style: italic;">(No readable text found in ${DOMPurify5.sanitize(filename)})</p>`;
3960
+ }
3961
+ };
3962
+ function docPlugin() {
3963
+ return new DocPlugin();
3964
+ }
3965
+ var PptPlugin = class {
3966
+ id = "ppt";
3967
+ name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
3968
+ extensions = [".ppt", ".pps", ".pot"];
3969
+ mimeTypes = ["application/vnd.ms-powerpoint"];
3970
+ weight = 75;
3971
+ supports(file) {
3972
+ const ext = file.metadata.extension?.toLowerCase();
3973
+ const mime = file.metadata.mimeType?.toLowerCase();
3974
+ return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3975
+ }
3976
+ getToolbarActions(instance) {
3977
+ return [
3978
+ {
3979
+ id: "thumbnails",
3980
+ icon: "thumbnails",
3981
+ label: "Slide Thumbnails",
3982
+ type: "button",
3983
+ group: "navigation",
3984
+ execute: () => instance.toggleThumbnails?.()
3985
+ },
3986
+ {
3987
+ id: "page-prev",
3988
+ icon: "page-prev",
3989
+ label: "Previous Slide",
3990
+ type: "button",
3991
+ group: "navigation",
3992
+ execute: () => {
3993
+ const cur = instance.getCurrentPage?.() ?? 1;
3994
+ if (cur > 1) instance.goToPage?.(cur - 1);
3995
+ }
3996
+ },
3997
+ {
3998
+ id: "page-nav",
3999
+ icon: "",
4000
+ label: "Slide Number",
4001
+ type: "page-nav",
4002
+ group: "navigation",
4003
+ execute: (p) => instance.goToPage?.(Number(p))
4004
+ },
4005
+ {
4006
+ id: "page-next",
4007
+ icon: "page-next",
4008
+ label: "Next Slide",
4009
+ type: "button",
4010
+ group: "navigation",
4011
+ execute: () => {
4012
+ const cur = instance.getCurrentPage?.() ?? 1;
4013
+ const total = instance.getPageCount?.() ?? 1;
4014
+ if (cur < total) instance.goToPage?.(cur + 1);
4015
+ }
4016
+ },
4017
+ {
4018
+ id: "zoom-out",
4019
+ icon: "zoom-out",
4020
+ label: "Zoom Out",
4021
+ type: "button",
4022
+ group: "zoom",
4023
+ execute: () => instance.zoomOut?.()
4024
+ },
4025
+ {
4026
+ id: "zoom-in",
4027
+ icon: "zoom-in",
4028
+ label: "Zoom In",
4029
+ type: "button",
4030
+ group: "zoom",
4031
+ execute: () => instance.zoomIn?.()
4032
+ },
4033
+ {
4034
+ id: "fit-page",
4035
+ icon: "fit-page",
4036
+ label: "Fit to Slide",
4037
+ type: "button",
4038
+ group: "zoom",
4039
+ execute: () => instance.fitToPage?.()
4040
+ },
4041
+ {
4042
+ id: "download",
4043
+ icon: "download",
4044
+ label: "Download",
4045
+ type: "button",
4046
+ group: "actions",
4047
+ execute: () => instance.download?.()
4048
+ },
4049
+ {
4050
+ id: "print",
4051
+ icon: "print",
4052
+ label: "Print",
4053
+ type: "button",
4054
+ group: "actions",
4055
+ execute: () => instance.print?.()
4056
+ }
4057
+ ];
4058
+ }
4059
+ async render(ctx) {
4060
+ const container = document.createElement("div");
4061
+ container.className = "fp-ppt-container";
4062
+ container.style.width = "100%";
4063
+ container.style.height = "100%";
4064
+ container.style.overflow = "auto";
4065
+ container.style.display = "flex";
4066
+ container.style.justifyContent = "center";
4067
+ container.style.alignItems = "center";
4068
+ container.style.padding = "32px 16px";
4069
+ container.style.backgroundColor = "#0f172a";
4070
+ const slideCard = document.createElement("div");
4071
+ slideCard.className = "fp-ppt-slide-card";
4072
+ slideCard.style.width = "960px";
4073
+ slideCard.style.maxWidth = "90%";
4074
+ slideCard.style.aspectRatio = "16 / 9";
4075
+ slideCard.style.backgroundColor = "#ffffff";
4076
+ slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
4077
+ slideCard.style.borderRadius = "8px";
4078
+ slideCard.style.padding = "48px";
4079
+ slideCard.style.display = "flex";
4080
+ slideCard.style.flexDirection = "column";
4081
+ slideCard.style.justifyContent = "center";
4082
+ slideCard.style.alignItems = "center";
4083
+ slideCard.style.boxSizing = "border-box";
4084
+ slideCard.style.position = "relative";
4085
+ slideCard.style.overflow = "hidden";
4086
+ slideCard.style.transformOrigin = "center center";
4087
+ slideCard.style.transition = "transform 0.2s ease";
4088
+ container.appendChild(slideCard);
4089
+ ctx.container.appendChild(container);
4090
+ let scale = 1;
4091
+ let currentSlide = 1;
4092
+ let slides = [];
4093
+ try {
4094
+ const cfbf = new CfbfReader(ctx.buffer);
4095
+ const pptStream = cfbf.readStream("PowerPoint Document");
4096
+ if (!pptStream || pptStream.length < 512) {
4097
+ throw new Error("PowerPoint Document stream not found in CFBF container");
4098
+ }
4099
+ slides = this.extractSlides(pptStream);
4100
+ } catch (err) {
4101
+ console.warn("[PptPlugin] Error extracting binary slides:", err);
4102
+ }
4103
+ if (slides.length === 0) {
4104
+ slides = [
4105
+ {
4106
+ title: ctx.metadata.name || "PowerPoint Presentation",
4107
+ texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
4108
+ }
4109
+ ];
4110
+ }
4111
+ const totalSlides = slides.length;
4112
+ const renderSlide = (idx) => {
4113
+ currentSlide = idx;
4114
+ const s = slides[idx - 1];
4115
+ if (!s) return;
4116
+ slideCard.innerHTML = `
4117
+ <div style="position: absolute; top: 20px; right: 24px; font-size: 12px; color: #94a3b8; font-weight: 600;">
4118
+ Slide ${idx} of ${totalSlides}
4119
+ </div>
4120
+ <div style="text-align: center; width: 100%;">
4121
+ <h1 style="font-size: ${idx === 1 ? "36px" : "28px"}; color: #1e3a8a; margin: 0 0 24px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 700;">
4122
+ ${DOMPurify5.sanitize(s.title || `Slide ${idx}`)}
4123
+ </h1>
4124
+ <div style="display: flex; flex-direction: column; gap: 12px; max-width: 80%; margin: 0 auto; text-align: ${idx === 1 ? "center" : "left"};">
4125
+ ${s.texts.map((t) => `<div style="font-size: 18px; color: #334155; line-height: 1.5; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${DOMPurify5.sanitize(t)}</div>`).join("")}
4126
+ </div>
4127
+ </div>
4128
+ `;
4129
+ ctx.emit("page-change", { page: currentSlide, total: totalSlides });
4130
+ };
4131
+ renderSlide(1);
4132
+ const cleanup = () => {
4133
+ container.remove();
4134
+ ctx.container.innerHTML = "";
4135
+ };
4136
+ ctx.signal.addEventListener("abort", cleanup);
4137
+ return {
4138
+ destroy: cleanup,
4139
+ zoomIn: () => {
4140
+ scale += 0.1;
4141
+ slideCard.style.transform = `scale(${scale})`;
4142
+ },
4143
+ zoomOut: () => {
4144
+ scale = Math.max(0.3, scale - 0.1);
4145
+ slideCard.style.transform = `scale(${scale})`;
4146
+ },
4147
+ getZoom: () => scale,
4148
+ setZoom: (level) => {
4149
+ scale = level;
4150
+ slideCard.style.transform = `scale(${scale})`;
4151
+ },
4152
+ fitToPage: () => {
4153
+ scale = 1;
4154
+ slideCard.style.transform = "scale(1)";
4155
+ },
4156
+ goToPage: (page) => {
4157
+ if (page >= 1 && page <= totalSlides) {
4158
+ renderSlide(page);
4159
+ }
4160
+ },
4161
+ getPageCount: () => totalSlides,
4162
+ getCurrentPage: () => currentSlide,
4163
+ getThumbnails: async () => {
4164
+ return slides.map((s, idx) => ({
4165
+ index: idx,
4166
+ label: `Slide ${idx + 1}`,
4167
+ render: async (canvas) => {
4168
+ const ctx2d = canvas.getContext("2d");
4169
+ if (!ctx2d) return;
4170
+ canvas.width = 160;
4171
+ canvas.height = 90;
4172
+ ctx2d.fillStyle = "#ffffff";
4173
+ ctx2d.fillRect(0, 0, 160, 90);
4174
+ ctx2d.fillStyle = "#1e3a8a";
4175
+ ctx2d.font = "bold 11px sans-serif";
4176
+ ctx2d.textAlign = "center";
4177
+ const title = s.title.slice(0, 18) || `Slide ${idx + 1}`;
4178
+ ctx2d.fillText(title, 80, 50);
4179
+ }
4180
+ }));
4181
+ },
4182
+ download: () => {
4183
+ const blob = new Blob([ctx.buffer], { type: "application/vnd.ms-powerpoint" });
4184
+ const url = URL.createObjectURL(blob);
4185
+ const a = document.createElement("a");
4186
+ a.href = url;
4187
+ a.download = ctx.metadata.name || "presentation.ppt";
4188
+ a.click();
4189
+ URL.revokeObjectURL(url);
4190
+ },
4191
+ print: () => {
4192
+ window.print();
4193
+ }
4194
+ };
4195
+ }
4196
+ /**
4197
+ * Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
4198
+ */
4199
+ extractSlides(stream) {
4200
+ const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
4201
+ const len = stream.length;
4202
+ let offset = 0;
4203
+ const slides = [];
4204
+ let currentSlideTexts = [];
4205
+ while (offset + 8 <= len) {
4206
+ const recVerInst = view.getUint16(offset, true);
4207
+ const recType = view.getUint16(offset + 2, true);
4208
+ const recLen = view.getUint32(offset + 4, true);
4209
+ if (recType === 1006) {
4210
+ if (currentSlideTexts.length > 0) {
4211
+ const title = currentSlideTexts[0] || "Slide";
4212
+ const texts = currentSlideTexts.slice(1);
4213
+ slides.push({ title, texts });
4214
+ currentSlideTexts = [];
4215
+ }
4216
+ offset += 8;
4217
+ continue;
4218
+ }
4219
+ if (recType === 3998 && recLen > 0 && offset + 8 + recLen <= len) {
4220
+ const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4221
+ const text = new TextDecoder("latin1").decode(bytes).trim();
4222
+ if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4223
+ currentSlideTexts.push(text);
4224
+ }
4225
+ }
4226
+ if (recType === 3999 && recLen > 0 && offset + 8 + recLen <= len) {
4227
+ const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4228
+ const text = new TextDecoder("utf-16le").decode(bytes).trim();
4229
+ if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4230
+ currentSlideTexts.push(text);
4231
+ }
4232
+ }
4233
+ const isContainer = (recVerInst & 15) === 15;
4234
+ if (isContainer) {
4235
+ offset += 8;
4236
+ } else {
4237
+ offset += 8 + recLen;
4238
+ }
4239
+ }
4240
+ if (currentSlideTexts.length > 0) {
4241
+ const title = currentSlideTexts[0] || "Slide";
4242
+ const texts = currentSlideTexts.slice(1);
4243
+ slides.push({ title, texts });
4244
+ }
4245
+ if (slides.length === 0) {
4246
+ const rawText = new TextDecoder("latin1", { fatal: false }).decode(stream);
4247
+ const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{4,}/g) || [];
4248
+ const filtered = matches.map((m) => m.trim()).filter((m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times"));
4249
+ if (filtered.length > 0) {
4250
+ const chunkSize = 4;
4251
+ for (let i = 0; i < filtered.length; i += chunkSize) {
4252
+ const chunk = filtered.slice(i, i + chunkSize);
4253
+ slides.push({
4254
+ title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
4255
+ texts: chunk.slice(1)
4256
+ });
4257
+ }
4258
+ }
4259
+ }
4260
+ return slides;
4261
+ }
4262
+ };
4263
+ function pptPlugin() {
4264
+ return new PptPlugin();
4265
+ }
2695
4266
 
2696
4267
  // src/index.ts
2697
4268
  function getDefaultPlugins() {
@@ -2701,6 +4272,11 @@ function getDefaultPlugins() {
2701
4272
  docxPlugin(),
2702
4273
  excelPlugin(),
2703
4274
  pptxPlugin(),
4275
+ docPlugin(),
4276
+ pptPlugin(),
4277
+ openDocumentPlugin(),
4278
+ rtfPlugin(),
4279
+ htmlPreviewPlugin(),
2704
4280
  csvPlugin(),
2705
4281
  archivePlugin(),
2706
4282
  markdownPlugin(),