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