@open-file-viewer/core 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,8 @@ async function renderDocx(panel, arrayBuffer) {
5024
5036
  experimental: true,
5025
5037
  useBase64URL: true
5026
5038
  });
5039
+ normalizeDocxLayout(content);
5040
+ return fitDocxPages(content);
5027
5041
  } catch (error) {
5028
5042
  content.replaceChildren();
5029
5043
  const fallbackNote = document.createElement("div");
@@ -5038,6 +5052,97 @@ async function renderDocx(panel, arrayBuffer) {
5038
5052
  }
5039
5053
  console.warn("DOCX layout preview failed, fell back to Mammoth:", error);
5040
5054
  }
5055
+ return () => void 0;
5056
+ }
5057
+ function normalizeDocxLayout(container) {
5058
+ const pages = container.querySelectorAll("section.ofv-docx");
5059
+ for (const page of pages) {
5060
+ for (const element of page.querySelectorAll("[style*='line-height']")) {
5061
+ const lineHeight = parseCssLineHeight(element.style.lineHeight);
5062
+ if (lineHeight > 0 && lineHeight < 1) {
5063
+ element.style.lineHeight = "1.2";
5064
+ }
5065
+ }
5066
+ }
5067
+ }
5068
+ function parseCssLineHeight(value) {
5069
+ const trimmed = value.trim();
5070
+ if (!trimmed || trimmed === "normal") {
5071
+ return 0;
5072
+ }
5073
+ if (trimmed.endsWith("%")) {
5074
+ const parsedPercent = Number.parseFloat(trimmed);
5075
+ return Number.isFinite(parsedPercent) ? parsedPercent / 100 : 0;
5076
+ }
5077
+ const parsed = Number.parseFloat(trimmed);
5078
+ return Number.isFinite(parsed) ? parsed : 0;
5079
+ }
5080
+ function fitDocxPages(container) {
5081
+ const wrapper = container.querySelector(".ofv-docx-wrapper");
5082
+ if (!wrapper) {
5083
+ return () => void 0;
5084
+ }
5085
+ const update = () => {
5086
+ const frames = ensureDocxPageFrames(wrapper);
5087
+ if (frames.length === 0) {
5088
+ wrapper.style.removeProperty("--ofv-docx-scale");
5089
+ return;
5090
+ }
5091
+ const availableWidth = Math.max(1, container.clientWidth - 48);
5092
+ const pageWidth = Math.max(
5093
+ 1,
5094
+ ...frames.map(({ page }) => {
5095
+ const rectWidth = page.getBoundingClientRect().width;
5096
+ return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
5097
+ })
5098
+ );
5099
+ const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
5100
+ wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
5101
+ wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
5102
+ for (const { frame, page } of frames) {
5103
+ const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
5104
+ if (pageHeight > 0) {
5105
+ frame.style.height = `${Math.ceil(pageHeight * scale)}px`;
5106
+ }
5107
+ }
5108
+ };
5109
+ update();
5110
+ const timers = [0, 80, 240].map((delay) => window.setTimeout(update, delay));
5111
+ if (typeof ResizeObserver === "undefined") {
5112
+ window.addEventListener("resize", update);
5113
+ return () => {
5114
+ timers.forEach((timer) => window.clearTimeout(timer));
5115
+ window.removeEventListener("resize", update);
5116
+ };
5117
+ }
5118
+ const observer = new ResizeObserver(update);
5119
+ observer.observe(container);
5120
+ observer.observe(wrapper);
5121
+ return () => {
5122
+ timers.forEach((timer) => window.clearTimeout(timer));
5123
+ observer.disconnect();
5124
+ };
5125
+ }
5126
+ function ensureDocxPageFrames(wrapper) {
5127
+ const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
5128
+ return pages.map((page) => {
5129
+ const parent = page.parentElement;
5130
+ if (parent?.classList.contains("ofv-docx-page-frame")) {
5131
+ return { frame: parent, page };
5132
+ }
5133
+ const frame = document.createElement("div");
5134
+ frame.className = "ofv-docx-page-frame";
5135
+ page.before(frame);
5136
+ frame.append(page);
5137
+ return { frame, page };
5138
+ });
5139
+ }
5140
+ function parseCssPixelValue(value) {
5141
+ const parsed = Number.parseFloat(value);
5142
+ return Number.isFinite(parsed) ? parsed : 0;
5143
+ }
5144
+ function formatCssNumber(value) {
5145
+ return Number.isFinite(value) ? value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "") : "1";
5041
5146
  }
