@open-file-viewer/core 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.innerHTML = icon;
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
- const editorButton = document.createElement("button");
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(url);
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.1, Math.min(5, (size.width - 32) / meta.width * zoomFactor));
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(viewport.width);
4057
- canvas.height = Math.floor(viewport.height);
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 = `${Math.floor(viewport.width)}px`;
4075
- textLayer.style.height = `${Math.floor(viewport.height)}px`;
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.1, Math.min(5, (size.width - 32) / meta.width * zoomFactor));
4132
+ const scale = ctx.options.fit === "actual" ? zoomFactor : Math.max(0.05, Math.min(5, getPdfAvailableWidth(size.width) / meta.width * zoomFactor));
4137
4133
  const w = Math.floor(meta.width * scale);
4138
4134
  const h = Math.floor(meta.height * scale);
4139
4135
  const wrapper = document.createElement("div");
@@ -4188,6 +4184,7 @@ function pdfPlugin(options = {}) {
4188
4184
  }, 120);
4189
4185
  },
4190
4186
  destroy() {
4187
+ ctx.toolbar?.setZoom(void 0);
4191
4188
  window.clearTimeout(resizeTimer);
4192
4189
  observer?.disconnect();
4193
4190
  pageStates.forEach((state) => {
@@ -4206,6 +4203,19 @@ function pdfPlugin(options = {}) {
4206
4203
  }
4207
4204
  };
4208
4205
  }
4206
+ function getPdfOutputScale() {
4207
+ if (typeof window === "undefined") {
4208
+ return 1;
4209
+ }
4210
+ return Math.max(1, Math.min(window.devicePixelRatio || 1, 2.5));
4211
+ }
4212
+ function getPdfAvailableWidth(width) {
4213
+ if (!Number.isFinite(width) || width <= 0) {
4214
+ return 1;
4215
+ }
4216
+ const gutter = width < 160 ? 16 : 32;
4217
+ return Math.max(1, width - gutter);
4218
+ }
4209
4219
  function renderPdfSummary(summary, pages, pagesMeta, fit, zoomFactor) {
4210
4220
  summary.replaceChildren();
4211
4221
  appendPdfSummary(summary, "\u9875\u6570", String(pages));
@@ -4965,8 +4975,9 @@ function officePlugin() {
4965
4975
  ctx.viewport.append(panel);
4966
4976
  const extension = resolveFormat(ctx.file, officeMimeFormatMap);
4967
4977
  const arrayBuffer = await readArrayBuffer(ctx.file);
4978
+ let disposeDocxFit;
4968
4979
  if (fileIsDocx(extension)) {
4969
- await renderDocx(panel, arrayBuffer);
4980
+ disposeDocxFit = await renderDocx(panel, arrayBuffer);
4970
4981
  } else if (extension === "rtf") {
4971
4982
  renderPlainDocument(panel, "RTF \u6587\u6863", rtfToText(await readTextFromBuffer(arrayBuffer)));
4972
4983
  } else if (extension === "odt") {
@@ -4991,6 +5002,7 @@ function officePlugin() {
4991
5002
  }
4992
5003
  return {
4993
5004
  destroy() {
5005
+ disposeDocxFit?.();
4994
5006
  panel.remove();
4995
5007
  }
4996
5008
  };
@@ -5024,6 +5036,7 @@ async function renderDocx(panel, arrayBuffer) {
5024
5036
  experimental: true,
5025
5037
  useBase64URL: true
5026
5038
  });
5039
+ return fitDocxPages(content);
5027
5040
  } catch (error) {
5028
5041
  content.replaceChildren();
5029
5042
  const fallbackNote = document.createElement("div");
@@ -5038,6 +5051,74 @@ async function renderDocx(panel, arrayBuffer) {
5038
5051
  }
5039
5052
  console.warn("DOCX layout preview failed, fell back to Mammoth:", error);
5040
5053
  }
5054
+ return () => void 0;
5055
+ }
5056
+ function fitDocxPages(container) {
5057
+ const wrapper = container.querySelector(".ofv-docx-wrapper");
5058
+ if (!wrapper) {
5059
+ return () => void 0;
5060
+ }
5061
+ const update = () => {
5062
+ const frames = ensureDocxPageFrames(wrapper);
5063
+ if (frames.length === 0) {
5064
+ wrapper.style.removeProperty("--ofv-docx-scale");
5065
+ return;
5066
+ }
5067
+ const availableWidth = Math.max(1, container.clientWidth - 48);
5068
+ const pageWidth = Math.max(
5069
+ 1,
5070
+ ...frames.map(({ page }) => {
5071
+ const rectWidth = page.getBoundingClientRect().width;
5072
+ return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
5073
+ })
5074
+ );
5075
+ const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
5076
+ wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
5077
+ wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
5078
+ for (const { frame, page } of frames) {
5079
+ const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
5080
+ if (pageHeight > 0) {
5081
+ frame.style.height = `${Math.ceil(pageHeight * scale)}px`;
5082
+ }
5083
+ }
5084
+ };
5085
+ update();
5086
+ const timers = [0, 80, 240].map((delay) => window.setTimeout(update, delay));
5087
+ if (typeof ResizeObserver === "undefined") {
5088
+ window.addEventListener("resize", update);
5089
+ return () => {
5090
+ timers.forEach((timer) => window.clearTimeout(timer));
5091
+ window.removeEventListener("resize", update);
5092
+ };
5093
+ }
5094
+ const observer = new ResizeObserver(update);
5095
+ observer.observe(container);
5096
+ observer.observe(wrapper);
5097
+ return () => {
5098
+ timers.forEach((timer) => window.clearTimeout(timer));
5099
+ observer.disconnect();
5100
+ };
5101
+ }
5102
+ function ensureDocxPageFrames(wrapper) {
5103
+ const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
5104
+ return pages.map((page) => {
5105
+ const parent = page.parentElement;
5106
+ if (parent?.classList.contains("ofv-docx-page-frame")) {
5107
+ return { frame: parent, page };
5108
+ }
5109
+ const frame = document.createElement("div");
5110
+ frame.className = "ofv-docx-page-frame";
5111
+ page.before(frame);
5112
+ frame.append(page);
5113
+ return { frame, page };
5114
+ });
5115
+ }
5116
+ function parseCssPixelValue(value) {
5117
+ const parsed = Number.parseFloat(value);
5118
+ return Number.isFinite(parsed) ? parsed : 0;
5119
+ }
5120
+ function formatCssNumber(value) {
5121
+ return Number.isFinite(value) ? value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "") : "1";
5041
5122
  }
