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