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