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