@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.cjs
CHANGED
|
@@ -914,6 +914,8 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
914
914
|
let queueLabel;
|
|
915
915
|
let previousButton;
|
|
916
916
|
let nextButton;
|
|
917
|
+
let zoomResetButton;
|
|
918
|
+
let currentZoom;
|
|
917
919
|
const commandButtons = [];
|
|
918
920
|
const customButtons = [];
|
|
919
921
|
const disposers = [];
|
|
@@ -929,7 +931,9 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
929
931
|
queue,
|
|
930
932
|
element,
|
|
931
933
|
search,
|
|
932
|
-
canCommand: canRunCommand
|
|
934
|
+
canCommand: canRunCommand,
|
|
935
|
+
zoom: currentZoom,
|
|
936
|
+
setZoom
|
|
933
937
|
});
|
|
934
938
|
const addButton = (label, title, action, className, icon) => {
|
|
935
939
|
const button = document.createElement("button");
|
|
@@ -996,6 +1000,8 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
996
1000
|
}
|
|
997
1001
|
if (id === "zoom-reset" && options.zoom) {
|
|
998
1002
|
addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-reset");
|
|
1003
|
+
zoomResetButton = commandButtons[commandButtons.length - 1]?.button;
|
|
1004
|
+
updateZoomLabel();
|
|
999
1005
|
return;
|
|
1000
1006
|
}
|
|
1001
1007
|
if (id === "rotate-right" && options.rotate) {
|
|
@@ -1098,6 +1104,22 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
1098
1104
|
searchCount.textContent = "";
|
|
1099
1105
|
}
|
|
1100
1106
|
};
|
|
1107
|
+
function setZoom(zoom) {
|
|
1108
|
+
currentZoom = typeof zoom === "number" && Number.isFinite(zoom) && zoom > 0 ? zoom : void 0;
|
|
1109
|
+
updateZoomLabel();
|
|
1110
|
+
updateCustomButtons();
|
|
1111
|
+
refreshCustomRender();
|
|
1112
|
+
}
|
|
1113
|
+
function updateZoomLabel() {
|
|
1114
|
+
if (!zoomResetButton) {
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
setToolbarButtonContent(
|
|
1118
|
+
zoomResetButton,
|
|
1119
|
+
currentZoom === void 0 ? getToolbarLabel(options, "zoom-reset") : formatToolbarZoom(currentZoom),
|
|
1120
|
+
options.icons?.["zoom-reset"]
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1101
1123
|
const refreshCustomRender = () => {
|
|
1102
1124
|
if (!options.render) {
|
|
1103
1125
|
return;
|
|
@@ -1114,6 +1136,8 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
1114
1136
|
file = nextFile;
|
|
1115
1137
|
currentIndex = index;
|
|
1116
1138
|
currentLength = length;
|
|
1139
|
+
currentZoom = void 0;
|
|
1140
|
+
updateZoomLabel();
|
|
1117
1141
|
resetSearch();
|
|
1118
1142
|
commandButtons.forEach(({ button }) => {
|
|
1119
1143
|
button.disabled = true;
|
|
@@ -1139,6 +1163,7 @@ function createToolbar(toolbar, viewport, queue) {
|
|
|
1139
1163
|
refreshCustomRender();
|
|
1140
1164
|
},
|
|
1141
1165
|
getContext,
|
|
1166
|
+
setZoom,
|
|
1142
1167
|
destroy() {
|
|
1143
1168
|
search.clear();
|
|
1144
1169
|
for (const dispose of disposers) {
|
|
@@ -1156,7 +1181,9 @@ function createToolbarContext({
|
|
|
1156
1181
|
queue,
|
|
1157
1182
|
element,
|
|
1158
1183
|
search,
|
|
1159
|
-
canCommand
|
|
1184
|
+
canCommand,
|
|
1185
|
+
zoom,
|
|
1186
|
+
setZoom
|
|
1160
1187
|
}) {
|
|
1161
1188
|
return {
|
|
1162
1189
|
file,
|
|
@@ -1165,6 +1192,8 @@ function createToolbarContext({
|
|
|
1165
1192
|
viewport,
|
|
1166
1193
|
canPrevious: index > 0,
|
|
1167
1194
|
canNext: index < length - 1,
|
|
1195
|
+
zoom,
|
|
1196
|
+
zoomLabel: zoom === void 0 ? void 0 : formatToolbarZoom(zoom),
|
|
1168
1197
|
async previous() {
|
|
1169
1198
|
await queue.previous();
|
|
1170
1199
|
},
|
|
@@ -1173,6 +1202,7 @@ function createToolbarContext({
|
|
|
1173
1202
|
},
|
|
1174
1203
|
command: queue.command,
|
|
1175
1204
|
canCommand,
|
|
1205
|
+
setZoom,
|
|
1176
1206
|
download() {
|
|
1177
1207
|
if (file) {
|
|
1178
1208
|
downloadFile(file);
|
|
@@ -1220,6 +1250,9 @@ function getToolbarLabel(options, id) {
|
|
|
1220
1250
|
function getToolbarTitle(options, id) {
|
|
1221
1251
|
return options.titles?.[id] ?? options.labels?.[id] ?? defaultToolbarTitles[id];
|
|
1222
1252
|
}
|
|
1253
|
+
function formatToolbarZoom(zoom) {
|
|
1254
|
+
return `${Math.round(zoom * 100)}%`;
|
|
1255
|
+
}
|
|
1223
1256
|
function getToolbarOrder(options, queueLength) {
|
|
1224
1257
|
if (options.order) {
|
|
1225
1258
|
return options.order;
|
|
@@ -1267,7 +1300,7 @@ function setToolbarButtonContent(button, label, icon) {
|
|
|
1267
1300
|
iconElement.className = "ofv-toolbar-icon";
|
|
1268
1301
|
iconElement.setAttribute("aria-hidden", "true");
|
|
1269
1302
|
if (typeof icon === "string") {
|
|
1270
|
-
iconElement.
|
|
1303
|
+
iconElement.append(sanitizeToolbarIcon(icon));
|
|
1271
1304
|
} else {
|
|
1272
1305
|
iconElement.append(icon.cloneNode(true));
|
|
1273
1306
|
}
|
|
@@ -1276,6 +1309,96 @@ function setToolbarButtonContent(button, label, icon) {
|
|
|
1276
1309
|
labelElement.textContent = label;
|
|
1277
1310
|
button.append(iconElement, labelElement);
|
|
1278
1311
|
}
|
|
1312
|
+
var allowedToolbarIconTags = /* @__PURE__ */ new Set([
|
|
1313
|
+
"svg",
|
|
1314
|
+
"g",
|
|
1315
|
+
"path",
|
|
1316
|
+
"circle",
|
|
1317
|
+
"rect",
|
|
1318
|
+
"line",
|
|
1319
|
+
"polyline",
|
|
1320
|
+
"polygon",
|
|
1321
|
+
"ellipse",
|
|
1322
|
+
"defs",
|
|
1323
|
+
"title",
|
|
1324
|
+
"desc"
|
|
1325
|
+
]);
|
|
1326
|
+
var allowedToolbarIconAttrs = /* @__PURE__ */ new Set([
|
|
1327
|
+
"aria-hidden",
|
|
1328
|
+
"class",
|
|
1329
|
+
"cx",
|
|
1330
|
+
"cy",
|
|
1331
|
+
"d",
|
|
1332
|
+
"fill",
|
|
1333
|
+
"focusable",
|
|
1334
|
+
"height",
|
|
1335
|
+
"id",
|
|
1336
|
+
"points",
|
|
1337
|
+
"r",
|
|
1338
|
+
"rx",
|
|
1339
|
+
"ry",
|
|
1340
|
+
"stroke",
|
|
1341
|
+
"stroke-linecap",
|
|
1342
|
+
"stroke-linejoin",
|
|
1343
|
+
"stroke-width",
|
|
1344
|
+
"transform",
|
|
1345
|
+
"viewBox",
|
|
1346
|
+
"width",
|
|
1347
|
+
"x",
|
|
1348
|
+
"x1",
|
|
1349
|
+
"x2",
|
|
1350
|
+
"y",
|
|
1351
|
+
"y1",
|
|
1352
|
+
"y2"
|
|
1353
|
+
]);
|
|
1354
|
+
function sanitizeToolbarIcon(icon) {
|
|
1355
|
+
const template = document.createElement("template");
|
|
1356
|
+
template.innerHTML = icon.trim();
|
|
1357
|
+
const fragment = document.createDocumentFragment();
|
|
1358
|
+
for (const child of Array.from(template.content.childNodes)) {
|
|
1359
|
+
const sanitized = sanitizeToolbarIconNode(child);
|
|
1360
|
+
if (sanitized) {
|
|
1361
|
+
fragment.append(sanitized);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return fragment;
|
|
1365
|
+
}
|
|
1366
|
+
function sanitizeToolbarIconNode(node) {
|
|
1367
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1368
|
+
const text = node.textContent || "";
|
|
1369
|
+
return text.trim() ? document.createTextNode(text) : null;
|
|
1370
|
+
}
|
|
1371
|
+
if (!(node instanceof Element)) {
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
const tagName = node.tagName.toLowerCase();
|
|
1375
|
+
if (!allowedToolbarIconTags.has(tagName)) {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
const sanitized = document.createElementNS("http://www.w3.org/2000/svg", tagName);
|
|
1379
|
+
for (const attr of Array.from(node.attributes)) {
|
|
1380
|
+
if (isSafeToolbarIconAttribute(attr.name, attr.value)) {
|
|
1381
|
+
sanitized.setAttribute(attr.name, attr.value);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
for (const child of Array.from(node.childNodes)) {
|
|
1385
|
+
const sanitizedChild = sanitizeToolbarIconNode(child);
|
|
1386
|
+
if (sanitizedChild) {
|
|
1387
|
+
sanitized.append(sanitizedChild);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return sanitized;
|
|
1391
|
+
}
|
|
1392
|
+
function isSafeToolbarIconAttribute(name, value) {
|
|
1393
|
+
const attrName = name.toLowerCase();
|
|
1394
|
+
if (attrName.startsWith("on") || attrName.includes(":")) {
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
if (!allowedToolbarIconAttrs.has(name) && !allowedToolbarIconAttrs.has(attrName) && !attrName.startsWith("data-")) {
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
return !/^\s*(?:javascript|data:text\/html|vbscript):/i.test(value);
|
|
1401
|
+
}
|
|
1279
1402
|
function isBuiltInToolbarAction(id) {
|
|
1280
1403
|
return id in defaultToolbarLabels;
|
|
1281
1404
|
}
|
|
@@ -1687,6 +1810,7 @@ function imagePlugin() {
|
|
|
1687
1810
|
const updateTransform = () => {
|
|
1688
1811
|
image.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale}) rotate(${rotation}deg)`;
|
|
1689
1812
|
zoomLabel.textContent = `${Math.round(scale * 100)}%`;
|
|
1813
|
+
ctx.toolbar?.setZoom(scale);
|
|
1690
1814
|
};
|
|
1691
1815
|
const showImageFallback = () => {
|
|
1692
1816
|
stage.replaceChildren(createImageFallback(ctx.file.name, url));
|
|
@@ -1802,6 +1926,7 @@ function imagePlugin() {
|
|
|
1802
1926
|
image.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
|
|
1803
1927
|
},
|
|
1804
1928
|
destroy() {
|
|
1929
|
+
ctx.toolbar?.setZoom(void 0);
|
|
1805
1930
|
for (const dispose of disposers) {
|
|
1806
1931
|
dispose();
|
|
1807
1932
|
}
|
|
@@ -3360,7 +3485,6 @@ var mimeLangMap = {
|
|
|
3360
3485
|
};
|
|
3361
3486
|
var MAX_HIGHLIGHT_CHARS = 18e4;
|
|
3362
3487
|
var MAX_RENDER_CHARS = 6e5;
|
|
3363
|
-
var MONACO_LOADER_KEY = "__OFV_MONACO_LOADER__";
|
|
3364
3488
|
function loadPrismCss(theme) {
|
|
3365
3489
|
const lightId = "ofv-prism-css-light";
|
|
3366
3490
|
const darkId = "ofv-prism-css-dark";
|
|
@@ -3584,10 +3708,6 @@ function textPlugin() {
|
|
|
3584
3708
|
wrapButton.addEventListener("click", () => {
|
|
3585
3709
|
const wrapped = wrapper.classList.toggle("is-wrapped");
|
|
3586
3710
|
wrapButton.setAttribute("aria-pressed", String(wrapped));
|
|
3587
|
-
monacoEditor?.updateOptions?.({ wordWrap: wrapped ? "on" : "off" });
|
|
3588
|
-
if (fallbackEditor) {
|
|
3589
|
-
fallbackEditor.wrap = wrapped ? "soft" : "off";
|
|
3590
|
-
}
|
|
3591
3711
|
});
|
|
3592
3712
|
const copyButton = document.createElement("button");
|
|
3593
3713
|
copyButton.type = "button";
|
|
@@ -3612,12 +3732,7 @@ function textPlugin() {
|
|
|
3612
3732
|
downloadText(ctx.file.name, text);
|
|
3613
3733
|
status.textContent = "Download ready";
|
|
3614
3734
|
});
|
|
3615
|
-
|
|
3616
|
-
editorButton.type = "button";
|
|
3617
|
-
editorButton.className = "ofv-code-action";
|
|
3618
|
-
editorButton.textContent = "Editor";
|
|
3619
|
-
editorButton.setAttribute("aria-pressed", "false");
|
|
3620
|
-
actions.append(status, editorButton, wrapButton, copyButton, downloadButton);
|
|
3735
|
+
actions.append(wrapButton, copyButton, downloadButton, status);
|
|
3621
3736
|
header.append(title, actions);
|
|
3622
3737
|
const structureSummary = createTextStructureSummary(text, ext, lang, ctx.file.mimeType);
|
|
3623
3738
|
const body = document.createElement("div");
|
|
@@ -3633,93 +3748,6 @@ function textPlugin() {
|
|
|
3633
3748
|
code.textContent = codeText;
|
|
3634
3749
|
pre.appendChild(code);
|
|
3635
3750
|
body.append(gutter, pre);
|
|
3636
|
-
const editorHost = document.createElement("div");
|
|
3637
|
-
editorHost.className = "ofv-code-editor";
|
|
3638
|
-
editorHost.hidden = true;
|
|
3639
|
-
let monacoEditor;
|
|
3640
|
-
let monacoModel;
|
|
3641
|
-
let fallbackEditor;
|
|
3642
|
-
let editorReady = false;
|
|
3643
|
-
let editorLoading;
|
|
3644
|
-
const showReader = () => {
|
|
3645
|
-
editorHost.hidden = true;
|
|
3646
|
-
body.hidden = false;
|
|
3647
|
-
wrapper.classList.remove("is-editor");
|
|
3648
|
-
editorButton.textContent = "Editor";
|
|
3649
|
-
editorButton.setAttribute("aria-pressed", "false");
|
|
3650
|
-
};
|
|
3651
|
-
const showEditor = async () => {
|
|
3652
|
-
if (text.length > MAX_RENDER_CHARS) {
|
|
3653
|
-
status.textContent = "Editor skipped for large file";
|
|
3654
|
-
return;
|
|
3655
|
-
}
|
|
3656
|
-
if (editorReady) {
|
|
3657
|
-
body.hidden = true;
|
|
3658
|
-
editorHost.hidden = false;
|
|
3659
|
-
wrapper.classList.add("is-editor");
|
|
3660
|
-
editorButton.textContent = "Reader";
|
|
3661
|
-
editorButton.setAttribute("aria-pressed", "true");
|
|
3662
|
-
monacoEditor?.layout?.();
|
|
3663
|
-
return;
|
|
3664
|
-
}
|
|
3665
|
-
if (!editorLoading) {
|
|
3666
|
-
editorLoading = (async () => {
|
|
3667
|
-
editorButton.disabled = true;
|
|
3668
|
-
status.textContent = "Loading editor";
|
|
3669
|
-
try {
|
|
3670
|
-
const monaco = await loadMonaco();
|
|
3671
|
-
if (!monaco.editor?.create) {
|
|
3672
|
-
throw new Error("Monaco editor API is unavailable.");
|
|
3673
|
-
}
|
|
3674
|
-
const monacoLanguage = toMonacoLanguage(ext, lang);
|
|
3675
|
-
monaco.editor.setTheme?.(isDark ? "vs-dark" : "vs");
|
|
3676
|
-
monacoModel = monaco.editor.createModel?.(text, monacoLanguage);
|
|
3677
|
-
monacoEditor = monaco.editor.create(editorHost, {
|
|
3678
|
-
...monacoModel ? { model: monacoModel } : { value: text, language: monacoLanguage },
|
|
3679
|
-
automaticLayout: true,
|
|
3680
|
-
fontFamily: "var(--ofv-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace)",
|
|
3681
|
-
fontSize: 13,
|
|
3682
|
-
lineNumbers: "on",
|
|
3683
|
-
minimap: { enabled: text.length <= 8e4 },
|
|
3684
|
-
readOnly: true,
|
|
3685
|
-
renderLineHighlight: "line",
|
|
3686
|
-
scrollBeyondLastLine: false,
|
|
3687
|
-
wordWrap: wrapper.classList.contains("is-wrapped") ? "on" : "off"
|
|
3688
|
-
});
|
|
3689
|
-
editorReady = true;
|
|
3690
|
-
status.textContent = "Editor ready";
|
|
3691
|
-
body.hidden = true;
|
|
3692
|
-
editorHost.hidden = false;
|
|
3693
|
-
wrapper.classList.add("is-editor");
|
|
3694
|
-
editorButton.textContent = "Reader";
|
|
3695
|
-
editorButton.setAttribute("aria-pressed", "true");
|
|
3696
|
-
monacoEditor.layout?.();
|
|
3697
|
-
} catch (error) {
|
|
3698
|
-
console.warn("Monaco editor failed to load; using built-in code editor fallback:", error);
|
|
3699
|
-
fallbackEditor = createBasicCodeEditor(text, wrapper.classList.contains("is-wrapped"));
|
|
3700
|
-
editorHost.replaceChildren(fallbackEditor);
|
|
3701
|
-
editorReady = true;
|
|
3702
|
-
status.textContent = "Basic editor";
|
|
3703
|
-
body.hidden = true;
|
|
3704
|
-
editorHost.hidden = false;
|
|
3705
|
-
wrapper.classList.add("is-editor");
|
|
3706
|
-
editorButton.textContent = "Reader";
|
|
3707
|
-
editorButton.setAttribute("aria-pressed", "true");
|
|
3708
|
-
} finally {
|
|
3709
|
-
editorButton.disabled = false;
|
|
3710
|
-
}
|
|
3711
|
-
})();
|
|
3712
|
-
}
|
|
3713
|
-
await editorLoading;
|
|
3714
|
-
};
|
|
3715
|
-
editorButton.addEventListener("click", () => {
|
|
3716
|
-
if (editorHost.hidden) {
|
|
3717
|
-
void showEditor();
|
|
3718
|
-
} else {
|
|
3719
|
-
showReader();
|
|
3720
|
-
status.textContent = "Reader mode";
|
|
3721
|
-
}
|
|
3722
|
-
});
|
|
3723
3751
|
wrapper.append(header);
|
|
3724
3752
|
if (structureSummary) {
|
|
3725
3753
|
wrapper.append(structureSummary);
|
|
@@ -3737,7 +3765,6 @@ function textPlugin() {
|
|
|
3737
3765
|
wrapper.append(notice);
|
|
3738
3766
|
}
|
|
3739
3767
|
wrapper.appendChild(body);
|
|
3740
|
-
wrapper.appendChild(editorHost);
|
|
3741
3768
|
ctx.viewport.appendChild(wrapper);
|
|
3742
3769
|
if (shouldHighlight) {
|
|
3743
3770
|
try {
|
|
@@ -3747,12 +3774,7 @@ function textPlugin() {
|
|
|
3747
3774
|
}
|
|
3748
3775
|
}
|
|
3749
3776
|
return {
|
|
3750
|
-
resize() {
|
|
3751
|
-
monacoEditor?.layout?.();
|
|
3752
|
-
},
|
|
3753
3777
|
destroy() {
|
|
3754
|
-
monacoEditor?.dispose?.();
|
|
3755
|
-
monacoModel?.dispose?.();
|
|
3756
3778
|
wrapper.remove();
|
|
3757
3779
|
}
|
|
3758
3780
|
};
|
|
@@ -3767,45 +3789,6 @@ function normalizeFileName2(name) {
|
|
|
3767
3789
|
const baseName = name.split(/[\\/]/).pop() || name;
|
|
3768
3790
|
return baseName.toLowerCase();
|
|
3769
3791
|
}
|
|
3770
|
-
async function loadMonaco() {
|
|
3771
|
-
const injectedLoader = globalThis[MONACO_LOADER_KEY];
|
|
3772
|
-
if (typeof injectedLoader === "function") {
|
|
3773
|
-
return await injectedLoader();
|
|
3774
|
-
}
|
|
3775
|
-
const importer = new Function("specifier", "return import(specifier)");
|
|
3776
|
-
return importer("monaco-editor");
|
|
3777
|
-
}
|
|
3778
|
-
function toMonacoLanguage(ext, prismLanguage) {
|
|
3779
|
-
if (ext === "vue") {
|
|
3780
|
-
return "html";
|
|
3781
|
-
}
|
|
3782
|
-
if (ext === "tsx") {
|
|
3783
|
-
return "typescript";
|
|
3784
|
-
}
|
|
3785
|
-
if (ext === "jsx") {
|
|
3786
|
-
return "javascript";
|
|
3787
|
-
}
|
|
3788
|
-
if (prismLanguage === "markup") {
|
|
3789
|
-
return ext === "xml" ? "xml" : "html";
|
|
3790
|
-
}
|
|
3791
|
-
if (prismLanguage === "bash") {
|
|
3792
|
-
return "shell";
|
|
3793
|
-
}
|
|
3794
|
-
if (prismLanguage === "none") {
|
|
3795
|
-
return "plaintext";
|
|
3796
|
-
}
|
|
3797
|
-
return prismLanguage;
|
|
3798
|
-
}
|
|
3799
|
-
function createBasicCodeEditor(text, wrapped) {
|
|
3800
|
-
const textarea = document.createElement("textarea");
|
|
3801
|
-
textarea.className = "ofv-code-editor-fallback";
|
|
3802
|
-
textarea.readOnly = true;
|
|
3803
|
-
textarea.spellcheck = false;
|
|
3804
|
-
textarea.wrap = wrapped ? "soft" : "off";
|
|
3805
|
-
textarea.value = text;
|
|
3806
|
-
textarea.setAttribute("aria-label", "Code editor preview");
|
|
3807
|
-
return textarea;
|
|
3808
|
-
}
|
|
3809
3792
|
function createTextFallback(fileName, url) {
|
|
3810
3793
|
const fallback = document.createElement("div");
|
|
3811
3794
|
fallback.className = "ofv-fallback";
|
|
@@ -4043,7 +4026,13 @@ function pdfPlugin(options = {}) {
|
|
|
4043
4026
|
scroller.className = "ofv-pdf ofv-pdf-pages";
|
|
4044
4027
|
viewer.append(summary, scroller);
|
|
4045
4028
|
ctx.viewport.append(viewer);
|
|
4046
|
-
const documentTask = pdf.getDocument(
|
|
4029
|
+
const documentTask = pdf.getDocument({
|
|
4030
|
+
url,
|
|
4031
|
+
cMapUrl: normalizedOptions.cMapUrl ?? `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdf.version}/cmaps/`,
|
|
4032
|
+
cMapPacked: normalizedOptions.cMapPacked ?? true,
|
|
4033
|
+
standardFontDataUrl: normalizedOptions.standardFontDataUrl ?? `https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdf.version}/standard_fonts/`,
|
|
4034
|
+
useSystemFonts: normalizedOptions.useSystemFonts ?? true
|
|
4035
|
+
});
|
|
4047
4036
|
const doc = await documentTask.promise.catch((error) => {
|
|
4048
4037
|
viewer.remove();
|
|
4049
4038
|
ctx.viewport.classList.add("ofv-center");
|
|
@@ -4079,6 +4068,7 @@ function pdfPlugin(options = {}) {
|
|
|
4079
4068
|
let zoomFactor = 1;
|
|
4080
4069
|
const updateSummary = () => {
|
|
4081
4070
|
renderPdfSummary(summary, doc.numPages, pagesMeta, ctx.options.fit, zoomFactor);
|
|
4071
|
+
ctx.toolbar?.setZoom(zoomFactor);
|
|
4082
4072
|
};
|
|
4083
4073
|
const clearPage = (pageIdx) => {
|
|
4084
4074
|
const state = pageStates[pageIdx];
|
|
@@ -4102,12 +4092,17 @@ function pdfPlugin(options = {}) {
|
|
|
4102
4092
|
try {
|
|
4103
4093
|
const page = await doc.getPage(pageIdx + 1);
|
|
4104
4094
|
const meta = pagesMeta[pageIdx];
|
|
4105
|
-
const scale = ctx.options.fit === "actual" ? zoomFactor : Math.max(0.
|
|
4095
|
+
const scale = ctx.options.fit === "actual" ? zoomFactor : Math.max(0.05, Math.min(5, getPdfAvailableWidth(size.width) / meta.width * zoomFactor));
|
|
4106
4096
|
const viewport = page.getViewport({ scale });
|
|
4097
|
+
const outputScale = getPdfOutputScale();
|
|
4098
|
+
const cssWidth = Math.floor(viewport.width);
|
|
4099
|
+
const cssHeight = Math.floor(viewport.height);
|
|
4107
4100
|
const canvas = document.createElement("canvas");
|
|
4108
4101
|
canvas.className = "ofv-pdf-page";
|
|
4109
|
-
canvas.width = Math.floor(
|
|
4110
|
-
canvas.height = Math.floor(
|
|
4102
|
+
canvas.width = Math.floor(cssWidth * outputScale);
|
|
4103
|
+
canvas.height = Math.floor(cssHeight * outputScale);
|
|
4104
|
+
canvas.style.width = `${cssWidth}px`;
|
|
4105
|
+
canvas.style.height = `${cssHeight}px`;
|
|
4111
4106
|
const context = canvas.getContext("2d");
|
|
4112
4107
|
if (!context) {
|
|
4113
4108
|
throw new Error("Canvas 2D context is not available.");
|
|
@@ -4116,7 +4111,8 @@ function pdfPlugin(options = {}) {
|
|
|
4116
4111
|
state.canvas = canvas;
|
|
4117
4112
|
const renderTask = page.render({
|
|
4118
4113
|
canvasContext: context,
|
|
4119
|
-
viewport
|
|
4114
|
+
viewport,
|
|
4115
|
+
transform: outputScale === 1 ? void 0 : [outputScale, 0, 0, outputScale, 0, 0]
|
|
4120
4116
|
});
|
|
4121
4117
|
state.renderTask = renderTask;
|
|
4122
4118
|
await renderTask.promise;
|
|
@@ -4124,8 +4120,8 @@ function pdfPlugin(options = {}) {
|
|
|
4124
4120
|
const textContent = await page.getTextContent();
|
|
4125
4121
|
const textLayer = document.createElement("div");
|
|
4126
4122
|
textLayer.className = "ofv-pdf-text-layer";
|
|
4127
|
-
textLayer.style.width = `${
|
|
4128
|
-
textLayer.style.height = `${
|
|
4123
|
+
textLayer.style.width = `${cssWidth}px`;
|
|
4124
|
+
textLayer.style.height = `${cssHeight}px`;
|
|
4129
4125
|
state.wrapper.appendChild(textLayer);
|
|
4130
4126
|
for (const item of textContent.items) {
|
|
4131
4127
|
if (!("str" in item)) continue;
|
|
@@ -4186,7 +4182,7 @@ function pdfPlugin(options = {}) {
|
|
|
4186
4182
|
}
|
|
4187
4183
|
for (let i = 0; i < doc.numPages; i++) {
|
|
4188
4184
|
const meta = pagesMeta[i];
|
|
4189
|
-
const scale = ctx.options.fit === "actual" ? zoomFactor : Math.max(0.
|
|
4185
|
+
const scale = ctx.options.fit === "actual" ? zoomFactor : Math.max(0.05, Math.min(5, getPdfAvailableWidth(size.width) / meta.width * zoomFactor));
|
|
4190
4186
|
const w = Math.floor(meta.width * scale);
|
|
4191
4187
|
const h = Math.floor(meta.height * scale);
|
|
4192
4188
|
const wrapper = document.createElement("div");
|
|
@@ -4241,6 +4237,7 @@ function pdfPlugin(options = {}) {
|
|
|
4241
4237
|
}, 120);
|
|
4242
4238
|
},
|
|
4243
4239
|
destroy() {
|
|
4240
|
+
ctx.toolbar?.setZoom(void 0);
|
|
4244
4241
|
window.clearTimeout(resizeTimer);
|
|
4245
4242
|
observer?.disconnect();
|
|
4246
4243
|
pageStates.forEach((state) => {
|
|
@@ -4259,6 +4256,19 @@ function pdfPlugin(options = {}) {
|
|
|
4259
4256
|
}
|
|
4260
4257
|
};
|
|
4261
4258
|
}
|
|
4259
|
+
function getPdfOutputScale() {
|
|
4260
|
+
if (typeof window === "undefined") {
|
|
4261
|
+
return 1;
|
|
4262
|
+
}
|
|
4263
|
+
return Math.max(1, Math.min(window.devicePixelRatio || 1, 2.5));
|
|
4264
|
+
}
|
|
4265
|
+
function getPdfAvailableWidth(width) {
|
|
4266
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
4267
|
+
return 1;
|
|
4268
|
+
}
|
|
4269
|
+
const gutter = width < 160 ? 16 : 32;
|
|
4270
|
+
return Math.max(1, width - gutter);
|
|
4271
|
+
}
|
|
4262
4272
|
function renderPdfSummary(summary, pages, pagesMeta, fit, zoomFactor) {
|
|
4263
4273
|
summary.replaceChildren();
|
|
4264
4274
|
appendPdfSummary(summary, "\u9875\u6570", String(pages));
|
|
@@ -5018,8 +5028,9 @@ function officePlugin() {
|
|
|
5018
5028
|
ctx.viewport.append(panel);
|
|
5019
5029
|
const extension = resolveFormat(ctx.file, officeMimeFormatMap);
|
|
5020
5030
|
const arrayBuffer = await readArrayBuffer(ctx.file);
|
|
5031
|
+
let disposeDocxFit;
|
|
5021
5032
|
if (fileIsDocx(extension)) {
|
|
5022
|
-
await renderDocx(panel, arrayBuffer);
|
|
5033
|
+
disposeDocxFit = await renderDocx(panel, arrayBuffer);
|
|
5023
5034
|
} else if (extension === "rtf") {
|
|
5024
5035
|
renderPlainDocument(panel, "RTF \u6587\u6863", rtfToText(await readTextFromBuffer(arrayBuffer)));
|
|
5025
5036
|
} else if (extension === "odt") {
|
|
@@ -5044,6 +5055,7 @@ function officePlugin() {
|
|
|
5044
5055
|
}
|
|
5045
5056
|
return {
|
|
5046
5057
|
destroy() {
|
|
5058
|
+
disposeDocxFit?.();
|
|
5047
5059
|
panel.remove();
|
|
5048
5060
|
}
|
|
5049
5061
|
};
|
|
@@ -5077,6 +5089,7 @@ async function renderDocx(panel, arrayBuffer) {
|
|
|
5077
5089
|
experimental: true,
|
|
5078
5090
|
useBase64URL: true
|
|
5079
5091
|
});
|
|
5092
|
+
return fitDocxPages(content);
|
|
5080
5093
|
} catch (error) {
|
|
5081
5094
|
content.replaceChildren();
|
|
5082
5095
|
const fallbackNote = document.createElement("div");
|
|
@@ -5091,6 +5104,74 @@ async function renderDocx(panel, arrayBuffer) {
|
|
|
5091
5104
|
}
|
|
5092
5105
|
console.warn("DOCX layout preview failed, fell back to Mammoth:", error);
|
|
5093
5106
|
}
|
|
5107
|
+
return () => void 0;
|
|
5108
|
+
}
|
|
5109
|
+
function fitDocxPages(container) {
|
|
5110
|
+
const wrapper = container.querySelector(".ofv-docx-wrapper");
|
|
5111
|
+
if (!wrapper) {
|
|
5112
|
+
return () => void 0;
|
|
5113
|
+
}
|
|
5114
|
+
const update = () => {
|
|
5115
|
+
const frames = ensureDocxPageFrames(wrapper);
|
|
5116
|
+
if (frames.length === 0) {
|
|
5117
|
+
wrapper.style.removeProperty("--ofv-docx-scale");
|
|
5118
|
+
return;
|
|
5119
|
+
}
|
|
5120
|
+
const availableWidth = Math.max(1, container.clientWidth - 48);
|
|
5121
|
+
const pageWidth = Math.max(
|
|
5122
|
+
1,
|
|
5123
|
+
...frames.map(({ page }) => {
|
|
5124
|
+
const rectWidth = page.getBoundingClientRect().width;
|
|
5125
|
+
return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
|
|
5126
|
+
})
|
|
5127
|
+
);
|
|
5128
|
+
const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
|
|
5129
|
+
wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
|
|
5130
|
+
wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
|
|
5131
|
+
for (const { frame, page } of frames) {
|
|
5132
|
+
const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
|
|
5133
|
+
if (pageHeight > 0) {
|
|
5134
|
+
frame.style.height = `${Math.ceil(pageHeight * scale)}px`;
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
};
|
|
5138
|
+
update();
|
|
5139
|
+
const timers = [0, 80, 240].map((delay) => window.setTimeout(update, delay));
|
|
5140
|
+
if (typeof ResizeObserver === "undefined") {
|
|
5141
|
+
window.addEventListener("resize", update);
|
|
5142
|
+
return () => {
|
|
5143
|
+
timers.forEach((timer) => window.clearTimeout(timer));
|
|
5144
|
+
window.removeEventListener("resize", update);
|
|
5145
|
+
};
|
|
5146
|
+
}
|
|
5147
|
+
const observer = new ResizeObserver(update);
|
|
5148
|
+
observer.observe(container);
|
|
5149
|
+
observer.observe(wrapper);
|
|
5150
|
+
return () => {
|
|
5151
|
+
timers.forEach((timer) => window.clearTimeout(timer));
|
|
5152
|
+
observer.disconnect();
|
|
5153
|
+
};
|
|
5154
|
+
}
|
|
5155
|
+
function ensureDocxPageFrames(wrapper) {
|
|
5156
|
+
const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
|
|
5157
|
+
return pages.map((page) => {
|
|
5158
|
+
const parent = page.parentElement;
|
|
5159
|
+
if (parent?.classList.contains("ofv-docx-page-frame")) {
|
|
5160
|
+
return { frame: parent, page };
|
|
5161
|
+
}
|
|
5162
|
+
const frame = document.createElement("div");
|
|
5163
|
+
frame.className = "ofv-docx-page-frame";
|
|
5164
|
+
page.before(frame);
|
|
5165
|
+
frame.append(page);
|
|
5166
|
+
return { frame, page };
|
|
5167
|
+
});
|
|
5168
|
+
}
|
|
5169
|
+
function parseCssPixelValue(value) {
|
|
5170
|
+
const parsed = Number.parseFloat(value);
|
|
5171
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
5172
|
+
}
|
|
5173
|
+
function formatCssNumber(value) {
|
|
5174
|
+
return Number.isFinite(value) ? value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "") : "1";
|
|
5094
5175
|
}
|
|
5095
5176
|
async function renderDocxTextFallback(container, arrayBuffer) {
|
|
5096
5177
|
const article = document.createElement("article");
|
|
@@ -5189,7 +5270,7 @@ async function renderSheet(panel, arrayBuffer, extension) {
|
|
|
5189
5270
|
workbook = xlsx.read(arrayBuffer, { type: "array" });
|
|
5190
5271
|
} catch (error) {
|
|
5191
5272
|
if (isLegacyOfficeBinary(extension)) {
|
|
5192
|
-
renderLegacyOfficeBinary(panel, extension, arrayBuffer, normalizeOfficeError(error));
|
|
5273
|
+
renderLegacyOfficeBinary(panel, extension, arrayBuffer, `\u8868\u683C\u89E3\u6790\u5931\u8D25\uFF1A${normalizeOfficeError(error)}`);
|
|
5193
5274
|
return;
|
|
5194
5275
|
}
|
|
5195
5276
|
renderSheetFallback(panel, extension, normalizeOfficeError(error));
|
|
@@ -6294,14 +6375,15 @@ function legacyOfficeFormatLabel(extension) {
|
|
|
6294
6375
|
function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
|
|
6295
6376
|
const fragments = extractLegacyOfficeText(arrayBuffer);
|
|
6296
6377
|
panel.replaceChildren();
|
|
6297
|
-
const section = createSection("Office \
|
|
6378
|
+
const section = createSection("Office \u8F6C\u6362\u63D0\u793A");
|
|
6379
|
+
section.classList.add("ofv-office-conversion");
|
|
6298
6380
|
const format = document.createElement("p");
|
|
6299
6381
|
const strong = document.createElement("strong");
|
|
6300
6382
|
strong.textContent = `.${extension}`;
|
|
6301
6383
|
format.append(
|
|
6302
6384
|
strong,
|
|
6303
6385
|
document.createTextNode(
|
|
6304
|
-
" \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\
|
|
6386
|
+
" \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"
|
|
6305
6387
|
)
|
|
6306
6388
|
);
|
|
6307
6389
|
const meta = document.createElement("dl");
|
|
@@ -6310,12 +6392,15 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
|
|
|
6310
6392
|
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");
|
|
6311
6393
|
appendOfficeBinaryMeta(meta, "\u6587\u672C\u7247\u6BB5", `${fragments.length} \u6BB5`);
|
|
6312
6394
|
if (parseError) {
|
|
6313
|
-
appendOfficeBinaryMeta(meta, "\
|
|
6395
|
+
appendOfficeBinaryMeta(meta, "\u89E3\u6790\u72B6\u6001", parseError);
|
|
6314
6396
|
}
|
|
6315
6397
|
section.append(format, meta);
|
|
6316
6398
|
if (fragments.length > 0) {
|
|
6317
6399
|
const article = document.createElement("article");
|
|
6318
|
-
article.className = "ofv-document";
|
|
6400
|
+
article.className = "ofv-document ofv-office-binary-fragments";
|
|
6401
|
+
const heading = document.createElement("h4");
|
|
6402
|
+
heading.textContent = "\u53EF\u8BFB\u6587\u672C\u7247\u6BB5";
|
|
6403
|
+
article.append(heading);
|
|
6319
6404
|
for (const fragment of fragments.slice(0, 80)) {
|
|
6320
6405
|
const paragraph = document.createElement("p");
|
|
6321
6406
|
paragraph.textContent = fragment;
|
|
@@ -6324,7 +6409,8 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
|
|
|
6324
6409
|
section.append(article);
|
|
6325
6410
|
} else {
|
|
6326
6411
|
const empty = document.createElement("p");
|
|
6327
|
-
empty.
|
|
6412
|
+
empty.className = "ofv-office-binary-empty";
|
|
6413
|
+
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";
|
|
6328
6414
|
section.append(empty);
|
|
6329
6415
|
}
|
|
6330
6416
|
panel.append(section);
|
|
@@ -6357,12 +6443,12 @@ function normalizeOfficeError(error) {
|
|
|
6357
6443
|
function extractLegacyOfficeText(arrayBuffer) {
|
|
6358
6444
|
const bytes = new Uint8Array(arrayBuffer);
|
|
6359
6445
|
const fragments = [
|
|
6360
|
-
...extractPrintableRuns(bytes),
|
|
6361
|
-
...extractUtf16Runs(bytes)
|
|
6362
|
-
].map((
|
|
6446
|
+
...extractPrintableRuns(bytes).map((text) => ({ text, source: "ascii" })),
|
|
6447
|
+
...extractUtf16Runs(bytes).map((text) => ({ text, source: "utf16" }))
|
|
6448
|
+
].map(({ text, source }) => ({ text: normalizeLegacyText(text), source })).filter(({ text, source }) => isReadableLegacyTextFragment(text, source));
|
|
6363
6449
|
const unique = [];
|
|
6364
6450
|
const seen = /* @__PURE__ */ new Set();
|
|
6365
|
-
for (const fragment of fragments) {
|
|
6451
|
+
for (const { text: fragment } of fragments) {
|
|
6366
6452
|
const key = fragment.toLowerCase();
|
|
6367
6453
|
if (!seen.has(key)) {
|
|
6368
6454
|
seen.add(key);
|
|
@@ -6371,6 +6457,104 @@ function extractLegacyOfficeText(arrayBuffer) {
|
|
|
6371
6457
|
}
|
|
6372
6458
|
return unique.slice(0, 160);
|
|
6373
6459
|
}
|
|
6460
|
+
function isReadableLegacyTextFragment(fragment, source) {
|
|
6461
|
+
if (fragment.length > 600) {
|
|
6462
|
+
return false;
|
|
6463
|
+
}
|
|
6464
|
+
if (isLegacyOfficeMetadataNoise(fragment)) {
|
|
6465
|
+
return false;
|
|
6466
|
+
}
|
|
6467
|
+
if (!/[\p{L}\p{N}]/u.test(fragment)) {
|
|
6468
|
+
return false;
|
|
6469
|
+
}
|
|
6470
|
+
const chars = Array.from(fragment);
|
|
6471
|
+
const letters = chars.filter((char) => /\p{L}/u.test(char)).length;
|
|
6472
|
+
const digits = chars.filter((char) => /\p{N}/u.test(char)).length;
|
|
6473
|
+
const spaces = chars.filter((char) => /\s/u.test(char)).length;
|
|
6474
|
+
const asciiLetters = chars.filter((char) => /[A-Za-z]/.test(char)).length;
|
|
6475
|
+
const cjkLetters = chars.filter((char) => /[\u3400-\u9fff]/u.test(char)).length;
|
|
6476
|
+
const punctuation = chars.filter((char) => /[^\p{L}\p{N}\s]/u.test(char)).length;
|
|
6477
|
+
const alphaNumeric = letters + digits;
|
|
6478
|
+
const readableRatio = alphaNumeric / chars.length;
|
|
6479
|
+
const punctuationRatio = punctuation / chars.length;
|
|
6480
|
+
if (fragment.length < 4 || readableRatio < 0.55 || punctuationRatio > 0.24) {
|
|
6481
|
+
return false;
|
|
6482
|
+
}
|
|
6483
|
+
if (/([\p{L}\p{N}])\1{4,}/u.test(fragment)) {
|
|
6484
|
+
return false;
|
|
6485
|
+
}
|
|
6486
|
+
if (cjkLetters >= 2) {
|
|
6487
|
+
const suspiciousCjk = chars.filter((char) => isAsciiBytePairCjk(char)).length;
|
|
6488
|
+
if (suspiciousCjk / cjkLetters > 0.65) {
|
|
6489
|
+
return false;
|
|
6490
|
+
}
|
|
6491
|
+
if (isLikelyCjkHeading(fragment)) {
|
|
6492
|
+
return true;
|
|
6493
|
+
}
|
|
6494
|
+
if (punctuation > 0 && fragment.length < 12) {
|
|
6495
|
+
return false;
|
|
6496
|
+
}
|
|
6497
|
+
return cjkLetters >= 8 || cjkLetters >= 4 && spaces > 0;
|
|
6498
|
+
}
|
|
6499
|
+
if (asciiLetters >= 4) {
|
|
6500
|
+
if (punctuation > 0 && spaces === 0) {
|
|
6501
|
+
return false;
|
|
6502
|
+
}
|
|
6503
|
+
if (source === "ascii" && /^[A-Z]{2,8}$/.test(fragment)) {
|
|
6504
|
+
return false;
|
|
6505
|
+
}
|
|
6506
|
+
if (spaces > 0) {
|
|
6507
|
+
return letters >= 3;
|
|
6508
|
+
}
|
|
6509
|
+
return fragment.length >= 6;
|
|
6510
|
+
}
|
|
6511
|
+
if (spaces > 0 && letters >= 3) {
|
|
6512
|
+
return true;
|
|
6513
|
+
}
|
|
6514
|
+
return false;
|
|
6515
|
+
}
|
|
6516
|
+
function isLegacyOfficeMetadataNoise(fragment) {
|
|
6517
|
+
if (/[$�\uFFFD]/u.test(fragment)) {
|
|
6518
|
+
return true;
|
|
6519
|
+
}
|
|
6520
|
+
if (/^(?:Root Entry|WordDocument|Workbook|Book|SummaryInformation|DocumentSummaryInformation|CompObj|ObjectPool|Data|PowerPoint Document|Pictures)$/i.test(fragment)) {
|
|
6521
|
+
return true;
|
|
6522
|
+
}
|
|
6523
|
+
if (/\.(dotm?|docm?|pptx?|ppsx?|xlsm?|xlsx?)\b/i.test(fragment)) {
|
|
6524
|
+
return true;
|
|
6525
|
+
}
|
|
6526
|
+
if (/^(?:默认段落字体|普通表格|正文|标题|副标题|目录|页眉|页脚|批注|超链接)(?:\s*\d+)?$/.test(fragment)) {
|
|
6527
|
+
return true;
|
|
6528
|
+
}
|
|
6529
|
+
if (/\b(?:Normal|Default|Calibri|Times New Roman|WPS Office|Microsoft Office|KSOP?ProductBuildVer)\b/i.test(fragment)) {
|
|
6530
|
+
return true;
|
|
6531
|
+
}
|
|
6532
|
+
if (/^\d+(?:Table|List|Heading|Title|Style)$/i.test(fragment)) {
|
|
6533
|
+
return true;
|
|
6534
|
+
}
|
|
6535
|
+
if (/[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[})]?/i.test(fragment)) {
|
|
6536
|
+
return true;
|
|
6537
|
+
}
|
|
6538
|
+
if (/^[A-Z_]{3,}$/.test(fragment) || /^[A-Za-z]+(?:Information|Document|Storage|Stream|Table|Data|Pool|Obj|Props)$/i.test(fragment)) {
|
|
6539
|
+
return true;
|
|
6540
|
+
}
|
|
6541
|
+
return false;
|
|
6542
|
+
}
|
|
6543
|
+
function isLikelyCjkHeading(fragment) {
|
|
6544
|
+
return /^(?:标题|第[一二三四五六七八九十\d]+[章节条]|[一二三四五六七八九十\d]+[、..])\s*[\p{L}\p{N}\s-]*$/u.test(fragment);
|
|
6545
|
+
}
|
|
6546
|
+
function isAsciiBytePairCjk(char) {
|
|
6547
|
+
const code = char.codePointAt(0) || 0;
|
|
6548
|
+
if (code < 13312 || code > 40959) {
|
|
6549
|
+
return false;
|
|
6550
|
+
}
|
|
6551
|
+
const low = code & 255;
|
|
6552
|
+
const high = code >> 8;
|
|
6553
|
+
return isPrintableAsciiByte(low) && isPrintableAsciiByte(high);
|
|
6554
|
+
}
|
|
6555
|
+
function isPrintableAsciiByte(value) {
|
|
6556
|
+
return value >= 32 && value <= 126;
|
|
6557
|
+
}
|
|
6374
6558
|
function extractPrintableRuns(bytes) {
|
|
6375
6559
|
const fragments = [];
|
|
6376
6560
|
let current = "";
|
|
@@ -6393,6 +6577,13 @@ function extractUtf16Runs(bytes) {
|
|
|
6393
6577
|
const fragments = [];
|
|
6394
6578
|
let current = "";
|
|
6395
6579
|
for (let index = 0; index < bytes.length - 1; index += 2) {
|
|
6580
|
+
if (looksLikeMisalignedAsciiUtf16(bytes[index], bytes[index + 1])) {
|
|
6581
|
+
if (current.length >= 3) {
|
|
6582
|
+
fragments.push(current);
|
|
6583
|
+
}
|
|
6584
|
+
current = "";
|
|
6585
|
+
continue;
|
|
6586
|
+
}
|
|
6396
6587
|
const code = bytes[index] | bytes[index + 1] << 8;
|
|
6397
6588
|
if (code >= 32 && code <= 55295 || code === 9) {
|
|
6398
6589
|
current += String.fromCharCode(code);
|
|
@@ -6408,6 +6599,9 @@ function extractUtf16Runs(bytes) {
|
|
|
6408
6599
|
}
|
|
6409
6600
|
return fragments;
|
|
6410
6601
|
}
|
|
6602
|
+
function looksLikeMisalignedAsciiUtf16(lowByte, highByte) {
|
|
6603
|
+
return lowByte === 0 && (highByte >= 48 && highByte <= 57 || highByte >= 65 && highByte <= 90 || highByte >= 97 && highByte <= 122);
|
|
6604
|
+
}
|
|
6411
6605
|
function normalizeLegacyText(value) {
|
|
6412
6606
|
return value.replace(/[\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
6413
6607
|
}
|
|
@@ -7045,18 +7239,45 @@ function archivePlugin() {
|
|
|
7045
7239
|
layout.className = "ofv-archive-layout";
|
|
7046
7240
|
const sidebar = document.createElement("div");
|
|
7047
7241
|
sidebar.className = "ofv-archive-sidebar";
|
|
7242
|
+
const sidebarPanel = document.createElement("div");
|
|
7243
|
+
sidebarPanel.className = "ofv-archive-sidebar-panel";
|
|
7048
7244
|
const header = document.createElement("div");
|
|
7049
7245
|
header.className = "ofv-archive-header";
|
|
7050
|
-
|
|
7051
|
-
|
|
7246
|
+
const sidebarTitle = document.createElement("span");
|
|
7247
|
+
sidebarTitle.className = "ofv-archive-header-title";
|
|
7248
|
+
sidebarTitle.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
|
|
7249
|
+
const sidebarToggle = document.createElement("button");
|
|
7250
|
+
sidebarToggle.className = "ofv-archive-sidebar-toggle";
|
|
7251
|
+
sidebarToggle.type = "button";
|
|
7252
|
+
sidebarToggle.setAttribute("aria-label", "\u5C55\u5F00\u6587\u4EF6\u5217\u8868");
|
|
7253
|
+
sidebarToggle.setAttribute("aria-expanded", "false");
|
|
7254
|
+
sidebarToggle.title = "\u5C55\u5F00\u6587\u4EF6\u5217\u8868";
|
|
7255
|
+
sidebarToggle.textContent = "\u2039";
|
|
7256
|
+
header.append(sidebarToggle, sidebarTitle);
|
|
7257
|
+
sidebarPanel.append(header);
|
|
7052
7258
|
const tree = document.createElement("div");
|
|
7053
7259
|
tree.className = "ofv-archive-tree";
|
|
7054
|
-
|
|
7260
|
+
sidebarPanel.append(tree);
|
|
7261
|
+
sidebar.append(sidebarPanel);
|
|
7055
7262
|
const mainPanel = document.createElement("div");
|
|
7056
7263
|
mainPanel.className = "ofv-archive-main";
|
|
7057
7264
|
layout.append(sidebar, mainPanel);
|
|
7058
7265
|
panel.append(layout);
|
|
7059
7266
|
let currentSubInstance = null;
|
|
7267
|
+
const getSidebarViewportWidth = () => ctx.viewport.clientWidth || ctx.size.width;
|
|
7268
|
+
const shouldAutoCollapseSidebar = () => getSidebarViewportWidth() <= 520;
|
|
7269
|
+
const setSidebarCollapsed = (collapsed) => {
|
|
7270
|
+
layout.classList.toggle("is-sidebar-collapsed", collapsed);
|
|
7271
|
+
sidebarToggle.setAttribute("aria-expanded", String(!collapsed));
|
|
7272
|
+
const label = collapsed ? "\u5C55\u5F00\u6587\u4EF6\u5217\u8868" : "\u6536\u8D77\u6587\u4EF6\u5217\u8868";
|
|
7273
|
+
sidebarToggle.setAttribute("aria-label", label);
|
|
7274
|
+
sidebarToggle.title = label;
|
|
7275
|
+
sidebarToggle.textContent = collapsed ? "\u203A" : "\u2039";
|
|
7276
|
+
};
|
|
7277
|
+
setSidebarCollapsed(false);
|
|
7278
|
+
sidebarToggle.addEventListener("click", () => {
|
|
7279
|
+
setSidebarCollapsed(!layout.classList.contains("is-sidebar-collapsed"));
|
|
7280
|
+
});
|
|
7060
7281
|
const showDefaultSummary = () => {
|
|
7061
7282
|
mainPanel.replaceChildren();
|
|
7062
7283
|
const summary = document.createElement("div");
|
|
@@ -7096,6 +7317,9 @@ function archivePlugin() {
|
|
|
7096
7317
|
if (destroyed) {
|
|
7097
7318
|
return;
|
|
7098
7319
|
}
|
|
7320
|
+
if (shouldAutoCollapseSidebar()) {
|
|
7321
|
+
setSidebarCollapsed(true);
|
|
7322
|
+
}
|
|
7099
7323
|
const token = ++renderToken;
|
|
7100
7324
|
sidebar.querySelectorAll(".ofv-archive-item").forEach((el) => {
|
|
7101
7325
|
el.classList.remove("is-active");
|
|
@@ -9378,7 +9602,7 @@ function cadPlugin() {
|
|
|
9378
9602
|
return { destroy: () => panel.remove() };
|
|
9379
9603
|
}
|
|
9380
9604
|
const dxf = await readTextFile(ctx.file);
|
|
9381
|
-
const viewer = renderDxf(panel, dxf);
|
|
9605
|
+
const viewer = renderDxf(panel, dxf, ctx);
|
|
9382
9606
|
return {
|
|
9383
9607
|
canCommand(command) {
|
|
9384
9608
|
return viewer.canCommand(command);
|
|
@@ -9847,7 +10071,7 @@ function hexPreview(bytes) {
|
|
|
9847
10071
|
}
|
|
9848
10072
|
return rows.join("\n") || "\u65E0\u53EF\u5C55\u793A\u5B57\u8282\u3002";
|
|
9849
10073
|
}
|
|
9850
|
-
function renderDxf(panel, dxf) {
|
|
10074
|
+
function renderDxf(panel, dxf, ctx) {
|
|
9851
10075
|
const pairs = dxf.split(/\r?\n/).map((line) => line.trim());
|
|
9852
10076
|
const lines = [];
|
|
9853
10077
|
const circles = [];
|
|
@@ -10025,6 +10249,10 @@ function renderDxf(panel, dxf) {
|
|
|
10025
10249
|
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`;
|
|
10026
10250
|
section.append(note, svg);
|
|
10027
10251
|
panel.append(section);
|
|
10252
|
+
const updateToolbarZoom = () => {
|
|
10253
|
+
ctx.toolbar?.setZoom(initialViewBox.width / currentViewBox.width);
|
|
10254
|
+
};
|
|
10255
|
+
updateToolbarZoom();
|
|
10028
10256
|
return {
|
|
10029
10257
|
canCommand(command) {
|
|
10030
10258
|
return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset";
|
|
@@ -10039,14 +10267,19 @@ function renderDxf(panel, dxf) {
|
|
|
10039
10267
|
currentViewBox.x = centerX - currentViewBox.width / 2;
|
|
10040
10268
|
currentViewBox.y = centerY - currentViewBox.height / 2;
|
|
10041
10269
|
applyViewBox();
|
|
10270
|
+
updateToolbarZoom();
|
|
10042
10271
|
return true;
|
|
10043
10272
|
}
|
|
10044
10273
|
if (command === "zoom-reset") {
|
|
10045
10274
|
currentViewBox = { ...initialViewBox };
|
|
10046
10275
|
applyViewBox();
|
|
10276
|
+
updateToolbarZoom();
|
|
10047
10277
|
return true;
|
|
10048
10278
|
}
|
|
10049
10279
|
return false;
|
|
10280
|
+
},
|
|
10281
|
+
destroy() {
|
|
10282
|
+
ctx.toolbar?.setZoom(void 0);
|
|
10050
10283
|
}
|
|
10051
10284
|
};
|
|
10052
10285
|
}
|
|
@@ -10322,6 +10555,12 @@ function model3dPlugin() {
|
|
|
10322
10555
|
renderer.setSize(width, height, false);
|
|
10323
10556
|
};
|
|
10324
10557
|
resize(ctx.size);
|
|
10558
|
+
const updateToolbarZoom = () => {
|
|
10559
|
+
const currentDistance = vectorDistance(camera.position, controls.target);
|
|
10560
|
+
const initialDistance = vectorDistance(initialFrame.cameraPosition, initialFrame.target);
|
|
10561
|
+
ctx.toolbar?.setZoom(initialDistance > 0 ? initialDistance / currentDistance : void 0);
|
|
10562
|
+
};
|
|
10563
|
+
updateToolbarZoom();
|
|
10325
10564
|
return {
|
|
10326
10565
|
canCommand(command) {
|
|
10327
10566
|
return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset" || command === "rotate-right" || command === "rotate-left";
|
|
@@ -10332,6 +10571,7 @@ function model3dPlugin() {
|
|
|
10332
10571
|
camera.position.sub(controls.target).multiplyScalar(factor).add(controls.target);
|
|
10333
10572
|
camera.updateProjectionMatrix();
|
|
10334
10573
|
controls.update();
|
|
10574
|
+
updateToolbarZoom();
|
|
10335
10575
|
return true;
|
|
10336
10576
|
}
|
|
10337
10577
|
if (command === "zoom-reset") {
|
|
@@ -10341,6 +10581,7 @@ function model3dPlugin() {
|
|
|
10341
10581
|
camera.far = initialFrame.far;
|
|
10342
10582
|
camera.updateProjectionMatrix();
|
|
10343
10583
|
controls.update();
|
|
10584
|
+
updateToolbarZoom();
|
|
10344
10585
|
return true;
|
|
10345
10586
|
}
|
|
10346
10587
|
if (command === "rotate-right" || command === "rotate-left") {
|
|
@@ -10351,6 +10592,7 @@ function model3dPlugin() {
|
|
|
10351
10592
|
},
|
|
10352
10593
|
resize,
|
|
10353
10594
|
destroy() {
|
|
10595
|
+
ctx.toolbar?.setZoom(void 0);
|
|
10354
10596
|
window.cancelAnimationFrame(animationFrame);
|
|
10355
10597
|
controls.dispose();
|
|
10356
10598
|
renderer.dispose();
|
|
@@ -10362,6 +10604,12 @@ function model3dPlugin() {
|
|
|
10362
10604
|
}
|
|
10363
10605
|
};
|
|
10364
10606
|
}
|
|
10607
|
+
function vectorDistance(a, b) {
|
|
10608
|
+
if (typeof a.distanceTo === "function") {
|
|
10609
|
+
return a.distanceTo(b);
|
|
10610
|
+
}
|
|
10611
|
+
return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
|
|
10612
|
+
}
|
|
10365
10613
|
function renderModelFallback(ctx, url, isExternal, message) {
|
|
10366
10614
|
const panel = document.createElement("div");
|
|
10367
10615
|
panel.className = "ofv-fallback";
|
|
@@ -10718,34 +10966,65 @@ function gisPlugin() {
|
|
|
10718
10966
|
}
|
|
10719
10967
|
const wrapper = document.createElement("div");
|
|
10720
10968
|
wrapper.className = "ofv-gis-viewer";
|
|
10721
|
-
|
|
10969
|
+
const summary = summarizeGeoJson(geojson);
|
|
10970
|
+
wrapper.append(createGisSummary(summary));
|
|
10722
10971
|
ctx.viewport.appendChild(wrapper);
|
|
10723
10972
|
const mapContainer = document.createElement("div");
|
|
10724
10973
|
mapContainer.className = "ofv-map-stage";
|
|
10725
10974
|
wrapper.appendChild(mapContainer);
|
|
10975
|
+
if (summary.features === 0) {
|
|
10976
|
+
mapContainer.append(createEmptyMapState());
|
|
10977
|
+
} else {
|
|
10978
|
+
mapContainer.append(createMapLegend(summary));
|
|
10979
|
+
}
|
|
10726
10980
|
const map = Leaflet.map(mapContainer).setView([0, 0], 2);
|
|
10727
10981
|
Leaflet.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
10728
10982
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
10729
10983
|
}).addTo(map);
|
|
10730
10984
|
const geojsonLayer = Leaflet.geoJSON(geojson, {
|
|
10731
10985
|
style: () => ({
|
|
10732
|
-
|
|
10986
|
+
className: "ofv-map-feature",
|
|
10987
|
+
color: "#e11d48",
|
|
10733
10988
|
weight: 2,
|
|
10734
|
-
opacity: 0.
|
|
10735
|
-
fillColor: "#
|
|
10736
|
-
fillOpacity: 0.
|
|
10989
|
+
opacity: 0.92,
|
|
10990
|
+
fillColor: "#fb923c",
|
|
10991
|
+
fillOpacity: 0.3,
|
|
10992
|
+
lineCap: "round",
|
|
10993
|
+
lineJoin: "round"
|
|
10737
10994
|
}),
|
|
10738
10995
|
pointToLayer: (feature, latlng) => {
|
|
10739
10996
|
return Leaflet.circleMarker(latlng, {
|
|
10740
|
-
|
|
10741
|
-
|
|
10997
|
+
className: "ofv-map-feature ofv-map-point",
|
|
10998
|
+
radius: 7,
|
|
10999
|
+
fillColor: "#e11d48",
|
|
10742
11000
|
color: "#ffffff",
|
|
10743
|
-
weight:
|
|
11001
|
+
weight: 2.5,
|
|
10744
11002
|
opacity: 1,
|
|
10745
|
-
fillOpacity: 0.
|
|
11003
|
+
fillOpacity: 0.9
|
|
10746
11004
|
});
|
|
10747
11005
|
},
|
|
10748
11006
|
onEachFeature: (feature, layer) => {
|
|
11007
|
+
const label = feature.properties?.name || feature.properties?.title || feature.properties?.label;
|
|
11008
|
+
if (label) {
|
|
11009
|
+
layer.bindTooltip?.(String(label), {
|
|
11010
|
+
className: "ofv-map-tooltip",
|
|
11011
|
+
direction: "top",
|
|
11012
|
+
sticky: true
|
|
11013
|
+
});
|
|
11014
|
+
}
|
|
11015
|
+
layer.on?.({
|
|
11016
|
+
mouseover(event) {
|
|
11017
|
+
event.target?.setStyle?.({
|
|
11018
|
+
weight: 4,
|
|
11019
|
+
opacity: 1,
|
|
11020
|
+
fillOpacity: 0.4
|
|
11021
|
+
});
|
|
11022
|
+
event.target?.bringToFront?.();
|
|
11023
|
+
},
|
|
11024
|
+
mouseout(event) {
|
|
11025
|
+
geojsonLayer.resetStyle?.(event.target);
|
|
11026
|
+
}
|
|
11027
|
+
});
|
|
10749
11028
|
if (feature.properties) {
|
|
10750
11029
|
const props = feature.properties;
|
|
10751
11030
|
const keys = Object.keys(props);
|
|
@@ -10780,15 +11059,20 @@ function gisPlugin() {
|
|
|
10780
11059
|
const bounds = geojsonLayer.getBounds();
|
|
10781
11060
|
if (bounds.isValid()) {
|
|
10782
11061
|
map.fitBounds(bounds, { padding: [20, 20] });
|
|
11062
|
+
map.invalidateSize();
|
|
10783
11063
|
}
|
|
10784
11064
|
} catch (e) {
|
|
10785
11065
|
console.warn("Could not fit bounds for GeoJSON data:", e);
|
|
10786
11066
|
}
|
|
11067
|
+
const resizeTimers = [0, 80, 240].map((delay) => window.setTimeout(() => {
|
|
11068
|
+
map.invalidateSize();
|
|
11069
|
+
}, delay));
|
|
10787
11070
|
return {
|
|
10788
11071
|
resize() {
|
|
10789
11072
|
map.invalidateSize();
|
|
10790
11073
|
},
|
|
10791
11074
|
destroy() {
|
|
11075
|
+
resizeTimers.forEach((timer) => window.clearTimeout(timer));
|
|
10792
11076
|
map.remove();
|
|
10793
11077
|
wrapper.remove();
|
|
10794
11078
|
}
|
|
@@ -10813,8 +11097,7 @@ function normalizeGisError(error, fileName) {
|
|
|
10813
11097
|
function isGisFallback(value) {
|
|
10814
11098
|
return typeof value === "object" && value !== null && "fallback" in value;
|
|
10815
11099
|
}
|
|
10816
|
-
function createGisSummary(
|
|
10817
|
-
const summary = summarizeGeoJson(geojson);
|
|
11100
|
+
function createGisSummary(summary) {
|
|
10818
11101
|
const bar = document.createElement("div");
|
|
10819
11102
|
bar.className = "ofv-gis-summary";
|
|
10820
11103
|
appendSummaryItem(bar, "\u8981\u7D20", String(summary.features));
|
|
@@ -10828,6 +11111,26 @@ function createGisSummary(geojson) {
|
|
|
10828
11111
|
}
|
|
10829
11112
|
return bar;
|
|
10830
11113
|
}
|
|
11114
|
+
function createEmptyMapState() {
|
|
11115
|
+
const empty = document.createElement("div");
|
|
11116
|
+
empty.className = "ofv-map-empty";
|
|
11117
|
+
const title = document.createElement("strong");
|
|
11118
|
+
title.textContent = "\u6682\u65E0\u53EF\u5C55\u793A\u7684\u5730\u56FE\u8981\u7D20";
|
|
11119
|
+
const detail = document.createElement("span");
|
|
11120
|
+
detail.textContent = "GeoJSON \u5DF2\u8BC6\u522B\uFF0C\u4F46 features \u4E3A\u7A7A\u3002";
|
|
11121
|
+
empty.append(title, detail);
|
|
11122
|
+
return empty;
|
|
11123
|
+
}
|
|
11124
|
+
function createMapLegend(summary) {
|
|
11125
|
+
const legend = document.createElement("div");
|
|
11126
|
+
legend.className = "ofv-map-legend";
|
|
11127
|
+
const title = document.createElement("strong");
|
|
11128
|
+
title.textContent = "GeoJSON";
|
|
11129
|
+
const detail = document.createElement("span");
|
|
11130
|
+
detail.textContent = `${summary.features} \u4E2A\u8981\u7D20 \xB7 ${formatGeometryCounts(summary.geometryCounts)}`;
|
|
11131
|
+
legend.append(title, detail);
|
|
11132
|
+
return legend;
|
|
11133
|
+
}
|
|
10831
11134
|
function appendSummaryItem(parent, label, value) {
|
|
10832
11135
|
const item = document.createElement("span");
|
|
10833
11136
|
const key = document.createElement("span");
|