5042
5123
  async function renderDocxTextFallback(container, arrayBuffer) {
5043
5124
  const article = document.createElement("article");
@@ -5136,7 +5217,7 @@ async function renderSheet(panel, arrayBuffer, extension) {
5136
5217
  workbook = xlsx.read(arrayBuffer, { type: "array" });
5137
5218
  } catch (error) {
5138
5219
  if (isLegacyOfficeBinary(extension)) {
5139
- renderLegacyOfficeBinary(panel, extension, arrayBuffer, normalizeOfficeError(error));
5220
+ renderLegacyOfficeBinary(panel, extension, arrayBuffer, `\u8868\u683C\u89E3\u6790\u5931\u8D25\uFF1A${normalizeOfficeError(error)}`);
5140
5221
  return;
5141
5222
  }
5142
5223
  renderSheetFallback(panel, extension, normalizeOfficeError(error));
@@ -6241,14 +6322,15 @@ function legacyOfficeFormatLabel(extension) {
6241
6322
  function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6242
6323
  const fragments = extractLegacyOfficeText(arrayBuffer);
6243
6324
  panel.replaceChildren();
6244
- const section = createSection("Office \u4E8C\u8FDB\u5236\u57FA\u7840\u9884\u89C8");
6325
+ const section = createSection("Office \u8F6C\u6362\u63D0\u793A");
6326
+ section.classList.add("ofv-office-conversion");
6245
6327
  const format = document.createElement("p");
6246
6328
  const strong = document.createElement("strong");
6247
6329
  strong.textContent = `.${extension}`;
6248
6330
  format.append(
6249
6331
  strong,
6250
6332
  document.createTextNode(
6251
- " \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\u5148\u63D0\u53D6\u53EF\u8BFB\u6587\u672C\u7247\u6BB5\u548C\u7ED3\u6784\u6307\u7EB9\uFF0C\u5B8C\u6574\u7248\u5F0F\u5EFA\u8BAE\u63A5\u5165 LibreOffice/OnlyOffice \u670D\u52A1\u7AEF\u8F6C\u6362\u3002"
6333
+ " \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\u4EC5\u5C55\u793A\u53EF\u4FE1\u6587\u672C\u7247\u6BB5\u548C\u7ED3\u6784\u6307\u7EB9\uFF0C\u5B8C\u6574\u6392\u7248\u5EFA\u8BAE\u63A5\u5165 LibreOffice/OnlyOffice \u670D\u52A1\u7AEF\u8F6C\u6362\u4E3A PDF/HTML\u3002"
6252
6334
  )
6253
6335
  );
6254
6336
  const meta = document.createElement("dl");
@@ -6257,12 +6339,15 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6257
6339
  appendOfficeBinaryMeta(meta, "\u6587\u4EF6\u7ED3\u6784", hasOleSignature(arrayBuffer) ? "\u68C0\u6D4B\u5230 OLE Compound File \u7B7E\u540D" : "\u672A\u68C0\u6D4B\u5230\u6807\u51C6 OLE \u7B7E\u540D\uFF0C\u6309\u539F\u59CB\u4E8C\u8FDB\u5236\u5C1D\u8BD5\u63D0\u53D6");
6258
6340
  appendOfficeBinaryMeta(meta, "\u6587\u672C\u7247\u6BB5", `${fragments.length} \u6BB5`);
6259
6341
  if (parseError) {
6260
- appendOfficeBinaryMeta(meta, "\u8868\u683C\u89E3\u6790", `xlsx \u8BFB\u53D6\u5931\u8D25\uFF0C\u5DF2\u5207\u6362\u4E8C\u8FDB\u5236\u6307\u7EB9\uFF1A${parseError}`);
6342
+ appendOfficeBinaryMeta(meta, "\u89E3\u6790\u72B6\u6001", parseError);
6261
6343
  }
6262
6344
  section.append(format, meta);
6263
6345
  if (fragments.length > 0) {
6264
6346
  const article = document.createElement("article");
6265
- article.className = "ofv-document";
6347
+ article.className = "ofv-document ofv-office-binary-fragments";
6348
+ const heading = document.createElement("h4");
6349
+ heading.textContent = "\u53EF\u8BFB\u6587\u672C\u7247\u6BB5";
6350
+ article.append(heading);
6266
6351
  for (const fragment of fragments.slice(0, 80)) {
6267
6352
  const paragraph = document.createElement("p");
6268
6353
  paragraph.textContent = fragment;
@@ -6271,7 +6356,8 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6271
6356
  section.append(article);
6272
6357
  } else {
6273
6358
  const empty = document.createElement("p");
6274
- empty.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\u9700\u8981\u5B8C\u6574\u4E8C\u8FDB\u5236\u89E3\u6790\u5668\u3002";
6359
+ empty.className = "ofv-office-binary-empty";
6360
+ empty.textContent = "\u672A\u63D0\u53D6\u5230\u7A33\u5B9A\u53EF\u8BFB\u6587\u672C\u3002\u8BE5\u6587\u4EF6\u53EF\u80FD\u7ECF\u8FC7\u538B\u7F29\u3001\u52A0\u5BC6\uFF0C\u6216\u6587\u672C\u7F16\u7801\u65E0\u6CD5\u5728\u6D4F\u89C8\u5668\u7AEF\u53EF\u9760\u8BC6\u522B\uFF1B\u8BF7\u4F7F\u7528\u670D\u52A1\u7AEF LibreOffice/OnlyOffice \u8F6C\u6362\u540E\u9884\u89C8\u3002";
6275
6361
  section.append(empty);
6276
6362
  }
6277
6363
  panel.append(section);
@@ -6304,12 +6390,12 @@ function normalizeOfficeError(error) {
6304
6390
  function extractLegacyOfficeText(arrayBuffer) {
6305
6391
  const bytes = new Uint8Array(arrayBuffer);
6306
6392
  const fragments = [
6307
- ...extractPrintableRuns(bytes),
6308
- ...extractUtf16Runs(bytes)
6309
- ].map((fragment) => normalizeLegacyText(fragment)).filter((fragment) => fragment.length >= 3 && /[\p{L}\p{N}]/u.test(fragment));
6393
+ ...extractPrintableRuns(bytes).map((text) => ({ text, source: "ascii" })),
6394
+ ...extractUtf16Runs(bytes).map((text) => ({ text, source: "utf16" }))
6395
+ ].map(({ text, source }) => ({ text: normalizeLegacyText(text), source })).filter(({ text, source }) => isReadableLegacyTextFragment(text, source));
6310
6396
  const unique = [];
6311
6397
  const seen = /* @__PURE__ */ new Set();
6312
- for (const fragment of fragments) {
6398
+ for (const { text: fragment } of fragments) {
6313
6399
  const key = fragment.toLowerCase();
6314
6400
  if (!seen.has(key)) {
6315
6401
  seen.add(key);
@@ -6318,6 +6404,104 @@ function extractLegacyOfficeText(arrayBuffer) {
6318
6404
  }
6319
6405
  return unique.slice(0, 160);
6320
6406
  }
6407
+ function isReadableLegacyTextFragment(fragment, source) {
6408
+ if (fragment.length > 600) {
6409
+ return false;
6410
+ }
6411
+ if (isLegacyOfficeMetadataNoise(fragment)) {
6412
+ return false;
6413
+ }
6414
+ if (!/[\p{L}\p{N}]/u.test(fragment)) {
6415
+ return false;
6416
+ }
6417
+ const chars = Array.from(fragment);
6418
+ const letters = chars.filter((char) => /\p{L}/u.test(char)).length;
6419
+ const digits = chars.filter((char) => /\p{N}/u.test(char)).length;
6420
+ const spaces = chars.filter((char) => /\s/u.test(char)).length;
6421
+ const asciiLetters = chars.filter((char) => /[A-Za-z]/.test(char)).length;
6422
+ const cjkLetters = chars.filter((char) => /[\u3400-\u9fff]/u.test(char)).length;
6423
+ const punctuation = chars.filter((char) => /[^\p{L}\p{N}\s]/u.test(char)).length;
6424
+ const alphaNumeric = letters + digits;
6425
+ const readableRatio = alphaNumeric / chars.length;
6426
+ const punctuationRatio = punctuation / chars.length;
6427
+ if (fragment.length < 4 || readableRatio < 0.55 || punctuationRatio > 0.24) {
6428
+ return false;
6429
+ }
6430
+ if (/([\p{L}\p{N}])\1{4,}/u.test(fragment)) {
6431
+ return false;
6432
+ }
6433
+ if (cjkLetters >= 2) {
6434
+ const suspiciousCjk = chars.filter((char) => isAsciiBytePairCjk(char)).length;
6435
+ if (suspiciousCjk / cjkLetters > 0.65) {
6436
+ return false;
6437
+ }
6438
+ if (isLikelyCjkHeading(fragment)) {
6439
+ return true;
6440
+ }
6441
+ if (punctuation > 0 && fragment.length < 12) {
6442
+ return false;
6443
+ }
6444
+ return cjkLetters >= 8 || cjkLetters >= 4 && spaces > 0;
6445
+ }
6446
+ if (asciiLetters >= 4) {
6447
+ if (punctuation > 0 && spaces === 0) {
6448
+ return false;
6449
+ }
6450
+ if (source === "ascii" && /^[A-Z]{2,8}$/.test(fragment)) {
6451
+ return false;
6452
+ }
6453
+ if (spaces > 0) {
6454
+ return letters >= 3;
6455
+ }
6456
+ return fragment.length >= 6;
6457
+ }
6458
+ if (spaces > 0 && letters >= 3) {
6459
+ return true;
6460
+ }
6461
+ return false;
6462
+ }
6463
+ function isLegacyOfficeMetadataNoise(fragment) {
6464
+ if (/[$�\uFFFD]/u.test(fragment)) {
6465
+ return true;
6466
+ }
6467
+ if (/^(?:Root Entry|WordDocument|Workbook|Book|SummaryInformation|DocumentSummaryInformation|CompObj|ObjectPool|Data|PowerPoint Document|Pictures)$/i.test(fragment)) {
6468
+ return true;
6469
+ }
6470
+ if (/\.(dotm?|docm?|pptx?|ppsx?|xlsm?|xlsx?)\b/i.test(fragment)) {
6471
+ return true;
6472
+ }
6473
+ if (/^(?:默认段落字体|普通表格|正文|标题|副标题|目录|页眉|页脚|批注|超链接)(?:\s*\d+)?$/.test(fragment)) {
6474
+ return true;
6475
+ }
6476
+ if (/\b(?:Normal|Default|Calibri|Times New Roman|WPS Office|Microsoft Office|KSOP?ProductBuildVer)\b/i.test(fragment)) {
6477
+ return true;
6478
+ }
6479
+ if (/^\d+(?:Table|List|Heading|Title|Style)$/i.test(fragment)) {
6480
+ return true;
6481
+ }
6482
+ if (/[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[})]?/i.test(fragment)) {
6483
+ return true;
6484
+ }
6485
+ if (/^[A-Z_]{3,}$/.test(fragment) || /^[A-Za-z]+(?:Information|Document|Storage|Stream|Table|Data|Pool|Obj|Props)$/i.test(fragment)) {
6486
+ return true;
6487
+ }
6488
+ return false;
6489
+ }
6490
+ function isLikelyCjkHeading(fragment) {
6491
+ return /^(?:标题|第[一二三四五六七八九十\d]+[章节条]|[一二三四五六七八九十\d]+[、..])\s*[\p{L}\p{N}\s-]*$/u.test(fragment);
6492
+ }
6493
+ function isAsciiBytePairCjk(char) {
6494
+ const code = char.codePointAt(0) || 0;
6495
+ if (code < 13312 || code > 40959) {
6496
+ return false;
6497
+ }
6498
+ const low = code & 255;
6499
+ const high = code >> 8;
6500
+ return isPrintableAsciiByte(low) && isPrintableAsciiByte(high);
6501
+ }
6502
+ function isPrintableAsciiByte(value) {
6503
+ return value >= 32 && value <= 126;
6504
+ }
6321
6505
  function extractPrintableRuns(bytes) {
6322
6506
  const fragments = [];
6323
6507
  let current = "";
@@ -6340,6 +6524,13 @@ function extractUtf16Runs(bytes) {
6340
6524
  const fragments = [];
6341
6525
  let current = "";
6342
6526
  for (let index = 0; index < bytes.length - 1; index += 2) {
6527
+ if (looksLikeMisalignedAsciiUtf16(bytes[index], bytes[index + 1])) {
6528
+ if (current.length >= 3) {
6529
+ fragments.push(current);
6530
+ }
6531
+ current = "";
6532
+ continue;
6533
+ }
6343
6534
  const code = bytes[index] | bytes[index + 1] << 8;
6344
6535
  if (code >= 32 && code <= 55295 || code === 9) {
6345
6536
  current += String.fromCharCode(code);
@@ -6355,6 +6546,9 @@ function extractUtf16Runs(bytes) {
6355
6546
  }
6356
6547
  return fragments;
6357
6548
  }
6549
+ function looksLikeMisalignedAsciiUtf16(lowByte, highByte) {
6550
+ return lowByte === 0 && (highByte >= 48 && highByte <= 57 || highByte >= 65 && highByte <= 90 || highByte >= 97 && highByte <= 122);
6551
+ }
6358
6552
  function normalizeLegacyText(value) {
6359
6553
  return value.replace(/[\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim();
6360
6554
  }
@@ -6992,18 +7186,45 @@ function archivePlugin() {
6992
7186
  layout.className = "ofv-archive-layout";
6993
7187
  const sidebar = document.createElement("div");
6994
7188
  sidebar.className = "ofv-archive-sidebar";
7189
+ const sidebarPanel = document.createElement("div");
7190
+ sidebarPanel.className = "ofv-archive-sidebar-panel";
6995
7191
  const header = document.createElement("div");
6996
7192
  header.className = "ofv-archive-header";
6997
- header.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
6998
- sidebar.append(header);
7193
+ const sidebarTitle = document.createElement("span");
7194
+ sidebarTitle.className = "ofv-archive-header-title";
7195
+ sidebarTitle.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
7196
+ const sidebarToggle = document.createElement("button");
7197
+ sidebarToggle.className = "ofv-archive-sidebar-toggle";
7198
+ sidebarToggle.type = "button";
7199
+ sidebarToggle.setAttribute("aria-label", "\u5C55\u5F00\u6587\u4EF6\u5217\u8868");
7200
+ sidebarToggle.setAttribute("aria-expanded", "false");
7201
+ sidebarToggle.title = "\u5C55\u5F00\u6587\u4EF6\u5217\u8868";
7202
+ sidebarToggle.textContent = "\u2039";
7203
+ header.append(sidebarToggle, sidebarTitle);
7204
+ sidebarPanel.append(header);
6999
7205
  const tree = document.createElement("div");
7000
7206
  tree.className = "ofv-archive-tree";
7001
- sidebar.append(tree);
7207
+ sidebarPanel.append(tree);
7208
+ sidebar.append(sidebarPanel);
7002
7209
  const mainPanel = document.createElement("div");
7003
7210
  mainPanel.className = "ofv-archive-main";
7004
7211
  layout.append(sidebar, mainPanel);
7005
7212
  panel.append(layout);
7006
7213
  let currentSubInstance = null;
7214
+ const getSidebarViewportWidth = () => ctx.viewport.clientWidth || ctx.size.width;
7215
+ const shouldAutoCollapseSidebar = () => getSidebarViewportWidth() <= 520;
7216
+ const setSidebarCollapsed = (collapsed) => {
7217
+ layout.classList.toggle("is-sidebar-collapsed", collapsed);
7218
+ sidebarToggle.setAttribute("aria-expanded", String(!collapsed));
7219
+ const label = collapsed ? "\u5C55\u5F00\u6587\u4EF6\u5217\u8868" : "\u6536\u8D77\u6587\u4EF6\u5217\u8868";
7220
+ sidebarToggle.setAttribute("aria-label", label);
7221
+ sidebarToggle.title = label;
7222
+ sidebarToggle.textContent = collapsed ? "\u203A" : "\u2039";
7223
+ };
7224
+ setSidebarCollapsed(false);
7225
+ sidebarToggle.addEventListener("click", () => {
7226
+ setSidebarCollapsed(!layout.classList.contains("is-sidebar-collapsed"));
7227
+ });
7007
7228
  const showDefaultSummary = () => {
7008
7229
  mainPanel.replaceChildren();
7009
7230
  const summary = document.createElement("div");
@@ -7043,6 +7264,9 @@ function archivePlugin() {
7043
7264
  if (destroyed) {
7044
7265
  return;
7045
7266
  }
7267
+ if (shouldAutoCollapseSidebar()) {
7268
+ setSidebarCollapsed(true);
7269
+ }
7046
7270
  const token = ++renderToken;
7047
7271
  sidebar.querySelectorAll(".ofv-archive-item").forEach((el) => {
7048
7272
  el.classList.remove("is-active");
@@ -9325,7 +9549,7 @@ function cadPlugin() {
9325
9549
  return { destroy: () => panel.remove() };
9326
9550
  }
9327
9551
  const dxf = await readTextFile(ctx.file);
9328
- const viewer = renderDxf(panel, dxf);
9552
+ const viewer = renderDxf(panel, dxf, ctx);
9329
9553
  return {
9330
9554
  canCommand(command) {
9331
9555
  return viewer.canCommand(command);
@@ -9794,7 +10018,7 @@ function hexPreview(bytes) {
9794
10018
  }
9795
10019
  return rows.join("\n") || "\u65E0\u53EF\u5C55\u793A\u5B57\u8282\u3002";
9796
10020
  }
9797
- function renderDxf(panel, dxf) {
10021
+ function renderDxf(panel, dxf, ctx) {
9798
10022
  const pairs = dxf.split(/\r?\n/).map((line) => line.trim());
9799
10023
  const lines = [];
9800
10024
  const circles = [];
@@ -9972,6 +10196,10 @@ function renderDxf(panel, dxf) {
9972
10196
  note.textContent = `\u5DF2\u63D0\u53D6 LINE ${lines.length} \u4E2A\u3001CIRCLE ${circles.length} \u4E2A\u3001ARC ${arcs.length} \u4E2A\u3001POLYLINE ${polylines.length} \u4E2A\u3001POINT ${points.length} \u4E2A\u3001TEXT ${texts.length} \u4E2A\u3002`;
9973
10197
  section.append(note, svg);
9974
10198
  panel.append(section);
10199
+ const updateToolbarZoom = () => {
10200
+ ctx.toolbar?.setZoom(initialViewBox.width / currentViewBox.width);
10201
+ };
10202
+ updateToolbarZoom();
9975
10203
  return {
9976
10204
  canCommand(command) {
9977
10205
  return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset";
@@ -9986,14 +10214,19 @@ function renderDxf(panel, dxf) {
9986
10214
  currentViewBox.x = centerX - currentViewBox.width / 2;
9987
10215
  currentViewBox.y = centerY - currentViewBox.height / 2;
9988
10216
  applyViewBox();
10217
+ updateToolbarZoom();
9989
10218
  return true;
9990
10219
  }
9991
10220
  if (command === "zoom-reset") {
9992
10221
  currentViewBox = { ...initialViewBox };
9993
10222
  applyViewBox();
10223
+ updateToolbarZoom();
9994
10224
  return true;
9995
10225
  }
9996
10226
  return false;
10227
+ },
10228
+ destroy() {
10229
+ ctx.toolbar?.setZoom(void 0);
9997
10230
  }
9998
10231
  };
9999
10232
  }
@@ -10269,6 +10502,12 @@ function model3dPlugin() {
10269
10502
  renderer.setSize(width, height, false);
10270
10503
  };
10271
10504
  resize(ctx.size);
10505
+ const updateToolbarZoom = () => {
10506
+ const currentDistance = vectorDistance(camera.position, controls.target);
10507
+ const initialDistance = vectorDistance(initialFrame.cameraPosition, initialFrame.target);
10508
+ ctx.toolbar?.setZoom(initialDistance > 0 ? initialDistance / currentDistance : void 0);
10509
+ };
10510
+ updateToolbarZoom();
10272
10511
  return {
10273
10512
  canCommand(command) {
10274
10513
  return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset" || command === "rotate-right" || command === "rotate-left";
@@ -10279,6 +10518,7 @@ function model3dPlugin() {
10279
10518
  camera.position.sub(controls.target).multiplyScalar(factor).add(controls.target);
10280
10519
  camera.updateProjectionMatrix();
10281
10520
  controls.update();
10521
+ updateToolbarZoom();
10282
10522
  return true;
10283
10523
  }
10284
10524
  if (command === "zoom-reset") {
@@ -10288,6 +10528,7 @@ function model3dPlugin() {
10288
10528
  camera.far = initialFrame.far;
10289
10529
  camera.updateProjectionMatrix();
10290
10530
  controls.update();
10531
+ updateToolbarZoom();
10291
10532
  return true;
10292
10533
  }
10293
10534
  if (command === "rotate-right" || command === "rotate-left") {
@@ -10298,6 +10539,7 @@ function model3dPlugin() {
10298
10539
  },
10299
10540
  resize,
10300
10541
  destroy() {
10542
+ ctx.toolbar?.setZoom(void 0);
10301
10543
  window.cancelAnimationFrame(animationFrame);
10302
10544
  controls.dispose();
10303
10545
  renderer.dispose();
@@ -10309,6 +10551,12 @@ function model3dPlugin() {
10309
10551
  }
10310
10552
  };
10311
10553
  }
10554
+ function vectorDistance(a, b) {
10555
+ if (typeof a.distanceTo === "function") {
10556
+ return a.distanceTo(b);
10557
+ }
10558
+ return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
10559
+ }
10312
10560
  function renderModelFallback(ctx, url, isExternal, message) {
10313
10561
  const panel = document.createElement("div");
10314
10562
  panel.className = "ofv-fallback";
@@ -10665,34 +10913,65 @@ function gisPlugin() {
10665
10913
  }
10666
10914
  const wrapper = document.createElement("div");
10667
10915
  wrapper.className = "ofv-gis-viewer";
10668
- wrapper.append(createGisSummary(geojson));
10916
+ const summary = summarizeGeoJson(geojson);
10917
+ wrapper.append(createGisSummary(summary));
10669
10918
  ctx.viewport.appendChild(wrapper);
10670
10919
  const mapContainer = document.createElement("div");
10671
10920
  mapContainer.className = "ofv-map-stage";
10672
10921
  wrapper.appendChild(mapContainer);
10922
+ if (summary.features === 0) {
10923
+ mapContainer.append(createEmptyMapState());
10924
+ } else {
10925
+ mapContainer.append(createMapLegend(summary));
10926
+ }
10673
10927
  const map = Leaflet.map(mapContainer).setView([0, 0], 2);
10674
10928
  Leaflet.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
10675
10929
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
10676
10930
  }).addTo(map);
10677
10931
  const geojsonLayer = Leaflet.geoJSON(geojson, {
10678
10932
  style: () => ({
10679
- color: "#3b82f6",
10933
+ className: "ofv-map-feature",
10934
+ color: "#e11d48",
10680
10935
  weight: 2,
10681
- opacity: 0.8,
10682
- fillColor: "#93c5fd",
10683
- fillOpacity: 0.35
10936
+ opacity: 0.92,
10937
+ fillColor: "#fb923c",
10938
+ fillOpacity: 0.3,
10939
+ lineCap: "round",
10940
+ lineJoin: "round"
10684
10941
  }),
10685
10942
  pointToLayer: (feature, latlng) => {
10686
10943
  return Leaflet.circleMarker(latlng, {
10687
- radius: 6,
10688
- fillColor: "#3b82f6",
10944
+ className: "ofv-map-feature ofv-map-point",
10945
+ radius: 7,
10946
+ fillColor: "#e11d48",
10689
10947
  color: "#ffffff",
10690
- weight: 1.5,
10948
+ weight: 2.5,
10691
10949
  opacity: 1,
10692
- fillOpacity: 0.8
10950
+ fillOpacity: 0.9
10693
10951
  });
10694
10952
  },
10695
10953
  onEachFeature: (feature, layer) => {
10954
+ const label = feature.properties?.name || feature.properties?.title || feature.properties?.label;
10955
+ if (label) {
10956
+ layer.bindTooltip?.(String(label), {
10957
+ className: "ofv-map-tooltip",
10958
+ direction: "top",
10959
+ sticky: true
10960
+ });
10961
+ }
10962
+ layer.on?.({
10963
+ mouseover(event) {
10964
+ event.target?.setStyle?.({
10965
+ weight: 4,
10966
+ opacity: 1,
10967
+ fillOpacity: 0.4
10968
+ });
10969
+ event.target?.bringToFront?.();
10970
+ },
10971
+ mouseout(event) {
10972
+ geojsonLayer.resetStyle?.(event.target);
10973
+ }
10974
+ });
10696
10975
  if (feature.properties) {
10697
10976
  const props = feature.properties;
10698
10977
  const keys = Object.keys(props);
@@ -10727,15 +11006,20 @@ function gisPlugin() {
10727
11006
  const bounds = geojsonLayer.getBounds();
10728
11007
  if (bounds.isValid()) {
10729
11008
  map.fitBounds(bounds, { padding: [20, 20] });
11009
+ map.invalidateSize();
10730
11010
  }
10731
11011
  } catch (e) {
10732
11012
  console.warn("Could not fit bounds for GeoJSON data:", e);
10733
11013
  }
11014
+ const resizeTimers = [0, 80, 240].map((delay) => window.setTimeout(() => {
11015
+ map.invalidateSize();
11016
+ }, delay));
10734
11017
  return {
10735
11018
  resize() {
10736
11019
  map.invalidateSize();
10737
11020
  },
10738
11021
  destroy() {
11022
+ resizeTimers.forEach((timer) => window.clearTimeout(timer));
10739
11023
  map.remove();
10740
11024
  wrapper.remove();
10741
11025
  }
@@ -10760,8 +11044,7 @@ function normalizeGisError(error, fileName) {
10760
11044
  function isGisFallback(value) {
10761
11045
  return typeof value === "object" && value !== null && "fallback" in value;
10762
11046
  }
10763
- function createGisSummary(geojson) {
10764
- const summary = summarizeGeoJson(geojson);
11047
+ function createGisSummary(summary) {
10765
11048
  const bar = document.createElement("div");
10766
11049
  bar.className = "ofv-gis-summary";
10767
11050
  appendSummaryItem(bar, "\u8981\u7D20", String(summary.features));
@@ -10775,6 +11058,26 @@ function createGisSummary(geojson) {
10775
11058
  }
10776
11059
  return bar;
10777
11060
  }
11061
+ function createEmptyMapState() {
11062
+ const empty = document.createElement("div");
11063
+ empty.className = "ofv-map-empty";
11064
+ const title = document.createElement("strong");
11065
+ title.textContent = "\u6682\u65E0\u53EF\u5C55\u793A\u7684\u5730\u56FE\u8981\u7D20";
11066
+ const detail = document.createElement("span");
11067
+ detail.textContent = "GeoJSON \u5DF2\u8BC6\u522B\uFF0C\u4F46 features \u4E3A\u7A7A\u3002";
11068
+ empty.append(title, detail);
11069
+ return empty;
11070
+ }
11071
+ function createMapLegend(summary) {
11072
+ const legend = document.createElement("div");
11073
+ legend.className = "ofv-map-legend";
11074
+ const title = document.createElement("strong");
11075
+ title.textContent = "GeoJSON";
11076
+ const detail = document.createElement("span");
11077
+ detail.textContent = `${summary.features} \u4E2A\u8981\u7D20 \xB7 ${formatGeometryCounts(summary.geometryCounts)}`;
11078
+ legend.append(title, detail);
11079
+ return legend;
11080
+ }
10778
11081
  function appendSummaryItem(parent, label, value) {
10779
11082
  const item = document.createElement("span");
10780
11083
  const key = document.createElement("span");