@open-file-viewer/core 0.1.2 → 0.1.4
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 +508 -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 +508 -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,8 @@ async function renderDocx(panel, arrayBuffer) {
|
|
|
5077
5089
|
experimental: true,
|
|
5078
5090
|
useBase64URL: true
|
|
5079
5091
|
});
|
|
5092
|
+
normalizeDocxLayout(content);
|
|
5093
|
+
return fitDocxPages(content);
|
|
5080
5094
|
} catch (error) {
|
|
5081
5095
|
content.replaceChildren();
|
|
5082
5096
|
const fallbackNote = document.createElement("div");
|
|
@@ -5091,6 +5105,97 @@ async function renderDocx(panel, arrayBuffer) {
|
|
|
5091
5105
|
}
|
|
5092
5106
|
console.warn("DOCX layout preview failed, fell back to Mammoth:", error);
|
|
5093
5107
|
}
|
|
5108
|
+
return () => void 0;
|
|
5109
|
+
}
|
|
5110
|
+
function normalizeDocxLayout(container) {
|
|
5111
|
+
const pages = container.querySelectorAll("section.ofv-docx");
|
|
5112
|
+
for (const page of pages) {
|
|
5113
|
+
for (const element of page.querySelectorAll("[style*='line-height']")) {
|
|
5114
|
+
const lineHeight = parseCssLineHeight(element.style.lineHeight);
|
|
5115
|
+
if (lineHeight > 0 && lineHeight < 1) {
|
|
5116
|
+
element.style.lineHeight = "1.2";
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
function parseCssLineHeight(value) {
|
|
5122
|
+
const trimmed = value.trim();
|
|
5123
|
+
if (!trimmed || trimmed === "normal") {
|
|
5124
|
+
return 0;
|
|
5125
|
+
}
|
|
5126
|
+
if (trimmed.endsWith("%")) {
|
|
5127
|
+
const parsedPercent = Number.parseFloat(trimmed);
|
|
5128
|
+
return Number.isFinite(parsedPercent) ? parsedPercent / 100 : 0;
|
|
5129
|
+
}
|
|
5130
|
+
const parsed = Number.parseFloat(trimmed);
|
|
5131
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
5132
|
+
}
|
|
5133
|
+
function fitDocxPages(container) {
|
|
5134
|
+
const wrapper = container.querySelector(".ofv-docx-wrapper");
|
|
5135
|
+
if (!wrapper) {
|
|
5136
|
+
return () => void 0;
|
|
5137
|
+
}
|
|
5138
|
+
const update = () => {
|
|
5139
|
+
const frames = ensureDocxPageFrames(wrapper);
|
|
5140
|
+
if (frames.length === 0) {
|
|
5141
|
+
wrapper.style.removeProperty("--ofv-docx-scale");
|
|
5142
|
+
return;
|
|
5143
|
+
}
|
|
5144
|
+
const availableWidth = Math.max(1, container.clientWidth - 48);
|
|
5145
|
+
const pageWidth = Math.max(
|
|
5146
|
+
1,
|
|
5147
|
+
...frames.map(({ page }) => {
|
|
5148
|
+
const rectWidth = page.getBoundingClientRect().width;
|
|
5149
|
+
return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
|
|
5150
|
+
})
|
|
5151
|
+
);
|
|
5152
|
+
const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
|
|
5153
|
+
wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
|
|
5154
|
+
wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
|
|
5155
|
+
for (const { frame, page } of frames) {
|
|
5156
|
+
const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
|
|
5157
|
+
if (pageHeight > 0) {
|
|
5158
|
+
frame.style.height = `${Math.ceil(pageHeight * scale)}px`;
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
};
|
|
5162
|
+
update();
|
|
5163
|
+
const timers = [0, 80, 240].map((delay) => window.setTimeout(update, delay));
|
|
5164
|
+
if (typeof ResizeObserver === "undefined") {
|
|
5165
|
+
window.addEventListener("resize", update);
|
|
5166
|
+
return () => {
|
|
5167
|
+
timers.forEach((timer) => window.clearTimeout(timer));
|
|
5168
|
+
window.removeEventListener("resize", update);
|
|
5169
|
+
};
|
|
5170
|
+
}
|
|
5171
|
+
const observer = new ResizeObserver(update);
|
|
5172
|
+
observer.observe(container);
|
|
5173
|
+
observer.observe(wrapper);
|
|
5174
|
+
return () => {
|
|
5175
|
+
timers.forEach((timer) => window.clearTimeout(timer));
|
|
5176
|
+
observer.disconnect();
|
|
5177
|
+
};
|
|
5178
|
+
}
|
|
5179
|
+
function ensureDocxPageFrames(wrapper) {
|
|
5180
|
+
const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
|
|
5181
|
+
return pages.map((page) => {
|
|
5182
|
+
const parent = page.parentElement;
|
|
5183
|
+
if (parent?.classList.contains("ofv-docx-page-frame")) {
|
|
5184
|
+
return { frame: parent, page };
|
|
5185
|
+
}
|
|
5186
|
+
const frame = document.createElement("div");
|
|
5187
|
+
frame.className = "ofv-docx-page-frame";
|
|
5188
|
+
page.before(frame);
|
|
5189
|
+
frame.append(page);
|
|
5190
|
+
return { frame, page };
|
|
5191
|
+
});
|
|
5192
|
+
}
|
|
5193
|
+
function parseCssPixelValue(value) {
|
|
5194
|
+
const parsed = Number.parseFloat(value);
|
|
5195
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
5196
|
+
}
|
|
5197
|
+
function formatCssNumber(value) {
|
|
5198
|
+
return Number.isFinite(value) ? value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "") : "1";
|
|
5094
5199
|
}
|
|
5095
5200
|
async function renderDocxTextFallback(container, arrayBuffer) {
|
|
5096
5201
|
const article = document.createElement("article");
|
|
@@ -5189,7 +5294,7 @@ async function renderSheet(panel, arrayBuffer, extension) {
|
|
|
5189
5294
|
workbook = xlsx.read(arrayBuffer, { type: "array" });
|
|
5190
5295
|
} catch (error) {
|
|
5191
5296
|
if (isLegacyOfficeBinary(extension)) {
|
|
5192
|
-
renderLegacyOfficeBinary(panel, extension, arrayBuffer, normalizeOfficeError(error));
|
|
5297
|
+
renderLegacyOfficeBinary(panel, extension, arrayBuffer, `\u8868\u683C\u89E3\u6790\u5931\u8D25\uFF1A${normalizeOfficeError(error)}`);
|
|
5193
5298
|
return;
|
|
5194
5299
|
}
|
|
5195
5300
|
renderSheetFallback(panel, extension, normalizeOfficeError(error));
|
|
@@ -6294,14 +6399,15 @@ function legacyOfficeFormatLabel(extension) {
|
|
|
6294
6399
|
function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
|
|
6295
6400
|
const fragments = extractLegacyOfficeText(arrayBuffer);
|
|
6296
6401
|
panel.replaceChildren();
|
|
6297
|
-
const section = createSection("Office \
|
|
6402
|
+
const section = createSection("Office \u8F6C\u6362\u63D0\u793A");
|
|
6403
|
+
section.classList.add("ofv-office-conversion");
|
|
6298
6404
|
const format = document.createElement("p");
|
|
6299
6405
|
const strong = document.createElement("strong");
|
|
6300
6406
|
strong.textContent = `.${extension}`;
|
|
6301
6407
|
format.append(
|
|
6302
6408
|
strong,
|
|
6303
6409
|
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\
|
|
6410
|
+
" \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
6411
|
)
|
|
6306
6412
|
);
|
|
6307
6413
|
const meta = document.createElement("dl");
|
|
@@ -6310,12 +6416,15 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
|
|
|
6310
6416
|
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
6417
|
appendOfficeBinaryMeta(meta, "\u6587\u672C\u7247\u6BB5", `${fragments.length} \u6BB5`);
|
|
6312
6418
|
if (parseError) {
|
|
6313
|
-
appendOfficeBinaryMeta(meta, "\
|
|
6419
|
+
appendOfficeBinaryMeta(meta, "\u89E3\u6790\u72B6\u6001", parseError);
|
|
6314
6420
|
}
|
|
6315
6421
|
section.append(format, meta);
|
|
6316
6422
|
if (fragments.length > 0) {
|
|
6317
6423
|
const article = document.createElement("article");
|
|
6318
|
-
article.className = "ofv-document";
|
|
6424
|
+
article.className = "ofv-document ofv-office-binary-fragments";
|
|
6425
|
+
const heading = document.createElement("h4");
|
|
6426
|
+
heading.textContent = "\u53EF\u8BFB\u6587\u672C\u7247\u6BB5";
|
|
6427
|
+
article.append(heading);
|
|
6319
6428
|
for (const fragment of fragments.slice(0, 80)) {
|
|
6320
6429
|
const paragraph = document.createElement("p");
|
|
6321
6430
|
paragraph.textContent = fragment;
|
|
@@ -6324,7 +6433,8 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
|
|
|
6324
6433
|
section.append(article);
|
|
6325
6434
|
} else {
|
|
6326
6435
|
const empty = document.createElement("p");
|
|
6327
|
-
empty.
|
|
6436
|
+
empty.className = "ofv-office-binary-empty";
|
|
6437
|
+
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
6438
|
section.append(empty);
|
|
6329
6439
|
}
|
|
6330
6440
|
panel.append(section);
|
|
@@ -6357,12 +6467,12 @@ function normalizeOfficeError(error) {
|
|
|
6357
6467
|
function extractLegacyOfficeText(arrayBuffer) {
|
|
6358
6468
|
const bytes = new Uint8Array(arrayBuffer);
|
|
6359
6469
|
const fragments = [
|
|
6360
|
-
...extractPrintableRuns(bytes),
|
|
6361
|
-
...extractUtf16Runs(bytes)
|
|
6362
|
-
].map((
|
|
6470
|
+
...extractPrintableRuns(bytes).map((text) => ({ text, source: "ascii" })),
|
|
6471
|
+
...extractUtf16Runs(bytes).map((text) => ({ text, source: "utf16" }))
|
|
6472
|
+
].map(({ text, source }) => ({ text: normalizeLegacyText(text), source })).filter(({ text, source }) => isReadableLegacyTextFragment(text, source));
|
|
6363
6473
|
const unique = [];
|
|
6364
6474
|
const seen = /* @__PURE__ */ new Set();
|
|
6365
|
-
for (const fragment of fragments) {
|
|
6475
|
+
for (const { text: fragment } of fragments) {
|
|
6366
6476
|
const key = fragment.toLowerCase();
|
|
6367
6477
|
if (!seen.has(key)) {
|
|
6368
6478
|
seen.add(key);
|
|
@@ -6371,6 +6481,104 @@ function extractLegacyOfficeText(arrayBuffer) {
|
|
|
6371
6481
|
}
|
|
6372
6482
|
return unique.slice(0, 160);
|
|
6373
6483
|
}
|
|
6484
|
+
function isReadableLegacyTextFragment(fragment, source) {
|
|
6485
|
+
if (fragment.length > 600) {
|
|
6486
|
+
return false;
|
|
6487
|
+
}
|
|
6488
|
+
if (isLegacyOfficeMetadataNoise(fragment)) {
|
|
6489
|
+
return false;
|
|
6490
|
+
}
|
|
6491
|
+
if (!/[\p{L}\p{N}]/u.test(fragment)) {
|
|
6492
|
+
return false;
|
|
6493
|
+
}
|
|
6494
|
+
const chars = Array.from(fragment);
|
|
6495
|
+
const letters = chars.filter((char) => /\p{L}/u.test(char)).length;
|
|
6496
|
+
const digits = chars.filter((char) => /\p{N}/u.test(char)).length;
|
|
6497
|
+
const spaces = chars.filter((char) => /\s/u.test(char)).length;
|
|
6498
|
+
const asciiLetters = chars.filter((char) => /[A-Za-z]/.test(char)).length;
|
|
6499
|
+
const cjkLetters = chars.filter((char) => /[\u3400-\u9fff]/u.test(char)).length;
|
|
6500
|
+
const punctuation = chars.filter((char) => /[^\p{L}\p{N}\s]/u.test(char)).length;
|
|
6501
|
+
const alphaNumeric = letters + digits;
|
|
6502
|
+
const readableRatio = alphaNumeric / chars.length;
|
|
6503
|
+
const punctuationRatio = punctuation / chars.length;
|
|
6504
|
+
if (fragment.length < 4 || readableRatio < 0.55 || punctuationRatio > 0.24) {
|
|
6505
|
+
return false;
|
|
6506
|
+
}
|
|
6507
|
+
if (/([\p{L}\p{N}])\1{4,}/u.test(fragment)) {
|
|
6508
|
+
return false;
|
|
6509
|
+
}
|
|
6510
|
+
if (cjkLetters >= 2) {
|
|
6511
|
+
const suspiciousCjk = chars.filter((char) => isAsciiBytePairCjk(char)).length;
|
|
6512
|
+
if (suspiciousCjk / cjkLetters > 0.65) {
|
|
6513
|
+
return false;
|
|
6514
|
+
}
|
|
6515
|
+
if (isLikelyCjkHeading(fragment)) {
|
|
6516
|
+
return true;
|
|
6517
|
+
}
|
|
6518
|
+
if (punctuation > 0 && fragment.length < 12) {
|
|
6519
|
+
return false;
|
|
6520
|
+
}
|
|
6521
|
+
return cjkLetters >= 8 || cjkLetters >= 4 && spaces > 0;
|
|
6522
|
+
}
|
|
6523
|
+
if (asciiLetters >= 4) {
|
|
6524
|
+
if (punctuation > 0 && spaces === 0) {
|
|
6525
|
+
return false;
|
|
6526
|
+
}
|
|
6527
|
+
if (source === "ascii" && /^[A-Z]{2,8}$/.test(fragment)) {
|
|
6528
|
+
return false;
|
|
6529
|
+
}
|
|
6530
|
+
if (spaces > 0) {
|
|
6531
|
+
return letters >= 3;
|
|
6532
|
+
}
|
|
6533
|
+
return fragment.length >= 6;
|
|
6534
|
+
}
|
|
6535
|
+
if (spaces > 0 && letters >= 3) {
|
|
6536
|
+
return true;
|
|
6537
|
+
}
|
|
6538
|
+
return false;
|
|
6539
|
+
}
|
|
6540
|
+
function isLegacyOfficeMetadataNoise(fragment) {
|
|
6541
|
+
if (/[$�\uFFFD]/u.test(fragment)) {
|
|
6542
|
+
return true;
|
|
6543
|
+
}
|
|
6544
|
+
if (/^(?:Root Entry|WordDocument|Workbook|Book|SummaryInformation|DocumentSummaryInformation|CompObj|ObjectPool|Data|PowerPoint Document|Pictures)$/i.test(fragment)) {
|
|
6545
|
+
return true;
|
|
6546
|
+
}
|
|
6547
|
+
if (/\.(dotm?|docm?|pptx?|ppsx?|xlsm?|xlsx?)\b/i.test(fragment)) {
|
|
6548
|
+
return true;
|
|
6549
|
+
}
|
|
6550
|
+
if (/^(?:默认段落字体|普通表格|正文|标题|副标题|目录|页眉|页脚|批注|超链接)(?:\s*\d+)?$/.test(fragment)) {
|
|
6551
|
+
return true;
|
|
6552
|
+
}
|
|
6553
|
+
if (/\b(?:Normal|Default|Calibri|Times New Roman|WPS Office|Microsoft Office|KSOP?ProductBuildVer)\b/i.test(fragment)) {
|
|
6554
|
+
return true;
|
|
6555
|
+
}
|
|
6556
|
+
if (/^\d+(?:Table|List|Heading|Title|Style)$/i.test(fragment)) {
|
|
6557
|
+
return true;
|
|
6558
|
+
}
|
|
6559
|
+
if (/[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[})]?/i.test(fragment)) {
|
|
6560
|
+
return true;
|
|
6561
|
+
}
|
|
6562
|
+
if (/^[A-Z_]{3,}$/.test(fragment) || /^[A-Za-z]+(?:Information|Document|Storage|Stream|Table|Data|Pool|Obj|Props)$/i.test(fragment)) {
|
|
6563
|
+
return true;
|
|
6564
|
+
}
|
|
6565
|
+
return false;
|
|
6566
|
+
}
|
|
6567
|
+
function isLikelyCjkHeading(fragment) {
|
|
6568
|
+
return /^(?:标题|第[一二三四五六七八九十\d]+[章节条]|[一二三四五六七八九十\d]+[、..])\s*[\p{L}\p{N}\s-]*$/u.test(fragment);
|
|
6569
|
+
}
|
|
6570
|
+
function isAsciiBytePairCjk(char) {
|
|
6571
|
+
const code = char.codePointAt(0) || 0;
|
|
6572
|
+
if (code < 13312 || code > 40959) {
|
|
6573
|
+
return false;
|
|
6574
|
+
}
|
|
6575
|
+
const low = code & 255;
|
|
6576
|
+
const high = code >> 8;
|
|
6577
|
+
return isPrintableAsciiByte(low) && isPrintableAsciiByte(high);
|
|
6578
|
+
}
|
|
6579
|
+
function isPrintableAsciiByte(value) {
|
|
6580
|
+
return value >= 32 && value <= 126;
|
|
6581
|
+
}
|
|
6374
6582
|
function extractPrintableRuns(bytes) {
|
|
6375
6583
|
const fragments = [];
|
|
6376
6584
|
let current = "";
|
|
@@ -6393,6 +6601,13 @@ function extractUtf16Runs(bytes) {
|
|
|
6393
6601
|
const fragments = [];
|
|
6394
6602
|
let current = "";
|
|
6395
6603
|
for (let index = 0; index < bytes.length - 1; index += 2) {
|
|
6604
|
+
if (looksLikeMisalignedAsciiUtf16(bytes[index], bytes[index + 1])) {
|
|
6605
|
+
if (current.length >= 3) {
|
|
6606
|
+
fragments.push(current);
|
|
6607
|
+
}
|
|
6608
|
+
current = "";
|
|
6609
|
+
continue;
|
|
6610
|
+
}
|
|
6396
6611
|
const code = bytes[index] | bytes[index + 1] << 8;
|
|
6397
6612
|
if (code >= 32 && code <= 55295 || code === 9) {
|
|
6398
6613
|
current += String.fromCharCode(code);
|
|
@@ -6408,6 +6623,9 @@ function extractUtf16Runs(bytes) {
|
|
|
6408
6623
|
}
|
|
6409
6624
|
return fragments;
|
|
6410
6625
|
}
|
|
6626
|
+
function looksLikeMisalignedAsciiUtf16(lowByte, highByte) {
|
|
6627
|
+
return lowByte === 0 && (highByte >= 48 && highByte <= 57 || highByte >= 65 && highByte <= 90 || highByte >= 97 && highByte <= 122);
|
|
6628
|
+
}
|
|
6411
6629
|
function normalizeLegacyText(value) {
|
|
6412
6630
|
return value.replace(/[\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
6413
6631
|
}
|
|
@@ -7045,18 +7263,45 @@ function archivePlugin() {
|
|
|
7045
7263
|
layout.className = "ofv-archive-layout";
|
|
7046
7264
|
const sidebar = document.createElement("div");
|
|
7047
7265
|
sidebar.className = "ofv-archive-sidebar";
|
|
7266
|
+
const sidebarPanel = document.createElement("div");
|
|
7267
|
+
sidebarPanel.className = "ofv-archive-sidebar-panel";
|
|
7048
7268
|
const header = document.createElement("div");
|
|
7049
7269
|
header.className = "ofv-archive-header";
|
|
7050
|
-
|
|
7051
|
-
|
|
7270
|
+
const sidebarTitle = document.createElement("span");
|
|
7271
|
+
sidebarTitle.className = "ofv-archive-header-title";
|
|
7272
|
+
sidebarTitle.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
|
|
7273
|
+
const sidebarToggle = document.createElement("button");
|
|
7274
|
+
sidebarToggle.className = "ofv-archive-sidebar-toggle";
|
|
7275
|
+
sidebarToggle.type = "button";
|
|
7276
|
+
sidebarToggle.setAttribute("aria-label", "\u5C55\u5F00\u6587\u4EF6\u5217\u8868");
|
|
7277
|
+
sidebarToggle.setAttribute("aria-expanded", "false");
|
|
7278
|
+
sidebarToggle.title = "\u5C55\u5F00\u6587\u4EF6\u5217\u8868";
|
|
7279
|
+
sidebarToggle.textContent = "\u2039";
|
|
7280
|
+
header.append(sidebarToggle, sidebarTitle);
|
|
7281
|
+
sidebarPanel.append(header);
|
|
7052
7282
|
const tree = document.createElement("div");
|
|
7053
7283
|
tree.className = "ofv-archive-tree";
|
|
7054
|
-
|
|
7284
|
+
sidebarPanel.append(tree);
|
|
7285
|
+
sidebar.append(sidebarPanel);
|
|
7055
7286
|
const mainPanel = document.createElement("div");
|
|
7056
7287
|
mainPanel.className = "ofv-archive-main";
|
|
7057
7288
|
layout.append(sidebar, mainPanel);
|
|
7058
7289
|
panel.append(layout);
|
|
7059
7290
|
let currentSubInstance = null;
|
|
7291
|
+
const getSidebarViewportWidth = () => ctx.viewport.clientWidth || ctx.size.width;
|
|
7292
|
+
const shouldAutoCollapseSidebar = () => getSidebarViewportWidth() <= 520;
|
|
7293
|
+
const setSidebarCollapsed = (collapsed) => {
|
|
7294
|
+
layout.classList.toggle("is-sidebar-collapsed", collapsed);
|
|
7295
|
+
sidebarToggle.setAttribute("aria-expanded", String(!collapsed));
|
|
7296
|
+
const label = collapsed ? "\u5C55\u5F00\u6587\u4EF6\u5217\u8868" : "\u6536\u8D77\u6587\u4EF6\u5217\u8868";
|
|
7297
|
+
sidebarToggle.setAttribute("aria-label", label);
|
|
7298
|
+
sidebarToggle.title = label;
|
|
7299
|
+
sidebarToggle.textContent = collapsed ? "\u203A" : "\u2039";
|
|
7300
|
+
};
|
|
7301
|
+
setSidebarCollapsed(false);
|
|
7302
|
+
sidebarToggle.addEventListener("click", () => {
|
|
7303
|
+
setSidebarCollapsed(!layout.classList.contains("is-sidebar-collapsed"));
|
|
7304
|
+
});
|
|
7060
7305
|
const showDefaultSummary = () => {
|
|
7061
7306
|
mainPanel.replaceChildren();
|
|
7062
7307
|
const summary = document.createElement("div");
|
|
@@ -7096,6 +7341,9 @@ function archivePlugin() {
|
|
|
7096
7341
|
if (destroyed) {
|
|
7097
7342
|
return;
|
|
7098
7343
|
}
|
|
7344
|
+
if (shouldAutoCollapseSidebar()) {
|
|
7345
|
+
setSidebarCollapsed(true);
|
|
7346
|
+
}
|
|
7099
7347
|
const token = ++renderToken;
|
|
7100
7348
|
sidebar.querySelectorAll(".ofv-archive-item").forEach((el) => {
|
|
7101
7349
|
el.classList.remove("is-active");
|
|
@@ -9378,7 +9626,7 @@ function cadPlugin() {
|
|
|
9378
9626
|
return { destroy: () => panel.remove() };
|
|
9379
9627
|
}
|
|
9380
9628
|
const dxf = await readTextFile(ctx.file);
|
|
9381
|
-
const viewer = renderDxf(panel, dxf);
|
|
9629
|
+
const viewer = renderDxf(panel, dxf, ctx);
|
|
9382
9630
|
return {
|
|
9383
9631
|
canCommand(command) {
|
|
9384
9632
|
return viewer.canCommand(command);
|
|
@@ -9847,7 +10095,7 @@ function hexPreview(bytes) {
|
|
|
9847
10095
|
}
|
|
9848
10096
|
return rows.join("\n") || "\u65E0\u53EF\u5C55\u793A\u5B57\u8282\u3002";
|
|
9849
10097
|
}
|
|
9850
|
-
function renderDxf(panel, dxf) {
|
|
10098
|
+
function renderDxf(panel, dxf, ctx) {
|
|
9851
10099
|
const pairs = dxf.split(/\r?\n/).map((line) => line.trim());
|
|
9852
10100
|
const lines = [];
|
|
9853
10101
|
const circles = [];
|
|
@@ -10025,6 +10273,10 @@ function renderDxf(panel, dxf) {
|
|
|
10025
10273
|
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
10274
|
section.append(note, svg);
|
|
10027
10275
|
panel.append(section);
|
|
10276
|
+
const updateToolbarZoom = () => {
|
|
10277
|
+
ctx.toolbar?.setZoom(initialViewBox.width / currentViewBox.width);
|
|
10278
|
+
};
|
|
10279
|
+
updateToolbarZoom();
|
|
10028
10280
|
return {
|
|
10029
10281
|
canCommand(command) {
|
|
10030
10282
|
return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset";
|
|
@@ -10039,14 +10291,19 @@ function renderDxf(panel, dxf) {
|
|
|
10039
10291
|
currentViewBox.x = centerX - currentViewBox.width / 2;
|
|
10040
10292
|
currentViewBox.y = centerY - currentViewBox.height / 2;
|
|
10041
10293
|
applyViewBox();
|
|
10294
|
+
updateToolbarZoom();
|
|
10042
10295
|
return true;
|
|
10043
10296
|
}
|
|
10044
10297
|
if (command === "zoom-reset") {
|
|
10045
10298
|
currentViewBox = { ...initialViewBox };
|
|
10046
10299
|
applyViewBox();
|
|
10300
|
+
updateToolbarZoom();
|
|
10047
10301
|
return true;
|
|
10048
10302
|
}
|
|
10049
10303
|
return false;
|
|
10304
|
+
},
|
|
10305
|
+
destroy() {
|
|
10306
|
+
ctx.toolbar?.setZoom(void 0);
|
|
10050
10307
|
}
|
|
10051
10308
|
};
|
|
10052
10309
|
}
|
|
@@ -10322,6 +10579,12 @@ function model3dPlugin() {
|
|
|
10322
10579
|
renderer.setSize(width, height, false);
|
|
10323
10580
|
};
|
|
10324
10581
|
resize(ctx.size);
|
|
10582
|
+
const updateToolbarZoom = () => {
|
|
10583
|
+
const currentDistance = vectorDistance(camera.position, controls.target);
|
|
10584
|
+
const initialDistance = vectorDistance(initialFrame.cameraPosition, initialFrame.target);
|
|
10585
|
+
ctx.toolbar?.setZoom(initialDistance > 0 ? initialDistance / currentDistance : void 0);
|
|
10586
|
+
};
|
|
10587
|
+
updateToolbarZoom();
|
|
10325
10588
|
return {
|
|
10326
10589
|
canCommand(command) {
|
|
10327
10590
|
return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset" || command === "rotate-right" || command === "rotate-left";
|
|
@@ -10332,6 +10595,7 @@ function model3dPlugin() {
|
|
|
10332
10595
|
camera.position.sub(controls.target).multiplyScalar(factor).add(controls.target);
|
|
10333
10596
|
camera.updateProjectionMatrix();
|
|
10334
10597
|
controls.update();
|
|
10598
|
+
updateToolbarZoom();
|
|
10335
10599
|
return true;
|
|
10336
10600
|
}
|
|
10337
10601
|
if (command === "zoom-reset") {
|
|
@@ -10341,6 +10605,7 @@ function model3dPlugin() {
|
|
|
10341
10605
|
camera.far = initialFrame.far;
|
|
10342
10606
|
camera.updateProjectionMatrix();
|
|
10343
10607
|
controls.update();
|
|
10608
|
+
updateToolbarZoom();
|
|
10344
10609
|
return true;
|
|
10345
10610
|
}
|
|
10346
10611
|
if (command === "rotate-right" || command === "rotate-left") {
|
|
@@ -10351,6 +10616,7 @@ function model3dPlugin() {
|
|
|
10351
10616
|
},
|
|
10352
10617
|
resize,
|
|
10353
10618
|
destroy() {
|
|
10619
|
+
ctx.toolbar?.setZoom(void 0);
|
|
10354
10620
|
window.cancelAnimationFrame(animationFrame);
|
|
10355
10621
|
controls.dispose();
|
|
10356
10622
|
renderer.dispose();
|
|
@@ -10362,6 +10628,12 @@ function model3dPlugin() {
|
|
|
10362
10628
|
}
|
|
10363
10629
|
};
|
|
10364
10630
|
}
|
|
10631
|
+
function vectorDistance(a, b) {
|
|
10632
|
+
if (typeof a.distanceTo === "function") {
|
|
10633
|
+
return a.distanceTo(b);
|
|
10634
|
+
}
|
|
10635
|
+
return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
|
|
10636
|
+
}
|
|
10365
10637
|
function renderModelFallback(ctx, url, isExternal, message) {
|
|
10366
10638
|
const panel = document.createElement("div");
|
|
10367
10639
|
panel.className = "ofv-fallback";
|
|
@@ -10718,34 +10990,65 @@ function gisPlugin() {
|
|
|
10718
10990
|
}
|
|
10719
10991
|
const wrapper = document.createElement("div");
|
|
10720
10992
|
wrapper.className = "ofv-gis-viewer";
|
|
10721
|
-
|
|
10993
|
+
const summary = summarizeGeoJson(geojson);
|
|
10994
|
+
wrapper.append(createGisSummary(summary));
|
|
10722
10995
|
ctx.viewport.appendChild(wrapper);
|
|
10723
10996
|
const mapContainer = document.createElement("div");
|
|
10724
10997
|
mapContainer.className = "ofv-map-stage";
|
|
10725
10998
|
wrapper.appendChild(mapContainer);
|
|
10999
|
+
if (summary.features === 0) {
|
|
11000
|
+
mapContainer.append(createEmptyMapState());
|
|
11001
|
+
} else {
|
|
11002
|
+
mapContainer.append(createMapLegend(summary));
|
|
11003
|
+
}
|
|
10726
11004
|
const map = Leaflet.map(mapContainer).setView([0, 0], 2);
|
|
10727
11005
|
Leaflet.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
10728
11006
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
10729
11007
|
}).addTo(map);
|
|
10730
11008
|
const geojsonLayer = Leaflet.geoJSON(geojson, {
|
|
10731
11009
|
style: () => ({
|
|
10732
|
-
|
|
11010
|
+
className: "ofv-map-feature",
|
|
11011
|
+
color: "#e11d48",
|
|
10733
11012
|
weight: 2,
|
|
10734
|
-
opacity: 0.
|
|
10735
|
-
fillColor: "#
|
|
10736
|
-
fillOpacity: 0.
|
|
11013
|
+
opacity: 0.92,
|
|
11014
|
+
fillColor: "#fb923c",
|
|
11015
|
+
fillOpacity: 0.3,
|
|
11016
|
+
lineCap: "round",
|
|
11017
|
+
lineJoin: "round"
|
|
10737
11018
|
}),
|
|
10738
11019
|
pointToLayer: (feature, latlng) => {
|
|
10739
11020
|
return Leaflet.circleMarker(latlng, {
|
|
10740
|
-
|
|
10741
|
-
|
|
11021
|
+
className: "ofv-map-feature ofv-map-point",
|
|
11022
|
+
radius: 7,
|
|
11023
|
+
fillColor: "#e11d48",
|
|
10742
11024
|
color: "#ffffff",
|
|
10743
|
-
weight:
|
|
11025
|
+
weight: 2.5,
|
|
10744
11026
|
opacity: 1,
|
|
10745
|
-
fillOpacity: 0.
|
|
11027
|
+
fillOpacity: 0.9
|
|
10746
11028
|
});
|
|
10747
11029
|
},
|
|
10748
11030
|
onEachFeature: (feature, layer) => {
|
|
11031
|
+
const label = feature.properties?.name || feature.properties?.title || feature.properties?.label;
|
|
11032
|
+
if (label) {
|
|
11033
|
+
layer.bindTooltip?.(String(label), {
|
|
11034
|
+
className: "ofv-map-tooltip",
|
|
11035
|
+
direction: "top",
|
|
11036
|
+
sticky: true
|
|
11037
|
+
});
|
|
11038
|
+
}
|
|
11039
|
+
layer.on?.({
|
|
11040
|
+
mouseover(event) {
|
|
11041
|
+
event.target?.setStyle?.({
|
|
11042
|
+
weight: 4,
|
|
11043
|
+
opacity: 1,
|
|
11044
|
+
fillOpacity: 0.4
|
|
11045
|
+
});
|
|
11046
|
+
event.target?.bringToFront?.();
|
|
11047
|
+
},
|
|
11048
|
+
mouseout(event) {
|
|
11049
|
+
geojsonLayer.resetStyle?.(event.target);
|
|
11050
|
+
}
|
|
11051
|
+
});
|
|
10749
11052
|
if (feature.properties) {
|
|
10750
11053
|
const props = feature.properties;
|
|
10751
11054
|
const keys = Object.keys(props);
|
|
@@ -10780,15 +11083,20 @@ function gisPlugin() {
|
|
|
10780
11083
|
const bounds = geojsonLayer.getBounds();
|
|
10781
11084
|
if (bounds.isValid()) {
|
|
10782
11085
|
map.fitBounds(bounds, { padding: [20, 20] });
|
|
11086
|
+
map.invalidateSize();
|
|
10783
11087
|
}
|
|
10784
11088
|
} catch (e) {
|
|
10785
11089
|
console.warn("Could not fit bounds for GeoJSON data:", e);
|
|
10786
11090
|
}
|
|
11091
|
+
const resizeTimers = [0, 80, 240].map((delay) => window.setTimeout(() => {
|
|
11092
|
+
map.invalidateSize();
|
|
11093
|
+
}, delay));
|
|
10787
11094
|
return {
|
|
10788
11095
|
resize() {
|
|
10789
11096
|
map.invalidateSize();
|
|
10790
11097
|
},
|
|
10791
11098
|
destroy() {
|
|
11099
|
+
resizeTimers.forEach((timer) => window.clearTimeout(timer));
|
|
10792
11100
|
map.remove();
|
|
10793
11101
|
wrapper.remove();
|
|
10794
11102
|
}
|
|
@@ -10813,8 +11121,7 @@ function normalizeGisError(error, fileName) {
|
|
|
10813
11121
|
function isGisFallback(value) {
|
|
10814
11122
|
return typeof value === "object" && value !== null && "fallback" in value;
|
|
10815
11123
|
}
|
|
10816
|
-
function createGisSummary(
|
|
10817
|
-
const summary = summarizeGeoJson(geojson);
|
|
11124
|
+
function createGisSummary(summary) {
|
|
10818
11125
|
const bar = document.createElement("div");
|
|
10819
11126
|
bar.className = "ofv-gis-summary";
|
|
10820
11127
|
appendSummaryItem(bar, "\u8981\u7D20", String(summary.features));
|
|
@@ -10828,6 +11135,26 @@ function createGisSummary(geojson) {
|
|
|
10828
11135
|
}
|
|
10829
11136
|
return bar;
|
|
10830
11137
|
}
|
|
11138
|
+
function createEmptyMapState() {
|
|
11139
|
+
const empty = document.createElement("div");
|
|
11140
|
+
empty.className = "ofv-map-empty";
|
|
11141
|
+
const title = document.createElement("strong");
|
|
11142
|
+
title.textContent = "\u6682\u65E0\u53EF\u5C55\u793A\u7684\u5730\u56FE\u8981\u7D20";
|
|
11143
|
+
const detail = document.createElement("span");
|
|
11144
|
+
detail.textContent = "GeoJSON \u5DF2\u8BC6\u522B\uFF0C\u4F46 features \u4E3A\u7A7A\u3002";
|
|
11145
|
+
empty.append(title, detail);
|
|
11146
|
+
return empty;
|
|
11147
|
+
}
|
|
11148
|
+
function createMapLegend(summary) {
|
|
11149
|
+
const legend = document.createElement("div");
|
|
11150
|
+
legend.className = "ofv-map-legend";
|
|
11151
|
+
const title = document.createElement("strong");
|
|
11152
|
+
title.textContent = "GeoJSON";
|
|
11153
|
+
const detail = document.createElement("span");
|
|
11154
|
+
detail.textContent = `${summary.features} \u4E2A\u8981\u7D20 \xB7 ${formatGeometryCounts(summary.geometryCounts)}`;
|
|
11155
|
+
legend.append(title, detail);
|
|
11156
|
+
return legend;
|
|
11157
|
+
}
|
|
10831
11158
|
function appendSummaryItem(parent, label, value) {
|
|
10832
11159
|
const item = document.createElement("span");
|
|
10833
11160
|
const key = document.createElement("span");
|