@open-file-viewer/core 0.1.2 → 0.1.3
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/README.md +2 -1
- package/dist/index.cjs +484 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +484 -181
- package/dist/index.js.map +1 -1
- package/dist/style.css +452 -55
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -861,6 +861,8 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
861
861
|
let queueLabel;
|
|
862
862
|
let previousButton;
|
|
863
863
|
let nextButton;
|
|
864
|
+
let zoomResetButton;
|
|
865
|
+
let currentZoom;
|
|
864
866
|
const commandButtons = [];
|
|
865
867
|
const customButtons = [];
|
|
866
868
|
const disposers = [];
|
|
@@ -876,7 +878,9 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
876
878
|
queue,
|
|
877
879
|
element,
|
|
878
880
|
search,
|
|
879
|
-
canCommand: canRunCommand
|
|
881
|
+
canCommand: canRunCommand,
|
|
882
|
+
zoom: currentZoom,
|
|
883
|
+
setZoom
|
|
880
884
|
});
|
|
881
885
|
const addButton = (label, title, action, className, icon) => {
|
|
882
886
|
const button = document.createElement("button");
|
|
@@ -943,6 +947,8 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
943
947
|
}
|
|
944
948
|
if (id === "zoom-reset" && options.zoom) {
|
|
945
949
|
addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-reset");
|
|
950
|
+
zoomResetButton = commandButtons[commandButtons.length - 1]?.button;
|
|
951
|
+
updateZoomLabel();
|
|
946
952
|
return;
|
|
947
953
|
}
|
|
948
954
|
if (id === "rotate-right" && options.rotate) {
|
|
@@ -1045,6 +1051,22 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
1045
1051
|
searchCount.textContent = "";
|
|
1046
1052
|
}
|
|
1047
1053
|
};
|
|
1054
|
+
function setZoom(zoom) {
|
|
1055
|
+
currentZoom = typeof zoom === "number" && Number.isFinite(zoom) && zoom > 0 ? zoom : void 0;
|
|
1056
|
+
updateZoomLabel();
|
|
1057
|
+
updateCustomButtons();
|
|
1058
|
+
refreshCustomRender();
|
|
1059
|
+
}
|
|
1060
|
+
function updateZoomLabel() {
|
|
1061
|
+
if (!zoomResetButton) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
setToolbarButtonContent(
|
|
1065
|
+
zoomResetButton,
|
|
1066
|
+
currentZoom === void 0 ? getToolbarLabel(options, "zoom-reset") : formatToolbarZoom(currentZoom),
|
|
1067
|
+
options.icons?.["zoom-reset"]
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1048
1070
|
const refreshCustomRender = () => {
|
|
1049
1071
|
if (!options.render) {
|
|
1050
1072
|
return;
|
|
@@ -1061,6 +1083,8 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
1061
1083
|
file = nextFile;
|
|
1062
1084
|
currentIndex = index;
|
|
1063
1085
|
currentLength = length;
|
|
1086
|
+
currentZoom = void 0;
|
|
1087
|
+
updateZoomLabel();
|
|
1064
1088
|
resetSearch();
|
|
1065
1089
|
commandButtons.forEach(({ button }) => {
|
|
1066
1090
|
button.disabled = true;
|
|
@@ -1086,6 +1110,7 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
1086
1110
|
refreshCustomRender();
|
|
1087
1111
|
},
|
|
1088
1112
|
getContext,
|
|
1113
|
+
setZoom,
|
|
1089
1114
|
destroy() {
|
|
1090
1115
|
search.clear();
|
|
1091
1116
|
for (const dispose of disposers) {
|
|
@@ -1103,7 +1128,9 @@ function createToolbarContext({
|
|
|
1103
1128
|
queue,
|
|
1104
1129
|
element,
|
|
1105
1130
|
search,
|
|
1106
|
-
canCommand
|
|
1131
|
+
canCommand,
|
|
1132
|
+
zoom,
|
|
1133
|
+
setZoom
|
|
1107
1134
|
}) {
|
|
1108
1135
|
return {
|
|
1109
1136
|
file,
|
|
@@ -1112,6 +1139,8 @@ function createToolbarContext({
|
|
|
1112
1139
|
viewport,
|
|
1113
1140
|
canPrevious: index > 0,
|
|
1114
1141
|
canNext: index < length - 1,
|
|
1142
|
+
zoom,
|
|
1143
|
+
zoomLabel: zoom === void 0 ? void 0 : formatToolbarZoom(zoom),
|
|
1115
1144
|
async previous() {
|
|
1116
1145
|
await queue.previous();
|
|
1117
1146
|
},
|
|
@@ -1120,6 +1149,7 @@ function createToolbarContext({
|
|
|
1120
1149
|
},
|
|
1121
1150
|
command: queue.command,
|
|
1122
1151
|
canCommand,
|
|
1152
|
+
setZoom,
|
|
1123
1153
|
download() {
|
|
1124
1154
|
if (file) {
|
|
1125
1155
|
downloadFile(file);
|
|
@@ -1167,6 +1197,9 @@ function getToolbarLabel(options, id) {
|
|
|
1167
1197
|
function getToolbarTitle(options, id) {
|
|
1168
1198
|
return options.titles?.[id] ?? options.labels?.[id] ?? defaultToolbarTitles[id];
|
|
1169
1199
|
}
|
|
1200
|
+
function formatToolbarZoom(zoom) {
|
|
1201
|
+
return `${Math.round(zoom * 100)}%`;
|
|
1202
|
+
}
|
|
1170
1203
|
function getToolbarOrder(options, queueLength) {
|
|
1171
1204
|
if (options.order) {
|
|
1172
1205
|
return options.order;
|
|
@@ -1214,7 +1247,7 @@ function setToolbarButtonContent(button, label, icon) {
|
|
|
1214
1247
|
iconElement.className = "ofv-toolbar-icon";
|
|
1215
1248
|
iconElement.setAttribute("aria-hidden", "true");
|
|
1216
1249
|
if (typeof icon === "string") {
|
|
1217
|
-
iconElement.
|
|
1250
|
+
iconElement.append(sanitizeToolbarIcon(icon));
|
|
1218
1251
|
} else {
|
|
1219
1252
|
iconElement.append(icon.cloneNode(true));
|
|
1220
1253
|
}
|
|
@@ -1223,6 +1256,96 @@ function setToolbarButtonContent(button, label, icon) {
|
|
|
1223
1256
|
labelElement.textContent = label;
|
|
1224
1257
|
button.append(iconElement, labelElement);
|
|
1225
1258
|
}
|
|
1259
|
+
var allowedToolbarIconTags = /* @__PURE__ */ new Set([
|
|
1260
|
+
"svg",
|
|
1261
|
+
"g",
|
|
1262
|
+
"path",
|
|
1263
|
+
"circle",
|
|
1264
|
+
"rect",
|
|
1265
|
+
"line",
|
|
1266
|
+
"polyline",
|
|
1267
|
+
"polygon",
|
|
1268
|
+
"ellipse",
|
|
1269
|
+
"defs",
|
|
1270
|
+
"title",
|
|
1271
|
+
"desc"
|
|
1272
|
+
]);
|
|
1273
|
+
var allowedToolbarIconAttrs = /* @__PURE__ */ new Set([
|
|
1274
|
+
"aria-hidden",
|
|
1275
|
+
"class",
|
|
1276
|
+
"cx",
|
|
1277
|
+
"cy",
|
|
1278
|
+
"d",
|
|
1279
|
+
"fill",
|
|
1280
|
+
"focusable",
|
|
1281
|
+
"height",
|
|
1282
|
+
"id",
|
|
1283
|
+
"points",
|
|
1284
|
+
"r",
|
|
1285
|
+
"rx",
|
|
1286
|
+
"ry",
|
|
1287
|
+
"stroke",
|
|
1288
|
+
"stroke-linecap",
|
|
1289
|
+
"stroke-linejoin",
|
|
1290
|
+
"stroke-width",
|
|
1291
|
+
"transform",
|
|
1292
|
+
"viewBox",
|
|
1293
|
+
"width",
|
|
1294
|
+
"x",
|
|
1295
|
+
"x1",
|
|
1296
|
+
"x2",
|
|
1297
|
+
"y",
|
|
1298
|
+
"y1",
|
|
1299
|
+
"y2"
|
|
1300
|
+
]);
|
|
1301
|
+
function sanitizeToolbarIcon(icon) {
|
|
1302
|
+
const template = document.createElement("template");
|
|
1303
|
+
template.innerHTML = icon.trim();
|
|
1304
|
+
const fragment = document.createDocumentFragment();
|
|
1305
|
+
for (const child of Array.from(template.content.childNodes)) {
|
|
1306
|
+
const sanitized = sanitizeToolbarIconNode(child);
|
|
1307
|
+
if (sanitized) {
|
|
1308
|
+
fragment.append(sanitized);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return fragment;
|
|
1312
|
+
}
|
|
1313
|
+
function sanitizeToolbarIconNode(node) {
|
|
1314
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1315
|
+
const text = node.textContent || "";
|
|
1316
|
+
return text.trim() ? document.createTextNode(text) : null;
|
|
1317
|
+
}
|
|
1318
|
+
if (!(node instanceof Element)) {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
const tagName = node.tagName.toLowerCase();
|
|
1322
|
+
if (!allowedToolbarIconTags.has(tagName)) {
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
const sanitized = document.createElementNS("http://www.w3.org/2000/svg", tagName);
|
|
1326
|
+
for (const attr of Array.from(node.attributes)) {
|
|
1327
|
+
if (isSafeToolbarIconAttribute(attr.name, attr.value)) {
|
|
1328
|
+
sanitized.setAttribute(attr.name, attr.value);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
for (const child of Array.from(node.childNodes)) {
|
|
1332
|
+
const sanitizedChild = sanitizeToolbarIconNode(child);
|
|
1333
|
+
if (sanitizedChild) {
|
|
1334
|
+
sanitized.append(sanitizedChild);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return sanitized;
|
|
1338
|
+
}
|
|
1339
|
+
function isSafeToolbarIconAttribute(name, value) {
|
|
1340
|
+
const attrName = name.toLowerCase();
|
|
1341
|
+
if (attrName.startsWith("on") || attrName.includes(":")) {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
if (!allowedToolbarIconAttrs.has(name) && !allowedToolbarIconAttrs.has(attrName) && !attrName.startsWith("data-")) {
|
|
1345
|
+
return false;
|
|
1346
|
+
}
|
|
1347
|
+
return !/^\s*(?:javascript|data:text\/html|vbscript):/i.test(value);
|
|
1348
|
+
}
|
|
1226
1349
|
function isBuiltInToolbarAction(id) {
|
|
1227
1350
|
return id in defaultToolbarLabels;
|
|
1228
1351
|
}
|
|
@@ -1634,6 +1757,7 @@ function imagePlugin() {
|
|
|
1634
1757
|
const updateTransform = () => {
|
|
1635
1758
|
image.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale}) rotate(${rotation}deg)`;
|
|
1636
1759
|
zoomLabel.textContent = `${Math.round(scale * 100)}%`;
|
|
1760
|
+
ctx.toolbar?.setZoom(scale);
|
|
1637
1761
|
};
|
|
1638
1762
|
const showImageFallback = () => {
|
|
1639
1763
|
stage.replaceChildren(createImageFallback(ctx.file.name, url));
|
|
@@ -1749,6 +1873,7 @@ function imagePlugin() {
|
|
|
1749
1873
|
image.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
|
|
1750
1874
|
},
|
|
1751
1875
|
destroy() {
|
|
1876
|
+
ctx.toolbar?.setZoom(void 0);
|
|
1752
1877
|
for (const dispose of disposers) {
|
|
1753
1878
|
dispose();
|
|
1754
1879
|
}
|
|
@@ -3307,7 +3432,6 @@ var mimeLangMap = {
|
|
|
3307
3432
|
};
|
|
3308
3433
|
var MAX_HIGHLIGHT_CHARS = 18e4;
|
|
3309
3434
|
var MAX_RENDER_CHARS = 6e5;
|
|
3310
|
-
var MONACO_LOADER_KEY = "__OFV_MONACO_LOADER__";
|
|
3311
3435
|
function loadPrismCss(theme) {
|
|
3312
3436
|
const lightId = "ofv-prism-css-light";
|
|
3313
3437
|
const darkId = "ofv-prism-css-dark";
|
|
@@ -3531,10 +3655,6 @@ function textPlugin() {
|
|
|
3531
3655
|
wrapButton.addEventListener("click", () => {
|
|
3532
3656
|
const wrapped = wrapper.classList.toggle("is-wrapped");
|
|
3533
3657
|
wrapButton.setAttribute("aria-pressed", String(wrapped));
|
|
3534
|
-
monacoEditor?.updateOptions?.({ wordWrap: wrapped ? "on" : "off" });
|
|
3535
|
-
if (fallbackEditor) {
|
|
3536
|
-
fallbackEditor.wrap = wrapped ? "soft" : "off";
|
|
3537
|
-
}
|
|
3538
3658
|
});
|
|
3539
3659
|
const copyButton = document.createElement("button");
|
|
3540
3660
|
copyButton.type = "button";
|
|
@@ -3559,12 +3679,7 @@ function textPlugin() {
|
|
|
3559
3679
|
downloadText(ctx.file.name, text);
|
|
3560
3680
|
status.textContent = "Download ready";
|
|
3561
3681
|
});
|
|
3562
|
-
|
|
3563
|
-
editorButton.type = "button";
|
|
3564
|
-
editorButton.className = "ofv-code-action";
|
|
3565
|
-
editorButton.textContent = "Editor";
|
|
3566
|
-
editorButton.setAttribute("aria-pressed", "false");
|
|
3567
|
-
actions.append(status, editorButton, wrapButton, copyButton, downloadButton);
|
|
3682
|
+
actions.append(wrapButton, copyButton, downloadButton, status);
|
|
3568
3683
|
header.append(title, actions);
|
|
3569
3684
|
const structureSummary = createTextStructureSummary(text, ext, lang, ctx.file.mimeType);
|
|
3570
3685
|
const body = document.createElement("div");
|
|
@@ -3580,93 +3695,6 @@ function textPlugin() {
|
|
|
3580
3695
|
code.textContent = codeText;
|
|
3581
3696
|
pre.appendChild(code);
|
|
3582
3697
|
body.append(gutter, pre);
|
|
3583
|
-
const editorHost = document.createElement("div");
|
|
3584
|
-
editorHost.className = "ofv-code-editor";
|
|
3585
|
-
editorHost.hidden = true;
|
|
3586
|
-
let monacoEditor;
|
|
3587
|
-
let monacoModel;
|
|
3588
|
-
let fallbackEditor;
|
|
3589
|
-
let editorReady = false;
|
|
3590
|
-
let editorLoading;
|
|
3591
|
-
const showReader = () => {
|
|
3592
|
-
editorHost.hidden = true;
|
|
3593
|
-
body.hidden = false;
|
|
3594
|
-
wrapper.classList.remove("is-editor");
|
|
3595
|
-
editorButton.textContent = "Editor";
|
|
3596
|
-
editorButton.setAttribute("aria-pressed", "false");
|
|
3597
|
-
};
|
|
3598
|
-
const showEditor = async () => {
|
|
3599
|
-
if (text.length > MAX_RENDER_CHARS) {
|
|
3600
|
-
status.textContent = "Editor skipped for large file";
|
|
3601
|
-
return;
|
|
3602
|
-
}
|
|
3603
|
-
if (editorReady) {
|
|
3604
|
-
body.hidden = true;
|
|
3605
|
-
editorHost.hidden = false;
|
|
3606
|
-
wrapper.classList.add("is-editor");
|
|
3607
|
-
editorButton.textContent = "Reader";
|
|
3608
|
-
editorButton.setAttribute("aria-pressed", "true");
|
|
3609
|
-
monacoEditor?.layout?.();
|
|
3610
|
-
return;
|
|
3611
|
-
}
|
|
3612
|
-
if (!editorLoading) {
|
|
3613
|
-
editorLoading = (async () => {
|
|
3614
|
-
editorButton.disabled = true;
|
|
3615
|
-
status.textContent = "Loading editor";
|
|
3616
|
-
try {
|
|
3617
|
-
const monaco = await loadMonaco();
|
|
3618
|
-
if (!monaco.editor?.create) {
|
|
3619
|
-
throw new Error("Monaco editor API is unavailable.");
|
|
3620
|
-
}
|
|
3621
|
-
const monacoLanguage = toMonacoLanguage(ext, lang);
|
|
3622
|
-
monaco.editor.setTheme?.(isDark ? "vs-dark" : "vs");
|
|
3623
|
-
monacoModel = monaco.editor.createModel?.(text, monacoLanguage);
|
|
3624
|
-
monacoEditor = monaco.editor.create(editorHost, {
|
|
3625
|
-
...monacoModel ? { model: monacoModel } : { value: text, language: monacoLanguage },
|
|
3626
|
-
automaticLayout: true,
|
|
3627
|
-
fontFamily: "var(--ofv-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace)",
|
|
3628
|
-
fontSize: 13,
|
|
3629
|
-
lineNumbers: "on",
|
|
3630
|
-
minimap: { enabled: text.length <= 8e4 },
|
|
3631
|
-
readOnly: true,
|
|
3632
|
-
renderLineHighlight: "line",
|
|
3633
|
-
scrollBeyondLastLine: false,
|
|
3634
|
-
wordWrap: wrapper.classList.contains("is-wrapped") ? "on" : "off"
|
|
3635
|
-
});
|
|
3636
|
-
editorReady = true;
|
|
3637
|
-
status.textContent = "Editor ready";
|
|
3638
|
-
body.hidden = true;
|
|
3639
|
-
editorHost.hidden = false;
|
|
3640
|
-
wrapper.classList.add("is-editor");
|
|
3641
|
-
editorButton.textContent = "Reader";
|
|
3642
|
-
editorButton.setAttribute("aria-pressed", "true");
|
|
3643
|
-
monacoEditor.layout?.();
|
|
3644
|
-
} catch (error) {
|
|
3645
|
-
console.warn("Monaco editor failed to load; using built-in code editor fallback:", error);
|
|
3646
|
-
fallbackEditor = createBasicCodeEditor(text, wrapper.classList.contains("is-wrapped"));
|
|
3647
|
-
editorHost.replaceChildren(fallbackEditor);
|
|
3648
|
-
editorReady = true;
|
|
3649
|
-
status.textContent = "Basic editor";
|
|
3650
|
-
body.hidden = true;
|
|
3651
|
-
editorHost.hidden = false;
|
|
3652
|
-
wrapper.classList.add("is-editor");
|
|
3653
|
-
editorButton.textContent = "Reader";
|
|
3654
|
-
editorButton.setAttribute("aria-pressed", "true");
|
|
3655
|
-
} finally {
|
|
3656
|
-
editorButton.disabled = false;
|
|
3657
|
-
}
|
|
3658
|
-
})();
|
|
3659
|
-
}
|
|
3660
|
-
await editorLoading;
|
|
3661
|
-
};
|
|
3662
|
-
editorButton.addEventListener("click", () => {
|
|
3663
|
-
if (editorHost.hidden) {
|
|
3664
|
-
void showEditor();
|
|
3665
|
-
} else {
|
|
3666
|
-
showReader();
|
|
3667
|
-
status.textContent = "Reader mode";
|
|
3668
|
-
}
|
|
3669
|
-
});
|
|
3670
3698
|
wrapper.append(header);
|
|
3671
3699
|
if (structureSummary) {
|
|
3672
3700
|
wrapper.append(structureSummary);
|
|
@@ -3684,7 +3712,6 @@ function textPlugin() {
|
|
|
3684
3712
|
wrapper.append(notice);
|
|
3685
3713
|
}
|
|
3686
3714
|
wrapper.appendChild(body);
|
|
3687
|
-
wrapper.appendChild(editorHost);
|
|
3688
3715
|
ctx.viewport.appendChild(wrapper);
|
|
3689
3716
|
if (shouldHighlight) {
|
|
3690
3717
|
try {
|
|
@@ -3694,12 +3721,7 @@ function textPlugin() {
|
|
|
3694
3721
|
}
|
|
3695
3722
|
}
|
|
3696
3723
|
return {
|
|
3697
|
-
resize() {
|
|
3698
|
-
monacoEditor?.layout?.();
|
|
3699
|
-
},
|
|
3700
3724
|
destroy() {
|
|
3701
|
-
monacoEditor?.dispose?.();
|
|
3702
|
-
monacoModel?.dispose?.();
|
|
3703
3725
|
wrapper.remove();
|
|
3704
3726
|
}
|
|
3705
3727
|
};
|
|
@@ -3714,45 +3736,6 @@ function normalizeFileName2(name) {
|
|
|
3714
3736
|
const baseName = name.split(/[\\/]/).pop() || name;
|
|
3715
3737
|
return baseName.toLowerCase();
|
|
3716
3738
|
}
|
|
3717
|
-
async function loadMonaco() {
|
|
3718
|
-
const injectedLoader = globalThis[MONACO_LOADER_KEY];
|
|
3719
|
-
if (typeof injectedLoader === "function") {
|
|
3720
|
-
return await injectedLoader();
|
|
3721
|
-
}
|
|
3722
|
-
const importer = new Function("specifier", "return import(specifier)");
|
|
3723
|
-
return importer("monaco-editor");
|
|
3724
|
-
}
|
|
3725
|
-
function toMonacoLanguage(ext, prismLanguage) {
|
|
3726
|
-
if (ext === "vue") {
|
|
3727
|
-
return "html";
|
|
3728
|
-
}
|
|
3729
|
-
if (ext === "tsx") {
|
|
3730
|
-
return "typescript";
|
|
3731
|
-
}
|
|
3732
|
-
if (ext === "jsx") {
|
|
3733
|
-
return "javascript";
|
|
3734
|
-
}
|
|
3735
|
-
if (prismLanguage === "markup") {
|
|
3736
|
-
return ext === "xml" ? "xml" : "html";
|
|
3737
|
-
}
|
|
3738
|
-
if (prismLanguage === "bash") {
|
|
3739
|
-
return "shell";
|
|
3740
|
-
}
|
|
3741
|
-
if (prismLanguage === "none") {
|
|
3742
|
-
return "plaintext";
|
|
3743
|
-
}
|
|
3744
|
-
return prismLanguage;
|
|
3745
|
-
}
|
|
3746
|
-
function createBasicCodeEditor(text, wrapped) {
|
|
3747
|
-
const textarea = document.createElement("textarea");
|
|
3748
|
-
textarea.className = "ofv-code-editor-fallback";
|
|
3749
|
-
textarea.readOnly = true;
|
|
3750
|
-
textarea.spellcheck = false;
|
|
3751
|
-
textarea.wrap = wrapped ? "soft" : "off";
|
|
3752
|
-
textarea.value = text;
|
|
3753
|
-
textarea.setAttribute("aria-label", "Code editor preview");
|
|
3754
|
-
return textarea;
|
|
3755
|
-
}
|
|
3756
3739
|
function createTextFallback(fileName, url) {
|
|
3757
3740
|
const fallback = document.createElement("div");
|
|
3758
3741
|
fallback.className = "ofv-fallback";
|
|
@@ -3990,7 +3973,13 @@ function pdfPlugin(options = {}) {
|
|
|
3990
3973
|
scroller.className = "ofv-pdf ofv-pdf-pages";
|
|
3991
3974
|
viewer.append(summary, scroller);
|
|
3992
3975
|
ctx.viewport.append(viewer);
|
|
3993
|
-
const documentTask = pdf.getDocument(
|
|
3976
|
+
const documentTask = pdf.getDocument({
|
|
3977
|
+
url,
|
|
3978
|
+
cMapUrl: normalizedOptions.cMapUrl ?? `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdf.version}/cmaps/`,
|
|
3979
|
+
cMapPacked: normalizedOptions.cMapPacked ?? true,
|
|
3980
|
+
standardFontDataUrl: normalizedOptions.standardFontDataUrl ?? `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdf.version}/standard_fonts/`,
|
|
3981
|
+
useSystemFonts: normalizedOptions.useSystemFonts ?? true
|
|
3982
|
+
});
|
|
3994
3983
|
const doc = await documentTask.promise.catch((error) => {
|
|
3995
3984
|
viewer.remove();
|
|
3996
3985
|
ctx.viewport.classList.add("ofv-center");
|
|
@@ -4026,6 +4015,7 @@ function pdfPlugin(options = {}) {
|
|
|
4026
4015
|
let zoomFactor = 1;
|
|
4027
4016
|
const updateSummary = () => {
|
|
4028
4017
|
renderPdfSummary(summary, doc.numPages, pagesMeta, ctx.options.fit, zoomFactor);
|
|
4018
|
+
ctx.toolbar?.setZoom(zoomFactor);
|
|
4029
4019
|
};
|
|
4030
4020
|
const clearPage = (pageIdx) => {
|
|
4031
4021
|
const state = pageStates[pageIdx];
|
|
@@ -4049,12 +4039,17 @@ function pdfPlugin(options = {}) {
|
|
|
4049
4039
|
try {
|
|
4050
4040
|
const page = await doc.getPage(pageIdx + 1);
|
|
4051
4041
|
const meta = pagesMeta[pageIdx];
|
|
4052
|
-
const scale = ctx.options.fit === "actual" ? zoomFactor : Math.max(0.
|
|
4042
|
+
const scale = ctx.options.fit === "actual" ? zoomFactor : Math.max(0.05, Math.min(5, getPdfAvailableWidth(size.width) / meta.width * zoomFactor));
|
|
4053
4043
|
const viewport = page.getViewport({ scale });
|
|
4044
|
+
const outputScale = getPdfOutputScale();
|
|
4045
|
+
const cssWidth = Math.floor(viewport.width);
|
|
4046
|
+
const cssHeight = Math.floor(viewport.height);
|
|
4054
4047
|
const canvas = document.createElement("canvas");
|
|
4055
4048
|
canvas.className = "ofv-pdf-page";
|
|
4056
|
-
canvas.width = Math.floor(
|
|
4057
|
-
canvas.height = Math.floor(
|
|
4049
|
+
canvas.width = Math.floor(cssWidth * outputScale);
|
|
4050
|
+
canvas.height = Math.floor(cssHeight * outputScale);
|
|
4051
|
+
canvas.style.width = `${cssWidth}px`;
|
|
4052
|
+
canvas.style.height = `${cssHeight}px`;
|
|
4058
4053
|
const context = canvas.getContext("2d");
|
|
4059
4054
|
if (!context) {
|
|
4060
4055
|
throw new Error("Canvas 2D context is not available.");
|
|
@@ -4063,7 +4058,8 @@ function pdfPlugin(options = {}) {
|
|
|
4063
4058
|
state.canvas = canvas;
|
|
4064
4059
|
const renderTask = page.render({
|
|
4065
4060
|
canvasContext: context,
|
|
4066
|
-
viewport
|
|
4061
|
+
viewport,
|
|
4062
|
+
transform: outputScale === 1 ? void 0 : [outputScale, 0, 0, outputScale, 0, 0]
|
|
4067
4063
|
});
|
|
4068
4064
|
state.renderTask = renderTask;
|
|
4069
4065
|
await renderTask.promise;
|
|
@@ -4071,8 +4067,8 @@ function pdfPlugin(options = {}) {
|
|
|
4071
4067
|
const textContent = await page.getTextContent();
|
|
4072
4068
|
const textLayer = document.createElement("div");
|
|
4073
4069
|
textLayer.className = "ofv-pdf-text-layer";
|
|
4074
|
-
textLayer.style.width = `${
|
|
4075
|
-
textLayer.style.height = `${
|
|
4070
|
+
textLayer.style.width = `${cssWidth}px`;
|
|
4071
|
+
textLayer.style.height = `${cssHeight}px`;
|
|
4076
4072
|
state.wrapper.appendChild(textLayer);
|
|
4077
4073
|
for (const item of textContent.items) {
|
|
4078
4074
|
if (!("str" in item)) continue;
|
|
@@ -4133,7 +4129,7 @@ function pdfPlugin(options = {}) {
|
|
|
4133
4129
|
}
|
|
4134
4130
|
for (let i = 0; i < doc.numPages; i++) {
|
|
4135
4131
|
const meta = pagesMeta[i];
|
|
4136
|
-
const scale = ctx.options.fit === "actual" ? zoomFactor : Math.max(0.
|
|
4132
|
+
const scale = ctx.options.fit === "actual" ? zoomFactor : Math.max(0.05, Math.min(5, getPdfAvailableWidth(size.width) / meta.width * zoomFactor));
|
|
4137
4133
|
const w = Math.floor(meta.width * scale);
|
|
4138
4134
|
const h = Math.floor(meta.height * scale);
|
|
4139
4135
|
const wrapper = document.createElement("div");
|
|
@@ -4188,6 +4184,7 @@ function pdfPlugin(options = {}) {
|
|
|
4188
4184
|
}, 120);
|
|
4189
4185
|
},
|
|
4190
4186
|
destroy() {
|
|
4187
|
+
ctx.toolbar?.setZoom(void 0);
|
|
4191
4188
|
window.clearTimeout(resizeTimer);
|
|
4192
4189
|
observer?.disconnect();
|
|
4193
4190
|
pageStates.forEach((state) => {
|
|
@@ -4206,6 +4203,19 @@ function pdfPlugin(options = {}) {
|
|
|
4206
4203
|
}
|
|
4207
4204
|
};
|
|
4208
4205
|
}
|
|
4206
|
+
function getPdfOutputScale() {
|
|
4207
|
+
if (typeof window === "undefined") {
|
|
4208
|
+
return 1;
|
|
4209
|
+
}
|
|
4210
|
+
return Math.max(1, Math.min(window.devicePixelRatio || 1, 2.5));
|
|
4211
|
+
}
|
|
4212
|
+
function getPdfAvailableWidth(width) {
|
|
4213
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
4214
|
+
return 1;
|
|
4215
|
+
}
|
|
4216
|
+
const gutter = width < 160 ? 16 : 32;
|
|
4217
|
+
return Math.max(1, width - gutter);
|
|
4218
|
+
}
|
|
4209
4219
|
function renderPdfSummary(summary, pages, pagesMeta, fit, zoomFactor) {
|
|
4210
4220
|
summary.replaceChildren();
|
|
4211
4221
|
appendPdfSummary(summary, "\u9875\u6570", String(pages));
|
|
@@ -4965,8 +4975,9 @@ function officePlugin() {
|
|
|
4965
4975
|
ctx.viewport.append(panel);
|
|
4966
4976
|
const extension = resolveFormat(ctx.file, officeMimeFormatMap);
|
|
4967
4977
|
const arrayBuffer = await readArrayBuffer(ctx.file);
|
|
4978
|
+
let disposeDocxFit;
|
|
4968
4979
|
if (fileIsDocx(extension)) {
|
|
4969
|
-
await renderDocx(panel, arrayBuffer);
|
|
4980
|
+
disposeDocxFit = await renderDocx(panel, arrayBuffer);
|
|
4970
4981
|
} else if (extension === "rtf") {
|
|
4971
4982
|
renderPlainDocument(panel, "RTF \u6587\u6863", rtfToText(await readTextFromBuffer(arrayBuffer)));
|
|
4972
4983
|
} else if (extension === "odt") {
|
|
@@ -4991,6 +5002,7 @@ function officePlugin() {
|
|
|
4991
5002
|
}
|
|
4992
5003
|
return {
|
|
4993
5004
|
destroy() {
|
|
5005
|
+
disposeDocxFit?.();
|
|
4994
5006
|
panel.remove();
|
|
4995
5007
|
}
|
|
4996
5008
|
};
|
|
@@ -5024,6 +5036,7 @@ async function renderDocx(panel, arrayBuffer) {
|
|
|
5024
5036
|
experimental: true,
|
|
5025
5037
|
useBase64URL: true
|
|
5026
5038
|
});
|
|
5039
|
+
return fitDocxPages(content);
|
|
5027
5040
|
} catch (error) {
|
|
5028
5041
|
content.replaceChildren();
|
|
5029
5042
|
const fallbackNote = document.createElement("div");
|
|
@@ -5038,6 +5051,74 @@ async function renderDocx(panel, arrayBuffer) {
|
|
|
5038
5051
|
}
|
|
5039
5052
|
console.warn("DOCX layout preview failed, fell back to Mammoth:", error);
|
|
5040
5053
|
}
|
|
5054
|
+
return () => void 0;
|
|
5055
|
+
}
|
|
5056
|
+
function fitDocxPages(container) {
|
|
5057
|
+
const wrapper = container.querySelector(".ofv-docx-wrapper");
|
|
5058
|
+
if (!wrapper) {
|
|
5059
|
+
return () => void 0;
|
|
5060
|
+
}
|
|
5061
|
+
const update = () => {
|
|
5062
|
+
const frames = ensureDocxPageFrames(wrapper);
|
|
5063
|
+
if (frames.length === 0) {
|
|
5064
|
+
wrapper.style.removeProperty("--ofv-docx-scale");
|
|
5065
|
+
return;
|
|
5066
|
+
}
|
|
5067
|
+
const availableWidth = Math.max(1, container.clientWidth - 48);
|
|
5068
|
+
const pageWidth = Math.max(
|
|
5069
|
+
1,
|
|
5070
|
+
...frames.map(({ page }) => {
|
|
5071
|
+
const rectWidth = page.getBoundingClientRect().width;
|
|
5072
|
+
return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
|
|
5073
|
+
})
|
|
5074
|
+
);
|
|
5075
|
+
const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
|
|
5076
|
+
wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
|
|
5077
|
+
wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
|
|
5078
|
+
for (const { frame, page } of frames) {
|
|
5079
|
+
const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
|
|
5080
|
+
if (pageHeight > 0) {
|
|
5081
|
+
frame.style.height = `${Math.ceil(pageHeight * scale)}px`;
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
};
|
|
5085
|
+
update();
|
|
5086
|
+
const timers = [0, 80, 240].map((delay) => window.setTimeout(update, delay));
|
|
5087
|
+
if (typeof ResizeObserver === "undefined") {
|
|
5088
|
+
window.addEventListener("resize", update);
|
|
5089
|
+
return () => {
|
|
5090
|
+
timers.forEach((timer) => window.clearTimeout(timer));
|
|
5091
|
+
window.removeEventListener("resize", update);
|
|
5092
|
+
};
|
|
5093
|
+
}
|
|
5094
|
+
const observer = new ResizeObserver(update);
|
|
5095
|
+
observer.observe(container);
|
|
5096
|
+
observer.observe(wrapper);
|
|
5097
|
+
return () => {
|
|
5098
|
+
timers.forEach((timer) => window.clearTimeout(timer));
|
|
5099
|
+
observer.disconnect();
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5102
|
+
function ensureDocxPageFrames(wrapper) {
|
|
5103
|
+
const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
|
|
5104
|
+
return pages.map((page) => {
|
|
5105
|
+
const parent = page.parentElement;
|
|
5106
|
+
if (parent?.classList.contains("ofv-docx-page-frame")) {
|
|
5107
|
+
return { frame: parent, page };
|
|
5108
|
+
}
|
|
5109
|
+
const frame = document.createElement("div");
|
|
5110
|
+
frame.className = "ofv-docx-page-frame";
|
|
5111
|
+
page.before(frame);
|
|
5112
|
+
frame.append(page);
|
|
5113
|
+
return { frame, page };
|
|
5114
|
+
});
|
|
5115
|
+
}
|
|
5116
|
+
function parseCssPixelValue(value) {
|
|
5117
|
+
const parsed = Number.parseFloat(value);
|
|
5118
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
5119
|
+
}
|
|
5120
|
+
function formatCssNumber(value) {
|
|
5121
|
+
return Number.isFinite(value) ? value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "") : "1";
|
|
5041
5122
|
}
|
|
5042
5123
|
async function renderDocxTextFallback(container, arrayBuffer) {
|
|
5043
5124
|
const article = document.createElement("article");
|
|
@@ -5136,7 +5217,7 @@ async function renderSheet(panel, arrayBuffer, extension) {
|
|
|
5136
5217
|
workbook = xlsx.read(arrayBuffer, { type: "array" });
|
|
5137
5218
|
} catch (error) {
|
|
5138
5219
|
if (isLegacyOfficeBinary(extension)) {
|
|
5139
|
-
renderLegacyOfficeBinary(panel, extension, arrayBuffer, normalizeOfficeError(error));
|
|
5220
|
+
renderLegacyOfficeBinary(panel, extension, arrayBuffer, `\u8868\u683C\u89E3\u6790\u5931\u8D25\uFF1A${normalizeOfficeError(error)}`);
|
|
5140
5221
|
return;
|
|
5141
5222
|
}
|
|
5142
5223
|
renderSheetFallback(panel, extension, normalizeOfficeError(error));
|
|
@@ -6241,14 +6322,15 @@ function legacyOfficeFormatLabel(extension) {
|
|
|
6241
6322
|
function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
|
|
6242
6323
|
const fragments = extractLegacyOfficeText(arrayBuffer);
|
|
6243
6324
|
panel.replaceChildren();
|
|
6244
|
-
const section = createSection("Office \
|
|
6325
|
+
const section = createSection("Office \u8F6C\u6362\u63D0\u793A");
|
|
6326
|
+
section.classList.add("ofv-office-conversion");
|
|
6245
6327
|
const format = document.createElement("p");
|
|
6246
6328
|
const strong = document.createElement("strong");
|
|
6247
6329
|
strong.textContent = `.${extension}`;
|
|
6248
6330
|
format.append(
|
|
6249
6331
|
strong,
|
|
6250
6332
|
document.createTextNode(
|
|
6251
|
-
" \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\
|
|
6333
|
+
" \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\u4EC5\u5C55\u793A\u53EF\u4FE1\u6587\u672C\u7247\u6BB5\u548C\u7ED3\u6784\u6307\u7EB9\uFF0C\u5B8C\u6574\u6392\u7248\u5EFA\u8BAE\u63A5\u5165 LibreOffice/OnlyOffice \u670D\u52A1\u7AEF\u8F6C\u6362\u4E3A PDF/HTML\u3002"
|
|
6252
6334
|
)
|
|
6253
6335
|
);
|
|
6254
6336
|
const meta = document.createElement("dl");
|
|
@@ -6257,12 +6339,15 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
|
|
|
6257
6339
|
appendOfficeBinaryMeta(meta, "\u6587\u4EF6\u7ED3\u6784", hasOleSignature(arrayBuffer) ? "\u68C0\u6D4B\u5230 OLE Compound File \u7B7E\u540D" : "\u672A\u68C0\u6D4B\u5230\u6807\u51C6 OLE \u7B7E\u540D\uFF0C\u6309\u539F\u59CB\u4E8C\u8FDB\u5236\u5C1D\u8BD5\u63D0\u53D6");
|
|
6258
6340
|
appendOfficeBinaryMeta(meta, "\u6587\u672C\u7247\u6BB5", `${fragments.length} \u6BB5`);
|
|
6259
6341
|
if (parseError) {
|
|
6260
|
-
appendOfficeBinaryMeta(meta, "\
|
|
6342
|
+
appendOfficeBinaryMeta(meta, "\u89E3\u6790\u72B6\u6001", parseError);
|
|
6261
6343
|
}
|
|
6262
6344
|
section.append(format, meta);
|
|
6263
6345
|
if (fragments.length > 0) {
|
|
6264
6346
|
const article = document.createElement("article");
|
|
6265
|
-
article.className = "ofv-document";
|
|
6347
|
+
article.className = "ofv-document ofv-office-binary-fragments";
|
|
6348
|
+
const heading = document.createElement("h4");
|
|
6349
|
+
heading.textContent = "\u53EF\u8BFB\u6587\u672C\u7247\u6BB5";
|
|
6350
|
+
article.append(heading);
|
|
6266
6351
|
for (const fragment of fragments.slice(0, 80)) {
|
|
6267
6352
|
const paragraph = document.createElement("p");
|
|
6268
6353
|
paragraph.textContent = fragment;
|
|
@@ -6271,7 +6356,8 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
|
|
|
6271
6356
|
section.append(article);
|
|
6272
6357
|
} else {
|
|
6273
6358
|
const empty = document.createElement("p");
|
|
6274
|
-
empty.
|
|
6359
|
+
empty.className = "ofv-office-binary-empty";
|
|
6360
|
+
empty.textContent = "\u672A\u63D0\u53D6\u5230\u7A33\u5B9A\u53EF\u8BFB\u6587\u672C\u3002\u8BE5\u6587\u4EF6\u53EF\u80FD\u7ECF\u8FC7\u538B\u7F29\u3001\u52A0\u5BC6\uFF0C\u6216\u6587\u672C\u7F16\u7801\u65E0\u6CD5\u5728\u6D4F\u89C8\u5668\u7AEF\u53EF\u9760\u8BC6\u522B\uFF1B\u8BF7\u4F7F\u7528\u670D\u52A1\u7AEF LibreOffice/OnlyOffice \u8F6C\u6362\u540E\u9884\u89C8\u3002";
|
|
6275
6361
|
section.append(empty);
|
|
6276
6362
|
}
|
|
6277
6363
|
panel.append(section);
|
|
@@ -6304,12 +6390,12 @@ function normalizeOfficeError(error) {
|
|
|
6304
6390
|
function extractLegacyOfficeText(arrayBuffer) {
|
|
6305
6391
|
const bytes = new Uint8Array(arrayBuffer);
|
|
6306
6392
|
const fragments = [
|
|
6307
|
-
...extractPrintableRuns(bytes),
|
|
6308
|
-
...extractUtf16Runs(bytes)
|
|
6309
|
-
].map((
|
|
6393
|
+
...extractPrintableRuns(bytes).map((text) => ({ text, source: "ascii" })),
|
|
6394
|
+
...extractUtf16Runs(bytes).map((text) => ({ text, source: "utf16" }))
|
|
6395
|
+
].map(({ text, source }) => ({ text: normalizeLegacyText(text), source })).filter(({ text, source }) => isReadableLegacyTextFragment(text, source));
|
|
6310
6396
|
const unique = [];
|
|
6311
6397
|
const seen = /* @__PURE__ */ new Set();
|
|
6312
|
-
for (const fragment of fragments) {
|
|
6398
|
+
for (const { text: fragment } of fragments) {
|
|
6313
6399
|
const key = fragment.toLowerCase();
|
|
6314
6400
|
if (!seen.has(key)) {
|
|
6315
6401
|
seen.add(key);
|
|
@@ -6318,6 +6404,104 @@ function extractLegacyOfficeText(arrayBuffer) {
|
|
|
6318
6404
|
}
|
|
6319
6405
|
return unique.slice(0, 160);
|
|
6320
6406
|
}
|
|
6407
|
+
function isReadableLegacyTextFragment(fragment, source) {
|
|
6408
|
+
if (fragment.length > 600) {
|
|
6409
|
+
return false;
|
|
6410
|
+
}
|
|
6411
|
+
if (isLegacyOfficeMetadataNoise(fragment)) {
|
|
6412
|
+
return false;
|
|
6413
|
+
}
|
|
6414
|
+
if (!/[\p{L}\p{N}]/u.test(fragment)) {
|
|
6415
|
+
return false;
|
|
6416
|
+
}
|
|
6417
|
+
const chars = Array.from(fragment);
|
|
6418
|
+
const letters = chars.filter((char) => /\p{L}/u.test(char)).length;
|
|
6419
|
+
const digits = chars.filter((char) => /\p{N}/u.test(char)).length;
|
|
6420
|
+
const spaces = chars.filter((char) => /\s/u.test(char)).length;
|
|
6421
|
+
const asciiLetters = chars.filter((char) => /[A-Za-z]/.test(char)).length;
|
|
6422
|
+
const cjkLetters = chars.filter((char) => /[\u3400-\u9fff]/u.test(char)).length;
|
|
6423
|
+
const punctuation = chars.filter((char) => /[^\p{L}\p{N}\s]/u.test(char)).length;
|
|
6424
|
+
const alphaNumeric = letters + digits;
|
|
6425
|
+
const readableRatio = alphaNumeric / chars.length;
|
|
6426
|
+
const punctuationRatio = punctuation / chars.length;
|
|
6427
|
+
if (fragment.length < 4 || readableRatio < 0.55 || punctuationRatio > 0.24) {
|
|
6428
|
+
return false;
|
|
6429
|
+
}
|
|
6430
|
+
if (/([\p{L}\p{N}])\1{4,}/u.test(fragment)) {
|
|
6431
|
+
return false;
|
|
6432
|
+
}
|
|
6433
|
+
if (cjkLetters >= 2) {
|
|
6434
|
+
const suspiciousCjk = chars.filter((char) => isAsciiBytePairCjk(char)).length;
|
|
6435
|
+
if (suspiciousCjk / cjkLetters > 0.65) {
|
|
6436
|
+
return false;
|
|
6437
|
+
}
|
|
6438
|
+
if (isLikelyCjkHeading(fragment)) {
|
|
6439
|
+
return true;
|
|
6440
|
+
}
|
|
6441
|
+
if (punctuation > 0 && fragment.length < 12) {
|
|
6442
|
+
return false;
|
|
6443
|
+
}
|
|
6444
|
+
return cjkLetters >= 8 || cjkLetters >= 4 && spaces > 0;
|
|
6445
|
+
}
|
|
6446
|
+
if (asciiLetters >= 4) {
|
|
6447
|
+
if (punctuation > 0 && spaces === 0) {
|
|
6448
|
+
return false;
|
|
6449
|
+
}
|
|
6450
|
+
if (source === "ascii" && /^[A-Z]{2,8}$/.test(fragment)) {
|
|
6451
|
+
return false;
|
|
6452
|
+
}
|
|
6453
|
+
if (spaces > 0) {
|
|
6454
|
+
return letters >= 3;
|
|
6455
|
+
}
|
|
6456
|
+
return fragment.length >= 6;
|
|
6457
|
+
}
|
|
6458
|
+
if (spaces > 0 && letters >= 3) {
|
|
6459
|
+
return true;
|
|
6460
|
+
}
|
|
6461
|
+
return false;
|
|
6462
|
+
}
|
|
6463
|
+
function isLegacyOfficeMetadataNoise(fragment) {
|
|
6464
|
+
if (/[$�\uFFFD]/u.test(fragment)) {
|
|
6465
|
+
return true;
|
|
6466
|
+
}
|
|
6467
|
+
if (/^(?:Root Entry|WordDocument|Workbook|Book|SummaryInformation|DocumentSummaryInformation|CompObj|ObjectPool|Data|PowerPoint Document|Pictures)$/i.test(fragment)) {
|
|
6468
|
+
return true;
|
|
6469
|
+
}
|
|
6470
|
+
if (/\.(dotm?|docm?|pptx?|ppsx?|xlsm?|xlsx?)\b/i.test(fragment)) {
|
|
6471
|
+
return true;
|
|
6472
|
+
}
|
|
6473
|
+
if (/^(?:默认段落字体|普通表格|正文|标题|副标题|目录|页眉|页脚|批注|超链接)(?:\s*\d+)?$/.test(fragment)) {
|
|
6474
|
+
return true;
|
|
6475
|
+
}
|
|
6476
|
+
if (/\b(?:Normal|Default|Calibri|Times New Roman|WPS Office|Microsoft Office|KSOP?ProductBuildVer)\b/i.test(fragment)) {
|
|
6477
|
+
return true;
|
|
6478
|
+
}
|
|
6479
|
+
if (/^\d+(?:Table|List|Heading|Title|Style)$/i.test(fragment)) {
|
|
6480
|
+
return true;
|
|
6481
|
+
}
|
|
6482
|
+
if (/[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[})]?/i.test(fragment)) {
|
|
6483
|
+
return true;
|
|
6484
|
+
}
|
|
6485
|
+
if (/^[A-Z_]{3,}$/.test(fragment) || /^[A-Za-z]+(?:Information|Document|Storage|Stream|Table|Data|Pool|Obj|Props)$/i.test(fragment)) {
|
|
6486
|
+
return true;
|
|
6487
|
+
}
|
|
6488
|
+
return false;
|
|
6489
|
+
}
|
|
6490
|
+
function isLikelyCjkHeading(fragment) {
|
|
6491
|
+
return /^(?:标题|第[一二三四五六七八九十\d]+[章节条]|[一二三四五六七八九十\d]+[、..])\s*[\p{L}\p{N}\s-]*$/u.test(fragment);
|
|
6492
|
+
}
|
|
6493
|
+
function isAsciiBytePairCjk(char) {
|
|
6494
|
+
const code = char.codePointAt(0) || 0;
|
|
6495
|
+
if (code < 13312 || code > 40959) {
|
|
6496
|
+
return false;
|
|
6497
|
+
}
|
|
6498
|
+
const low = code & 255;
|
|
6499
|
+
const high = code >> 8;
|
|
6500
|
+
return isPrintableAsciiByte(low) && isPrintableAsciiByte(high);
|
|
6501
|
+
}
|
|
6502
|
+
function isPrintableAsciiByte(value) {
|
|
6503
|
+
return value >= 32 && value <= 126;
|
|
6504
|
+
}
|
|
6321
6505
|
function extractPrintableRuns(bytes) {
|
|
6322
6506
|
const fragments = [];
|
|
6323
6507
|
let current = "";
|
|
@@ -6340,6 +6524,13 @@ function extractUtf16Runs(bytes) {
|
|
|
6340
6524
|
const fragments = [];
|
|
6341
6525
|
let current = "";
|
|
6342
6526
|
for (let index = 0; index < bytes.length - 1; index += 2) {
|
|
6527
|
+
if (looksLikeMisalignedAsciiUtf16(bytes[index], bytes[index + 1])) {
|
|
6528
|
+
if (current.length >= 3) {
|
|
6529
|
+
fragments.push(current);
|
|
6530
|
+
}
|
|
6531
|
+
current = "";
|
|
6532
|
+
continue;
|
|
6533
|
+
}
|
|
6343
6534
|
const code = bytes[index] | bytes[index + 1] << 8;
|
|
6344
6535
|
if (code >= 32 && code <= 55295 || code === 9) {
|
|
6345
6536
|
current += String.fromCharCode(code);
|
|
@@ -6355,6 +6546,9 @@ function extractUtf16Runs(bytes) {
|
|
|
6355
6546
|
}
|
|
6356
6547
|
return fragments;
|
|
6357
6548
|
}
|
|
6549
|
+
function looksLikeMisalignedAsciiUtf16(lowByte, highByte) {
|
|
6550
|
+
return lowByte === 0 && (highByte >= 48 && highByte <= 57 || highByte >= 65 && highByte <= 90 || highByte >= 97 && highByte <= 122);
|
|
6551
|
+
}
|
|
6358
6552
|
function normalizeLegacyText(value) {
|
|
6359
6553
|
return value.replace(/[\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
6360
6554
|
}
|
|
@@ -6992,18 +7186,45 @@ function archivePlugin() {
|
|
|
6992
7186
|
layout.className = "ofv-archive-layout";
|
|
6993
7187
|
const sidebar = document.createElement("div");
|
|
6994
7188
|
sidebar.className = "ofv-archive-sidebar";
|
|
7189
|
+
const sidebarPanel = document.createElement("div");
|
|
7190
|
+
sidebarPanel.className = "ofv-archive-sidebar-panel";
|
|
6995
7191
|
const header = document.createElement("div");
|
|
6996
7192
|
header.className = "ofv-archive-header";
|
|
6997
|
-
|
|
6998
|
-
|
|
7193
|
+
const sidebarTitle = document.createElement("span");
|
|
7194
|
+
sidebarTitle.className = "ofv-archive-header-title";
|
|
7195
|
+
sidebarTitle.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
|
|
7196
|
+
const sidebarToggle = document.createElement("button");
|
|
7197
|
+
sidebarToggle.className = "ofv-archive-sidebar-toggle";
|
|
7198
|
+
sidebarToggle.type = "button";
|
|
7199
|
+
sidebarToggle.setAttribute("aria-label", "\u5C55\u5F00\u6587\u4EF6\u5217\u8868");
|
|
7200
|
+
sidebarToggle.setAttribute("aria-expanded", "false");
|
|
7201
|
+
sidebarToggle.title = "\u5C55\u5F00\u6587\u4EF6\u5217\u8868";
|
|
7202
|
+
sidebarToggle.textContent = "\u2039";
|
|
7203
|
+
header.append(sidebarToggle, sidebarTitle);
|
|
7204
|
+
sidebarPanel.append(header);
|
|
6999
7205
|
const tree = document.createElement("div");
|
|
7000
7206
|
tree.className = "ofv-archive-tree";
|
|
7001
|
-
|
|
7207
|
+
sidebarPanel.append(tree);
|
|
7208
|
+
sidebar.append(sidebarPanel);
|
|
7002
7209
|
const mainPanel = document.createElement("div");
|
|
7003
7210
|
mainPanel.className = "ofv-archive-main";
|
|
7004
7211
|
layout.append(sidebar, mainPanel);
|
|
7005
7212
|
panel.append(layout);
|
|
7006
7213
|
let currentSubInstance = null;
|
|
7214
|
+
const getSidebarViewportWidth = () => ctx.viewport.clientWidth || ctx.size.width;
|
|
7215
|
+
const shouldAutoCollapseSidebar = () => getSidebarViewportWidth() <= 520;
|
|
7216
|
+
const setSidebarCollapsed = (collapsed) => {
|
|
7217
|
+
layout.classList.toggle("is-sidebar-collapsed", collapsed);
|
|
7218
|
+
sidebarToggle.setAttribute("aria-expanded", String(!collapsed));
|
|
7219
|
+
const label = collapsed ? "\u5C55\u5F00\u6587\u4EF6\u5217\u8868" : "\u6536\u8D77\u6587\u4EF6\u5217\u8868";
|
|
7220
|
+
sidebarToggle.setAttribute("aria-label", label);
|
|
7221
|
+
sidebarToggle.title = label;
|
|
7222
|
+
sidebarToggle.textContent = collapsed ? "\u203A" : "\u2039";
|
|
7223
|
+
};
|
|
7224
|
+
setSidebarCollapsed(false);
|
|
7225
|
+
sidebarToggle.addEventListener("click", () => {
|
|
7226
|
+
setSidebarCollapsed(!layout.classList.contains("is-sidebar-collapsed"));
|
|
7227
|
+
});
|
|
7007
7228
|
const showDefaultSummary = () => {
|
|
7008
7229
|
mainPanel.replaceChildren();
|
|
7009
7230
|
const summary = document.createElement("div");
|
|
@@ -7043,6 +7264,9 @@ function archivePlugin() {
|
|
|
7043
7264
|
if (destroyed) {
|
|
7044
7265
|
return;
|
|
7045
7266
|
}
|
|
7267
|
+
if (shouldAutoCollapseSidebar()) {
|
|
7268
|
+
setSidebarCollapsed(true);
|
|
7269
|
+
}
|
|
7046
7270
|
const token = ++renderToken;
|
|
7047
7271
|
sidebar.querySelectorAll(".ofv-archive-item").forEach((el) => {
|
|
7048
7272
|
el.classList.remove("is-active");
|
|
@@ -9325,7 +9549,7 @@ function cadPlugin() {
|
|
|
9325
9549
|
return { destroy: () => panel.remove() };
|
|
9326
9550
|
}
|
|
9327
9551
|
const dxf = await readTextFile(ctx.file);
|
|
9328
|
-
const viewer = renderDxf(panel, dxf);
|
|
9552
|
+
const viewer = renderDxf(panel, dxf, ctx);
|
|
9329
9553
|
return {
|
|
9330
9554
|
canCommand(command) {
|
|
9331
9555
|
return viewer.canCommand(command);
|
|
@@ -9794,7 +10018,7 @@ function hexPreview(bytes) {
|
|
|
9794
10018
|
}
|
|
9795
10019
|
return rows.join("\n") || "\u65E0\u53EF\u5C55\u793A\u5B57\u8282\u3002";
|
|
9796
10020
|
}
|
|
9797
|
-
function renderDxf(panel, dxf) {
|
|
10021
|
+
function renderDxf(panel, dxf, ctx) {
|
|
9798
10022
|
const pairs = dxf.split(/\r?\n/).map((line) => line.trim());
|
|
9799
10023
|
const lines = [];
|
|
9800
10024
|
const circles = [];
|
|
@@ -9972,6 +10196,10 @@ function renderDxf(panel, dxf) {
|
|
|
9972
10196
|
note.textContent = `\u5DF2\u63D0\u53D6 LINE ${lines.length} \u4E2A\u3001CIRCLE ${circles.length} \u4E2A\u3001ARC ${arcs.length} \u4E2A\u3001POLYLINE ${polylines.length} \u4E2A\u3001POINT ${points.length} \u4E2A\u3001TEXT ${texts.length} \u4E2A\u3002`;
|
|
9973
10197
|
section.append(note, svg);
|
|
9974
10198
|
panel.append(section);
|
|
10199
|
+
const updateToolbarZoom = () => {
|
|
10200
|
+
ctx.toolbar?.setZoom(initialViewBox.width / currentViewBox.width);
|
|
10201
|
+
};
|
|
10202
|
+
updateToolbarZoom();
|
|
9975
10203
|
return {
|
|
9976
10204
|
canCommand(command) {
|
|
9977
10205
|
return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset";
|
|
@@ -9986,14 +10214,19 @@ function renderDxf(panel, dxf) {
|
|
|
9986
10214
|
currentViewBox.x = centerX - currentViewBox.width / 2;
|
|
9987
10215
|
currentViewBox.y = centerY - currentViewBox.height / 2;
|
|
9988
10216
|
applyViewBox();
|
|
10217
|
+
updateToolbarZoom();
|
|
9989
10218
|
return true;
|
|
9990
10219
|
}
|
|
9991
10220
|
if (command === "zoom-reset") {
|
|
9992
10221
|
currentViewBox = { ...initialViewBox };
|
|
9993
10222
|
applyViewBox();
|
|
10223
|
+
updateToolbarZoom();
|
|
9994
10224
|
return true;
|
|
9995
10225
|
}
|
|
9996
10226
|
return false;
|
|
10227
|
+
},
|
|
10228
|
+
destroy() {
|
|
10229
|
+
ctx.toolbar?.setZoom(void 0);
|
|
9997
10230
|
}
|
|
9998
10231
|
};
|
|
9999
10232
|
}
|
|
@@ -10269,6 +10502,12 @@ function model3dPlugin() {
|
|
|
10269
10502
|
renderer.setSize(width, height, false);
|
|
10270
10503
|
};
|
|
10271
10504
|
resize(ctx.size);
|
|
10505
|
+
const updateToolbarZoom = () => {
|
|
10506
|
+
const currentDistance = vectorDistance(camera.position, controls.target);
|
|
10507
|
+
const initialDistance = vectorDistance(initialFrame.cameraPosition, initialFrame.target);
|
|
10508
|
+
ctx.toolbar?.setZoom(initialDistance > 0 ? initialDistance / currentDistance : void 0);
|
|
10509
|
+
};
|
|
10510
|
+
updateToolbarZoom();
|
|
10272
10511
|
return {
|
|
10273
10512
|
canCommand(command) {
|
|
10274
10513
|
return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset" || command === "rotate-right" || command === "rotate-left";
|
|
@@ -10279,6 +10518,7 @@ function model3dPlugin() {
|
|
|
10279
10518
|
camera.position.sub(controls.target).multiplyScalar(factor).add(controls.target);
|
|
10280
10519
|
camera.updateProjectionMatrix();
|
|
10281
10520
|
controls.update();
|
|
10521
|
+
updateToolbarZoom();
|
|
10282
10522
|
return true;
|
|
10283
10523
|
}
|
|
10284
10524
|
if (command === "zoom-reset") {
|
|
@@ -10288,6 +10528,7 @@ function model3dPlugin() {
|
|
|
10288
10528
|
camera.far = initialFrame.far;
|
|
10289
10529
|
camera.updateProjectionMatrix();
|
|
10290
10530
|
controls.update();
|
|
10531
|
+
updateToolbarZoom();
|
|
10291
10532
|
return true;
|
|
10292
10533
|
}
|
|
10293
10534
|
if (command === "rotate-right" || command === "rotate-left") {
|
|
@@ -10298,6 +10539,7 @@ function model3dPlugin() {
|
|
|
10298
10539
|
},
|
|
10299
10540
|
resize,
|
|
10300
10541
|
destroy() {
|
|
10542
|
+
ctx.toolbar?.setZoom(void 0);
|
|
10301
10543
|
window.cancelAnimationFrame(animationFrame);
|
|
10302
10544
|
controls.dispose();
|
|
10303
10545
|
renderer.dispose();
|
|
@@ -10309,6 +10551,12 @@ function model3dPlugin() {
|
|
|
10309
10551
|
}
|
|
10310
10552
|
};
|
|
10311
10553
|
}
|
|
10554
|
+
function vectorDistance(a, b) {
|
|
10555
|
+
if (typeof a.distanceTo === "function") {
|
|
10556
|
+
return a.distanceTo(b);
|
|
10557
|
+
}
|
|
10558
|
+
return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
|
|
10559
|
+
}
|
|
10312
10560
|
function renderModelFallback(ctx, url, isExternal, message) {
|
|
10313
10561
|
const panel = document.createElement("div");
|
|
10314
10562
|
panel.className = "ofv-fallback";
|
|
@@ -10665,34 +10913,65 @@ function gisPlugin() {
|
|
|
10665
10913
|
}
|
|
10666
10914
|
const wrapper = document.createElement("div");
|
|
10667
10915
|
wrapper.className = "ofv-gis-viewer";
|
|
10668
|
-
|
|
10916
|
+
const summary = summarizeGeoJson(geojson);
|
|
10917
|
+
wrapper.append(createGisSummary(summary));
|
|
10669
10918
|
ctx.viewport.appendChild(wrapper);
|
|
10670
10919
|
const mapContainer = document.createElement("div");
|
|
10671
10920
|
mapContainer.className = "ofv-map-stage";
|
|
10672
10921
|
wrapper.appendChild(mapContainer);
|
|
10922
|
+
if (summary.features === 0) {
|
|
10923
|
+
mapContainer.append(createEmptyMapState());
|
|
10924
|
+
} else {
|
|
10925
|
+
mapContainer.append(createMapLegend(summary));
|
|
10926
|
+
}
|
|
10673
10927
|
const map = Leaflet.map(mapContainer).setView([0, 0], 2);
|
|
10674
10928
|
Leaflet.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
10675
10929
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
10676
10930
|
}).addTo(map);
|
|
10677
10931
|
const geojsonLayer = Leaflet.geoJSON(geojson, {
|
|
10678
10932
|
style: () => ({
|
|
10679
|
-
|
|
10933
|
+
className: "ofv-map-feature",
|
|
10934
|
+
color: "#e11d48",
|
|
10680
10935
|
weight: 2,
|
|
10681
|
-
opacity: 0.
|
|
10682
|
-
fillColor: "#
|
|
10683
|
-
fillOpacity: 0.
|
|
10936
|
+
opacity: 0.92,
|
|
10937
|
+
fillColor: "#fb923c",
|
|
10938
|
+
fillOpacity: 0.3,
|
|
10939
|
+
lineCap: "round",
|
|
10940
|
+
lineJoin: "round"
|
|
10684
10941
|
}),
|
|
10685
10942
|
pointToLayer: (feature, latlng) => {
|
|
10686
10943
|
return Leaflet.circleMarker(latlng, {
|
|
10687
|
-
|
|
10688
|
-
|
|
10944
|
+
className: "ofv-map-feature ofv-map-point",
|
|
10945
|
+
radius: 7,
|
|
10946
|
+
fillColor: "#e11d48",
|
|
10689
10947
|
color: "#ffffff",
|
|
10690
|
-
weight:
|
|
10948
|
+
weight: 2.5,
|
|
10691
10949
|
opacity: 1,
|
|
10692
|
-
fillOpacity: 0.
|
|
10950
|
+
fillOpacity: 0.9
|
|
10693
10951
|
});
|
|
10694
10952
|
},
|
|
10695
10953
|
onEachFeature: (feature, layer) => {
|
|
10954
|
+
const label = feature.properties?.name || feature.properties?.title || feature.properties?.label;
|
|
10955
|
+
if (label) {
|
|
10956
|
+
layer.bindTooltip?.(String(label), {
|
|
10957
|
+
className: "ofv-map-tooltip",
|
|
10958
|
+
direction: "top",
|
|
10959
|
+
sticky: true
|
|
10960
|
+
});
|
|
10961
|
+
}
|
|
10962
|
+
layer.on?.({
|
|
10963
|
+
mouseover(event) {
|
|
10964
|
+
event.target?.setStyle?.({
|
|
10965
|
+
weight: 4,
|
|
10966
|
+
opacity: 1,
|
|
10967
|
+
fillOpacity: 0.4
|
|
10968
|
+
});
|
|
10969
|
+
event.target?.bringToFront?.();
|
|
10970
|
+
},
|
|
10971
|
+
mouseout(event) {
|
|
10972
|
+
geojsonLayer.resetStyle?.(event.target);
|
|
10973
|
+
}
|
|
10974
|
+
});
|
|
10696
10975
|
if (feature.properties) {
|
|
10697
10976
|
const props = feature.properties;
|
|
10698
10977
|
const keys = Object.keys(props);
|
|
@@ -10727,15 +11006,20 @@ function gisPlugin() {
|
|
|
10727
11006
|
const bounds = geojsonLayer.getBounds();
|
|
10728
11007
|
if (bounds.isValid()) {
|
|
10729
11008
|
map.fitBounds(bounds, { padding: [20, 20] });
|
|
11009
|
+
map.invalidateSize();
|
|
10730
11010
|
}
|
|
10731
11011
|
} catch (e) {
|
|
10732
11012
|
console.warn("Could not fit bounds for GeoJSON data:", e);
|
|
10733
11013
|
}
|
|
11014
|
+
const resizeTimers = [0, 80, 240].map((delay) => window.setTimeout(() => {
|
|
11015
|
+
map.invalidateSize();
|
|
11016
|
+
}, delay));
|
|
10734
11017
|
return {
|
|
10735
11018
|
resize() {
|
|
10736
11019
|
map.invalidateSize();
|
|
10737
11020
|
},
|
|
10738
11021
|
destroy() {
|
|
11022
|
+
resizeTimers.forEach((timer) => window.clearTimeout(timer));
|
|
10739
11023
|
map.remove();
|
|
10740
11024
|
wrapper.remove();
|
|
10741
11025
|
}
|
|
@@ -10760,8 +11044,7 @@ function normalizeGisError(error, fileName) {
|
|
|
10760
11044
|
function isGisFallback(value) {
|
|
10761
11045
|
return typeof value === "object" && value !== null && "fallback" in value;
|
|
10762
11046
|
}
|
|
10763
|
-
function createGisSummary(
|
|
10764
|
-
const summary = summarizeGeoJson(geojson);
|
|
11047
|
+
function createGisSummary(summary) {
|
|
10765
11048
|
const bar = document.createElement("div");
|
|
10766
11049
|
bar.className = "ofv-gis-summary";
|
|
10767
11050
|
appendSummaryItem(bar, "\u8981\u7D20", String(summary.features));
|
|
@@ -10775,6 +11058,26 @@ function createGisSummary(geojson) {
|
|
|
10775
11058
|
}
|
|
10776
11059
|
return bar;
|
|
10777
11060
|
}
|
|
11061
|
+
function createEmptyMapState() {
|
|
11062
|
+
const empty = document.createElement("div");
|
|
11063
|
+
empty.className = "ofv-map-empty";
|
|
11064
|
+
const title = document.createElement("strong");
|
|
11065
|
+
title.textContent = "\u6682\u65E0\u53EF\u5C55\u793A\u7684\u5730\u56FE\u8981\u7D20";
|
|
11066
|
+
const detail = document.createElement("span");
|
|
11067
|
+
detail.textContent = "GeoJSON \u5DF2\u8BC6\u522B\uFF0C\u4F46 features \u4E3A\u7A7A\u3002";
|
|
11068
|
+
empty.append(title, detail);
|
|
11069
|
+
return empty;
|
|
11070
|
+
}
|
|
11071
|
+
function createMapLegend(summary) {
|
|
11072
|
+
const legend = document.createElement("div");
|
|
11073
|
+
legend.className = "ofv-map-legend";
|
|
11074
|
+
const title = document.createElement("strong");
|
|
11075
|
+
title.textContent = "GeoJSON";
|
|
11076
|
+
const detail = document.createElement("span");
|
|
11077
|
+
detail.textContent = `${summary.features} \u4E2A\u8981\u7D20 \xB7 ${formatGeometryCounts(summary.geometryCounts)}`;
|
|
11078
|
+
legend.append(title, detail);
|
|
11079
|
+
return legend;
|
|
11080
|
+
}
|
|
10778
11081
|
function appendSummaryItem(parent, label, value) {
|
|
10779
11082
|
const item = document.createElement("span");
|
|
10780
11083
|
const key = document.createElement("span");
|