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