5042
5147
  async function renderDocxTextFallback(container, arrayBuffer) {
5043
5148
  const article = document.createElement("article");
@@ -5136,7 +5241,7 @@ async function renderSheet(panel, arrayBuffer, extension) {
5136
5241
  workbook = xlsx.read(arrayBuffer, { type: "array" });
5137
5242
  } catch (error) {
5138
5243
  if (isLegacyOfficeBinary(extension)) {
5139
- renderLegacyOfficeBinary(panel, extension, arrayBuffer, normalizeOfficeError(error));
5244
+ renderLegacyOfficeBinary(panel, extension, arrayBuffer, `\u8868\u683C\u89E3\u6790\u5931\u8D25\uFF1A${normalizeOfficeError(error)}`);
5140
5245
  return;
5141
5246
  }
5142
5247
  renderSheetFallback(panel, extension, normalizeOfficeError(error));
@@ -6241,14 +6346,15 @@ function legacyOfficeFormatLabel(extension) {
6241
6346
  function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6242
6347
  const fragments = extractLegacyOfficeText(arrayBuffer);
6243
6348
  panel.replaceChildren();
6244
- const section = createSection("Office \u4E8C\u8FDB\u5236\u57FA\u7840\u9884\u89C8");
6349
+ const section = createSection("Office \u8F6C\u6362\u63D0\u793A");
6350
+ section.classList.add("ofv-office-conversion");
6245
6351
  const format = document.createElement("p");
6246
6352
  const strong = document.createElement("strong");
6247
6353
  strong.textContent = `.${extension}`;
6248
6354
  format.append(
6249
6355
  strong,
6250
6356
  document.createTextNode(
6251
- " \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\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"
6357
+ " \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\u4EC5\u5C55\u793A\u53EF\u4FE1\u6587\u672C\u7247\u6BB5\u548C\u7ED3\u6784\u6307\u7EB9\uFF0C\u5B8C\u6574\u6392\u7248\u5EFA\u8BAE\u63A5\u5165 LibreOffice/OnlyOffice \u670D\u52A1\u7AEF\u8F6C\u6362\u4E3A PDF/HTML\u3002"
6252
6358
  )
6253
6359
  );
6254
6360
  const meta = document.createElement("dl");
@@ -6257,12 +6363,15 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6257
6363
  appendOfficeBinaryMeta(meta, "\u6587\u4EF6\u7ED3\u6784", hasOleSignature(arrayBuffer) ? "\u68C0\u6D4B\u5230 OLE Compound File \u7B7E\u540D" : "\u672A\u68C0\u6D4B\u5230\u6807\u51C6 OLE \u7B7E\u540D\uFF0C\u6309\u539F\u59CB\u4E8C\u8FDB\u5236\u5C1D\u8BD5\u63D0\u53D6");
6258
6364
  appendOfficeBinaryMeta(meta, "\u6587\u672C\u7247\u6BB5", `${fragments.length} \u6BB5`);
6259
6365
  if (parseError) {
6260
- appendOfficeBinaryMeta(meta, "\u8868\u683C\u89E3\u6790", `xlsx \u8BFB\u53D6\u5931\u8D25\uFF0C\u5DF2\u5207\u6362\u4E8C\u8FDB\u5236\u6307\u7EB9\uFF1A${parseError}`);
6366
+ appendOfficeBinaryMeta(meta, "\u89E3\u6790\u72B6\u6001", parseError);
6261
6367
  }
6262
6368
  section.append(format, meta);
6263
6369
  if (fragments.length > 0) {
6264
6370
  const article = document.createElement("article");
6265
- article.className = "ofv-document";
6371
+ article.className = "ofv-document ofv-office-binary-fragments";
6372
+ const heading = document.createElement("h4");
6373
+ heading.textContent = "\u53EF\u8BFB\u6587\u672C\u7247\u6BB5";
6374
+ article.append(heading);
6266
6375
  for (const fragment of fragments.slice(0, 80)) {
6267
6376
  const paragraph = document.createElement("p");
6268
6377
  paragraph.textContent = fragment;
@@ -6271,7 +6380,8 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6271
6380
  section.append(article);
6272
6381
  } else {
6273
6382
  const empty = document.createElement("p");
6274
- empty.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";
6383
+ empty.className = "ofv-office-binary-empty";
6384
+ empty.textContent = "\u672A\u63D0\u53D6\u5230\u7A33\u5B9A\u53EF\u8BFB\u6587\u672C\u3002\u8BE5\u6587\u4EF6\u53EF\u80FD\u7ECF\u8FC7\u538B\u7F29\u3001\u52A0\u5BC6\uFF0C\u6216\u6587\u672C\u7F16\u7801\u65E0\u6CD5\u5728\u6D4F\u89C8\u5668\u7AEF\u53EF\u9760\u8BC6\u522B\uFF1B\u8BF7\u4F7F\u7528\u670D\u52A1\u7AEF LibreOffice/OnlyOffice \u8F6C\u6362\u540E\u9884\u89C8\u3002";
6275
6385
  section.append(empty);
6276
6386
  }
6277
6387
  panel.append(section);
@@ -6304,12 +6414,12 @@ function normalizeOfficeError(error) {
6304
6414
  function extractLegacyOfficeText(arrayBuffer) {
6305
6415
  const bytes = new Uint8Array(arrayBuffer);
6306
6416
  const fragments = [
6307
- ...extractPrintableRuns(bytes),
6308
- ...extractUtf16Runs(bytes)
6309
- ].map((fragment) => normalizeLegacyText(fragment)).filter((fragment) => fragment.length >= 3 && /[\p{L}\p{N}]/u.test(fragment));
6417
+ ...extractPrintableRuns(bytes).map((text) => ({ text, source: "ascii" })),
6418
+ ...extractUtf16Runs(bytes).map((text) => ({ text, source: "utf16" }))
6419
+ ].map(({ text, source }) => ({ text: normalizeLegacyText(text), source })).filter(({ text, source }) => isReadableLegacyTextFragment(text, source));
6310
6420
  const unique = [];
6311
6421
  const seen = /* @__PURE__ */ new Set();
6312
- for (const fragment of fragments) {
6422
+ for (const { text: fragment } of fragments) {
6313
6423
  const key = fragment.toLowerCase();
6314
6424
  if (!seen.has(key)) {
6315
6425
  seen.add(key);
@@ -6318,6 +6428,104 @@ function extractLegacyOfficeText(arrayBuffer) {
6318
6428
  }
6319
6429
  return unique.slice(0, 160);
6320
6430
  }
6431
+ function isReadableLegacyTextFragment(fragment, source) {
6432
+ if (fragment.length > 600) {
6433
+ return false;
6434
+ }
6435
+ if (isLegacyOfficeMetadataNoise(fragment)) {
6436
+ return false;
6437
+ }
6438
+ if (!/[\p{L}\p{N}]/u.test(fragment)) {
6439
+ return false;
6440
+ }
6441
+ const chars = Array.from(fragment);
6442
+ const letters = chars.filter((char) => /\p{L}/u.test(char)).length;
6443
+ const digits = chars.filter((char) => /\p{N}/u.test(char)).length;
6444
+ const spaces = chars.filter((char) => /\s/u.test(char)).length;
6445
+ const asciiLetters = chars.filter((char) => /[A-Za-z]/.test(char)).length;
6446
+ const cjkLetters = chars.filter((char) => /[\u3400-\u9fff]/u.test(char)).length;
6447
+ const punctuation = chars.filter((char) => /[^\p{L}\p{N}\s]/u.test(char)).length;
6448
+ const alphaNumeric = letters + digits;
6449
+ const readableRatio = alphaNumeric / chars.length;
6450
+ const punctuationRatio = punctuation / chars.length;
6451
+ if (fragment.length < 4 || readableRatio < 0.55 || punctuationRatio > 0.24) {
6452
+ return false;
6453
+ }
6454
+ if (/([\p{L}\p{N}])\1{4,}/u.test(fragment)) {
6455
+ return false;
6456
+ }
6457
+ if (cjkLetters >= 2) {
6458
+ const suspiciousCjk = chars.filter((char) => isAsciiBytePairCjk(char)).length;
6459
+ if (suspiciousCjk / cjkLetters > 0.65) {
6460
+ return false;
6461
+ }
6462
+ if (isLikelyCjkHeading(fragment)) {
6463
+ return true;
6464
+ }
6465
+ if (punctuation > 0 && fragment.length < 12) {
6466
+ return false;
6467
+ }
6468
+ return cjkLetters >= 8 || cjkLetters >= 4 && spaces > 0;
6469
+ }
6470
+ if (asciiLetters >= 4) {
6471
+ if (punctuation > 0 && spaces === 0) {
6472
+ return false;
6473
+ }
6474
+ if (source === "ascii" && /^[A-Z]{2,8}$/.test(fragment)) {
6475
+ return false;
6476
+ }
6477
+ if (spaces > 0) {
6478
+ return letters >= 3;
6479
+ }
6480
+ return fragment.length >= 6;
6481
+ }
6482
+ if (spaces > 0 && letters >= 3) {
6483
+ return true;
6484
+ }
6485
+ return false;
6486
+ }
6487
+ function isLegacyOfficeMetadataNoise(fragment) {
6488
+ if (/[$�\uFFFD]/u.test(fragment)) {
6489
+ return true;
6490
+ }
6491
+ if (/^(?:Root Entry|WordDocument|Workbook|Book|SummaryInformation|DocumentSummaryInformation|CompObj|ObjectPool|Data|PowerPoint Document|Pictures)$/i.test(fragment)) {
6492
+ return true;
6493
+ }
6494
+ if (/\.(dotm?|docm?|pptx?|ppsx?|xlsm?|xlsx?)\b/i.test(fragment)) {
6495
+ return true;
6496
+ }
6497
+ if (/^(?:默认段落字体|普通表格|正文|标题|副标题|目录|页眉|页脚|批注|超链接)(?:\s*\d+)?$/.test(fragment)) {
6498
+ return true;
6499
+ }
6500
+ if (/\b(?:Normal|Default|Calibri|Times New Roman|WPS Office|Microsoft Office|KSOP?ProductBuildVer)\b/i.test(fragment)) {
6501
+ return true;
6502
+ }
6503
+ if (/^\d+(?:Table|List|Heading|Title|Style)$/i.test(fragment)) {
6504
+ return true;
6505
+ }
6506
+ if (/[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[})]?/i.test(fragment)) {
6507
+ return true;
6508
+ }
6509
+ if (/^[A-Z_]{3,}$/.test(fragment) || /^[A-Za-z]+(?:Information|Document|Storage|Stream|Table|Data|Pool|Obj|Props)$/i.test(fragment)) {
6510
+ return true;
6511
+ }
6512
+ return false;
6513
+ }
6514
+ function isLikelyCjkHeading(fragment) {
6515
+ return /^(?:标题|第[一二三四五六七八九十\d]+[章节条]|[一二三四五六七八九十\d]+[、..])\s*[\p{L}\p{N}\s-]*$/u.test(fragment);
6516
+ }
6517
+ function isAsciiBytePairCjk(char) {
6518
+ const code = char.codePointAt(0) || 0;
6519
+ if (code < 13312 || code > 40959) {
6520
+ return false;
6521
+ }
6522
+ const low = code & 255;
6523
+ const high = code >> 8;
6524
+ return isPrintableAsciiByte(low) && isPrintableAsciiByte(high);
6525
+ }
6526
+ function isPrintableAsciiByte(value) {
6527
+ return value >= 32 && value <= 126;
6528
+ }
6321
6529
  function extractPrintableRuns(bytes) {
6322
6530
  const fragments = [];
6323
6531
  let current = "";
@@ -6340,6 +6548,13 @@ function extractUtf16Runs(bytes) {
6340
6548
  const fragments = [];
6341
6549
  let current = "";
6342
6550
  for (let index = 0; index < bytes.length - 1; index += 2) {
6551
+ if (looksLikeMisalignedAsciiUtf16(bytes[index], bytes[index + 1])) {
6552
+ if (current.length >= 3) {
6553
+ fragments.push(current);
6554
+ }
6555
+ current = "";
6556
+ continue;
6557
+ }
6343
6558
  const code = bytes[index] | bytes[index + 1] << 8;
6344
6559
  if (code >= 32 && code <= 55295 || code === 9) {
6345
6560
  current += String.fromCharCode(code);
@@ -6355,6 +6570,9 @@ function extractUtf16Runs(bytes) {
6355
6570
  }
6356
6571
  return fragments;
6357
6572
  }
6573
+ function looksLikeMisalignedAsciiUtf16(lowByte, highByte) {
6574
+ return lowByte === 0 && (highByte >= 48 && highByte <= 57 || highByte >= 65 && highByte <= 90 || highByte >= 97 && highByte <= 122);
6575
+ }
6358
6576
  function normalizeLegacyText(value) {
6359
6577
  return value.replace(/[\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim();
6360
6578
  }
@@ -6992,18 +7210,45 @@ function archivePlugin() {
6992
7210
  layout.className = "ofv-archive-layout";
6993
7211
  const sidebar = document.createElement("div");
6994
7212
  sidebar.className = "ofv-archive-sidebar";
7213
+ const sidebarPanel = document.createElement("div");
7214
+ sidebarPanel.className = "ofv-archive-sidebar-panel";
6995
7215
  const header = document.createElement("div");
6996
7216
  header.className = "ofv-archive-header";
6997
- header.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
6998
- sidebar.append(header);
7217
+ const sidebarTitle = document.createElement("span");
7218
+ sidebarTitle.className = "ofv-archive-header-title";
7219
+ sidebarTitle.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
7220
+ const sidebarToggle = document.createElement("button");
7221
+ sidebarToggle.className = "ofv-archive-sidebar-toggle";
7222
+ sidebarToggle.type = "button";
7223
+ sidebarToggle.setAttribute("aria-label", "\u5C55\u5F00\u6587\u4EF6\u5217\u8868");
7224
+ sidebarToggle.setAttribute("aria-expanded", "false");
7225
+ sidebarToggle.title = "\u5C55\u5F00\u6587\u4EF6\u5217\u8868";
7226
+ sidebarToggle.textContent = "\u2039";
7227
+ header.append(sidebarToggle, sidebarTitle);
7228
+ sidebarPanel.append(header);
6999
7229
  const tree = document.createElement("div");
7000
7230
  tree.className = "ofv-archive-tree";
7001
- sidebar.append(tree);
7231
+ sidebarPanel.append(tree);
7232
+ sidebar.append(sidebarPanel);
7002
7233
  const mainPanel = document.createElement("div");
7003
7234
  mainPanel.className = "ofv-archive-main";
7004
7235
  layout.append(sidebar, mainPanel);
7005
7236
  panel.append(layout);
7006
7237
  let currentSubInstance = null;
7238
+ const getSidebarViewportWidth = () => ctx.viewport.clientWidth || ctx.size.width;
7239
+ const shouldAutoCollapseSidebar = () => getSidebarViewportWidth() <= 520;
7240
+ const setSidebarCollapsed = (collapsed) => {
7241
+ layout.classList.toggle("is-sidebar-collapsed", collapsed);
7242
+ sidebarToggle.setAttribute("aria-expanded", String(!collapsed));
7243
+ const label = collapsed ? "\u5C55\u5F00\u6587\u4EF6\u5217\u8868" : "\u6536\u8D77\u6587\u4EF6\u5217\u8868";
7244
+ sidebarToggle.setAttribute("aria-label", label);
7245
+ sidebarToggle.title = label;
7246
+ sidebarToggle.textContent = collapsed ? "\u203A" : "\u2039";
7247
+ };
7248
+ setSidebarCollapsed(false);
7249
+ sidebarToggle.addEventListener("click", () => {
7250
+ setSidebarCollapsed(!layout.classList.contains("is-sidebar-collapsed"));
7251
+ });
7007
7252
  const showDefaultSummary = () => {
7008
7253
  mainPanel.replaceChildren();
7009
7254
  const summary = document.createElement("div");
@@ -7043,6 +7288,9 @@ function archivePlugin() {
7043
7288
  if (destroyed) {
7044
7289
  return;
7045
7290
  }
7291
+ if (shouldAutoCollapseSidebar()) {
7292
+ setSidebarCollapsed(true);
7293
+ }
7046
7294
  const token = ++renderToken;
7047
7295
  sidebar.querySelectorAll(".ofv-archive-item").forEach((el) => {
7048
7296
  el.classList.remove("is-active");
@@ -9325,7 +9573,7 @@ function cadPlugin() {
9325
9573
  return { destroy: () => panel.remove() };
9326
9574
  }
9327
9575
  const dxf = await readTextFile(ctx.file);
9328
- const viewer = renderDxf(panel, dxf);
9576
+ const viewer = renderDxf(panel, dxf, ctx);
9329
9577
  return {
9330
9578
  canCommand(command) {
9331
9579
  return viewer.canCommand(command);
@@ -9794,7 +10042,7 @@ function hexPreview(bytes) {
9794
10042
  }
9795
10043
  return rows.join("\n") || "\u65E0\u53EF\u5C55\u793A\u5B57\u8282\u3002";
9796
10044
  }
9797
- function renderDxf(panel, dxf) {
10045
+ function renderDxf(panel, dxf, ctx) {
9798
10046
  const pairs = dxf.split(/\r?\n/).map((line) => line.trim());
9799
10047
  const lines = [];
9800
10048
  const circles = [];
@@ -9972,6 +10220,10 @@ function renderDxf(panel, dxf) {
9972
10220
  note.textContent = `\u5DF2\u63D0\u53D6 LINE ${lines.length} \u4E2A\u3001CIRCLE ${circles.length} \u4E2A\u3001ARC ${arcs.length} \u4E2A\u3001POLYLINE ${polylines.length} \u4E2A\u3001POINT ${points.length} \u4E2A\u3001TEXT ${texts.length} \u4E2A\u3002`;
9973
10221
  section.append(note, svg);
9974
10222
  panel.append(section);
10223
+ const updateToolbarZoom = () => {
10224
+ ctx.toolbar?.setZoom(initialViewBox.width / currentViewBox.width);
10225
+ };
10226
+ updateToolbarZoom();
9975
10227
  return {
9976
10228
  canCommand(command) {
9977
10229
  return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset";
@@ -9986,14 +10238,19 @@ function renderDxf(panel, dxf) {
9986
10238
  currentViewBox.x = centerX - currentViewBox.width / 2;
9987
10239
  currentViewBox.y = centerY - currentViewBox.height / 2;
9988
10240
  applyViewBox();
10241
+ updateToolbarZoom();
9989
10242
  return true;
9990
10243
  }
9991
10244
  if (command === "zoom-reset") {
9992
10245
  currentViewBox = { ...initialViewBox };
9993
10246
  applyViewBox();
10247
+ updateToolbarZoom();
9994
10248
  return true;
9995
10249
  }
9996
10250
  return false;
10251
+ },
10252
+ destroy() {
10253
+ ctx.toolbar?.setZoom(void 0);
9997
10254
  }
9998
10255
  };
9999
10256
  }
@@ -10269,6 +10526,12 @@ function model3dPlugin() {
10269
10526
  renderer.setSize(width, height, false);
10270
10527
  };
10271
10528
  resize(ctx.size);
10529
+ const updateToolbarZoom = () => {
10530
+ const currentDistance = vectorDistance(camera.position, controls.target);
10531
+ const initialDistance = vectorDistance(initialFrame.cameraPosition, initialFrame.target);
10532
+ ctx.toolbar?.setZoom(initialDistance > 0 ? initialDistance / currentDistance : void 0);
10533
+ };
10534
+ updateToolbarZoom();
10272
10535
  return {
10273
10536
  canCommand(command) {
10274
10537
  return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset" || command === "rotate-right" || command === "rotate-left";
@@ -10279,6 +10542,7 @@ function model3dPlugin() {
10279
10542
  camera.position.sub(controls.target).multiplyScalar(factor).add(controls.target);
10280
10543
  camera.updateProjectionMatrix();
10281
10544
  controls.update();
10545
+ updateToolbarZoom();
10282
10546
  return true;
10283
10547
  }
10284
10548
  if (command === "zoom-reset") {
@@ -10288,6 +10552,7 @@ function model3dPlugin() {
10288
10552
  camera.far = initialFrame.far;
10289
10553
  camera.updateProjectionMatrix();
10290
10554
  controls.update();
10555
+ updateToolbarZoom();
10291
10556
  return true;
10292
10557
  }
10293
10558
  if (command === "rotate-right" || command === "rotate-left") {
@@ -10298,6 +10563,7 @@ function model3dPlugin() {
10298
10563
  },
10299
10564
  resize,
10300
10565
  destroy() {
10566
+ ctx.toolbar?.setZoom(void 0);
10301
10567
  window.cancelAnimationFrame(animationFrame);
10302
10568
  controls.dispose();
10303
10569
  renderer.dispose();
@@ -10309,6 +10575,12 @@ function model3dPlugin() {
10309
10575
  }
10310
10576
  };
10311
10577
  }
10578
+ function vectorDistance(a, b) {
10579
+ if (typeof a.distanceTo === "function") {
10580
+ return a.distanceTo(b);
10581
+ }
10582
+ return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
10583
+ }
10312
10584
  function renderModelFallback(ctx, url, isExternal, message) {
10313
10585
  const panel = document.createElement("div");
10314
10586
  panel.className = "ofv-fallback";
@@ -10665,34 +10937,65 @@ function gisPlugin() {
10665
10937
  }
10666
10938
  const wrapper = document.createElement("div");
10667
10939
  wrapper.className = "ofv-gis-viewer";
10668
- wrapper.append(createGisSummary(geojson));
10940
+ const summary = summarizeGeoJson(geojson);
10941
+ wrapper.append(createGisSummary(summary));
10669
10942
  ctx.viewport.appendChild(wrapper);
10670
10943
  const mapContainer = document.createElement("div");
10671
10944
  mapContainer.className = "ofv-map-stage";
10672
10945
  wrapper.appendChild(mapContainer);
10946
+ if (summary.features === 0) {
10947
+ mapContainer.append(createEmptyMapState());
10948
+ } else {
10949
+ mapContainer.append(createMapLegend(summary));
10950
+ }
10673
10951
  const map = Leaflet.map(mapContainer).setView([0, 0], 2);
10674
10952
  Leaflet.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
10675
10953
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
10676
10954
  }).addTo(map);
10677
10955
  const geojsonLayer = Leaflet.geoJSON(geojson, {
10678
10956
  style: () => ({
10679
- color: "#3b82f6",
10957
+ className: "ofv-map-feature",
10958
+ color: "#e11d48",
10680
10959
  weight: 2,
10681
- opacity: 0.8,
10682
- fillColor: "#93c5fd",
10683
- fillOpacity: 0.35
10960
+ opacity: 0.92,
10961
+ fillColor: "#fb923c",
10962
+ fillOpacity: 0.3,
10963
+ lineCap: "round",
10964
+ lineJoin: "round"
10684
10965
  }),
10685
10966
  pointToLayer: (feature, latlng) => {
10686
10967
  return Leaflet.circleMarker(latlng, {
10687
- radius: 6,
10688
- fillColor: "#3b82f6",
10968
+ className: "ofv-map-feature ofv-map-point",
10969
+ radius: 7,
10970
+ fillColor: "#e11d48",
10689
10971
  color: "#ffffff",
10690
- weight: 1.5,
10972
+ weight: 2.5,
10691
10973
  opacity: 1,
10692
- fillOpacity: 0.8
10974
+ fillOpacity: 0.9
10693
10975
  });
10694
10976
  },
10695
10977
  onEachFeature: (feature, layer) => {
10978
+ const label = feature.properties?.name || feature.properties?.title || feature.properties?.label;
10979
+ if (label) {
10980
+ layer.bindTooltip?.(String(label), {
10981
+ className: "ofv-map-tooltip",
10982
+ direction: "top",
10983
+ sticky: true
10984
+ });
10985
+ }
10986
+ layer.on?.({
10987
+ mouseover(event) {
10988
+ event.target?.setStyle?.({
10989
+ weight: 4,
10990
+ opacity: 1,
10991
+ fillOpacity: 0.4
10992
+ });
10993
+ event.target?.bringToFront?.();
10994
+ },
10995
+ mouseout(event) {
10996
+ geojsonLayer.resetStyle?.(event.target);
10997
+ }
10998
+ });
10696
10999
  if (feature.properties) {
10697
11000
  const props = feature.properties;
10698
11001
  const keys = Object.keys(props);
@@ -10727,15 +11030,20 @@ function gisPlugin() {
10727
11030
  const bounds = geojsonLayer.getBounds();
10728
11031
  if (bounds.isValid()) {
10729
11032
  map.fitBounds(bounds, { padding: [20, 20] });
11033
+ map.invalidateSize();
10730
11034
  }
10731
11035
  } catch (e) {
10732
11036
  console.warn("Could not fit bounds for GeoJSON data:", e);
10733
11037
  }
11038
+ const resizeTimers = [0, 80, 240].map((delay) => window.setTimeout(() => {
11039
+ map.invalidateSize();
11040
+ }, delay));
10734
11041
  return {
10735
11042
  resize() {
10736
11043
  map.invalidateSize();
10737
11044
  },
10738
11045
  destroy() {
11046
+ resizeTimers.forEach((timer) => window.clearTimeout(timer));
10739
11047
  map.remove();
10740
11048
  wrapper.remove();
10741
11049
  }
@@ -10760,8 +11068,7 @@ function normalizeGisError(error, fileName) {
10760
11068
  function isGisFallback(value) {
10761
11069
  return typeof value === "object" && value !== null && "fallback" in value;
10762
11070
  }
10763
- function createGisSummary(geojson) {
10764
- const summary = summarizeGeoJson(geojson);
11071
+ function createGisSummary(summary) {
10765
11072
  const bar = document.createElement("div");
10766
11073
  bar.className = "ofv-gis-summary";
10767
11074
  appendSummaryItem(bar, "\u8981\u7D20", String(summary.features));
@@ -10775,6 +11082,26 @@ function createGisSummary(geojson) {
10775
11082
  }
10776
11083
  return bar;
10777
11084
  }
11085
+ function createEmptyMapState() {
11086
+ const empty = document.createElement("div");
11087
+ empty.className = "ofv-map-empty";
11088
+ const title = document.createElement("strong");
11089
+ title.textContent = "\u6682\u65E0\u53EF\u5C55\u793A\u7684\u5730\u56FE\u8981\u7D20";
11090
+ const detail = document.createElement("span");
11091
+ detail.textContent = "GeoJSON \u5DF2\u8BC6\u522B\uFF0C\u4F46 features \u4E3A\u7A7A\u3002";
11092
+ empty.append(title, detail);
11093
+ return empty;
11094
+ }
11095
+ function createMapLegend(summary) {
11096
+ const legend = document.createElement("div");
11097
+ legend.className = "ofv-map-legend";
11098
+ const title = document.createElement("strong");
11099
+ title.textContent = "GeoJSON";
11100
+ const detail = document.createElement("span");
11101
+ detail.textContent = `${summary.features} \u4E2A\u8981\u7D20 \xB7 ${formatGeometryCounts(summary.geometryCounts)}`;
11102
+ legend.append(title, detail);
11103
+ return legend;
11104
+ }
10778
11105
  function appendSummaryItem(parent, label, value) {
10779
11106
  const item = document.createElement("span");
10780
11107
  const key = document.createElement("span");