@files-preview-app/preview-file 1.1.6 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/angular.cjs +1630 -54
- package/dist/angular.cjs.map +1 -1
- package/dist/angular.js +1629 -53
- package/dist/angular.js.map +1 -1
- package/dist/index.cjs +1637 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +1631 -55
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1630 -54
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +5 -0
- package/dist/react.d.ts +5 -0
- package/dist/react.js +1629 -53
- package/dist/react.js.map +1 -1
- package/dist/vue.cjs +1630 -54
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +1629 -53
- package/dist/vue.js.map +1 -1
- package/package.json +10 -3
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var DOMPurify5 = require('dompurify');
|
|
4
4
|
var docx = require('docx-preview');
|
|
5
|
-
var
|
|
5
|
+
var XLSX = require('xlsx');
|
|
6
6
|
var hljs = require('highlight.js');
|
|
7
7
|
var fflate = require('fflate');
|
|
8
8
|
var marked = require('marked');
|
|
@@ -11,6 +11,7 @@ var THREE = require('three');
|
|
|
11
11
|
var STLLoader_js = require('three/examples/jsm/loaders/STLLoader.js');
|
|
12
12
|
var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
|
|
13
13
|
var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
|
|
14
|
+
var rtf_js = require('rtf.js');
|
|
14
15
|
|
|
15
16
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
16
17
|
|
|
@@ -32,9 +33,9 @@ function _interopNamespace(e) {
|
|
|
32
33
|
return Object.freeze(n);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
var
|
|
36
|
+
var DOMPurify5__default = /*#__PURE__*/_interopDefault(DOMPurify5);
|
|
36
37
|
var docx__namespace = /*#__PURE__*/_interopNamespace(docx);
|
|
37
|
-
var
|
|
38
|
+
var XLSX__namespace = /*#__PURE__*/_interopNamespace(XLSX);
|
|
38
39
|
var hljs__default = /*#__PURE__*/_interopDefault(hljs);
|
|
39
40
|
var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
|
|
40
41
|
|
|
@@ -100,7 +101,9 @@ var MAGIC_NUMBERS = [
|
|
|
100
101
|
{ bytes: [37, 80, 68, 70], mime: "application/pdf" },
|
|
101
102
|
// %PDF
|
|
102
103
|
{ bytes: [80, 75, 3, 4], mime: "application/zip" },
|
|
103
|
-
// PK.. (ZIP/DOCX/XLSX/PPTX)
|
|
104
|
+
// PK.. (ZIP/DOCX/XLSX/PPTX/ODT/ODS/ODP)
|
|
105
|
+
{ bytes: [208, 207, 17, 224, 161, 177, 26, 225], mime: "application/x-cfbf" },
|
|
106
|
+
// CFBF/OLE2 (DOC/XLS/PPT)
|
|
104
107
|
{ bytes: [137, 80, 78, 71, 13, 10, 26, 10], mime: "image/png" },
|
|
105
108
|
// PNG
|
|
106
109
|
{ bytes: [255, 216, 255], mime: "image/jpeg" },
|
|
@@ -135,14 +138,40 @@ var MAGIC_NUMBERS = [
|
|
|
135
138
|
// RTF
|
|
136
139
|
];
|
|
137
140
|
var EXTENSION_MIME_MAP = {
|
|
138
|
-
// Documents
|
|
141
|
+
// Documents — Modern OOXML
|
|
139
142
|
".pdf": "application/pdf",
|
|
140
143
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
144
|
+
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
|
145
|
+
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
|
146
|
+
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
|
|
141
147
|
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
148
|
+
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
|
|
149
|
+
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
|
150
|
+
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
|
151
|
+
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
|
|
142
152
|
".xls": "application/vnd.ms-excel",
|
|
143
153
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
154
|
+
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
|
155
|
+
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
|
156
|
+
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
|
157
|
+
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
|
|
158
|
+
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
|
|
144
159
|
".csv": "text/csv",
|
|
145
160
|
".tsv": "text/tab-separated-values",
|
|
161
|
+
// Documents — Legacy Binary (CFBF/OLE2)
|
|
162
|
+
".doc": "application/msword",
|
|
163
|
+
".dot": "application/msword",
|
|
164
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
165
|
+
".pps": "application/vnd.ms-powerpoint",
|
|
166
|
+
".pot": "application/vnd.ms-powerpoint",
|
|
167
|
+
// Documents — OpenDocument (ODF)
|
|
168
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
169
|
+
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
170
|
+
".odp": "application/vnd.oasis.opendocument.presentation",
|
|
171
|
+
".odg": "application/vnd.oasis.opendocument.graphics",
|
|
172
|
+
".odf": "application/vnd.oasis.opendocument.formula",
|
|
173
|
+
// Documents — RTF
|
|
174
|
+
".rtf": "text/rtf",
|
|
146
175
|
// Images
|
|
147
176
|
".png": "image/png",
|
|
148
177
|
".jpg": "image/jpeg",
|
|
@@ -236,7 +265,7 @@ function detectMagicBytes(buffer) {
|
|
|
236
265
|
}
|
|
237
266
|
function detectOoxmlType(buffer) {
|
|
238
267
|
const text = new TextDecoder("ascii", { fatal: false }).decode(
|
|
239
|
-
new Uint8Array(buffer.slice(0,
|
|
268
|
+
new Uint8Array(buffer.slice(0, 8e3))
|
|
240
269
|
);
|
|
241
270
|
if (text.includes("word/")) {
|
|
242
271
|
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
@@ -247,6 +276,21 @@ function detectOoxmlType(buffer) {
|
|
|
247
276
|
if (text.includes("ppt/")) {
|
|
248
277
|
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
249
278
|
}
|
|
279
|
+
if (text.includes("application/vnd.oasis.opendocument.text")) {
|
|
280
|
+
return "application/vnd.oasis.opendocument.text";
|
|
281
|
+
}
|
|
282
|
+
if (text.includes("application/vnd.oasis.opendocument.spreadsheet")) {
|
|
283
|
+
return "application/vnd.oasis.opendocument.spreadsheet";
|
|
284
|
+
}
|
|
285
|
+
if (text.includes("application/vnd.oasis.opendocument.presentation")) {
|
|
286
|
+
return "application/vnd.oasis.opendocument.presentation";
|
|
287
|
+
}
|
|
288
|
+
if (text.includes("application/vnd.oasis.opendocument.graphics")) {
|
|
289
|
+
return "application/vnd.oasis.opendocument.graphics";
|
|
290
|
+
}
|
|
291
|
+
if (text.includes("application/vnd.oasis.opendocument.formula")) {
|
|
292
|
+
return "application/vnd.oasis.opendocument.formula";
|
|
293
|
+
}
|
|
250
294
|
return "application/zip";
|
|
251
295
|
}
|
|
252
296
|
function extractExtension(nameOrUrl) {
|
|
@@ -323,12 +367,12 @@ async function sourceToArrayBuffer(source, signal) {
|
|
|
323
367
|
return { buffer, metadata };
|
|
324
368
|
}
|
|
325
369
|
function sanitizeHTML(html) {
|
|
326
|
-
return
|
|
370
|
+
return DOMPurify5__default.default.sanitize(html, {
|
|
327
371
|
USE_PROFILES: { html: true }
|
|
328
372
|
});
|
|
329
373
|
}
|
|
330
374
|
function sanitizeSVG(svg) {
|
|
331
|
-
return
|
|
375
|
+
return DOMPurify5__default.default.sanitize(svg, {
|
|
332
376
|
USE_PROFILES: { svg: true, svgFilters: true }
|
|
333
377
|
});
|
|
334
378
|
}
|
|
@@ -935,6 +979,192 @@ var FilePreviewViewer = class {
|
|
|
935
979
|
this.contentEl.appendChild(errorEl);
|
|
936
980
|
}
|
|
937
981
|
};
|
|
982
|
+
var CfbfReader = class {
|
|
983
|
+
view;
|
|
984
|
+
u8;
|
|
985
|
+
sectorSize;
|
|
986
|
+
miniSectorSize;
|
|
987
|
+
miniStreamCutoff;
|
|
988
|
+
fat = [];
|
|
989
|
+
miniFat = [];
|
|
990
|
+
entries = /* @__PURE__ */ new Map();
|
|
991
|
+
miniStream = new Uint8Array(0);
|
|
992
|
+
constructor(buffer) {
|
|
993
|
+
this.u8 = new Uint8Array(buffer);
|
|
994
|
+
this.view = new DataView(buffer);
|
|
995
|
+
if (!this.isValid()) {
|
|
996
|
+
throw new Error("Invalid CFBF / OLE2 file header signature");
|
|
997
|
+
}
|
|
998
|
+
const sectorShift = this.view.getUint16(30, true);
|
|
999
|
+
this.sectorSize = 1 << sectorShift;
|
|
1000
|
+
const miniSectorShift = this.view.getUint16(32, true);
|
|
1001
|
+
this.miniSectorSize = 1 << miniSectorShift;
|
|
1002
|
+
this.miniStreamCutoff = this.view.getUint32(56, true) || 4096;
|
|
1003
|
+
this.parseFat();
|
|
1004
|
+
this.parseDirectory();
|
|
1005
|
+
this.parseMiniFat();
|
|
1006
|
+
}
|
|
1007
|
+
/** Check if the magic 8-byte signature matches [MS-CFB] */
|
|
1008
|
+
isValid() {
|
|
1009
|
+
if (this.u8.length < 512) return false;
|
|
1010
|
+
const sig = [208, 207, 17, 224, 161, 177, 26, 225];
|
|
1011
|
+
return sig.every((b, i) => this.u8[i] === b);
|
|
1012
|
+
}
|
|
1013
|
+
/** Return all directory stream/storage entries found in the file */
|
|
1014
|
+
listEntries() {
|
|
1015
|
+
return Array.from(this.entries.values());
|
|
1016
|
+
}
|
|
1017
|
+
/** Check if a stream exists */
|
|
1018
|
+
hasStream(name) {
|
|
1019
|
+
return this.entries.has(name) || this.findEntryCaseInsensitive(name) !== null;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Read raw bytes of a named stream (e.g. "WordDocument", "PowerPoint Document", "Workbook")
|
|
1023
|
+
*/
|
|
1024
|
+
readStream(name) {
|
|
1025
|
+
const entry = this.entries.get(name) || this.findEntryCaseInsensitive(name);
|
|
1026
|
+
if (!entry) return null;
|
|
1027
|
+
if (entry.size === 0) return new Uint8Array(0);
|
|
1028
|
+
if (entry.size < this.miniStreamCutoff && entry.type !== 5) {
|
|
1029
|
+
return this.readMiniStream(entry.startSector, entry.size);
|
|
1030
|
+
} else {
|
|
1031
|
+
return this.readRegularStream(entry.startSector, entry.size);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
findEntryCaseInsensitive(name) {
|
|
1035
|
+
const lower = name.toLowerCase();
|
|
1036
|
+
for (const [key, val] of this.entries.entries()) {
|
|
1037
|
+
if (key.toLowerCase() === lower) return val;
|
|
1038
|
+
}
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
getSectorOffset(sectId) {
|
|
1042
|
+
return (sectId + 1) * this.sectorSize;
|
|
1043
|
+
}
|
|
1044
|
+
parseFat() {
|
|
1045
|
+
this.view.getUint32(44, true);
|
|
1046
|
+
const difat = [];
|
|
1047
|
+
for (let i = 0; i < 109; i++) {
|
|
1048
|
+
const sect = this.view.getUint32(76 + i * 4, true);
|
|
1049
|
+
if (sect !== 4294967295 && sect !== 4294967294) {
|
|
1050
|
+
difat.push(sect);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
let difatSect = this.view.getUint32(68, true);
|
|
1054
|
+
const csectDif = this.view.getUint32(72, true);
|
|
1055
|
+
const entriesPerSector = this.sectorSize / 4 - 1;
|
|
1056
|
+
for (let i = 0; i < csectDif && difatSect < 4294967294; i++) {
|
|
1057
|
+
const offset = this.getSectorOffset(difatSect);
|
|
1058
|
+
for (let j = 0; j < entriesPerSector; j++) {
|
|
1059
|
+
const sect = this.view.getUint32(offset + j * 4, true);
|
|
1060
|
+
if (sect !== 4294967295 && sect !== 4294967294) {
|
|
1061
|
+
difat.push(sect);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
difatSect = this.view.getUint32(offset + entriesPerSector * 4, true);
|
|
1065
|
+
}
|
|
1066
|
+
for (const fatSect of difat) {
|
|
1067
|
+
if (fatSect >= 4294967294) continue;
|
|
1068
|
+
const offset = this.getSectorOffset(fatSect);
|
|
1069
|
+
const count = this.sectorSize / 4;
|
|
1070
|
+
for (let j = 0; j < count; j++) {
|
|
1071
|
+
this.fat.push(this.view.getUint32(offset + j * 4, true));
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
parseDirectory() {
|
|
1076
|
+
const dirStartSect = this.view.getUint32(48, true);
|
|
1077
|
+
const dirBytes = this.readRegularStream(dirStartSect);
|
|
1078
|
+
const dirView = new DataView(dirBytes.buffer, dirBytes.byteOffset, dirBytes.byteLength);
|
|
1079
|
+
const entryCount = dirBytes.length / 128;
|
|
1080
|
+
for (let i = 0; i < entryCount; i++) {
|
|
1081
|
+
const offset = i * 128;
|
|
1082
|
+
const type = dirView.getUint8(offset + 66);
|
|
1083
|
+
if (type === 0) continue;
|
|
1084
|
+
const nameLen = dirView.getUint16(offset + 64, true);
|
|
1085
|
+
let name = "";
|
|
1086
|
+
if (nameLen > 2) {
|
|
1087
|
+
const charCount = nameLen / 2 - 1;
|
|
1088
|
+
const chars = [];
|
|
1089
|
+
for (let c = 0; c < charCount; c++) {
|
|
1090
|
+
const charCode = dirView.getUint16(offset + c * 2, true);
|
|
1091
|
+
chars.push(String.fromCharCode(charCode));
|
|
1092
|
+
}
|
|
1093
|
+
name = chars.join("");
|
|
1094
|
+
}
|
|
1095
|
+
const startSector = dirView.getUint32(offset + 116, true);
|
|
1096
|
+
const size = dirView.getUint32(offset + 120, true);
|
|
1097
|
+
const entry = { name, type, size, startSector };
|
|
1098
|
+
this.entries.set(name, entry);
|
|
1099
|
+
if (type === 5) {
|
|
1100
|
+
this.miniStream = this.readRegularStream(startSector, size);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
parseMiniFat() {
|
|
1105
|
+
const miniFatStart = this.view.getUint32(60, true);
|
|
1106
|
+
const csectMiniFat = this.view.getUint32(64, true);
|
|
1107
|
+
if (miniFatStart >= 4294967294 || csectMiniFat === 0) return;
|
|
1108
|
+
let sect = miniFatStart;
|
|
1109
|
+
for (let i = 0; i < csectMiniFat && sect < 4294967294; i++) {
|
|
1110
|
+
const offset = this.getSectorOffset(sect);
|
|
1111
|
+
const count = this.sectorSize / 4;
|
|
1112
|
+
for (let j = 0; j < count; j++) {
|
|
1113
|
+
this.miniFat.push(this.view.getUint32(offset + j * 4, true));
|
|
1114
|
+
}
|
|
1115
|
+
sect = this.fat[sect] ?? 4294967294;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
readRegularStream(startSector, targetSize) {
|
|
1119
|
+
if (startSector >= 4294967294) return new Uint8Array(0);
|
|
1120
|
+
const chunks = [];
|
|
1121
|
+
let curr = startSector;
|
|
1122
|
+
let totalBytes = 0;
|
|
1123
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1124
|
+
while (curr < 4294967294 && !visited.has(curr)) {
|
|
1125
|
+
visited.add(curr);
|
|
1126
|
+
const offset = this.getSectorOffset(curr);
|
|
1127
|
+
if (offset + this.sectorSize <= this.u8.length) {
|
|
1128
|
+
chunks.push(this.u8.subarray(offset, offset + this.sectorSize));
|
|
1129
|
+
totalBytes += this.sectorSize;
|
|
1130
|
+
}
|
|
1131
|
+
curr = this.fat[curr] ?? 4294967294;
|
|
1132
|
+
}
|
|
1133
|
+
const result = new Uint8Array(totalBytes);
|
|
1134
|
+
let pos = 0;
|
|
1135
|
+
for (const chunk of chunks) {
|
|
1136
|
+
result.set(chunk, pos);
|
|
1137
|
+
pos += chunk.length;
|
|
1138
|
+
}
|
|
1139
|
+
if (targetSize !== void 0 && targetSize < result.length) {
|
|
1140
|
+
return result.subarray(0, targetSize);
|
|
1141
|
+
}
|
|
1142
|
+
return result;
|
|
1143
|
+
}
|
|
1144
|
+
readMiniStream(startSector, targetSize) {
|
|
1145
|
+
if (this.miniStream.length === 0 || startSector >= 4294967294) return new Uint8Array(0);
|
|
1146
|
+
const chunks = [];
|
|
1147
|
+
let curr = startSector;
|
|
1148
|
+
let totalBytes = 0;
|
|
1149
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1150
|
+
while (curr < 4294967294 && !visited.has(curr)) {
|
|
1151
|
+
visited.add(curr);
|
|
1152
|
+
const offset = curr * this.miniSectorSize;
|
|
1153
|
+
if (offset + this.miniSectorSize <= this.miniStream.length) {
|
|
1154
|
+
chunks.push(this.miniStream.subarray(offset, offset + this.miniSectorSize));
|
|
1155
|
+
totalBytes += this.miniSectorSize;
|
|
1156
|
+
}
|
|
1157
|
+
curr = this.miniFat[curr] ?? 4294967294;
|
|
1158
|
+
}
|
|
1159
|
+
const result = new Uint8Array(totalBytes);
|
|
1160
|
+
let pos = 0;
|
|
1161
|
+
for (const chunk of chunks) {
|
|
1162
|
+
result.set(chunk, pos);
|
|
1163
|
+
pos += chunk.length;
|
|
1164
|
+
}
|
|
1165
|
+
return result.subarray(0, targetSize);
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
938
1168
|
|
|
939
1169
|
// ../plugins/pdf/dist/index.js
|
|
940
1170
|
var PdfPlugin = class {
|
|
@@ -1345,13 +1575,18 @@ function mediaPlugin() {
|
|
|
1345
1575
|
var DocxPlugin = class {
|
|
1346
1576
|
id = "docx";
|
|
1347
1577
|
name = "Word Document Preview";
|
|
1348
|
-
extensions = [".docx"];
|
|
1349
|
-
mimeTypes = [
|
|
1578
|
+
extensions = [".docx", ".docm", ".dotx", ".dotm"];
|
|
1579
|
+
mimeTypes = [
|
|
1580
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1581
|
+
"application/vnd.ms-word.document.macroEnabled.12",
|
|
1582
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
|
1583
|
+
"application/vnd.ms-word.template.macroEnabled.12"
|
|
1584
|
+
];
|
|
1350
1585
|
weight = 80;
|
|
1351
1586
|
supports(file) {
|
|
1352
1587
|
const ext = file.metadata.extension?.toLowerCase();
|
|
1353
1588
|
const mime = file.metadata.mimeType?.toLowerCase();
|
|
1354
|
-
return ext
|
|
1589
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
1355
1590
|
}
|
|
1356
1591
|
getToolbarActions(instance) {
|
|
1357
1592
|
return [
|
|
@@ -1475,11 +1710,16 @@ function docxPlugin() {
|
|
|
1475
1710
|
}
|
|
1476
1711
|
var ExcelPlugin = class {
|
|
1477
1712
|
id = "excel";
|
|
1478
|
-
name = "Excel
|
|
1479
|
-
extensions = [".xlsx", ".xls"];
|
|
1713
|
+
name = "Spreadsheet Preview (Excel / OpenDocument)";
|
|
1714
|
+
extensions = [".xlsx", ".xls", ".xlsm", ".xlsb", ".xltx", ".xltm", ".ods"];
|
|
1480
1715
|
mimeTypes = [
|
|
1481
1716
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1482
|
-
"application/vnd.ms-excel"
|
|
1717
|
+
"application/vnd.ms-excel",
|
|
1718
|
+
"application/vnd.ms-excel.sheet.macroEnabled.12",
|
|
1719
|
+
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
|
1720
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
|
1721
|
+
"application/vnd.ms-excel.template.macroEnabled.12",
|
|
1722
|
+
"application/vnd.oasis.opendocument.spreadsheet"
|
|
1483
1723
|
];
|
|
1484
1724
|
weight = 80;
|
|
1485
1725
|
supports(file) {
|
|
@@ -1543,70 +1783,111 @@ var ExcelPlugin = class {
|
|
|
1543
1783
|
}
|
|
1544
1784
|
async render(ctx) {
|
|
1545
1785
|
const container = document.createElement("div");
|
|
1786
|
+
container.className = "fp-excel-container";
|
|
1546
1787
|
container.style.display = "flex";
|
|
1547
1788
|
container.style.flexDirection = "column";
|
|
1548
1789
|
container.style.width = "100%";
|
|
1549
1790
|
container.style.height = "100%";
|
|
1550
1791
|
container.style.overflow = "hidden";
|
|
1551
1792
|
const contentArea = document.createElement("div");
|
|
1793
|
+
contentArea.className = "fp-excel-content";
|
|
1552
1794
|
contentArea.style.flex = "1";
|
|
1553
1795
|
contentArea.style.overflow = "auto";
|
|
1554
1796
|
contentArea.style.padding = "16px";
|
|
1555
1797
|
contentArea.style.transformOrigin = "top left";
|
|
1556
1798
|
const tabsArea = document.createElement("div");
|
|
1799
|
+
tabsArea.className = "fp-excel-tabs";
|
|
1557
1800
|
tabsArea.style.display = "flex";
|
|
1558
|
-
tabsArea.style.gap = "
|
|
1801
|
+
tabsArea.style.gap = "6px";
|
|
1559
1802
|
tabsArea.style.padding = "8px 16px";
|
|
1560
1803
|
tabsArea.style.borderTop = "1px solid #e0e0e0";
|
|
1561
1804
|
tabsArea.style.backgroundColor = "#fafafa";
|
|
1562
1805
|
tabsArea.style.overflowX = "auto";
|
|
1806
|
+
tabsArea.style.flexShrink = "0";
|
|
1563
1807
|
container.appendChild(contentArea);
|
|
1564
1808
|
container.appendChild(tabsArea);
|
|
1565
1809
|
ctx.container.appendChild(container);
|
|
1566
1810
|
let scale = 1;
|
|
1567
1811
|
let currentSheetIndex = 1;
|
|
1568
|
-
|
|
1812
|
+
let sheetNames = [];
|
|
1813
|
+
let wb = null;
|
|
1569
1814
|
try {
|
|
1570
|
-
|
|
1571
|
-
|
|
1815
|
+
wb = XLSX__namespace.read(new Uint8Array(ctx.buffer), { type: "array", cellDates: true });
|
|
1816
|
+
sheetNames = wb.SheetNames || [];
|
|
1817
|
+
} catch (err) {
|
|
1818
|
+
console.warn("[ExcelPlugin] SheetJS parse failed, rendering fallback:", err);
|
|
1572
1819
|
}
|
|
1820
|
+
const tabButtons = [];
|
|
1573
1821
|
const renderSheet = (index) => {
|
|
1822
|
+
if (!wb || index < 1 || index > sheetNames.length) return;
|
|
1823
|
+
currentSheetIndex = index;
|
|
1824
|
+
const sheetName = sheetNames[index - 1];
|
|
1825
|
+
const ws = wb.Sheets[sheetName];
|
|
1574
1826
|
contentArea.innerHTML = "";
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
table
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1827
|
+
if (!ws) {
|
|
1828
|
+
contentArea.innerHTML = '<div style="padding: 24px; color: #888;">Empty sheet</div>';
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
const html = XLSX__namespace.utils.sheet_to_html(ws, { id: "fp-sheet-table", editable: false });
|
|
1832
|
+
contentArea.innerHTML = html;
|
|
1833
|
+
const table = contentArea.querySelector("table");
|
|
1834
|
+
if (table) {
|
|
1835
|
+
table.style.borderCollapse = "collapse";
|
|
1836
|
+
table.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
1837
|
+
table.style.fontSize = "13px";
|
|
1838
|
+
table.style.minWidth = "100%";
|
|
1839
|
+
table.style.border = "1px solid #d0d7de";
|
|
1840
|
+
table.style.background = "#fff";
|
|
1841
|
+
table.querySelectorAll("td, th").forEach((cell) => {
|
|
1842
|
+
const el = cell;
|
|
1843
|
+
el.style.border = "1px solid #d0d7de";
|
|
1844
|
+
el.style.padding = "6px 12px";
|
|
1845
|
+
el.style.whiteSpace = "nowrap";
|
|
1590
1846
|
});
|
|
1591
|
-
table.
|
|
1847
|
+
table.querySelectorAll("tr:first-child td, th").forEach((cell) => {
|
|
1848
|
+
const el = cell;
|
|
1849
|
+
el.style.fontWeight = "600";
|
|
1850
|
+
el.style.backgroundColor = "#f6f8fa";
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
tabButtons.forEach((b, i) => {
|
|
1854
|
+
if (i === index - 1) {
|
|
1855
|
+
b.style.backgroundColor = "#2563eb";
|
|
1856
|
+
b.style.color = "#fff";
|
|
1857
|
+
b.style.borderColor = "#2563eb";
|
|
1858
|
+
b.style.fontWeight = "600";
|
|
1859
|
+
} else {
|
|
1860
|
+
b.style.backgroundColor = "#fff";
|
|
1861
|
+
b.style.color = "#333";
|
|
1862
|
+
b.style.borderColor = "#ccc";
|
|
1863
|
+
b.style.fontWeight = "normal";
|
|
1864
|
+
}
|
|
1592
1865
|
});
|
|
1593
|
-
contentArea.appendChild(table);
|
|
1594
|
-
currentSheetIndex = index;
|
|
1595
1866
|
};
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1867
|
+
if (sheetNames.length > 0) {
|
|
1868
|
+
sheetNames.forEach((name, idx) => {
|
|
1869
|
+
const btn = document.createElement("button");
|
|
1870
|
+
btn.textContent = name;
|
|
1871
|
+
btn.style.padding = "4px 12px";
|
|
1872
|
+
btn.style.fontSize = "12px";
|
|
1873
|
+
btn.style.cursor = "pointer";
|
|
1874
|
+
btn.style.border = "1px solid #ccc";
|
|
1875
|
+
btn.style.borderRadius = "4px";
|
|
1876
|
+
btn.style.backgroundColor = "#fff";
|
|
1877
|
+
btn.style.transition = "all 0.15s ease";
|
|
1878
|
+
btn.onclick = () => renderSheet(idx + 1);
|
|
1879
|
+
tabsArea.appendChild(btn);
|
|
1880
|
+
tabButtons.push(btn);
|
|
1881
|
+
});
|
|
1609
1882
|
renderSheet(1);
|
|
1883
|
+
} else {
|
|
1884
|
+
contentArea.innerHTML = `
|
|
1885
|
+
<div style="text-align:center; padding: 40px; color: #666;">
|
|
1886
|
+
<div style="font-size:48px; margin-bottom: 16px;">\u{1F4CA}</div>
|
|
1887
|
+
<h3>${ctx.metadata.name || "Spreadsheet"}</h3>
|
|
1888
|
+
<p>Unable to load sheet data</p>
|
|
1889
|
+
</div>
|
|
1890
|
+
`;
|
|
1610
1891
|
}
|
|
1611
1892
|
const cleanup = () => {
|
|
1612
1893
|
container.remove();
|
|
@@ -1629,11 +1910,11 @@ var ExcelPlugin = class {
|
|
|
1629
1910
|
contentArea.style.transform = `scale(${scale})`;
|
|
1630
1911
|
},
|
|
1631
1912
|
goToPage: (page) => {
|
|
1632
|
-
if (page > 0 && page <=
|
|
1913
|
+
if (page > 0 && page <= sheetNames.length) {
|
|
1633
1914
|
renderSheet(page);
|
|
1634
1915
|
}
|
|
1635
1916
|
},
|
|
1636
|
-
getPageCount: () =>
|
|
1917
|
+
getPageCount: () => sheetNames.length,
|
|
1637
1918
|
getCurrentPage: () => currentSheetIndex,
|
|
1638
1919
|
download: () => {
|
|
1639
1920
|
const blob = new Blob([ctx.buffer], { type: this.mimeTypes[0] });
|
|
@@ -2234,7 +2515,7 @@ var MarkdownPlugin = class {
|
|
|
2234
2515
|
gfm: true,
|
|
2235
2516
|
breaks: true
|
|
2236
2517
|
});
|
|
2237
|
-
const cleanHtml =
|
|
2518
|
+
const cleanHtml = DOMPurify5__default.default.sanitize(rawHtml, {
|
|
2238
2519
|
USE_PROFILES: { html: true }
|
|
2239
2520
|
});
|
|
2240
2521
|
const wrapper = document.createElement("div");
|
|
@@ -2373,10 +2654,14 @@ function markdownPlugin() {
|
|
|
2373
2654
|
var PptxPlugin = class {
|
|
2374
2655
|
id = "pptx";
|
|
2375
2656
|
name = "PowerPoint Presentation";
|
|
2376
|
-
extensions = [".pptx", ".ppsx"];
|
|
2657
|
+
extensions = [".pptx", ".ppsx", ".pptm", ".ppsm", ".potx", ".potm"];
|
|
2377
2658
|
mimeTypes = [
|
|
2378
2659
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
2379
|
-
"application/vnd.openxmlformats-officedocument.presentationml.slideshow"
|
|
2660
|
+
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
|
2661
|
+
"application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
|
2662
|
+
"application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
|
2663
|
+
"application/vnd.openxmlformats-officedocument.presentationml.template",
|
|
2664
|
+
"application/vnd.ms-powerpoint.template.macroEnabled.12"
|
|
2380
2665
|
];
|
|
2381
2666
|
weight = 80;
|
|
2382
2667
|
supports(file) {
|
|
@@ -2762,6 +3047,1292 @@ var ThreeDPlugin = class {
|
|
|
2762
3047
|
function threeDPlugin() {
|
|
2763
3048
|
return new ThreeDPlugin();
|
|
2764
3049
|
}
|
|
3050
|
+
var RtfPlugin = class {
|
|
3051
|
+
id = "rtf";
|
|
3052
|
+
name = "Rich Text Format (RTF)";
|
|
3053
|
+
extensions = [".rtf"];
|
|
3054
|
+
mimeTypes = ["text/rtf", "application/rtf"];
|
|
3055
|
+
weight = 80;
|
|
3056
|
+
supports(file) {
|
|
3057
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
3058
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
3059
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
3060
|
+
}
|
|
3061
|
+
getToolbarActions(instance) {
|
|
3062
|
+
return [
|
|
3063
|
+
{
|
|
3064
|
+
id: "zoom-out",
|
|
3065
|
+
icon: "zoom-out",
|
|
3066
|
+
label: "Zoom Out",
|
|
3067
|
+
type: "button",
|
|
3068
|
+
group: "zoom",
|
|
3069
|
+
execute: () => instance.zoomOut?.()
|
|
3070
|
+
},
|
|
3071
|
+
{
|
|
3072
|
+
id: "zoom-in",
|
|
3073
|
+
icon: "zoom-in",
|
|
3074
|
+
label: "Zoom In",
|
|
3075
|
+
type: "button",
|
|
3076
|
+
group: "zoom",
|
|
3077
|
+
execute: () => instance.zoomIn?.()
|
|
3078
|
+
},
|
|
3079
|
+
{
|
|
3080
|
+
id: "fit-page",
|
|
3081
|
+
icon: "fit-page",
|
|
3082
|
+
label: "Fit to Page",
|
|
3083
|
+
type: "button",
|
|
3084
|
+
group: "zoom",
|
|
3085
|
+
execute: () => instance.fitToPage?.()
|
|
3086
|
+
},
|
|
3087
|
+
{
|
|
3088
|
+
id: "download",
|
|
3089
|
+
icon: "download",
|
|
3090
|
+
label: "Download",
|
|
3091
|
+
type: "button",
|
|
3092
|
+
group: "actions",
|
|
3093
|
+
execute: () => instance.download?.()
|
|
3094
|
+
},
|
|
3095
|
+
{
|
|
3096
|
+
id: "print",
|
|
3097
|
+
icon: "print",
|
|
3098
|
+
label: "Print",
|
|
3099
|
+
type: "button",
|
|
3100
|
+
group: "actions",
|
|
3101
|
+
execute: () => instance.print?.()
|
|
3102
|
+
}
|
|
3103
|
+
];
|
|
3104
|
+
}
|
|
3105
|
+
async render(ctx) {
|
|
3106
|
+
const wrapper = document.createElement("div");
|
|
3107
|
+
wrapper.className = "fp-rtf-wrapper";
|
|
3108
|
+
wrapper.style.padding = "32px";
|
|
3109
|
+
wrapper.style.maxWidth = "850px";
|
|
3110
|
+
wrapper.style.margin = "0 auto";
|
|
3111
|
+
wrapper.style.backgroundColor = "#fff";
|
|
3112
|
+
wrapper.style.boxShadow = "0 2px 8px rgba(0,0,0,0.08)";
|
|
3113
|
+
wrapper.style.borderRadius = "4px";
|
|
3114
|
+
wrapper.style.minHeight = "100%";
|
|
3115
|
+
wrapper.style.transformOrigin = "top center";
|
|
3116
|
+
wrapper.style.transition = "transform 0.2s ease";
|
|
3117
|
+
ctx.container.style.overflow = "auto";
|
|
3118
|
+
ctx.container.style.padding = "24px";
|
|
3119
|
+
ctx.container.style.backgroundColor = "#f1f5f9";
|
|
3120
|
+
ctx.container.appendChild(wrapper);
|
|
3121
|
+
let scale = 1;
|
|
3122
|
+
try {
|
|
3123
|
+
const doc = new rtf_js.RTFJS.Document(ctx.buffer, {});
|
|
3124
|
+
const htmlElements = await doc.render();
|
|
3125
|
+
for (const el of htmlElements) {
|
|
3126
|
+
wrapper.appendChild(el);
|
|
3127
|
+
}
|
|
3128
|
+
} catch (err) {
|
|
3129
|
+
console.warn("[RtfPlugin] RTF render error, fallback text:", err);
|
|
3130
|
+
const text = new TextDecoder("latin1").decode(ctx.buffer);
|
|
3131
|
+
const clean = text.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "");
|
|
3132
|
+
wrapper.innerHTML = `<pre style="white-space: pre-wrap; font-family: serif; color: #333;">${clean}</pre>`;
|
|
3133
|
+
}
|
|
3134
|
+
const cleanup = () => {
|
|
3135
|
+
wrapper.remove();
|
|
3136
|
+
ctx.container.innerHTML = "";
|
|
3137
|
+
};
|
|
3138
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
3139
|
+
return {
|
|
3140
|
+
destroy: cleanup,
|
|
3141
|
+
zoomIn: () => {
|
|
3142
|
+
scale += 0.1;
|
|
3143
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3144
|
+
},
|
|
3145
|
+
zoomOut: () => {
|
|
3146
|
+
scale = Math.max(0.2, scale - 0.1);
|
|
3147
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3148
|
+
},
|
|
3149
|
+
getZoom: () => scale,
|
|
3150
|
+
setZoom: (level) => {
|
|
3151
|
+
scale = level;
|
|
3152
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3153
|
+
},
|
|
3154
|
+
fitToPage: () => {
|
|
3155
|
+
scale = 1;
|
|
3156
|
+
wrapper.style.transform = "scale(1)";
|
|
3157
|
+
},
|
|
3158
|
+
download: () => {
|
|
3159
|
+
const blob = new Blob([ctx.buffer], { type: "application/rtf" });
|
|
3160
|
+
const url = URL.createObjectURL(blob);
|
|
3161
|
+
const a = document.createElement("a");
|
|
3162
|
+
a.href = url;
|
|
3163
|
+
a.download = ctx.metadata.name || "document.rtf";
|
|
3164
|
+
a.click();
|
|
3165
|
+
URL.revokeObjectURL(url);
|
|
3166
|
+
},
|
|
3167
|
+
print: () => {
|
|
3168
|
+
window.print();
|
|
3169
|
+
}
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
};
|
|
3173
|
+
function rtfPlugin() {
|
|
3174
|
+
return new RtfPlugin();
|
|
3175
|
+
}
|
|
3176
|
+
var HtmlPreviewPlugin = class {
|
|
3177
|
+
id = "html-preview";
|
|
3178
|
+
name = "HTML Document Preview";
|
|
3179
|
+
extensions = [".html", ".htm", ".xhtml"];
|
|
3180
|
+
mimeTypes = ["text/html", "application/xhtml+xml"];
|
|
3181
|
+
weight = 85;
|
|
3182
|
+
supports(file) {
|
|
3183
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
3184
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
3185
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
3186
|
+
}
|
|
3187
|
+
getToolbarActions(instance) {
|
|
3188
|
+
return [
|
|
3189
|
+
{
|
|
3190
|
+
id: "zoom-out",
|
|
3191
|
+
icon: "zoom-out",
|
|
3192
|
+
label: "Zoom Out",
|
|
3193
|
+
type: "button",
|
|
3194
|
+
group: "zoom",
|
|
3195
|
+
execute: () => instance.zoomOut?.()
|
|
3196
|
+
},
|
|
3197
|
+
{
|
|
3198
|
+
id: "zoom-in",
|
|
3199
|
+
icon: "zoom-in",
|
|
3200
|
+
label: "Zoom In",
|
|
3201
|
+
type: "button",
|
|
3202
|
+
group: "zoom",
|
|
3203
|
+
execute: () => instance.zoomIn?.()
|
|
3204
|
+
},
|
|
3205
|
+
{
|
|
3206
|
+
id: "fit-page",
|
|
3207
|
+
icon: "fit-page",
|
|
3208
|
+
label: "Fit to Page",
|
|
3209
|
+
type: "button",
|
|
3210
|
+
group: "zoom",
|
|
3211
|
+
execute: () => instance.fitToPage?.()
|
|
3212
|
+
},
|
|
3213
|
+
{
|
|
3214
|
+
id: "copy",
|
|
3215
|
+
icon: "copy",
|
|
3216
|
+
label: "Copy HTML",
|
|
3217
|
+
type: "button",
|
|
3218
|
+
group: "actions",
|
|
3219
|
+
execute: () => instance.copy?.()
|
|
3220
|
+
},
|
|
3221
|
+
{
|
|
3222
|
+
id: "download",
|
|
3223
|
+
icon: "download",
|
|
3224
|
+
label: "Download",
|
|
3225
|
+
type: "button",
|
|
3226
|
+
group: "actions",
|
|
3227
|
+
execute: () => instance.download?.()
|
|
3228
|
+
},
|
|
3229
|
+
{
|
|
3230
|
+
id: "print",
|
|
3231
|
+
icon: "print",
|
|
3232
|
+
label: "Print",
|
|
3233
|
+
type: "button",
|
|
3234
|
+
group: "actions",
|
|
3235
|
+
execute: () => instance.print?.()
|
|
3236
|
+
}
|
|
3237
|
+
];
|
|
3238
|
+
}
|
|
3239
|
+
async render(ctx) {
|
|
3240
|
+
const rawHtml = new TextDecoder("utf-8").decode(ctx.buffer);
|
|
3241
|
+
const sanitized = DOMPurify5__default.default.sanitize(rawHtml, {
|
|
3242
|
+
WHOLE_DOCUMENT: true,
|
|
3243
|
+
ADD_TAGS: ["style", "link"],
|
|
3244
|
+
ADD_ATTR: ["target", "rel"]
|
|
3245
|
+
});
|
|
3246
|
+
const iframe = document.createElement("iframe");
|
|
3247
|
+
iframe.className = "fp-html-iframe";
|
|
3248
|
+
iframe.style.width = "100%";
|
|
3249
|
+
iframe.style.height = "100%";
|
|
3250
|
+
iframe.style.border = "none";
|
|
3251
|
+
iframe.style.backgroundColor = "#ffffff";
|
|
3252
|
+
iframe.style.transformOrigin = "top left";
|
|
3253
|
+
iframe.style.transition = "transform 0.2s ease";
|
|
3254
|
+
iframe.sandbox.add("allow-same-origin");
|
|
3255
|
+
ctx.container.style.overflow = "auto";
|
|
3256
|
+
ctx.container.style.width = "100%";
|
|
3257
|
+
ctx.container.style.height = "100%";
|
|
3258
|
+
ctx.container.appendChild(iframe);
|
|
3259
|
+
iframe.srcdoc = sanitized;
|
|
3260
|
+
let scale = 1;
|
|
3261
|
+
const cleanup = () => {
|
|
3262
|
+
iframe.remove();
|
|
3263
|
+
ctx.container.innerHTML = "";
|
|
3264
|
+
};
|
|
3265
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
3266
|
+
return {
|
|
3267
|
+
destroy: cleanup,
|
|
3268
|
+
zoomIn: () => {
|
|
3269
|
+
scale += 0.1;
|
|
3270
|
+
iframe.style.transform = `scale(${scale})`;
|
|
3271
|
+
},
|
|
3272
|
+
zoomOut: () => {
|
|
3273
|
+
scale = Math.max(0.2, scale - 0.1);
|
|
3274
|
+
iframe.style.transform = `scale(${scale})`;
|
|
3275
|
+
},
|
|
3276
|
+
getZoom: () => scale,
|
|
3277
|
+
setZoom: (level) => {
|
|
3278
|
+
scale = level;
|
|
3279
|
+
iframe.style.transform = `scale(${scale})`;
|
|
3280
|
+
},
|
|
3281
|
+
fitToPage: () => {
|
|
3282
|
+
scale = 1;
|
|
3283
|
+
iframe.style.transform = "scale(1)";
|
|
3284
|
+
},
|
|
3285
|
+
copy: () => {
|
|
3286
|
+
navigator.clipboard.writeText(rawHtml);
|
|
3287
|
+
},
|
|
3288
|
+
download: () => {
|
|
3289
|
+
const blob = new Blob([ctx.buffer], { type: "text/html" });
|
|
3290
|
+
const url = URL.createObjectURL(blob);
|
|
3291
|
+
const a = document.createElement("a");
|
|
3292
|
+
a.href = url;
|
|
3293
|
+
a.download = ctx.metadata.name || "document.html";
|
|
3294
|
+
a.click();
|
|
3295
|
+
URL.revokeObjectURL(url);
|
|
3296
|
+
},
|
|
3297
|
+
print: () => {
|
|
3298
|
+
iframe.contentWindow?.print();
|
|
3299
|
+
}
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
};
|
|
3303
|
+
function htmlPreviewPlugin() {
|
|
3304
|
+
return new HtmlPreviewPlugin();
|
|
3305
|
+
}
|
|
3306
|
+
var OpenDocumentPlugin = class {
|
|
3307
|
+
id = "opendocument";
|
|
3308
|
+
name = "OpenDocument Preview (ODT, ODP, ODS, ODG, ODF)";
|
|
3309
|
+
extensions = [".odt", ".odp", ".ods", ".odg", ".odf"];
|
|
3310
|
+
mimeTypes = [
|
|
3311
|
+
"application/vnd.oasis.opendocument.text",
|
|
3312
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
3313
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
3314
|
+
"application/vnd.oasis.opendocument.graphics",
|
|
3315
|
+
"application/vnd.oasis.opendocument.formula"
|
|
3316
|
+
];
|
|
3317
|
+
weight = 80;
|
|
3318
|
+
supports(file) {
|
|
3319
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
3320
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
3321
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
3322
|
+
}
|
|
3323
|
+
getToolbarActions(instance) {
|
|
3324
|
+
const isPresentation = instance.isPresentation;
|
|
3325
|
+
const actions = [];
|
|
3326
|
+
if (isPresentation) {
|
|
3327
|
+
actions.push(
|
|
3328
|
+
{
|
|
3329
|
+
id: "thumbnails",
|
|
3330
|
+
icon: "thumbnails",
|
|
3331
|
+
label: "Slide Thumbnails",
|
|
3332
|
+
type: "button",
|
|
3333
|
+
group: "navigation",
|
|
3334
|
+
execute: () => instance.toggleThumbnails?.()
|
|
3335
|
+
},
|
|
3336
|
+
{
|
|
3337
|
+
id: "page-prev",
|
|
3338
|
+
icon: "page-prev",
|
|
3339
|
+
label: "Previous Slide",
|
|
3340
|
+
type: "button",
|
|
3341
|
+
group: "navigation",
|
|
3342
|
+
execute: () => {
|
|
3343
|
+
const cur = instance.getCurrentPage?.() ?? 1;
|
|
3344
|
+
if (cur > 1) instance.goToPage?.(cur - 1);
|
|
3345
|
+
}
|
|
3346
|
+
},
|
|
3347
|
+
{
|
|
3348
|
+
id: "page-nav",
|
|
3349
|
+
icon: "",
|
|
3350
|
+
label: "Slide Number",
|
|
3351
|
+
type: "page-nav",
|
|
3352
|
+
group: "navigation",
|
|
3353
|
+
execute: (p) => instance.goToPage?.(Number(p))
|
|
3354
|
+
},
|
|
3355
|
+
{
|
|
3356
|
+
id: "page-next",
|
|
3357
|
+
icon: "page-next",
|
|
3358
|
+
label: "Next Slide",
|
|
3359
|
+
type: "button",
|
|
3360
|
+
group: "navigation",
|
|
3361
|
+
execute: () => {
|
|
3362
|
+
const cur = instance.getCurrentPage?.() ?? 1;
|
|
3363
|
+
const total = instance.getPageCount?.() ?? 1;
|
|
3364
|
+
if (cur < total) instance.goToPage?.(cur + 1);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
actions.push(
|
|
3370
|
+
{
|
|
3371
|
+
id: "zoom-out",
|
|
3372
|
+
icon: "zoom-out",
|
|
3373
|
+
label: "Zoom Out",
|
|
3374
|
+
type: "button",
|
|
3375
|
+
group: "zoom",
|
|
3376
|
+
execute: () => instance.zoomOut?.()
|
|
3377
|
+
},
|
|
3378
|
+
{
|
|
3379
|
+
id: "zoom-in",
|
|
3380
|
+
icon: "zoom-in",
|
|
3381
|
+
label: "Zoom In",
|
|
3382
|
+
type: "button",
|
|
3383
|
+
group: "zoom",
|
|
3384
|
+
execute: () => instance.zoomIn?.()
|
|
3385
|
+
},
|
|
3386
|
+
{
|
|
3387
|
+
id: "fit-page",
|
|
3388
|
+
icon: "fit-page",
|
|
3389
|
+
label: "Fit to Page",
|
|
3390
|
+
type: "button",
|
|
3391
|
+
group: "zoom",
|
|
3392
|
+
execute: () => instance.fitToPage?.()
|
|
3393
|
+
},
|
|
3394
|
+
{
|
|
3395
|
+
id: "download",
|
|
3396
|
+
icon: "download",
|
|
3397
|
+
label: "Download",
|
|
3398
|
+
type: "button",
|
|
3399
|
+
group: "actions",
|
|
3400
|
+
execute: () => instance.download?.()
|
|
3401
|
+
},
|
|
3402
|
+
{
|
|
3403
|
+
id: "print",
|
|
3404
|
+
icon: "print",
|
|
3405
|
+
label: "Print",
|
|
3406
|
+
type: "button",
|
|
3407
|
+
group: "actions",
|
|
3408
|
+
execute: () => instance.print?.()
|
|
3409
|
+
}
|
|
3410
|
+
);
|
|
3411
|
+
return actions;
|
|
3412
|
+
}
|
|
3413
|
+
async render(ctx) {
|
|
3414
|
+
const ext = (ctx.metadata.extension || "").toLowerCase();
|
|
3415
|
+
const mime = (ctx.metadata.mimeType || "").toLowerCase();
|
|
3416
|
+
const isPresentation = ext === ".odp" || mime.includes("presentation");
|
|
3417
|
+
const isFormula = ext === ".odf" || mime.includes("formula");
|
|
3418
|
+
ext === ".odg" || mime.includes("graphics");
|
|
3419
|
+
let unzipped;
|
|
3420
|
+
try {
|
|
3421
|
+
unzipped = fflate.unzipSync(new Uint8Array(ctx.buffer));
|
|
3422
|
+
} catch {
|
|
3423
|
+
throw new Error("Failed to decompress OpenDocument package (invalid ZIP format)");
|
|
3424
|
+
}
|
|
3425
|
+
const imageUrls = /* @__PURE__ */ new Map();
|
|
3426
|
+
for (const [filePath, fileBytes] of Object.entries(unzipped)) {
|
|
3427
|
+
if (filePath.startsWith("Pictures/")) {
|
|
3428
|
+
const imageMime = filePath.endsWith(".png") ? "image/png" : filePath.endsWith(".jpg") || filePath.endsWith(".jpeg") ? "image/jpeg" : filePath.endsWith(".svg") ? "image/svg+xml" : "application/octet-stream";
|
|
3429
|
+
const blob = new Blob([fileBytes], { type: imageMime });
|
|
3430
|
+
imageUrls.set(filePath, URL.createObjectURL(blob));
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
const contentXmlStr = unzipped["content.xml"] ? fflate.strFromU8(unzipped["content.xml"]) : "";
|
|
3434
|
+
const stylesXmlStr = unzipped["styles.xml"] ? fflate.strFromU8(unzipped["styles.xml"]) : "";
|
|
3435
|
+
const parser = new DOMParser();
|
|
3436
|
+
const contentDoc = parser.parseFromString(contentXmlStr, "application/xml");
|
|
3437
|
+
const stylesDoc = stylesXmlStr ? parser.parseFromString(stylesXmlStr, "application/xml") : null;
|
|
3438
|
+
const styleMap = this.extractStyles(stylesDoc, contentDoc);
|
|
3439
|
+
const container = document.createElement("div");
|
|
3440
|
+
container.className = "fp-odf-container";
|
|
3441
|
+
container.style.width = "100%";
|
|
3442
|
+
container.style.height = "100%";
|
|
3443
|
+
container.style.overflow = "auto";
|
|
3444
|
+
container.style.padding = "24px";
|
|
3445
|
+
container.style.backgroundColor = "#f1f5f9";
|
|
3446
|
+
const wrapper = document.createElement("div");
|
|
3447
|
+
wrapper.className = "fp-odf-wrapper";
|
|
3448
|
+
wrapper.style.margin = "0 auto";
|
|
3449
|
+
wrapper.style.backgroundColor = "#ffffff";
|
|
3450
|
+
wrapper.style.boxShadow = "0 2px 10px rgba(0,0,0,0.08)";
|
|
3451
|
+
wrapper.style.borderRadius = "4px";
|
|
3452
|
+
wrapper.style.transformOrigin = "top center";
|
|
3453
|
+
wrapper.style.transition = "transform 0.2s ease";
|
|
3454
|
+
container.appendChild(wrapper);
|
|
3455
|
+
ctx.container.appendChild(container);
|
|
3456
|
+
let scale = 1;
|
|
3457
|
+
let currentPage = 1;
|
|
3458
|
+
let totalPages = 1;
|
|
3459
|
+
let slides = [];
|
|
3460
|
+
if (isPresentation) {
|
|
3461
|
+
wrapper.style.maxWidth = "960px";
|
|
3462
|
+
wrapper.style.aspectRatio = "16 / 9";
|
|
3463
|
+
wrapper.style.position = "relative";
|
|
3464
|
+
wrapper.style.overflow = "hidden";
|
|
3465
|
+
slides = this.renderSlides(contentDoc, styleMap, imageUrls);
|
|
3466
|
+
totalPages = Math.max(1, slides.length);
|
|
3467
|
+
slides.forEach((slide, idx) => {
|
|
3468
|
+
slide.style.display = idx === 0 ? "block" : "none";
|
|
3469
|
+
slide.style.width = "100%";
|
|
3470
|
+
slide.style.height = "100%";
|
|
3471
|
+
slide.style.position = "absolute";
|
|
3472
|
+
slide.style.top = "0";
|
|
3473
|
+
slide.style.left = "0";
|
|
3474
|
+
wrapper.appendChild(slide);
|
|
3475
|
+
});
|
|
3476
|
+
} else if (isFormula) {
|
|
3477
|
+
wrapper.style.maxWidth = "800px";
|
|
3478
|
+
wrapper.style.padding = "48px";
|
|
3479
|
+
wrapper.style.textAlign = "center";
|
|
3480
|
+
wrapper.style.fontSize = "24px";
|
|
3481
|
+
const mathEl = contentDoc.querySelector("math");
|
|
3482
|
+
if (mathEl) {
|
|
3483
|
+
wrapper.innerHTML = mathEl.outerHTML;
|
|
3484
|
+
} else {
|
|
3485
|
+
wrapper.textContent = contentDoc.documentElement.textContent || "Formula content";
|
|
3486
|
+
}
|
|
3487
|
+
} else {
|
|
3488
|
+
wrapper.style.maxWidth = "850px";
|
|
3489
|
+
wrapper.style.padding = "48px";
|
|
3490
|
+
wrapper.style.minHeight = "100%";
|
|
3491
|
+
const bodyHtml = this.renderOdfBody(contentDoc, styleMap, imageUrls);
|
|
3492
|
+
wrapper.innerHTML = DOMPurify5__default.default.sanitize(bodyHtml, {
|
|
3493
|
+
ADD_TAGS: ["math", "semantics", "mrow", "mi", "mo", "mn", "msup", "msub"],
|
|
3494
|
+
ADD_ATTR: ["style", "colspan", "rowspan"]
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
const cleanup = () => {
|
|
3498
|
+
imageUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
3499
|
+
container.remove();
|
|
3500
|
+
ctx.container.innerHTML = "";
|
|
3501
|
+
};
|
|
3502
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
3503
|
+
const goToPage = (page) => {
|
|
3504
|
+
if (!isPresentation || page < 1 || page > totalPages) return;
|
|
3505
|
+
currentPage = page;
|
|
3506
|
+
slides.forEach((s, idx) => {
|
|
3507
|
+
s.style.display = idx === page - 1 ? "block" : "none";
|
|
3508
|
+
});
|
|
3509
|
+
ctx.emit("page-change", { page: currentPage, total: totalPages });
|
|
3510
|
+
};
|
|
3511
|
+
return {
|
|
3512
|
+
destroy: cleanup,
|
|
3513
|
+
isPresentation,
|
|
3514
|
+
zoomIn: () => {
|
|
3515
|
+
scale += 0.1;
|
|
3516
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3517
|
+
},
|
|
3518
|
+
zoomOut: () => {
|
|
3519
|
+
scale = Math.max(0.2, scale - 0.1);
|
|
3520
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3521
|
+
},
|
|
3522
|
+
getZoom: () => scale,
|
|
3523
|
+
setZoom: (level) => {
|
|
3524
|
+
scale = level;
|
|
3525
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3526
|
+
},
|
|
3527
|
+
fitToPage: () => {
|
|
3528
|
+
scale = 1;
|
|
3529
|
+
wrapper.style.transform = "scale(1)";
|
|
3530
|
+
},
|
|
3531
|
+
goToPage,
|
|
3532
|
+
getPageCount: () => totalPages,
|
|
3533
|
+
getCurrentPage: () => currentPage,
|
|
3534
|
+
getThumbnails: async () => {
|
|
3535
|
+
if (!isPresentation) return [];
|
|
3536
|
+
return slides.map((_, idx) => ({
|
|
3537
|
+
index: idx,
|
|
3538
|
+
label: `Slide ${idx + 1}`,
|
|
3539
|
+
render: async (canvas) => {
|
|
3540
|
+
const ctx2d = canvas.getContext("2d");
|
|
3541
|
+
if (!ctx2d) return;
|
|
3542
|
+
canvas.width = 160;
|
|
3543
|
+
canvas.height = 90;
|
|
3544
|
+
ctx2d.fillStyle = "#f8fafc";
|
|
3545
|
+
ctx2d.fillRect(0, 0, 160, 90);
|
|
3546
|
+
ctx2d.fillStyle = "#334155";
|
|
3547
|
+
ctx2d.font = "bold 12px sans-serif";
|
|
3548
|
+
ctx2d.textAlign = "center";
|
|
3549
|
+
ctx2d.fillText(`Slide ${idx + 1}`, 80, 50);
|
|
3550
|
+
}
|
|
3551
|
+
}));
|
|
3552
|
+
},
|
|
3553
|
+
download: () => {
|
|
3554
|
+
const mimeType = this.mimeTypes.find((m) => m.includes(ext.replace(".", ""))) || "application/octet-stream";
|
|
3555
|
+
const blob = new Blob([ctx.buffer], { type: mimeType });
|
|
3556
|
+
const url = URL.createObjectURL(blob);
|
|
3557
|
+
const a = document.createElement("a");
|
|
3558
|
+
a.href = url;
|
|
3559
|
+
a.download = ctx.metadata.name || `document${ext}`;
|
|
3560
|
+
a.click();
|
|
3561
|
+
URL.revokeObjectURL(url);
|
|
3562
|
+
},
|
|
3563
|
+
print: () => {
|
|
3564
|
+
window.print();
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
extractStyles(stylesDoc, contentDoc) {
|
|
3569
|
+
const map = /* @__PURE__ */ new Map();
|
|
3570
|
+
const styleNodes = [];
|
|
3571
|
+
if (stylesDoc) {
|
|
3572
|
+
styleNodes.push(...Array.from(stylesDoc.querySelectorAll("style, [name]")));
|
|
3573
|
+
}
|
|
3574
|
+
styleNodes.push(...Array.from(contentDoc.querySelectorAll("style, [name]")));
|
|
3575
|
+
for (const node of styleNodes) {
|
|
3576
|
+
const name = node.getAttribute("style:name") || node.getAttribute("name");
|
|
3577
|
+
if (!name) continue;
|
|
3578
|
+
let css = "";
|
|
3579
|
+
const textProp = node.querySelector("text-properties, [font-weight], [font-style], [color]");
|
|
3580
|
+
if (textProp) {
|
|
3581
|
+
const weight = textProp.getAttribute("fo:font-weight") || textProp.getAttribute("font-weight");
|
|
3582
|
+
const style = textProp.getAttribute("fo:font-style") || textProp.getAttribute("font-style");
|
|
3583
|
+
const color = textProp.getAttribute("fo:color") || textProp.getAttribute("color");
|
|
3584
|
+
const size = textProp.getAttribute("fo:font-size") || textProp.getAttribute("font-size");
|
|
3585
|
+
if (weight === "bold") css += "font-weight: bold; ";
|
|
3586
|
+
if (style === "italic") css += "font-style: italic; ";
|
|
3587
|
+
if (color) css += `color: ${color}; `;
|
|
3588
|
+
if (size) css += `font-size: ${size}; `;
|
|
3589
|
+
}
|
|
3590
|
+
const paraProp = node.querySelector("paragraph-properties, [text-align]");
|
|
3591
|
+
if (paraProp) {
|
|
3592
|
+
const align = paraProp.getAttribute("fo:text-align") || paraProp.getAttribute("text-align");
|
|
3593
|
+
const mt = paraProp.getAttribute("fo:margin-top") || paraProp.getAttribute("margin-top");
|
|
3594
|
+
const mb = paraProp.getAttribute("fo:margin-bottom") || paraProp.getAttribute("margin-bottom");
|
|
3595
|
+
if (align) css += `text-align: ${align}; `;
|
|
3596
|
+
if (mt) css += `margin-top: ${mt}; `;
|
|
3597
|
+
if (mb) css += `margin-bottom: ${mb}; `;
|
|
3598
|
+
}
|
|
3599
|
+
if (css) map.set(name, css);
|
|
3600
|
+
}
|
|
3601
|
+
return map;
|
|
3602
|
+
}
|
|
3603
|
+
renderOdfBody(contentDoc, styles, images) {
|
|
3604
|
+
const body = contentDoc.querySelector("body") || contentDoc.documentElement;
|
|
3605
|
+
let html = "";
|
|
3606
|
+
const walk = (node) => {
|
|
3607
|
+
const tag = node.localName || node.tagName.toLowerCase();
|
|
3608
|
+
switch (tag) {
|
|
3609
|
+
case "h": {
|
|
3610
|
+
const level = Math.min(6, Math.max(1, Number(node.getAttribute("text:outline-level") || 1)));
|
|
3611
|
+
const style = styles.get(node.getAttribute("text:style-name") || "") || "";
|
|
3612
|
+
html += `<h${level} style="${style}; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${node.textContent || ""}</h${level}>`;
|
|
3613
|
+
break;
|
|
3614
|
+
}
|
|
3615
|
+
case "p": {
|
|
3616
|
+
const style = styles.get(node.getAttribute("text:style-name") || "") || "";
|
|
3617
|
+
const inner = this.renderSpans(node, styles, images);
|
|
3618
|
+
html += `<p style="${style}; line-height: 1.6; margin: 8px 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${inner}</p>`;
|
|
3619
|
+
break;
|
|
3620
|
+
}
|
|
3621
|
+
case "list": {
|
|
3622
|
+
html += '<ul style="margin: 8px 0; padding-left: 24px;">';
|
|
3623
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3624
|
+
html += "</ul>";
|
|
3625
|
+
break;
|
|
3626
|
+
}
|
|
3627
|
+
case "list-item": {
|
|
3628
|
+
html += "<li>";
|
|
3629
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3630
|
+
html += "</li>";
|
|
3631
|
+
break;
|
|
3632
|
+
}
|
|
3633
|
+
case "table": {
|
|
3634
|
+
html += '<table style="border-collapse: collapse; width: 100%; margin: 16px 0; border: 1px solid #d0d7de;">';
|
|
3635
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3636
|
+
html += "</table>";
|
|
3637
|
+
break;
|
|
3638
|
+
}
|
|
3639
|
+
case "table-row": {
|
|
3640
|
+
html += "<tr>";
|
|
3641
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3642
|
+
html += "</tr>";
|
|
3643
|
+
break;
|
|
3644
|
+
}
|
|
3645
|
+
case "table-cell": {
|
|
3646
|
+
html += '<td style="border: 1px solid #d0d7de; padding: 8px 12px; font-size: 13px;">';
|
|
3647
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3648
|
+
html += "</td>";
|
|
3649
|
+
break;
|
|
3650
|
+
}
|
|
3651
|
+
default:
|
|
3652
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
Array.from(body.children).forEach((c) => walk(c));
|
|
3656
|
+
return html || '<p style="color: #666; font-style: italic;">(Document is empty)</p>';
|
|
3657
|
+
}
|
|
3658
|
+
renderSpans(node, styles, images) {
|
|
3659
|
+
let result = "";
|
|
3660
|
+
for (const child of Array.from(node.childNodes)) {
|
|
3661
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
3662
|
+
result += child.textContent || "";
|
|
3663
|
+
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
3664
|
+
const el = child;
|
|
3665
|
+
const tag = el.localName || el.tagName.toLowerCase();
|
|
3666
|
+
if (tag === "span") {
|
|
3667
|
+
const style = styles.get(el.getAttribute("text:style-name") || "") || "";
|
|
3668
|
+
result += `<span style="${style}">${el.textContent || ""}</span>`;
|
|
3669
|
+
} else if (tag === "image") {
|
|
3670
|
+
const href = el.getAttribute("xlink:href") || el.getAttribute("href") || "";
|
|
3671
|
+
const blobUrl = images.get(href);
|
|
3672
|
+
if (blobUrl) {
|
|
3673
|
+
result += `<img src="${blobUrl}" style="max-width: 100%; height: auto; margin: 8px 0;" alt="Embedded image" />`;
|
|
3674
|
+
}
|
|
3675
|
+
} else if (tag === "s") {
|
|
3676
|
+
const count = Number(el.getAttribute("text:c") || 1);
|
|
3677
|
+
result += " ".repeat(count);
|
|
3678
|
+
} else if (tag === "tab") {
|
|
3679
|
+
result += " ";
|
|
3680
|
+
} else if (tag === "line-break") {
|
|
3681
|
+
result += "<br/>";
|
|
3682
|
+
} else {
|
|
3683
|
+
result += el.textContent || "";
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
return result;
|
|
3688
|
+
}
|
|
3689
|
+
renderSlides(contentDoc, styles, images) {
|
|
3690
|
+
const pages = Array.from(contentDoc.querySelectorAll("page, [draw\\:name]"));
|
|
3691
|
+
const slides = [];
|
|
3692
|
+
const pageNodes = pages.length > 0 ? pages : Array.from(contentDoc.getElementsByTagNameNS("*", "page"));
|
|
3693
|
+
if (pageNodes.length === 0) {
|
|
3694
|
+
const fallbackSlide = document.createElement("div");
|
|
3695
|
+
fallbackSlide.style.padding = "40px";
|
|
3696
|
+
fallbackSlide.style.textAlign = "center";
|
|
3697
|
+
fallbackSlide.innerHTML = "<h2>Presentation</h2><p>No slides found</p>";
|
|
3698
|
+
return [fallbackSlide];
|
|
3699
|
+
}
|
|
3700
|
+
pageNodes.forEach((pageEl, idx) => {
|
|
3701
|
+
const slide = document.createElement("div");
|
|
3702
|
+
slide.className = `fp-odp-slide fp-odp-slide-${idx + 1}`;
|
|
3703
|
+
slide.style.padding = "40px";
|
|
3704
|
+
slide.style.boxSizing = "border-box";
|
|
3705
|
+
slide.style.display = "flex";
|
|
3706
|
+
slide.style.flexDirection = "column";
|
|
3707
|
+
slide.style.justifyContent = "center";
|
|
3708
|
+
slide.style.alignItems = "center";
|
|
3709
|
+
slide.style.background = "#ffffff";
|
|
3710
|
+
const textFrames = Array.from(pageEl.querySelectorAll("frame, text-box, [draw\\:text-style-name]"));
|
|
3711
|
+
if (textFrames.length > 0) {
|
|
3712
|
+
textFrames.forEach((frame) => {
|
|
3713
|
+
const text = frame.textContent?.trim();
|
|
3714
|
+
if (text) {
|
|
3715
|
+
const p = document.createElement("div");
|
|
3716
|
+
p.style.margin = "12px 0";
|
|
3717
|
+
p.style.fontSize = idx === 0 ? "24px" : "16px";
|
|
3718
|
+
p.style.fontWeight = idx === 0 ? "bold" : "normal";
|
|
3719
|
+
p.style.color = "#1e293b";
|
|
3720
|
+
p.textContent = text;
|
|
3721
|
+
slide.appendChild(p);
|
|
3722
|
+
}
|
|
3723
|
+
});
|
|
3724
|
+
} else {
|
|
3725
|
+
slide.innerHTML = `<h3>Slide ${idx + 1}</h3><p>${pageEl.textContent?.trim() || ""}</p>`;
|
|
3726
|
+
}
|
|
3727
|
+
slides.push(slide);
|
|
3728
|
+
});
|
|
3729
|
+
return slides;
|
|
3730
|
+
}
|
|
3731
|
+
};
|
|
3732
|
+
function openDocumentPlugin() {
|
|
3733
|
+
return new OpenDocumentPlugin();
|
|
3734
|
+
}
|
|
3735
|
+
var DocPlugin = class {
|
|
3736
|
+
id = "doc";
|
|
3737
|
+
name = "Legacy Word Document Preview (.doc, .dot)";
|
|
3738
|
+
extensions = [".doc", ".dot"];
|
|
3739
|
+
mimeTypes = ["application/msword", "application/vnd.ms-word"];
|
|
3740
|
+
weight = 75;
|
|
3741
|
+
supports(file) {
|
|
3742
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
3743
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
3744
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
3745
|
+
}
|
|
3746
|
+
getToolbarActions(instance) {
|
|
3747
|
+
return [
|
|
3748
|
+
{
|
|
3749
|
+
id: "zoom-out",
|
|
3750
|
+
icon: "zoom-out",
|
|
3751
|
+
label: "Zoom Out",
|
|
3752
|
+
type: "button",
|
|
3753
|
+
group: "zoom",
|
|
3754
|
+
execute: () => instance.zoomOut?.()
|
|
3755
|
+
},
|
|
3756
|
+
{
|
|
3757
|
+
id: "zoom-in",
|
|
3758
|
+
icon: "zoom-in",
|
|
3759
|
+
label: "Zoom In",
|
|
3760
|
+
type: "button",
|
|
3761
|
+
group: "zoom",
|
|
3762
|
+
execute: () => instance.zoomIn?.()
|
|
3763
|
+
},
|
|
3764
|
+
{
|
|
3765
|
+
id: "fit-page",
|
|
3766
|
+
icon: "fit-page",
|
|
3767
|
+
label: "Fit to Page",
|
|
3768
|
+
type: "button",
|
|
3769
|
+
group: "zoom",
|
|
3770
|
+
execute: () => instance.fitToPage?.()
|
|
3771
|
+
},
|
|
3772
|
+
{
|
|
3773
|
+
id: "copy",
|
|
3774
|
+
icon: "copy",
|
|
3775
|
+
label: "Copy Text",
|
|
3776
|
+
type: "button",
|
|
3777
|
+
group: "actions",
|
|
3778
|
+
execute: () => instance.copy?.()
|
|
3779
|
+
},
|
|
3780
|
+
{
|
|
3781
|
+
id: "download",
|
|
3782
|
+
icon: "download",
|
|
3783
|
+
label: "Download",
|
|
3784
|
+
type: "button",
|
|
3785
|
+
group: "actions",
|
|
3786
|
+
execute: () => instance.download?.()
|
|
3787
|
+
},
|
|
3788
|
+
{
|
|
3789
|
+
id: "print",
|
|
3790
|
+
icon: "print",
|
|
3791
|
+
label: "Print",
|
|
3792
|
+
type: "button",
|
|
3793
|
+
group: "actions",
|
|
3794
|
+
execute: () => instance.print?.()
|
|
3795
|
+
}
|
|
3796
|
+
];
|
|
3797
|
+
}
|
|
3798
|
+
async render(ctx) {
|
|
3799
|
+
const container = document.createElement("div");
|
|
3800
|
+
container.className = "fp-doc-container";
|
|
3801
|
+
container.style.width = "100%";
|
|
3802
|
+
container.style.height = "100%";
|
|
3803
|
+
container.style.overflow = "auto";
|
|
3804
|
+
container.style.padding = "32px 16px";
|
|
3805
|
+
container.style.backgroundColor = "#f1f5f9";
|
|
3806
|
+
const wrapper = document.createElement("div");
|
|
3807
|
+
wrapper.className = "fp-doc-wrapper";
|
|
3808
|
+
wrapper.style.maxWidth = "850px";
|
|
3809
|
+
wrapper.style.margin = "0 auto";
|
|
3810
|
+
wrapper.style.backgroundColor = "#ffffff";
|
|
3811
|
+
wrapper.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
|
|
3812
|
+
wrapper.style.borderRadius = "4px";
|
|
3813
|
+
wrapper.style.padding = "56px 48px";
|
|
3814
|
+
wrapper.style.minHeight = "100%";
|
|
3815
|
+
wrapper.style.transformOrigin = "top center";
|
|
3816
|
+
wrapper.style.transition = "transform 0.2s ease";
|
|
3817
|
+
wrapper.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
|
|
3818
|
+
wrapper.style.color = "#1e293b";
|
|
3819
|
+
container.appendChild(wrapper);
|
|
3820
|
+
ctx.container.appendChild(container);
|
|
3821
|
+
let scale = 1;
|
|
3822
|
+
let extractedRawText = "";
|
|
3823
|
+
try {
|
|
3824
|
+
const cfbf = new CfbfReader(ctx.buffer);
|
|
3825
|
+
const wordDocStream = cfbf.readStream("WordDocument");
|
|
3826
|
+
if (!wordDocStream || wordDocStream.length < 512) {
|
|
3827
|
+
throw new Error("WordDocument stream not found or invalid in CFBF archive");
|
|
3828
|
+
}
|
|
3829
|
+
const view = new DataView(wordDocStream.buffer, wordDocStream.byteOffset, wordDocStream.byteLength);
|
|
3830
|
+
const flags = view.getUint16(10, true);
|
|
3831
|
+
const is1Table = (flags & 512) !== 0;
|
|
3832
|
+
const tableName = is1Table ? "1Table" : "0Table";
|
|
3833
|
+
const tableStream = cfbf.readStream(tableName);
|
|
3834
|
+
const text = this.extractDocText(wordDocStream, tableStream);
|
|
3835
|
+
extractedRawText = text;
|
|
3836
|
+
wrapper.innerHTML = this.formatDocToHtml(text, ctx.metadata.name || "Document");
|
|
3837
|
+
} catch (err) {
|
|
3838
|
+
console.warn("[DocPlugin] Binary parsing error, fallback text:", err);
|
|
3839
|
+
const fallback = this.heuristicTextExtraction(ctx.buffer);
|
|
3840
|
+
extractedRawText = fallback;
|
|
3841
|
+
wrapper.innerHTML = `
|
|
3842
|
+
<div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
|
|
3843
|
+
<h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${ctx.metadata.name || "Word Document (.doc)"}</h2>
|
|
3844
|
+
<span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
|
|
3845
|
+
</div>
|
|
3846
|
+
${this.formatDocToHtml(fallback, ctx.metadata.name || "Document")}
|
|
3847
|
+
`;
|
|
3848
|
+
}
|
|
3849
|
+
const cleanup = () => {
|
|
3850
|
+
container.remove();
|
|
3851
|
+
ctx.container.innerHTML = "";
|
|
3852
|
+
};
|
|
3853
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
3854
|
+
return {
|
|
3855
|
+
destroy: cleanup,
|
|
3856
|
+
zoomIn: () => {
|
|
3857
|
+
scale += 0.1;
|
|
3858
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3859
|
+
},
|
|
3860
|
+
zoomOut: () => {
|
|
3861
|
+
scale = Math.max(0.2, scale - 0.1);
|
|
3862
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3863
|
+
},
|
|
3864
|
+
getZoom: () => scale,
|
|
3865
|
+
setZoom: (level) => {
|
|
3866
|
+
scale = level;
|
|
3867
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3868
|
+
},
|
|
3869
|
+
fitToPage: () => {
|
|
3870
|
+
scale = 1;
|
|
3871
|
+
wrapper.style.transform = "scale(1)";
|
|
3872
|
+
},
|
|
3873
|
+
copy: () => {
|
|
3874
|
+
navigator.clipboard.writeText(extractedRawText);
|
|
3875
|
+
},
|
|
3876
|
+
download: () => {
|
|
3877
|
+
const blob = new Blob([ctx.buffer], { type: "application/msword" });
|
|
3878
|
+
const url = URL.createObjectURL(blob);
|
|
3879
|
+
const a = document.createElement("a");
|
|
3880
|
+
a.href = url;
|
|
3881
|
+
a.download = ctx.metadata.name || "document.doc";
|
|
3882
|
+
a.click();
|
|
3883
|
+
URL.revokeObjectURL(url);
|
|
3884
|
+
},
|
|
3885
|
+
print: () => {
|
|
3886
|
+
window.print();
|
|
3887
|
+
}
|
|
3888
|
+
};
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Extract document text from WordDocument stream and Table stream
|
|
3892
|
+
*/
|
|
3893
|
+
extractDocText(wordDoc, tableStream) {
|
|
3894
|
+
const view = new DataView(wordDoc.buffer, wordDoc.byteOffset, wordDoc.byteLength);
|
|
3895
|
+
let fcClx = 0;
|
|
3896
|
+
let lcbClx = 0;
|
|
3897
|
+
if (wordDoc.length >= 426) {
|
|
3898
|
+
fcClx = view.getUint32(418, true);
|
|
3899
|
+
lcbClx = view.getUint32(422, true);
|
|
3900
|
+
}
|
|
3901
|
+
if (tableStream && fcClx > 0 && lcbClx > 0 && fcClx + lcbClx <= tableStream.length) {
|
|
3902
|
+
try {
|
|
3903
|
+
const text = this.parsePieceTable(wordDoc, tableStream, fcClx, lcbClx);
|
|
3904
|
+
if (text && text.trim().length > 0) return text;
|
|
3905
|
+
} catch (err) {
|
|
3906
|
+
console.warn("[DocPlugin] Error in piece table parsing:", err);
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
if (wordDoc.length > 2560) {
|
|
3910
|
+
const textChunk = wordDoc.subarray(2560);
|
|
3911
|
+
const extracted = this.extractStringsFromBytes(textChunk);
|
|
3912
|
+
if (extracted.trim().length > 0) return extracted;
|
|
3913
|
+
}
|
|
3914
|
+
return this.extractStringsFromBytes(wordDoc);
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* Parse the CLX and Piece Table (Plcfpcd) according to [MS-DOC]
|
|
3918
|
+
*/
|
|
3919
|
+
parsePieceTable(wordDoc, table, fcClx, lcbClx) {
|
|
3920
|
+
let offset = fcClx;
|
|
3921
|
+
const end = fcClx + lcbClx;
|
|
3922
|
+
while (offset < end) {
|
|
3923
|
+
const clxt = table[offset];
|
|
3924
|
+
if (clxt === 1) {
|
|
3925
|
+
const cb = new DataView(table.buffer, table.byteOffset + offset + 1).getUint16(0, true);
|
|
3926
|
+
offset += 3 + cb;
|
|
3927
|
+
} else if (clxt === 2) {
|
|
3928
|
+
offset += 1;
|
|
3929
|
+
const lcb = new DataView(table.buffer, table.byteOffset + offset).getUint32(0, true);
|
|
3930
|
+
offset += 4;
|
|
3931
|
+
return this.readPlcfpcd(wordDoc, table, offset, lcb);
|
|
3932
|
+
} else {
|
|
3933
|
+
break;
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
return "";
|
|
3937
|
+
}
|
|
3938
|
+
readPlcfpcd(wordDoc, table, offset, lcb) {
|
|
3939
|
+
const view = new DataView(table.buffer, table.byteOffset + offset);
|
|
3940
|
+
const n = Math.floor((lcb - 4) / 12);
|
|
3941
|
+
if (n <= 0) return "";
|
|
3942
|
+
const cpOffsets = [];
|
|
3943
|
+
for (let i = 0; i <= n; i++) {
|
|
3944
|
+
cpOffsets.push(view.getUint32(i * 4, true));
|
|
3945
|
+
}
|
|
3946
|
+
const pcdOffset = (n + 1) * 4;
|
|
3947
|
+
let fullText = "";
|
|
3948
|
+
for (let i = 0; i < n; i++) {
|
|
3949
|
+
const fc = view.getUint32(pcdOffset + i * 8 + 2, true);
|
|
3950
|
+
const isCompressed = (fc & 1073741824) !== 0;
|
|
3951
|
+
const byteOffset = (fc & 1073741823) >> (isCompressed ? 1 : 0);
|
|
3952
|
+
const charCount = cpOffsets[i + 1] - cpOffsets[i];
|
|
3953
|
+
if (isCompressed) {
|
|
3954
|
+
const slice = wordDoc.subarray(byteOffset, byteOffset + charCount);
|
|
3955
|
+
fullText += new TextDecoder("latin1").decode(slice);
|
|
3956
|
+
} else {
|
|
3957
|
+
const slice = wordDoc.subarray(byteOffset, byteOffset + charCount * 2);
|
|
3958
|
+
fullText += new TextDecoder("utf-16le").decode(slice);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
return fullText;
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
|
|
3965
|
+
*/
|
|
3966
|
+
extractStringsFromBytes(bytes) {
|
|
3967
|
+
const chars = [];
|
|
3968
|
+
const len = bytes.length;
|
|
3969
|
+
for (let i = 0; i < len; i++) {
|
|
3970
|
+
const b = bytes[i];
|
|
3971
|
+
if (b === 13 || b === 10 || b === 9 || b >= 32 && b <= 126 || b >= 160 && b <= 255) {
|
|
3972
|
+
chars.push(String.fromCharCode(b));
|
|
3973
|
+
} else if (b === 0 && i + 1 < len && bytes[i + 1] >= 32 && bytes[i + 1] <= 126) {
|
|
3974
|
+
chars.push(String.fromCharCode(bytes[i + 1]));
|
|
3975
|
+
i++;
|
|
3976
|
+
} else if (b === 7) {
|
|
3977
|
+
chars.push(" ");
|
|
3978
|
+
} else if (b === 12) {
|
|
3979
|
+
chars.push("\n\n---PAGE---\n\n");
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
return chars.join("");
|
|
3983
|
+
}
|
|
3984
|
+
heuristicTextExtraction(buffer) {
|
|
3985
|
+
return this.extractStringsFromBytes(new Uint8Array(buffer));
|
|
3986
|
+
}
|
|
3987
|
+
/**
|
|
3988
|
+
* Format cleaned extracted text into attractive HTML paragraphs, headings, and lists
|
|
3989
|
+
*/
|
|
3990
|
+
formatDocToHtml(text, filename) {
|
|
3991
|
+
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x0C/g, "\n\n").split("\n");
|
|
3992
|
+
let html = "";
|
|
3993
|
+
let inList = false;
|
|
3994
|
+
for (const rawLine of lines) {
|
|
3995
|
+
const line = rawLine.trim();
|
|
3996
|
+
if (!line) {
|
|
3997
|
+
if (inList) {
|
|
3998
|
+
html += "</ul>";
|
|
3999
|
+
inList = false;
|
|
4000
|
+
}
|
|
4001
|
+
continue;
|
|
4002
|
+
}
|
|
4003
|
+
if (/^[\x00-\x1F\x7F-\x9F]+$/.test(line)) continue;
|
|
4004
|
+
if (line.includes("Normal.dot") || line.includes("Microsoft Word") || line.includes("Times New Roman") && line.length < 30) {
|
|
4005
|
+
continue;
|
|
4006
|
+
}
|
|
4007
|
+
if (line.length < 60 && !line.endsWith(".") && (/^[A-Z0-9\s:_-]+$/.test(line) || line.startsWith("#"))) {
|
|
4008
|
+
if (inList) {
|
|
4009
|
+
html += "</ul>";
|
|
4010
|
+
inList = false;
|
|
4011
|
+
}
|
|
4012
|
+
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>`;
|
|
4013
|
+
} else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
|
|
4014
|
+
if (!inList) {
|
|
4015
|
+
html += '<ul style="margin: 8px 0; padding-left: 24px;">';
|
|
4016
|
+
inList = true;
|
|
4017
|
+
}
|
|
4018
|
+
const bulletText = line.replace(/^[•\-\*]\s*/, "");
|
|
4019
|
+
html += `<li style="margin: 4px 0; line-height: 1.6;">${DOMPurify5__default.default.sanitize(bulletText)}</li>`;
|
|
4020
|
+
} else {
|
|
4021
|
+
if (inList) {
|
|
4022
|
+
html += "</ul>";
|
|
4023
|
+
inList = false;
|
|
4024
|
+
}
|
|
4025
|
+
html += `<p style="line-height: 1.7; margin: 10px 0; font-size: 14px; text-align: justify;">${DOMPurify5__default.default.sanitize(line)}</p>`;
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
if (inList) html += "</ul>";
|
|
4029
|
+
return html || `<p style="color: #64748b; font-style: italic;">(No readable text found in ${DOMPurify5__default.default.sanitize(filename)})</p>`;
|
|
4030
|
+
}
|
|
4031
|
+
};
|
|
4032
|
+
function docPlugin() {
|
|
4033
|
+
return new DocPlugin();
|
|
4034
|
+
}
|
|
4035
|
+
var PptPlugin = class {
|
|
4036
|
+
id = "ppt";
|
|
4037
|
+
name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
|
|
4038
|
+
extensions = [".ppt", ".pps", ".pot"];
|
|
4039
|
+
mimeTypes = ["application/vnd.ms-powerpoint"];
|
|
4040
|
+
weight = 75;
|
|
4041
|
+
supports(file) {
|
|
4042
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
4043
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
4044
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
4045
|
+
}
|
|
4046
|
+
getToolbarActions(instance) {
|
|
4047
|
+
return [
|
|
4048
|
+
{
|
|
4049
|
+
id: "thumbnails",
|
|
4050
|
+
icon: "thumbnails",
|
|
4051
|
+
label: "Slide Thumbnails",
|
|
4052
|
+
type: "button",
|
|
4053
|
+
group: "navigation",
|
|
4054
|
+
execute: () => instance.toggleThumbnails?.()
|
|
4055
|
+
},
|
|
4056
|
+
{
|
|
4057
|
+
id: "page-prev",
|
|
4058
|
+
icon: "page-prev",
|
|
4059
|
+
label: "Previous Slide",
|
|
4060
|
+
type: "button",
|
|
4061
|
+
group: "navigation",
|
|
4062
|
+
execute: () => {
|
|
4063
|
+
const cur = instance.getCurrentPage?.() ?? 1;
|
|
4064
|
+
if (cur > 1) instance.goToPage?.(cur - 1);
|
|
4065
|
+
}
|
|
4066
|
+
},
|
|
4067
|
+
{
|
|
4068
|
+
id: "page-nav",
|
|
4069
|
+
icon: "",
|
|
4070
|
+
label: "Slide Number",
|
|
4071
|
+
type: "page-nav",
|
|
4072
|
+
group: "navigation",
|
|
4073
|
+
execute: (p) => instance.goToPage?.(Number(p))
|
|
4074
|
+
},
|
|
4075
|
+
{
|
|
4076
|
+
id: "page-next",
|
|
4077
|
+
icon: "page-next",
|
|
4078
|
+
label: "Next Slide",
|
|
4079
|
+
type: "button",
|
|
4080
|
+
group: "navigation",
|
|
4081
|
+
execute: () => {
|
|
4082
|
+
const cur = instance.getCurrentPage?.() ?? 1;
|
|
4083
|
+
const total = instance.getPageCount?.() ?? 1;
|
|
4084
|
+
if (cur < total) instance.goToPage?.(cur + 1);
|
|
4085
|
+
}
|
|
4086
|
+
},
|
|
4087
|
+
{
|
|
4088
|
+
id: "zoom-out",
|
|
4089
|
+
icon: "zoom-out",
|
|
4090
|
+
label: "Zoom Out",
|
|
4091
|
+
type: "button",
|
|
4092
|
+
group: "zoom",
|
|
4093
|
+
execute: () => instance.zoomOut?.()
|
|
4094
|
+
},
|
|
4095
|
+
{
|
|
4096
|
+
id: "zoom-in",
|
|
4097
|
+
icon: "zoom-in",
|
|
4098
|
+
label: "Zoom In",
|
|
4099
|
+
type: "button",
|
|
4100
|
+
group: "zoom",
|
|
4101
|
+
execute: () => instance.zoomIn?.()
|
|
4102
|
+
},
|
|
4103
|
+
{
|
|
4104
|
+
id: "fit-page",
|
|
4105
|
+
icon: "fit-page",
|
|
4106
|
+
label: "Fit to Slide",
|
|
4107
|
+
type: "button",
|
|
4108
|
+
group: "zoom",
|
|
4109
|
+
execute: () => instance.fitToPage?.()
|
|
4110
|
+
},
|
|
4111
|
+
{
|
|
4112
|
+
id: "download",
|
|
4113
|
+
icon: "download",
|
|
4114
|
+
label: "Download",
|
|
4115
|
+
type: "button",
|
|
4116
|
+
group: "actions",
|
|
4117
|
+
execute: () => instance.download?.()
|
|
4118
|
+
},
|
|
4119
|
+
{
|
|
4120
|
+
id: "print",
|
|
4121
|
+
icon: "print",
|
|
4122
|
+
label: "Print",
|
|
4123
|
+
type: "button",
|
|
4124
|
+
group: "actions",
|
|
4125
|
+
execute: () => instance.print?.()
|
|
4126
|
+
}
|
|
4127
|
+
];
|
|
4128
|
+
}
|
|
4129
|
+
async render(ctx) {
|
|
4130
|
+
const container = document.createElement("div");
|
|
4131
|
+
container.className = "fp-ppt-container";
|
|
4132
|
+
container.style.width = "100%";
|
|
4133
|
+
container.style.height = "100%";
|
|
4134
|
+
container.style.overflow = "auto";
|
|
4135
|
+
container.style.display = "flex";
|
|
4136
|
+
container.style.justifyContent = "center";
|
|
4137
|
+
container.style.alignItems = "center";
|
|
4138
|
+
container.style.padding = "32px 16px";
|
|
4139
|
+
container.style.backgroundColor = "#0f172a";
|
|
4140
|
+
const slideCard = document.createElement("div");
|
|
4141
|
+
slideCard.className = "fp-ppt-slide-card";
|
|
4142
|
+
slideCard.style.width = "960px";
|
|
4143
|
+
slideCard.style.maxWidth = "90%";
|
|
4144
|
+
slideCard.style.aspectRatio = "16 / 9";
|
|
4145
|
+
slideCard.style.backgroundColor = "#ffffff";
|
|
4146
|
+
slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
|
|
4147
|
+
slideCard.style.borderRadius = "8px";
|
|
4148
|
+
slideCard.style.padding = "48px";
|
|
4149
|
+
slideCard.style.display = "flex";
|
|
4150
|
+
slideCard.style.flexDirection = "column";
|
|
4151
|
+
slideCard.style.justifyContent = "center";
|
|
4152
|
+
slideCard.style.alignItems = "center";
|
|
4153
|
+
slideCard.style.boxSizing = "border-box";
|
|
4154
|
+
slideCard.style.position = "relative";
|
|
4155
|
+
slideCard.style.overflow = "hidden";
|
|
4156
|
+
slideCard.style.transformOrigin = "center center";
|
|
4157
|
+
slideCard.style.transition = "transform 0.2s ease";
|
|
4158
|
+
container.appendChild(slideCard);
|
|
4159
|
+
ctx.container.appendChild(container);
|
|
4160
|
+
let scale = 1;
|
|
4161
|
+
let currentSlide = 1;
|
|
4162
|
+
let slides = [];
|
|
4163
|
+
try {
|
|
4164
|
+
const cfbf = new CfbfReader(ctx.buffer);
|
|
4165
|
+
const pptStream = cfbf.readStream("PowerPoint Document");
|
|
4166
|
+
if (!pptStream || pptStream.length < 512) {
|
|
4167
|
+
throw new Error("PowerPoint Document stream not found in CFBF container");
|
|
4168
|
+
}
|
|
4169
|
+
slides = this.extractSlides(pptStream);
|
|
4170
|
+
} catch (err) {
|
|
4171
|
+
console.warn("[PptPlugin] Error extracting binary slides:", err);
|
|
4172
|
+
}
|
|
4173
|
+
if (slides.length === 0) {
|
|
4174
|
+
slides = [
|
|
4175
|
+
{
|
|
4176
|
+
title: ctx.metadata.name || "PowerPoint Presentation",
|
|
4177
|
+
texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
|
|
4178
|
+
}
|
|
4179
|
+
];
|
|
4180
|
+
}
|
|
4181
|
+
const totalSlides = slides.length;
|
|
4182
|
+
const renderSlide = (idx) => {
|
|
4183
|
+
currentSlide = idx;
|
|
4184
|
+
const s = slides[idx - 1];
|
|
4185
|
+
if (!s) return;
|
|
4186
|
+
slideCard.innerHTML = `
|
|
4187
|
+
<div style="position: absolute; top: 20px; right: 24px; font-size: 12px; color: #94a3b8; font-weight: 600;">
|
|
4188
|
+
Slide ${idx} of ${totalSlides}
|
|
4189
|
+
</div>
|
|
4190
|
+
<div style="text-align: center; width: 100%;">
|
|
4191
|
+
<h1 style="font-size: ${idx === 1 ? "36px" : "28px"}; color: #1e3a8a; margin: 0 0 24px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 700;">
|
|
4192
|
+
${DOMPurify5__default.default.sanitize(s.title || `Slide ${idx}`)}
|
|
4193
|
+
</h1>
|
|
4194
|
+
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 80%; margin: 0 auto; text-align: ${idx === 1 ? "center" : "left"};">
|
|
4195
|
+
${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("")}
|
|
4196
|
+
</div>
|
|
4197
|
+
</div>
|
|
4198
|
+
`;
|
|
4199
|
+
ctx.emit("page-change", { page: currentSlide, total: totalSlides });
|
|
4200
|
+
};
|
|
4201
|
+
renderSlide(1);
|
|
4202
|
+
const cleanup = () => {
|
|
4203
|
+
container.remove();
|
|
4204
|
+
ctx.container.innerHTML = "";
|
|
4205
|
+
};
|
|
4206
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
4207
|
+
return {
|
|
4208
|
+
destroy: cleanup,
|
|
4209
|
+
zoomIn: () => {
|
|
4210
|
+
scale += 0.1;
|
|
4211
|
+
slideCard.style.transform = `scale(${scale})`;
|
|
4212
|
+
},
|
|
4213
|
+
zoomOut: () => {
|
|
4214
|
+
scale = Math.max(0.3, scale - 0.1);
|
|
4215
|
+
slideCard.style.transform = `scale(${scale})`;
|
|
4216
|
+
},
|
|
4217
|
+
getZoom: () => scale,
|
|
4218
|
+
setZoom: (level) => {
|
|
4219
|
+
scale = level;
|
|
4220
|
+
slideCard.style.transform = `scale(${scale})`;
|
|
4221
|
+
},
|
|
4222
|
+
fitToPage: () => {
|
|
4223
|
+
scale = 1;
|
|
4224
|
+
slideCard.style.transform = "scale(1)";
|
|
4225
|
+
},
|
|
4226
|
+
goToPage: (page) => {
|
|
4227
|
+
if (page >= 1 && page <= totalSlides) {
|
|
4228
|
+
renderSlide(page);
|
|
4229
|
+
}
|
|
4230
|
+
},
|
|
4231
|
+
getPageCount: () => totalSlides,
|
|
4232
|
+
getCurrentPage: () => currentSlide,
|
|
4233
|
+
getThumbnails: async () => {
|
|
4234
|
+
return slides.map((s, idx) => ({
|
|
4235
|
+
index: idx,
|
|
4236
|
+
label: `Slide ${idx + 1}`,
|
|
4237
|
+
render: async (canvas) => {
|
|
4238
|
+
const ctx2d = canvas.getContext("2d");
|
|
4239
|
+
if (!ctx2d) return;
|
|
4240
|
+
canvas.width = 160;
|
|
4241
|
+
canvas.height = 90;
|
|
4242
|
+
ctx2d.fillStyle = "#ffffff";
|
|
4243
|
+
ctx2d.fillRect(0, 0, 160, 90);
|
|
4244
|
+
ctx2d.fillStyle = "#1e3a8a";
|
|
4245
|
+
ctx2d.font = "bold 11px sans-serif";
|
|
4246
|
+
ctx2d.textAlign = "center";
|
|
4247
|
+
const title = s.title.slice(0, 18) || `Slide ${idx + 1}`;
|
|
4248
|
+
ctx2d.fillText(title, 80, 50);
|
|
4249
|
+
}
|
|
4250
|
+
}));
|
|
4251
|
+
},
|
|
4252
|
+
download: () => {
|
|
4253
|
+
const blob = new Blob([ctx.buffer], { type: "application/vnd.ms-powerpoint" });
|
|
4254
|
+
const url = URL.createObjectURL(blob);
|
|
4255
|
+
const a = document.createElement("a");
|
|
4256
|
+
a.href = url;
|
|
4257
|
+
a.download = ctx.metadata.name || "presentation.ppt";
|
|
4258
|
+
a.click();
|
|
4259
|
+
URL.revokeObjectURL(url);
|
|
4260
|
+
},
|
|
4261
|
+
print: () => {
|
|
4262
|
+
window.print();
|
|
4263
|
+
}
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
/**
|
|
4267
|
+
* Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
|
|
4268
|
+
*/
|
|
4269
|
+
extractSlides(stream) {
|
|
4270
|
+
const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
|
|
4271
|
+
const len = stream.length;
|
|
4272
|
+
let offset = 0;
|
|
4273
|
+
const slides = [];
|
|
4274
|
+
let currentSlideTexts = [];
|
|
4275
|
+
while (offset + 8 <= len) {
|
|
4276
|
+
const recVerInst = view.getUint16(offset, true);
|
|
4277
|
+
const recType = view.getUint16(offset + 2, true);
|
|
4278
|
+
const recLen = view.getUint32(offset + 4, true);
|
|
4279
|
+
if (recType === 1006) {
|
|
4280
|
+
if (currentSlideTexts.length > 0) {
|
|
4281
|
+
const title = currentSlideTexts[0] || "Slide";
|
|
4282
|
+
const texts = currentSlideTexts.slice(1);
|
|
4283
|
+
slides.push({ title, texts });
|
|
4284
|
+
currentSlideTexts = [];
|
|
4285
|
+
}
|
|
4286
|
+
offset += 8;
|
|
4287
|
+
continue;
|
|
4288
|
+
}
|
|
4289
|
+
if (recType === 3998 && recLen > 0 && offset + 8 + recLen <= len) {
|
|
4290
|
+
const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
|
|
4291
|
+
const text = new TextDecoder("latin1").decode(bytes).trim();
|
|
4292
|
+
if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
|
|
4293
|
+
currentSlideTexts.push(text);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
if (recType === 3999 && recLen > 0 && offset + 8 + recLen <= len) {
|
|
4297
|
+
const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
|
|
4298
|
+
const text = new TextDecoder("utf-16le").decode(bytes).trim();
|
|
4299
|
+
if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
|
|
4300
|
+
currentSlideTexts.push(text);
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
const isContainer = (recVerInst & 15) === 15;
|
|
4304
|
+
if (isContainer) {
|
|
4305
|
+
offset += 8;
|
|
4306
|
+
} else {
|
|
4307
|
+
offset += 8 + recLen;
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
if (currentSlideTexts.length > 0) {
|
|
4311
|
+
const title = currentSlideTexts[0] || "Slide";
|
|
4312
|
+
const texts = currentSlideTexts.slice(1);
|
|
4313
|
+
slides.push({ title, texts });
|
|
4314
|
+
}
|
|
4315
|
+
if (slides.length === 0) {
|
|
4316
|
+
const rawText = new TextDecoder("latin1", { fatal: false }).decode(stream);
|
|
4317
|
+
const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{4,}/g) || [];
|
|
4318
|
+
const filtered = matches.map((m) => m.trim()).filter((m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times"));
|
|
4319
|
+
if (filtered.length > 0) {
|
|
4320
|
+
const chunkSize = 4;
|
|
4321
|
+
for (let i = 0; i < filtered.length; i += chunkSize) {
|
|
4322
|
+
const chunk = filtered.slice(i, i + chunkSize);
|
|
4323
|
+
slides.push({
|
|
4324
|
+
title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
|
|
4325
|
+
texts: chunk.slice(1)
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
return slides;
|
|
4331
|
+
}
|
|
4332
|
+
};
|
|
4333
|
+
function pptPlugin() {
|
|
4334
|
+
return new PptPlugin();
|
|
4335
|
+
}
|
|
2765
4336
|
|
|
2766
4337
|
// src/index.ts
|
|
2767
4338
|
function getDefaultPlugins() {
|
|
@@ -2771,6 +4342,11 @@ function getDefaultPlugins() {
|
|
|
2771
4342
|
docxPlugin(),
|
|
2772
4343
|
excelPlugin(),
|
|
2773
4344
|
pptxPlugin(),
|
|
4345
|
+
docPlugin(),
|
|
4346
|
+
pptPlugin(),
|
|
4347
|
+
openDocumentPlugin(),
|
|
4348
|
+
rtfPlugin(),
|
|
4349
|
+
htmlPreviewPlugin(),
|
|
2774
4350
|
csvPlugin(),
|
|
2775
4351
|
archivePlugin(),
|
|
2776
4352
|
markdownPlugin(),
|
|
@@ -2787,6 +4363,7 @@ var FilePreviewViewer2 = class extends FilePreviewViewer {
|
|
|
2787
4363
|
}
|
|
2788
4364
|
};
|
|
2789
4365
|
|
|
4366
|
+
exports.CfbfReader = CfbfReader;
|
|
2790
4367
|
exports.EventEmitter = EventEmitter;
|
|
2791
4368
|
exports.FilePreviewViewer = FilePreviewViewer2;
|
|
2792
4369
|
exports.ThumbnailPanel = ThumbnailPanel;
|
|
@@ -2799,18 +4376,23 @@ exports.csvPlugin = csvPlugin;
|
|
|
2799
4376
|
exports.debounce = debounce;
|
|
2800
4377
|
exports.detectMagicBytes = detectMagicBytes;
|
|
2801
4378
|
exports.detectOoxmlType = detectOoxmlType;
|
|
4379
|
+
exports.docPlugin = docPlugin;
|
|
2802
4380
|
exports.docxPlugin = docxPlugin;
|
|
2803
4381
|
exports.downloadFile = downloadFile;
|
|
2804
4382
|
exports.excelPlugin = excelPlugin;
|
|
2805
4383
|
exports.extractExtension = extractExtension;
|
|
2806
4384
|
exports.formatFileSize = formatFileSize;
|
|
2807
4385
|
exports.getDefaultPlugins = getDefaultPlugins;
|
|
4386
|
+
exports.htmlPreviewPlugin = htmlPreviewPlugin;
|
|
2808
4387
|
exports.markdownPlugin = markdownPlugin;
|
|
2809
4388
|
exports.mediaPlugin = mediaPlugin;
|
|
2810
4389
|
exports.mimeFromExtension = mimeFromExtension;
|
|
4390
|
+
exports.openDocumentPlugin = openDocumentPlugin;
|
|
2811
4391
|
exports.pdfPlugin = pdfPlugin;
|
|
4392
|
+
exports.pptPlugin = pptPlugin;
|
|
2812
4393
|
exports.pptxPlugin = pptxPlugin;
|
|
2813
4394
|
exports.printElement = printElement;
|
|
4395
|
+
exports.rtfPlugin = rtfPlugin;
|
|
2814
4396
|
exports.sanitizeHTML = sanitizeHTML;
|
|
2815
4397
|
exports.sanitizeSVG = sanitizeSVG;
|
|
2816
4398
|
exports.sourceToArrayBuffer = sourceToArrayBuffer;
|