@open-file-viewer/core 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -682,6 +682,7 @@ function createViewer(options) {
682
682
  file,
683
683
  size: getElementSize(viewport),
684
684
  options: normalizedOptions,
685
+ toolbar: toolbar?.getContext(),
685
686
  setLoading,
686
687
  setError
687
688
  });
@@ -855,18 +856,36 @@ function createToolbar(toolbar, viewport, queue) {
855
856
  element.setAttribute("role", "toolbar");
856
857
  element.setAttribute("aria-label", "File preview toolbar");
857
858
  let file;
859
+ let currentIndex = 0;
860
+ let currentLength = queue.getLength();
858
861
  let queueLabel;
859
862
  let previousButton;
860
863
  let nextButton;
864
+ let zoomResetButton;
865
+ let currentZoom;
861
866
  const commandButtons = [];
867
+ const customButtons = [];
862
868
  const disposers = [];
863
869
  const search = createSearchController(viewport);
864
870
  let searchInput;
865
871
  let searchCount;
866
- const addButton = (label, title, action, className) => {
872
+ let canRunCommand = (_command) => false;
873
+ const getContext = () => createToolbarContext({
874
+ file,
875
+ index: currentIndex,
876
+ length: currentLength,
877
+ viewport,
878
+ queue,
879
+ element,
880
+ search,
881
+ canCommand: canRunCommand,
882
+ zoom: currentZoom,
883
+ setZoom
884
+ });
885
+ const addButton = (label, title, action, className, icon) => {
867
886
  const button = document.createElement("button");
868
887
  button.type = "button";
869
- button.textContent = label;
888
+ setToolbarButtonContent(button, label, icon);
870
889
  button.title = title;
871
890
  button.setAttribute("aria-label", title);
872
891
  if (className) {
@@ -877,68 +896,119 @@ function createToolbar(toolbar, viewport, queue) {
877
896
  disposers.push(() => button.removeEventListener("click", action));
878
897
  return button;
879
898
  };
880
- const addQueueButton = (label, title, action) => {
881
- const button = document.createElement("button");
882
- button.type = "button";
883
- button.textContent = label;
884
- button.title = title;
885
- button.setAttribute("aria-label", title);
886
- const listener = () => {
887
- void action();
888
- };
889
- button.addEventListener("click", listener);
890
- element.append(button);
891
- disposers.push(() => button.removeEventListener("click", listener));
892
- return button;
893
- };
894
- if (queue.getLength() > 1) {
895
- previousButton = addQueueButton("Prev", "Previous file", queue.previous);
896
- nextButton = addQueueButton("Next", "Next file", queue.next);
897
- queueLabel = document.createElement("span");
898
- queueLabel.className = "ofv-toolbar-queue";
899
- element.append(queueLabel);
900
- }
901
- const addCommandButton = (label, title, command) => {
899
+ const addCommandButton = (id, label, title, command) => {
902
900
  const button = addButton(label, title, () => {
903
901
  queue.command(command);
904
- });
902
+ }, void 0, options.icons?.[id]);
905
903
  button.disabled = true;
906
904
  commandButtons.push({ button, command });
907
905
  };
908
- if (options.zoom) {
909
- addCommandButton("-", "Zoom out", "zoom-out");
910
- addCommandButton("+", "Zoom in", "zoom-in");
911
- addCommandButton("100%", "Reset zoom", "zoom-reset");
912
- }
913
- if (options.rotate) {
914
- addCommandButton("Rotate", "Rotate right", "rotate-right");
915
- }
916
- if (options.download !== false) {
917
- addButton("Download", "Download file", () => {
918
- if (file) {
919
- downloadFile(file);
906
+ const renderDefaultAction = (id) => {
907
+ if (!isBuiltInToolbarAction(id)) {
908
+ const customAction = options.actions?.find((action) => action.id === id);
909
+ if (customAction) {
910
+ renderCustomAction(customAction);
920
911
  }
921
- });
922
- }
923
- if (options.fullscreen !== false) {
924
- addButton("Fullscreen", "Open preview fullscreen", () => {
925
- const target = element.parentElement;
926
- void target?.requestFullscreen?.();
927
- });
928
- }
929
- if (options.print) {
930
- addButton("Print", "Print preview", () => {
931
- printPreview(viewport);
932
- });
933
- }
934
- if (options.search !== false) {
912
+ return;
913
+ }
914
+ if (id === "previous" && queue.getLength() > 1) {
915
+ previousButton = addButton(
916
+ getToolbarLabel(options, "previous"),
917
+ getToolbarTitle(options, "previous"),
918
+ () => void queue.previous(),
919
+ void 0,
920
+ options.icons?.previous
921
+ );
922
+ return;
923
+ }
924
+ if (id === "next" && queue.getLength() > 1) {
925
+ nextButton = addButton(
926
+ getToolbarLabel(options, "next"),
927
+ getToolbarTitle(options, "next"),
928
+ () => void queue.next(),
929
+ void 0,
930
+ options.icons?.next
931
+ );
932
+ return;
933
+ }
934
+ if (id === "queue" && queue.getLength() > 1) {
935
+ queueLabel = document.createElement("span");
936
+ queueLabel.className = "ofv-toolbar-queue";
937
+ element.append(queueLabel);
938
+ return;
939
+ }
940
+ if (id === "zoom-out" && options.zoom) {
941
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-out");
942
+ return;
943
+ }
944
+ if (id === "zoom-in" && options.zoom) {
945
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-in");
946
+ return;
947
+ }
948
+ if (id === "zoom-reset" && options.zoom) {
949
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-reset");
950
+ zoomResetButton = commandButtons[commandButtons.length - 1]?.button;
951
+ updateZoomLabel();
952
+ return;
953
+ }
954
+ if (id === "rotate-right" && options.rotate) {
955
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "rotate-right");
956
+ return;
957
+ }
958
+ if (id === "download" && options.download !== false) {
959
+ addButton(
960
+ getToolbarLabel(options, id),
961
+ getToolbarTitle(options, id),
962
+ () => getContext().download(),
963
+ void 0,
964
+ options.icons?.download
965
+ );
966
+ return;
967
+ }
968
+ if (id === "fullscreen" && options.fullscreen !== false) {
969
+ addButton(
970
+ getToolbarLabel(options, id),
971
+ getToolbarTitle(options, id),
972
+ () => getContext().fullscreen(),
973
+ void 0,
974
+ options.icons?.fullscreen
975
+ );
976
+ return;
977
+ }
978
+ if (id === "print" && options.print) {
979
+ addButton(
980
+ getToolbarLabel(options, id),
981
+ getToolbarTitle(options, id),
982
+ () => getContext().print(),
983
+ void 0,
984
+ options.icons?.print
985
+ );
986
+ return;
987
+ }
988
+ if (id === "search" && options.search !== false) {
989
+ renderSearchControl();
990
+ return;
991
+ }
992
+ };
993
+ const renderCustomAction = (action) => {
994
+ const button = addButton(
995
+ action.label,
996
+ action.title || action.label,
997
+ () => void action.onClick(getContext()),
998
+ action.className,
999
+ action.icon
1000
+ );
1001
+ button.dataset.ofvToolbarAction = action.id;
1002
+ customButtons.push({ button, action });
1003
+ };
1004
+ const renderSearchControl = () => {
935
1005
  const searchGroup = document.createElement("div");
936
1006
  searchGroup.className = "ofv-toolbar-search";
937
- searchGroup.title = "Search preview text";
1007
+ searchGroup.title = getToolbarTitle(options, "search");
938
1008
  const nextSearchInput = document.createElement("input");
939
1009
  nextSearchInput.type = "search";
940
- nextSearchInput.placeholder = "Search";
941
- nextSearchInput.setAttribute("aria-label", "Search preview text");
1010
+ nextSearchInput.placeholder = getToolbarLabel(options, "search");
1011
+ nextSearchInput.setAttribute("aria-label", getToolbarTitle(options, "search"));
942
1012
  const nextSearchCount = document.createElement("span");
943
1013
  nextSearchCount.className = "ofv-toolbar-search-count";
944
1014
  searchInput = nextSearchInput;
@@ -951,18 +1021,71 @@ function createToolbar(toolbar, viewport, queue) {
951
1021
  searchGroup.append(nextSearchInput, nextSearchCount);
952
1022
  element.append(searchGroup);
953
1023
  disposers.push(() => nextSearchInput.removeEventListener("input", runSearch));
1024
+ };
1025
+ const renderToolbar = () => {
1026
+ if (options.render) {
1027
+ element.replaceChildren();
1028
+ const customElement = options.render(getContext());
1029
+ if (customElement) {
1030
+ element.append(customElement);
1031
+ }
1032
+ return;
1033
+ }
1034
+ getToolbarOrder(options, queue.getLength()).forEach(renderDefaultAction);
1035
+ getImplicitCustomActions(options).forEach(renderCustomAction);
1036
+ };
1037
+ renderToolbar();
1038
+ const updateCustomButtons = () => {
1039
+ const context = getContext();
1040
+ for (const { button, action } of customButtons) {
1041
+ button.disabled = evaluateToolbarFlag(action.disabled, context);
1042
+ button.hidden = evaluateToolbarFlag(action.hidden, context);
1043
+ }
1044
+ };
1045
+ const resetSearch = () => {
1046
+ search.clear();
1047
+ if (searchInput) {
1048
+ searchInput.value = "";
1049
+ }
1050
+ if (searchCount) {
1051
+ searchCount.textContent = "";
1052
+ }
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
+ );
954
1069
  }
1070
+ const refreshCustomRender = () => {
1071
+ if (!options.render) {
1072
+ return;
1073
+ }
1074
+ element.replaceChildren();
1075
+ const customElement = options.render(getContext());
1076
+ if (customElement) {
1077
+ element.append(customElement);
1078
+ }
1079
+ };
955
1080
  return {
956
1081
  element,
957
1082
  update(nextFile, index, length) {
958
1083
  file = nextFile;
959
- search.clear();
960
- if (searchInput) {
961
- searchInput.value = "";
962
- }
963
- if (searchCount) {
964
- searchCount.textContent = "";
965
- }
1084
+ currentIndex = index;
1085
+ currentLength = length;
1086
+ currentZoom = void 0;
1087
+ updateZoomLabel();
1088
+ resetSearch();
966
1089
  commandButtons.forEach(({ button }) => {
967
1090
  button.disabled = true;
968
1091
  });
@@ -975,20 +1098,257 @@ function createToolbar(toolbar, viewport, queue) {
975
1098
  if (nextButton) {
976
1099
  nextButton.disabled = index >= length - 1;
977
1100
  }
1101
+ updateCustomButtons();
1102
+ refreshCustomRender();
978
1103
  },
979
1104
  setCommandSupport(isSupported) {
1105
+ canRunCommand = isSupported;
980
1106
  commandButtons.forEach(({ button, command }) => {
981
1107
  button.disabled = !isSupported(command);
982
1108
  });
1109
+ updateCustomButtons();
1110
+ refreshCustomRender();
983
1111
  },
1112
+ getContext,
1113
+ setZoom,
984
1114
  destroy() {
985
1115
  search.clear();
986
1116
  for (const dispose of disposers) {
987
1117
  dispose();
988
1118
  }
1119
+ element.replaceChildren();
989
1120
  }
990
1121
  };
991
1122
  }
1123
+ function createToolbarContext({
1124
+ file,
1125
+ index,
1126
+ length,
1127
+ viewport,
1128
+ queue,
1129
+ element,
1130
+ search,
1131
+ canCommand,
1132
+ zoom,
1133
+ setZoom
1134
+ }) {
1135
+ return {
1136
+ file,
1137
+ index,
1138
+ length,
1139
+ viewport,
1140
+ canPrevious: index > 0,
1141
+ canNext: index < length - 1,
1142
+ zoom,
1143
+ zoomLabel: zoom === void 0 ? void 0 : formatToolbarZoom(zoom),
1144
+ async previous() {
1145
+ await queue.previous();
1146
+ },
1147
+ async next() {
1148
+ await queue.next();
1149
+ },
1150
+ command: queue.command,
1151
+ canCommand,
1152
+ setZoom,
1153
+ download() {
1154
+ if (file) {
1155
+ downloadFile(file);
1156
+ }
1157
+ },
1158
+ fullscreen() {
1159
+ void element.parentElement?.requestFullscreen?.();
1160
+ },
1161
+ print() {
1162
+ printPreview(viewport);
1163
+ },
1164
+ search: search.search,
1165
+ clearSearch: search.clear
1166
+ };
1167
+ }
1168
+ var defaultToolbarLabels = {
1169
+ previous: "Prev",
1170
+ next: "Next",
1171
+ queue: "",
1172
+ "zoom-out": "-",
1173
+ "zoom-in": "+",
1174
+ "zoom-reset": "100%",
1175
+ "rotate-right": "Rotate",
1176
+ download: "Download",
1177
+ fullscreen: "Fullscreen",
1178
+ print: "Print",
1179
+ search: "Search"
1180
+ };
1181
+ var defaultToolbarTitles = {
1182
+ previous: "Previous file",
1183
+ next: "Next file",
1184
+ queue: "Current file position",
1185
+ "zoom-out": "Zoom out",
1186
+ "zoom-in": "Zoom in",
1187
+ "zoom-reset": "Reset zoom",
1188
+ "rotate-right": "Rotate right",
1189
+ download: "Download file",
1190
+ fullscreen: "Open preview fullscreen",
1191
+ print: "Print preview",
1192
+ search: "Search preview text"
1193
+ };
1194
+ function getToolbarLabel(options, id) {
1195
+ return options.labels?.[id] ?? defaultToolbarLabels[id];
1196
+ }
1197
+ function getToolbarTitle(options, id) {
1198
+ return options.titles?.[id] ?? options.labels?.[id] ?? defaultToolbarTitles[id];
1199
+ }
1200
+ function formatToolbarZoom(zoom) {
1201
+ return `${Math.round(zoom * 100)}%`;
1202
+ }
1203
+ function getToolbarOrder(options, queueLength) {
1204
+ if (options.order) {
1205
+ return options.order;
1206
+ }
1207
+ const actions = [];
1208
+ if (queueLength > 1) {
1209
+ actions.push("previous", "next", "queue");
1210
+ }
1211
+ if (options.zoom) {
1212
+ actions.push("zoom-out", "zoom-in", "zoom-reset");
1213
+ }
1214
+ if (options.rotate) {
1215
+ actions.push("rotate-right");
1216
+ }
1217
+ if (options.download !== false) {
1218
+ actions.push("download");
1219
+ }
1220
+ if (options.fullscreen !== false) {
1221
+ actions.push("fullscreen");
1222
+ }
1223
+ if (options.print) {
1224
+ actions.push("print");
1225
+ }
1226
+ if (options.search !== false) {
1227
+ actions.push("search");
1228
+ }
1229
+ return actions;
1230
+ }
1231
+ function getImplicitCustomActions(options) {
1232
+ if (options.order || !options.actions) {
1233
+ return [];
1234
+ }
1235
+ return [...options.actions].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1236
+ }
1237
+ function evaluateToolbarFlag(value, context) {
1238
+ return typeof value === "function" ? value(context) : Boolean(value);
1239
+ }
1240
+ function setToolbarButtonContent(button, label, icon) {
1241
+ button.replaceChildren();
1242
+ if (!icon) {
1243
+ button.textContent = label;
1244
+ return;
1245
+ }
1246
+ const iconElement = document.createElement("span");
1247
+ iconElement.className = "ofv-toolbar-icon";
1248
+ iconElement.setAttribute("aria-hidden", "true");
1249
+ if (typeof icon === "string") {
1250
+ iconElement.append(sanitizeToolbarIcon(icon));
1251
+ } else {
1252
+ iconElement.append(icon.cloneNode(true));
1253
+ }
1254
+ const labelElement = document.createElement("span");
1255
+ labelElement.className = "ofv-toolbar-label";
1256
+ labelElement.textContent = label;
1257
+ button.append(iconElement, labelElement);
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
+ }
1349
+ function isBuiltInToolbarAction(id) {
1350
+ return id in defaultToolbarLabels;
1351
+ }
992
1352
  function createSearchController(root) {
993
1353
  const markerClass = "ofv-search-match";
994
1354
  const clear = () => {
@@ -1397,6 +1757,7 @@ function imagePlugin() {
1397
1757
  const updateTransform = () => {
1398
1758
  image.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale}) rotate(${rotation}deg)`;
1399
1759
  zoomLabel.textContent = `${Math.round(scale * 100)}%`;
1760
+ ctx.toolbar?.setZoom(scale);
1400
1761
  };
1401
1762
  const showImageFallback = () => {
1402
1763
  stage.replaceChildren(createImageFallback(ctx.file.name, url));
@@ -1512,6 +1873,7 @@ function imagePlugin() {
1512
1873
  image.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
1513
1874
  },
1514
1875
  destroy() {
1876
+ ctx.toolbar?.setZoom(void 0);
1515
1877
  for (const dispose of disposers) {
1516
1878
  dispose();
1517
1879
  }
@@ -3070,7 +3432,6 @@ var mimeLangMap = {
3070
3432
  };
3071
3433
  var MAX_HIGHLIGHT_CHARS = 18e4;
3072
3434
  var MAX_RENDER_CHARS = 6e5;
3073
- var MONACO_LOADER_KEY = "__OFV_MONACO_LOADER__";
3074
3435
  function loadPrismCss(theme) {
3075
3436
  const lightId = "ofv-prism-css-light";
3076
3437
  const darkId = "ofv-prism-css-dark";
@@ -3294,10 +3655,6 @@ function textPlugin() {
3294
3655
  wrapButton.addEventListener("click", () => {
3295
3656
  const wrapped = wrapper.classList.toggle("is-wrapped");
3296
3657
  wrapButton.setAttribute("aria-pressed", String(wrapped));
3297
- monacoEditor?.updateOptions?.({ wordWrap: wrapped ? "on" : "off" });
3298
- if (fallbackEditor) {
3299
- fallbackEditor.wrap = wrapped ? "soft" : "off";
3300
- }
3301
3658
  });
3302
3659
  const copyButton = document.createElement("button");
3303
3660
  copyButton.type = "button";
@@ -3322,12 +3679,7 @@ function textPlugin() {
3322
3679
  downloadText(ctx.file.name, text);
3323
3680
  status.textContent = "Download ready";
3324
3681
  });
3325
- const editorButton = document.createElement("button");
3326
- editorButton.type = "button";
3327
- editorButton.className = "ofv-code-action";
3328
- editorButton.textContent = "Editor";
3329
- editorButton.setAttribute("aria-pressed", "false");
3330
- actions.append(status, editorButton, wrapButton, copyButton, downloadButton);
3682
+ actions.append(wrapButton, copyButton, downloadButton, status);
3331
3683
  header.append(title, actions);
3332
3684
  const structureSummary = createTextStructureSummary(text, ext, lang, ctx.file.mimeType);
3333
3685
  const body = document.createElement("div");
@@ -3343,93 +3695,6 @@ function textPlugin() {
3343
3695
  code.textContent = codeText;
3344
3696
  pre.appendChild(code);
3345
3697
  body.append(gutter, pre);
3346
- const editorHost = document.createElement("div");
3347
- editorHost.className = "ofv-code-editor";
3348
- editorHost.hidden = true;
3349
- let monacoEditor;
3350
- let monacoModel;
3351
- let fallbackEditor;
3352
- let editorReady = false;
3353
- let editorLoading;
3354
- const showReader = () => {
3355
- editorHost.hidden = true;
3356
- body.hidden = false;
3357
- wrapper.classList.remove("is-editor");
3358
- editorButton.textContent = "Editor";
3359
- editorButton.setAttribute("aria-pressed", "false");
3360
- };
3361
- const showEditor = async () => {
3362
- if (text.length > MAX_RENDER_CHARS) {
3363
- status.textContent = "Editor skipped for large file";
3364
- return;
3365
- }
3366
- if (editorReady) {
3367
- body.hidden = true;
3368
- editorHost.hidden = false;
3369
- wrapper.classList.add("is-editor");
3370
- editorButton.textContent = "Reader";
3371
- editorButton.setAttribute("aria-pressed", "true");
3372
- monacoEditor?.layout?.();
3373
- return;
3374
- }
3375
- if (!editorLoading) {
3376
- editorLoading = (async () => {
3377
- editorButton.disabled = true;
3378
- status.textContent = "Loading editor";
3379
- try {
3380
- const monaco = await loadMonaco();
3381
- if (!monaco.editor?.create) {
3382
- throw new Error("Monaco editor API is unavailable.");
3383
- }
3384
- const monacoLanguage = toMonacoLanguage(ext, lang);
3385
- monaco.editor.setTheme?.(isDark ? "vs-dark" : "vs");
3386
- monacoModel = monaco.editor.createModel?.(text, monacoLanguage);
3387
- monacoEditor = monaco.editor.create(editorHost, {
3388
- ...monacoModel ? { model: monacoModel } : { value: text, language: monacoLanguage },
3389
- automaticLayout: true,
3390
- fontFamily: "var(--ofv-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace)",
3391
- fontSize: 13,
3392
- lineNumbers: "on",
3393
- minimap: { enabled: text.length <= 8e4 },
3394
- readOnly: true,
3395
- renderLineHighlight: "line",
3396
- scrollBeyondLastLine: false,
3397
- wordWrap: wrapper.classList.contains("is-wrapped") ? "on" : "off"
3398
- });
3399
- editorReady = true;
3400
- status.textContent = "Editor ready";
3401
- body.hidden = true;
3402
- editorHost.hidden = false;
3403
- wrapper.classList.add("is-editor");
3404
- editorButton.textContent = "Reader";
3405
- editorButton.setAttribute("aria-pressed", "true");
3406
- monacoEditor.layout?.();
3407
- } catch (error) {
3408
- console.warn("Monaco editor failed to load; using built-in code editor fallback:", error);
3409
- fallbackEditor = createBasicCodeEditor(text, wrapper.classList.contains("is-wrapped"));
3410
- editorHost.replaceChildren(fallbackEditor);
3411
- editorReady = true;
3412
- status.textContent = "Basic editor";
3413
- body.hidden = true;
3414
- editorHost.hidden = false;
3415
- wrapper.classList.add("is-editor");
3416
- editorButton.textContent = "Reader";
3417
- editorButton.setAttribute("aria-pressed", "true");
3418
- } finally {
3419
- editorButton.disabled = false;
3420
- }
3421
- })();
3422
- }
3423
- await editorLoading;
3424
- };
3425
- editorButton.addEventListener("click", () => {
3426
- if (editorHost.hidden) {
3427
- void showEditor();
3428
- } else {
3429
- showReader();
3430
- status.textContent = "Reader mode";
3431
- }
3432
- });
3433
3698
  wrapper.append(header);
3434
3699
  if (structureSummary) {
3435
3700
  wrapper.append(structureSummary);
@@ -3447,7 +3712,6 @@ function textPlugin() {
3447
3712
  wrapper.append(notice);
3448
3713
  }
3449
3714
  wrapper.appendChild(body);
3450
- wrapper.appendChild(editorHost);
3451
3715
  ctx.viewport.appendChild(wrapper);
3452
3716
  if (shouldHighlight) {
3453
3717
  try {
@@ -3457,12 +3721,7 @@ function textPlugin() {
3457
3721
  }
3458
3722
  }
3459
3723
  return {
3460
- resize() {
3461
- monacoEditor?.layout?.();
3462
- },
3463
3724
  destroy() {
3464
- monacoEditor?.dispose?.();
3465
- monacoModel?.dispose?.();
3466
3725
  wrapper.remove();
3467
3726
  }
3468
3727
  };
@@ -3477,45 +3736,6 @@ function normalizeFileName2(name) {
3477
3736
  const baseName = name.split(/[\\/]/).pop() || name;
3478
3737
  return baseName.toLowerCase();
3479
3738
  }
3480
- async function loadMonaco() {
3481
- const injectedLoader = globalThis[MONACO_LOADER_KEY];
3482
- if (typeof injectedLoader === "function") {
3483
- return await injectedLoader();
3484
- }
3485
- const importer = new Function("specifier", "return import(specifier)");
3486
- return importer("monaco-editor");
3487
- }
3488
- function toMonacoLanguage(ext, prismLanguage) {
3489
- if (ext === "vue") {
3490
- return "html";
3491
- }
3492
- if (ext === "tsx") {
3493
- return "typescript";
3494
- }
3495
- if (ext === "jsx") {
3496
- return "javascript";
3497
- }
3498
- if (prismLanguage === "markup") {
3499
- return ext === "xml" ? "xml" : "html";
3500
- }
3501
- if (prismLanguage === "bash") {
3502
- return "shell";
3503
- }
3504
- if (prismLanguage === "none") {
3505
- return "plaintext";
3506
- }
3507
- return prismLanguage;
3508
- }
3509
- function createBasicCodeEditor(text, wrapped) {
3510
- const textarea = document.createElement("textarea");
3511
- textarea.className = "ofv-code-editor-fallback";
3512
- textarea.readOnly = true;
3513
- textarea.spellcheck = false;
3514
- textarea.wrap = wrapped ? "soft" : "off";
3515
- textarea.value = text;
3516
- textarea.setAttribute("aria-label", "Code editor preview");
3517
- return textarea;
3518
- }
3519
3739
  function createTextFallback(fileName, url) {
3520
3740
  const fallback = document.createElement("div");
3521
3741
  fallback.className = "ofv-fallback";
@@ -3753,7 +3973,13 @@ function pdfPlugin(options = {}) {
3753
3973
  scroller.className = "ofv-pdf ofv-pdf-pages";
3754
3974
  viewer.append(summary, scroller);
3755
3975
  ctx.viewport.append(viewer);
3756
- 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
+ });
3757
3983
  const doc = await documentTask.promise.catch((error) => {
3758
3984
  viewer.remove();
3759
3985
  ctx.viewport.classList.add("ofv-center");
@@ -3789,6 +4015,7 @@ function pdfPlugin(options = {}) {
3789
4015
  let zoomFactor = 1;
3790
4016
  const updateSummary = () => {
3791
4017
  renderPdfSummary(summary, doc.numPages, pagesMeta, ctx.options.fit, zoomFactor);
4018
+ ctx.toolbar?.setZoom(zoomFactor);
3792
4019
  };
3793
4020
  const clearPage = (pageIdx) => {
3794
4021
  const state = pageStates[pageIdx];
@@ -3812,12 +4039,17 @@ function pdfPlugin(options = {}) {
3812
4039
  try {
3813
4040
  const page = await doc.getPage(pageIdx + 1);
3814
4041
  const meta = pagesMeta[pageIdx];
3815
- 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));
3816
4043
  const viewport = page.getViewport({ scale });
4044
+ const outputScale = getPdfOutputScale();
4045
+ const cssWidth = Math.floor(viewport.width);
4046
+ const cssHeight = Math.floor(viewport.height);
3817
4047
  const canvas = document.createElement("canvas");
3818
4048
  canvas.className = "ofv-pdf-page";
3819
- canvas.width = Math.floor(viewport.width);
3820
- 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`;
3821
4053
  const context = canvas.getContext("2d");
3822
4054
  if (!context) {
3823
4055
  throw new Error("Canvas 2D context is not available.");
@@ -3826,7 +4058,8 @@ function pdfPlugin(options = {}) {
3826
4058
  state.canvas = canvas;
3827
4059
  const renderTask = page.render({
3828
4060
  canvasContext: context,
3829
- viewport
4061
+ viewport,
4062
+ transform: outputScale === 1 ? void 0 : [outputScale, 0, 0, outputScale, 0, 0]
3830
4063
  });
3831
4064
  state.renderTask = renderTask;
3832
4065
  await renderTask.promise;
@@ -3834,8 +4067,8 @@ function pdfPlugin(options = {}) {
3834
4067
  const textContent = await page.getTextContent();
3835
4068
  const textLayer = document.createElement("div");
3836
4069
  textLayer.className = "ofv-pdf-text-layer";
3837
- textLayer.style.width = `${Math.floor(viewport.width)}px`;
3838
- textLayer.style.height = `${Math.floor(viewport.height)}px`;
4070
+ textLayer.style.width = `${cssWidth}px`;
4071
+ textLayer.style.height = `${cssHeight}px`;
3839
4072
  state.wrapper.appendChild(textLayer);
3840
4073
  for (const item of textContent.items) {
3841
4074
  if (!("str" in item)) continue;
@@ -3896,7 +4129,7 @@ function pdfPlugin(options = {}) {
3896
4129
  }
3897
4130
  for (let i = 0; i < doc.numPages; i++) {
3898
4131
  const meta = pagesMeta[i];
3899
- 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));
3900
4133
  const w = Math.floor(meta.width * scale);
3901
4134
  const h = Math.floor(meta.height * scale);
3902
4135
  const wrapper = document.createElement("div");
@@ -3951,6 +4184,7 @@ function pdfPlugin(options = {}) {
3951
4184
  }, 120);
3952
4185
  },
3953
4186
  destroy() {
4187
+ ctx.toolbar?.setZoom(void 0);
3954
4188
  window.clearTimeout(resizeTimer);
3955
4189
  observer?.disconnect();
3956
4190
  pageStates.forEach((state) => {
@@ -3969,6 +4203,19 @@ function pdfPlugin(options = {}) {
3969
4203
  }
3970
4204
  };
3971
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
+ }
3972
4219
  function renderPdfSummary(summary, pages, pagesMeta, fit, zoomFactor) {
3973
4220
  summary.replaceChildren();
3974
4221
  appendPdfSummary(summary, "\u9875\u6570", String(pages));
@@ -4728,8 +4975,9 @@ function officePlugin() {
4728
4975
  ctx.viewport.append(panel);
4729
4976
  const extension = resolveFormat(ctx.file, officeMimeFormatMap);
4730
4977
  const arrayBuffer = await readArrayBuffer(ctx.file);
4978
+ let disposeDocxFit;
4731
4979
  if (fileIsDocx(extension)) {
4732
- await renderDocx(panel, arrayBuffer);
4980
+ disposeDocxFit = await renderDocx(panel, arrayBuffer);
4733
4981
  } else if (extension === "rtf") {
4734
4982
  renderPlainDocument(panel, "RTF \u6587\u6863", rtfToText(await readTextFromBuffer(arrayBuffer)));
4735
4983
  } else if (extension === "odt") {
@@ -4754,6 +5002,7 @@ function officePlugin() {
4754
5002
  }
4755
5003
  return {
4756
5004
  destroy() {
5005
+ disposeDocxFit?.();
4757
5006
  panel.remove();
4758
5007
  }
4759
5008
  };
@@ -4787,6 +5036,7 @@ async function renderDocx(panel, arrayBuffer) {
4787
5036
  experimental: true,
4788
5037
  useBase64URL: true
4789
5038
  });
5039
+ return fitDocxPages(content);
4790
5040
  } catch (error) {
4791
5041
  content.replaceChildren();
4792
5042
  const fallbackNote = document.createElement("div");
@@ -4801,6 +5051,74 @@ async function renderDocx(panel, arrayBuffer) {
4801
5051
  }
4802
5052
  console.warn("DOCX layout preview failed, fell back to Mammoth:", error);
4803
5053
  }
5054
+ return () => void 0;
5055
+ }
5056
+ function fitDocxPages(container) {
5057
+ const wrapper = container.querySelector(".ofv-docx-wrapper");
5058
+ if (!wrapper) {
5059
+ return () => void 0;
5060
+ }
5061
+ const update = () => {
5062
+ const frames = ensureDocxPageFrames(wrapper);
5063
+ if (frames.length === 0) {
5064
+ wrapper.style.removeProperty("--ofv-docx-scale");
5065
+ return;
5066
+ }
5067
+ const availableWidth = Math.max(1, container.clientWidth - 48);
5068
+ const pageWidth = Math.max(
5069
+ 1,
5070
+ ...frames.map(({ page }) => {
5071
+ const rectWidth = page.getBoundingClientRect().width;
5072
+ return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
5073
+ })
5074
+ );
5075
+ const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
5076
+ wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
5077
+ wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
5078
+ for (const { frame, page } of frames) {
5079
+ const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
5080
+ if (pageHeight > 0) {
5081
+ frame.style.height = `${Math.ceil(pageHeight * scale)}px`;
5082
+ }
5083
+ }
5084
+ };
5085
+ update();
5086
+ const timers = [0, 80, 240].map((delay) => window.setTimeout(update, delay));
5087
+ if (typeof ResizeObserver === "undefined") {
5088
+ window.addEventListener("resize", update);
5089
+ return () => {
5090
+ timers.forEach((timer) => window.clearTimeout(timer));
5091
+ window.removeEventListener("resize", update);
5092
+ };
5093
+ }
5094
+ const observer = new ResizeObserver(update);
5095
+ observer.observe(container);
5096
+ observer.observe(wrapper);
5097
+ return () => {
5098
+ timers.forEach((timer) => window.clearTimeout(timer));
5099
+ observer.disconnect();
5100
+ };
5101
+ }
5102
+ function ensureDocxPageFrames(wrapper) {
5103
+ const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
5104
+ return pages.map((page) => {
5105
+ const parent = page.parentElement;
5106
+ if (parent?.classList.contains("ofv-docx-page-frame")) {
5107
+ return { frame: parent, page };
5108
+ }
5109
+ const frame = document.createElement("div");
5110
+ frame.className = "ofv-docx-page-frame";
5111
+ page.before(frame);
5112
+ frame.append(page);
5113
+ return { frame, page };
5114
+ });
5115
+ }
5116
+ function parseCssPixelValue(value) {
5117
+ const parsed = Number.parseFloat(value);
5118
+ return Number.isFinite(parsed) ? parsed : 0;
5119
+ }
5120
+ function formatCssNumber(value) {
5121
+ return Number.isFinite(value) ? value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "") : "1";
4804
5122
  }
4805
5123
  async function renderDocxTextFallback(container, arrayBuffer) {
4806
5124
  const article = document.createElement("article");
@@ -4899,7 +5217,7 @@ async function renderSheet(panel, arrayBuffer, extension) {
4899
5217
  workbook = xlsx.read(arrayBuffer, { type: "array" });
4900
5218
  } catch (error) {
4901
5219
  if (isLegacyOfficeBinary(extension)) {
4902
- renderLegacyOfficeBinary(panel, extension, arrayBuffer, normalizeOfficeError(error));
5220
+ renderLegacyOfficeBinary(panel, extension, arrayBuffer, `\u8868\u683C\u89E3\u6790\u5931\u8D25\uFF1A${normalizeOfficeError(error)}`);
4903
5221
  return;
4904
5222
  }
4905
5223
  renderSheetFallback(panel, extension, normalizeOfficeError(error));
@@ -6004,14 +6322,15 @@ function legacyOfficeFormatLabel(extension) {
6004
6322
  function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6005
6323
  const fragments = extractLegacyOfficeText(arrayBuffer);
6006
6324
  panel.replaceChildren();
6007
- const section = createSection("Office \u4E8C\u8FDB\u5236\u57FA\u7840\u9884\u89C8");
6325
+ const section = createSection("Office \u8F6C\u6362\u63D0\u793A");
6326
+ section.classList.add("ofv-office-conversion");
6008
6327
  const format = document.createElement("p");
6009
6328
  const strong = document.createElement("strong");
6010
6329
  strong.textContent = `.${extension}`;
6011
6330
  format.append(
6012
6331
  strong,
6013
6332
  document.createTextNode(
6014
- " \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\u5148\u63D0\u53D6\u53EF\u8BFB\u6587\u672C\u7247\u6BB5\u548C\u7ED3\u6784\u6307\u7EB9\uFF0C\u5B8C\u6574\u7248\u5F0F\u5EFA\u8BAE\u63A5\u5165 LibreOffice/OnlyOffice \u670D\u52A1\u7AEF\u8F6C\u6362\u3002"
6333
+ " \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\u4EC5\u5C55\u793A\u53EF\u4FE1\u6587\u672C\u7247\u6BB5\u548C\u7ED3\u6784\u6307\u7EB9\uFF0C\u5B8C\u6574\u6392\u7248\u5EFA\u8BAE\u63A5\u5165 LibreOffice/OnlyOffice \u670D\u52A1\u7AEF\u8F6C\u6362\u4E3A PDF/HTML\u3002"
6015
6334
  )
6016
6335
  );
6017
6336
  const meta = document.createElement("dl");
@@ -6020,12 +6339,15 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6020
6339
  appendOfficeBinaryMeta(meta, "\u6587\u4EF6\u7ED3\u6784", hasOleSignature(arrayBuffer) ? "\u68C0\u6D4B\u5230 OLE Compound File \u7B7E\u540D" : "\u672A\u68C0\u6D4B\u5230\u6807\u51C6 OLE \u7B7E\u540D\uFF0C\u6309\u539F\u59CB\u4E8C\u8FDB\u5236\u5C1D\u8BD5\u63D0\u53D6");
6021
6340
  appendOfficeBinaryMeta(meta, "\u6587\u672C\u7247\u6BB5", `${fragments.length} \u6BB5`);
6022
6341
  if (parseError) {
6023
- appendOfficeBinaryMeta(meta, "\u8868\u683C\u89E3\u6790", `xlsx \u8BFB\u53D6\u5931\u8D25\uFF0C\u5DF2\u5207\u6362\u4E8C\u8FDB\u5236\u6307\u7EB9\uFF1A${parseError}`);
6342
+ appendOfficeBinaryMeta(meta, "\u89E3\u6790\u72B6\u6001", parseError);
6024
6343
  }
6025
6344
  section.append(format, meta);
6026
6345
  if (fragments.length > 0) {
6027
6346
  const article = document.createElement("article");
6028
- article.className = "ofv-document";
6347
+ article.className = "ofv-document ofv-office-binary-fragments";
6348
+ const heading = document.createElement("h4");
6349
+ heading.textContent = "\u53EF\u8BFB\u6587\u672C\u7247\u6BB5";
6350
+ article.append(heading);
6029
6351
  for (const fragment of fragments.slice(0, 80)) {
6030
6352
  const paragraph = document.createElement("p");
6031
6353
  paragraph.textContent = fragment;
@@ -6034,7 +6356,8 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6034
6356
  section.append(article);
6035
6357
  } else {
6036
6358
  const empty = document.createElement("p");
6037
- empty.textContent = "\u672A\u63D0\u53D6\u5230\u7A33\u5B9A\u53EF\u8BFB\u6587\u672C\u3002\u8BE5\u6587\u4EF6\u53EF\u80FD\u7ECF\u8FC7\u538B\u7F29\u3001\u52A0\u5BC6\uFF0C\u6216\u9700\u8981\u5B8C\u6574\u4E8C\u8FDB\u5236\u89E3\u6790\u5668\u3002";
6359
+ empty.className = "ofv-office-binary-empty";
6360
+ empty.textContent = "\u672A\u63D0\u53D6\u5230\u7A33\u5B9A\u53EF\u8BFB\u6587\u672C\u3002\u8BE5\u6587\u4EF6\u53EF\u80FD\u7ECF\u8FC7\u538B\u7F29\u3001\u52A0\u5BC6\uFF0C\u6216\u6587\u672C\u7F16\u7801\u65E0\u6CD5\u5728\u6D4F\u89C8\u5668\u7AEF\u53EF\u9760\u8BC6\u522B\uFF1B\u8BF7\u4F7F\u7528\u670D\u52A1\u7AEF LibreOffice/OnlyOffice \u8F6C\u6362\u540E\u9884\u89C8\u3002";
6038
6361
  section.append(empty);
6039
6362
  }
6040
6363
  panel.append(section);
@@ -6067,12 +6390,12 @@ function normalizeOfficeError(error) {
6067
6390
  function extractLegacyOfficeText(arrayBuffer) {
6068
6391
  const bytes = new Uint8Array(arrayBuffer);
6069
6392
  const fragments = [
6070
- ...extractPrintableRuns(bytes),
6071
- ...extractUtf16Runs(bytes)
6072
- ].map((fragment) => normalizeLegacyText(fragment)).filter((fragment) => fragment.length >= 3 && /[\p{L}\p{N}]/u.test(fragment));
6393
+ ...extractPrintableRuns(bytes).map((text) => ({ text, source: "ascii" })),
6394
+ ...extractUtf16Runs(bytes).map((text) => ({ text, source: "utf16" }))
6395
+ ].map(({ text, source }) => ({ text: normalizeLegacyText(text), source })).filter(({ text, source }) => isReadableLegacyTextFragment(text, source));
6073
6396
  const unique = [];
6074
6397
  const seen = /* @__PURE__ */ new Set();
6075
- for (const fragment of fragments) {
6398
+ for (const { text: fragment } of fragments) {
6076
6399
  const key = fragment.toLowerCase();
6077
6400
  if (!seen.has(key)) {
6078
6401
  seen.add(key);
@@ -6081,6 +6404,104 @@ function extractLegacyOfficeText(arrayBuffer) {
6081
6404
  }
6082
6405
  return unique.slice(0, 160);
6083
6406
  }
6407
+ function isReadableLegacyTextFragment(fragment, source) {
6408
+ if (fragment.length > 600) {
6409
+ return false;
6410
+ }
6411
+ if (isLegacyOfficeMetadataNoise(fragment)) {
6412
+ return false;
6413
+ }
6414
+ if (!/[\p{L}\p{N}]/u.test(fragment)) {
6415
+ return false;
6416
+ }
6417
+ const chars = Array.from(fragment);
6418
+ const letters = chars.filter((char) => /\p{L}/u.test(char)).length;
6419
+ const digits = chars.filter((char) => /\p{N}/u.test(char)).length;
6420
+ const spaces = chars.filter((char) => /\s/u.test(char)).length;
6421
+ const asciiLetters = chars.filter((char) => /[A-Za-z]/.test(char)).length;
6422
+ const cjkLetters = chars.filter((char) => /[\u3400-\u9fff]/u.test(char)).length;
6423
+ const punctuation = chars.filter((char) => /[^\p{L}\p{N}\s]/u.test(char)).length;
6424
+ const alphaNumeric = letters + digits;
6425
+ const readableRatio = alphaNumeric / chars.length;
6426
+ const punctuationRatio = punctuation / chars.length;
6427
+ if (fragment.length < 4 || readableRatio < 0.55 || punctuationRatio > 0.24) {
6428
+ return false;
6429
+ }
6430
+ if (/([\p{L}\p{N}])\1{4,}/u.test(fragment)) {
6431
+ return false;
6432
+ }
6433
+ if (cjkLetters >= 2) {
6434
+ const suspiciousCjk = chars.filter((char) => isAsciiBytePairCjk(char)).length;
6435
+ if (suspiciousCjk / cjkLetters > 0.65) {
6436
+ return false;
6437
+ }
6438
+ if (isLikelyCjkHeading(fragment)) {
6439
+ return true;
6440
+ }
6441
+ if (punctuation > 0 && fragment.length < 12) {
6442
+ return false;
6443
+ }
6444
+ return cjkLetters >= 8 || cjkLetters >= 4 && spaces > 0;
6445
+ }
6446
+ if (asciiLetters >= 4) {
6447
+ if (punctuation > 0 && spaces === 0) {
6448
+ return false;
6449
+ }
6450
+ if (source === "ascii" && /^[A-Z]{2,8}$/.test(fragment)) {
6451
+ return false;
6452
+ }
6453
+ if (spaces > 0) {
6454
+ return letters >= 3;
6455
+ }
6456
+ return fragment.length >= 6;
6457
+ }
6458
+ if (spaces > 0 && letters >= 3) {
6459
+ return true;
6460
+ }
6461
+ return false;
6462
+ }
6463
+ function isLegacyOfficeMetadataNoise(fragment) {
6464
+ if (/[$�\uFFFD]/u.test(fragment)) {
6465
+ return true;
6466
+ }
6467
+ if (/^(?:Root Entry|WordDocument|Workbook|Book|SummaryInformation|DocumentSummaryInformation|CompObj|ObjectPool|Data|PowerPoint Document|Pictures)$/i.test(fragment)) {
6468
+ return true;
6469
+ }
6470
+ if (/\.(dotm?|docm?|pptx?|ppsx?|xlsm?|xlsx?)\b/i.test(fragment)) {
6471
+ return true;
6472
+ }
6473
+ if (/^(?:默认段落字体|普通表格|正文|标题|副标题|目录|页眉|页脚|批注|超链接)(?:\s*\d+)?$/.test(fragment)) {
6474
+ return true;
6475
+ }
6476
+ if (/\b(?:Normal|Default|Calibri|Times New Roman|WPS Office|Microsoft Office|KSOP?ProductBuildVer)\b/i.test(fragment)) {
6477
+ return true;
6478
+ }
6479
+ if (/^\d+(?:Table|List|Heading|Title|Style)$/i.test(fragment)) {
6480
+ return true;
6481
+ }
6482
+ if (/[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[})]?/i.test(fragment)) {
6483
+ return true;
6484
+ }
6485
+ if (/^[A-Z_]{3,}$/.test(fragment) || /^[A-Za-z]+(?:Information|Document|Storage|Stream|Table|Data|Pool|Obj|Props)$/i.test(fragment)) {
6486
+ return true;
6487
+ }
6488
+ return false;
6489
+ }
6490
+ function isLikelyCjkHeading(fragment) {
6491
+ return /^(?:标题|第[一二三四五六七八九十\d]+[章节条]|[一二三四五六七八九十\d]+[、..])\s*[\p{L}\p{N}\s-]*$/u.test(fragment);
6492
+ }
6493
+ function isAsciiBytePairCjk(char) {
6494
+ const code = char.codePointAt(0) || 0;
6495
+ if (code < 13312 || code > 40959) {
6496
+ return false;
6497
+ }
6498
+ const low = code & 255;
6499
+ const high = code >> 8;
6500
+ return isPrintableAsciiByte(low) && isPrintableAsciiByte(high);
6501
+ }
6502
+ function isPrintableAsciiByte(value) {
6503
+ return value >= 32 && value <= 126;
6504
+ }
6084
6505
  function extractPrintableRuns(bytes) {
6085
6506
  const fragments = [];
6086
6507
  let current = "";
@@ -6103,6 +6524,13 @@ function extractUtf16Runs(bytes) {
6103
6524
  const fragments = [];
6104
6525
  let current = "";
6105
6526
  for (let index = 0; index < bytes.length - 1; index += 2) {
6527
+ if (looksLikeMisalignedAsciiUtf16(bytes[index], bytes[index + 1])) {
6528
+ if (current.length >= 3) {
6529
+ fragments.push(current);
6530
+ }
6531
+ current = "";
6532
+ continue;
6533
+ }
6106
6534
  const code = bytes[index] | bytes[index + 1] << 8;
6107
6535
  if (code >= 32 && code <= 55295 || code === 9) {
6108
6536
  current += String.fromCharCode(code);
@@ -6118,6 +6546,9 @@ function extractUtf16Runs(bytes) {
6118
6546
  }
6119
6547
  return fragments;
6120
6548
  }
6549
+ function looksLikeMisalignedAsciiUtf16(lowByte, highByte) {
6550
+ return lowByte === 0 && (highByte >= 48 && highByte <= 57 || highByte >= 65 && highByte <= 90 || highByte >= 97 && highByte <= 122);
6551
+ }
6121
6552
  function normalizeLegacyText(value) {
6122
6553
  return value.replace(/[\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim();
6123
6554
  }
@@ -6755,18 +7186,45 @@ function archivePlugin() {
6755
7186
  layout.className = "ofv-archive-layout";
6756
7187
  const sidebar = document.createElement("div");
6757
7188
  sidebar.className = "ofv-archive-sidebar";
7189
+ const sidebarPanel = document.createElement("div");
7190
+ sidebarPanel.className = "ofv-archive-sidebar-panel";
6758
7191
  const header = document.createElement("div");
6759
7192
  header.className = "ofv-archive-header";
6760
- header.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
6761
- sidebar.append(header);
7193
+ const sidebarTitle = document.createElement("span");
7194
+ sidebarTitle.className = "ofv-archive-header-title";
7195
+ sidebarTitle.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
7196
+ const sidebarToggle = document.createElement("button");
7197
+ sidebarToggle.className = "ofv-archive-sidebar-toggle";
7198
+ sidebarToggle.type = "button";
7199
+ sidebarToggle.setAttribute("aria-label", "\u5C55\u5F00\u6587\u4EF6\u5217\u8868");
7200
+ sidebarToggle.setAttribute("aria-expanded", "false");
7201
+ sidebarToggle.title = "\u5C55\u5F00\u6587\u4EF6\u5217\u8868";
7202
+ sidebarToggle.textContent = "\u2039";
7203
+ header.append(sidebarToggle, sidebarTitle);
7204
+ sidebarPanel.append(header);
6762
7205
  const tree = document.createElement("div");
6763
7206
  tree.className = "ofv-archive-tree";
6764
- sidebar.append(tree);
7207
+ sidebarPanel.append(tree);
7208
+ sidebar.append(sidebarPanel);
6765
7209
  const mainPanel = document.createElement("div");
6766
7210
  mainPanel.className = "ofv-archive-main";
6767
7211
  layout.append(sidebar, mainPanel);
6768
7212
  panel.append(layout);
6769
7213
  let currentSubInstance = null;
7214
+ const getSidebarViewportWidth = () => ctx.viewport.clientWidth || ctx.size.width;
7215
+ const shouldAutoCollapseSidebar = () => getSidebarViewportWidth() <= 520;
7216
+ const setSidebarCollapsed = (collapsed) => {
7217
+ layout.classList.toggle("is-sidebar-collapsed", collapsed);
7218
+ sidebarToggle.setAttribute("aria-expanded", String(!collapsed));
7219
+ const label = collapsed ? "\u5C55\u5F00\u6587\u4EF6\u5217\u8868" : "\u6536\u8D77\u6587\u4EF6\u5217\u8868";
7220
+ sidebarToggle.setAttribute("aria-label", label);
7221
+ sidebarToggle.title = label;
7222
+ sidebarToggle.textContent = collapsed ? "\u203A" : "\u2039";
7223
+ };
7224
+ setSidebarCollapsed(false);
7225
+ sidebarToggle.addEventListener("click", () => {
7226
+ setSidebarCollapsed(!layout.classList.contains("is-sidebar-collapsed"));
7227
+ });
6770
7228
  const showDefaultSummary = () => {
6771
7229
  mainPanel.replaceChildren();
6772
7230
  const summary = document.createElement("div");
@@ -6806,6 +7264,9 @@ function archivePlugin() {
6806
7264
  if (destroyed) {
6807
7265
  return;
6808
7266
  }
7267
+ if (shouldAutoCollapseSidebar()) {
7268
+ setSidebarCollapsed(true);
7269
+ }
6809
7270
  const token = ++renderToken;
6810
7271
  sidebar.querySelectorAll(".ofv-archive-item").forEach((el) => {
6811
7272
  el.classList.remove("is-active");
@@ -9088,7 +9549,7 @@ function cadPlugin() {
9088
9549
  return { destroy: () => panel.remove() };
9089
9550
  }
9090
9551
  const dxf = await readTextFile(ctx.file);
9091
- const viewer = renderDxf(panel, dxf);
9552
+ const viewer = renderDxf(panel, dxf, ctx);
9092
9553
  return {
9093
9554
  canCommand(command) {
9094
9555
  return viewer.canCommand(command);
@@ -9557,7 +10018,7 @@ function hexPreview(bytes) {
9557
10018
  }
9558
10019
  return rows.join("\n") || "\u65E0\u53EF\u5C55\u793A\u5B57\u8282\u3002";
9559
10020
  }
9560
- function renderDxf(panel, dxf) {
10021
+ function renderDxf(panel, dxf, ctx) {
9561
10022
  const pairs = dxf.split(/\r?\n/).map((line) => line.trim());
9562
10023
  const lines = [];
9563
10024
  const circles = [];
@@ -9735,6 +10196,10 @@ function renderDxf(panel, dxf) {
9735
10196
  note.textContent = `\u5DF2\u63D0\u53D6 LINE ${lines.length} \u4E2A\u3001CIRCLE ${circles.length} \u4E2A\u3001ARC ${arcs.length} \u4E2A\u3001POLYLINE ${polylines.length} \u4E2A\u3001POINT ${points.length} \u4E2A\u3001TEXT ${texts.length} \u4E2A\u3002`;
9736
10197
  section.append(note, svg);
9737
10198
  panel.append(section);
10199
+ const updateToolbarZoom = () => {
10200
+ ctx.toolbar?.setZoom(initialViewBox.width / currentViewBox.width);
10201
+ };
10202
+ updateToolbarZoom();
9738
10203
  return {
9739
10204
  canCommand(command) {
9740
10205
  return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset";
@@ -9749,14 +10214,19 @@ function renderDxf(panel, dxf) {
9749
10214
  currentViewBox.x = centerX - currentViewBox.width / 2;
9750
10215
  currentViewBox.y = centerY - currentViewBox.height / 2;
9751
10216
  applyViewBox();
10217
+ updateToolbarZoom();
9752
10218
  return true;
9753
10219
  }
9754
10220
  if (command === "zoom-reset") {
9755
10221
  currentViewBox = { ...initialViewBox };
9756
10222
  applyViewBox();
10223
+ updateToolbarZoom();
9757
10224
  return true;
9758
10225
  }
9759
10226
  return false;
10227
+ },
10228
+ destroy() {
10229
+ ctx.toolbar?.setZoom(void 0);
9760
10230
  }
9761
10231
  };
9762
10232
  }
@@ -10032,6 +10502,12 @@ function model3dPlugin() {
10032
10502
  renderer.setSize(width, height, false);
10033
10503
  };
10034
10504
  resize(ctx.size);
10505
+ const updateToolbarZoom = () => {
10506
+ const currentDistance = vectorDistance(camera.position, controls.target);
10507
+ const initialDistance = vectorDistance(initialFrame.cameraPosition, initialFrame.target);
10508
+ ctx.toolbar?.setZoom(initialDistance > 0 ? initialDistance / currentDistance : void 0);
10509
+ };
10510
+ updateToolbarZoom();
10035
10511
  return {
10036
10512
  canCommand(command) {
10037
10513
  return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset" || command === "rotate-right" || command === "rotate-left";
@@ -10042,6 +10518,7 @@ function model3dPlugin() {
10042
10518
  camera.position.sub(controls.target).multiplyScalar(factor).add(controls.target);
10043
10519
  camera.updateProjectionMatrix();
10044
10520
  controls.update();
10521
+ updateToolbarZoom();
10045
10522
  return true;
10046
10523
  }
10047
10524
  if (command === "zoom-reset") {
@@ -10051,6 +10528,7 @@ function model3dPlugin() {
10051
10528
  camera.far = initialFrame.far;
10052
10529
  camera.updateProjectionMatrix();
10053
10530
  controls.update();
10531
+ updateToolbarZoom();
10054
10532
  return true;
10055
10533
  }
10056
10534
  if (command === "rotate-right" || command === "rotate-left") {
@@ -10061,6 +10539,7 @@ function model3dPlugin() {
10061
10539
  },
10062
10540
  resize,
10063
10541
  destroy() {
10542
+ ctx.toolbar?.setZoom(void 0);
10064
10543
  window.cancelAnimationFrame(animationFrame);
10065
10544
  controls.dispose();
10066
10545
  renderer.dispose();
@@ -10072,6 +10551,12 @@ function model3dPlugin() {
10072
10551
  }
10073
10552
  };
10074
10553
  }
10554
+ function vectorDistance(a, b) {
10555
+ if (typeof a.distanceTo === "function") {
10556
+ return a.distanceTo(b);
10557
+ }
10558
+ return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
10559
+ }
10075
10560
  function renderModelFallback(ctx, url, isExternal, message) {
10076
10561
  const panel = document.createElement("div");
10077
10562
  panel.className = "ofv-fallback";
@@ -10428,34 +10913,65 @@ function gisPlugin() {
10428
10913
  }
10429
10914
  const wrapper = document.createElement("div");
10430
10915
  wrapper.className = "ofv-gis-viewer";
10431
- wrapper.append(createGisSummary(geojson));
10916
+ const summary = summarizeGeoJson(geojson);
10917
+ wrapper.append(createGisSummary(summary));
10432
10918
  ctx.viewport.appendChild(wrapper);
10433
10919
  const mapContainer = document.createElement("div");
10434
10920
  mapContainer.className = "ofv-map-stage";
10435
10921
  wrapper.appendChild(mapContainer);
10922
+ if (summary.features === 0) {
10923
+ mapContainer.append(createEmptyMapState());
10924
+ } else {
10925
+ mapContainer.append(createMapLegend(summary));
10926
+ }
10436
10927
  const map = Leaflet.map(mapContainer).setView([0, 0], 2);
10437
10928
  Leaflet.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
10438
10929
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
10439
10930
  }).addTo(map);
10440
10931
  const geojsonLayer = Leaflet.geoJSON(geojson, {
10441
10932
  style: () => ({
10442
- color: "#3b82f6",
10933
+ className: "ofv-map-feature",
10934
+ color: "#e11d48",
10443
10935
  weight: 2,
10444
- opacity: 0.8,
10445
- fillColor: "#93c5fd",
10446
- fillOpacity: 0.35
10936
+ opacity: 0.92,
10937
+ fillColor: "#fb923c",
10938
+ fillOpacity: 0.3,
10939
+ lineCap: "round",
10940
+ lineJoin: "round"
10447
10941
  }),
10448
10942
  pointToLayer: (feature, latlng) => {
10449
10943
  return Leaflet.circleMarker(latlng, {
10450
- radius: 6,
10451
- fillColor: "#3b82f6",
10944
+ className: "ofv-map-feature ofv-map-point",
10945
+ radius: 7,
10946
+ fillColor: "#e11d48",
10452
10947
  color: "#ffffff",
10453
- weight: 1.5,
10948
+ weight: 2.5,
10454
10949
  opacity: 1,
10455
- fillOpacity: 0.8
10950
+ fillOpacity: 0.9
10456
10951
  });
10457
10952
  },
10458
10953
  onEachFeature: (feature, layer) => {
10954
+ const label = feature.properties?.name || feature.properties?.title || feature.properties?.label;
10955
+ if (label) {
10956
+ layer.bindTooltip?.(String(label), {
10957
+ className: "ofv-map-tooltip",
10958
+ direction: "top",
10959
+ sticky: true
10960
+ });
10961
+ }
10962
+ layer.on?.({
10963
+ mouseover(event) {
10964
+ event.target?.setStyle?.({
10965
+ weight: 4,
10966
+ opacity: 1,
10967
+ fillOpacity: 0.4
10968
+ });
10969
+ event.target?.bringToFront?.();
10970
+ },
10971
+ mouseout(event) {
10972
+ geojsonLayer.resetStyle?.(event.target);
10973
+ }
10974
+ });
10459
10975
  if (feature.properties) {
10460
10976
  const props = feature.properties;
10461
10977
  const keys = Object.keys(props);
@@ -10490,15 +11006,20 @@ function gisPlugin() {
10490
11006
  const bounds = geojsonLayer.getBounds();
10491
11007
  if (bounds.isValid()) {
10492
11008
  map.fitBounds(bounds, { padding: [20, 20] });
11009
+ map.invalidateSize();
10493
11010
  }
10494
11011
  } catch (e) {
10495
11012
  console.warn("Could not fit bounds for GeoJSON data:", e);
10496
11013
  }
11014
+ const resizeTimers = [0, 80, 240].map((delay) => window.setTimeout(() => {
11015
+ map.invalidateSize();
11016
+ }, delay));
10497
11017
  return {
10498
11018
  resize() {
10499
11019
  map.invalidateSize();
10500
11020
  },
10501
11021
  destroy() {
11022
+ resizeTimers.forEach((timer) => window.clearTimeout(timer));
10502
11023
  map.remove();
10503
11024
  wrapper.remove();
10504
11025
  }
@@ -10523,8 +11044,7 @@ function normalizeGisError(error, fileName) {
10523
11044
  function isGisFallback(value) {
10524
11045
  return typeof value === "object" && value !== null && "fallback" in value;
10525
11046
  }
10526
- function createGisSummary(geojson) {
10527
- const summary = summarizeGeoJson(geojson);
11047
+ function createGisSummary(summary) {
10528
11048
  const bar = document.createElement("div");
10529
11049
  bar.className = "ofv-gis-summary";
10530
11050
  appendSummaryItem(bar, "\u8981\u7D20", String(summary.features));
@@ -10538,6 +11058,26 @@ function createGisSummary(geojson) {
10538
11058
  }
10539
11059
  return bar;
10540
11060
  }
11061
+ function createEmptyMapState() {
11062
+ const empty = document.createElement("div");
11063
+ empty.className = "ofv-map-empty";
11064
+ const title = document.createElement("strong");
11065
+ title.textContent = "\u6682\u65E0\u53EF\u5C55\u793A\u7684\u5730\u56FE\u8981\u7D20";
11066
+ const detail = document.createElement("span");
11067
+ detail.textContent = "GeoJSON \u5DF2\u8BC6\u522B\uFF0C\u4F46 features \u4E3A\u7A7A\u3002";
11068
+ empty.append(title, detail);
11069
+ return empty;
11070
+ }
11071
+ function createMapLegend(summary) {
11072
+ const legend = document.createElement("div");
11073
+ legend.className = "ofv-map-legend";
11074
+ const title = document.createElement("strong");
11075
+ title.textContent = "GeoJSON";
11076
+ const detail = document.createElement("span");
11077
+ detail.textContent = `${summary.features} \u4E2A\u8981\u7D20 \xB7 ${formatGeometryCounts(summary.geometryCounts)}`;
11078
+ legend.append(title, detail);
11079
+ return legend;
11080
+ }
10541
11081
  function appendSummaryItem(parent, label, value) {
10542
11082
  const item = document.createElement("span");
10543
11083
  const key = document.createElement("span");