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