@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.cjs CHANGED
@@ -735,6 +735,7 @@ function createViewer(options) {
735
735
  file,
736
736
  size: getElementSize(viewport),
737
737
  options: normalizedOptions,
738
+ toolbar: toolbar?.getContext(),
738
739
  setLoading,
739
740
  setError
740
741
  });
@@ -908,18 +909,36 @@ function createToolbar(toolbar, viewport, queue) {
908
909
  element.setAttribute("role", "toolbar");
909
910
  element.setAttribute("aria-label", "File preview toolbar");
910
911
  let file;
912
+ let currentIndex = 0;
913
+ let currentLength = queue.getLength();
911
914
  let queueLabel;
912
915
  let previousButton;
913
916
  let nextButton;
917
+ let zoomResetButton;
918
+ let currentZoom;
914
919
  const commandButtons = [];
920
+ const customButtons = [];
915
921
  const disposers = [];
916
922
  const search = createSearchController(viewport);
917
923
  let searchInput;
918
924
  let searchCount;
919
- const addButton = (label, title, action, className) => {
925
+ let canRunCommand = (_command) => false;
926
+ const getContext = () => createToolbarContext({
927
+ file,
928
+ index: currentIndex,
929
+ length: currentLength,
930
+ viewport,
931
+ queue,
932
+ element,
933
+ search,
934
+ canCommand: canRunCommand,
935
+ zoom: currentZoom,
936
+ setZoom
937
+ });
938
+ const addButton = (label, title, action, className, icon) => {
920
939
  const button = document.createElement("button");
921
940
  button.type = "button";
922
- button.textContent = label;
941
+ setToolbarButtonContent(button, label, icon);
923
942
  button.title = title;
924
943
  button.setAttribute("aria-label", title);
925
944
  if (className) {
@@ -930,68 +949,119 @@ function createToolbar(toolbar, viewport, queue) {
930
949
  disposers.push(() => button.removeEventListener("click", action));
931
950
  return button;
932
951
  };
933
- const addQueueButton = (label, title, action) => {
934
- const button = document.createElement("button");
935
- button.type = "button";
936
- button.textContent = label;
937
- button.title = title;
938
- button.setAttribute("aria-label", title);
939
- const listener = () => {
940
- void action();
941
- };
942
- button.addEventListener("click", listener);
943
- element.append(button);
944
- disposers.push(() => button.removeEventListener("click", listener));
945
- return button;
946
- };
947
- if (queue.getLength() > 1) {
948
- previousButton = addQueueButton("Prev", "Previous file", queue.previous);
949
- nextButton = addQueueButton("Next", "Next file", queue.next);
950
- queueLabel = document.createElement("span");
951
- queueLabel.className = "ofv-toolbar-queue";
952
- element.append(queueLabel);
953
- }
954
- const addCommandButton = (label, title, command) => {
952
+ const addCommandButton = (id, label, title, command) => {
955
953
  const button = addButton(label, title, () => {
956
954
  queue.command(command);
957
- });
955
+ }, void 0, options.icons?.[id]);
958
956
  button.disabled = true;
959
957
  commandButtons.push({ button, command });
960
958
  };
961
- if (options.zoom) {
962
- addCommandButton("-", "Zoom out", "zoom-out");
963
- addCommandButton("+", "Zoom in", "zoom-in");
964
- addCommandButton("100%", "Reset zoom", "zoom-reset");
965
- }
966
- if (options.rotate) {
967
- addCommandButton("Rotate", "Rotate right", "rotate-right");
968
- }
969
- if (options.download !== false) {
970
- addButton("Download", "Download file", () => {
971
- if (file) {
972
- downloadFile(file);
959
+ const renderDefaultAction = (id) => {
960
+ if (!isBuiltInToolbarAction(id)) {
961
+ const customAction = options.actions?.find((action) => action.id === id);
962
+ if (customAction) {
963
+ renderCustomAction(customAction);
973
964
  }
974
- });
975
- }
976
- if (options.fullscreen !== false) {
977
- addButton("Fullscreen", "Open preview fullscreen", () => {
978
- const target = element.parentElement;
979
- void target?.requestFullscreen?.();
980
- });
981
- }
982
- if (options.print) {
983
- addButton("Print", "Print preview", () => {
984
- printPreview(viewport);
985
- });
986
- }
987
- if (options.search !== false) {
965
+ return;
966
+ }
967
+ if (id === "previous" && queue.getLength() > 1) {
968
+ previousButton = addButton(
969
+ getToolbarLabel(options, "previous"),
970
+ getToolbarTitle(options, "previous"),
971
+ () => void queue.previous(),
972
+ void 0,
973
+ options.icons?.previous
974
+ );
975
+ return;
976
+ }
977
+ if (id === "next" && queue.getLength() > 1) {
978
+ nextButton = addButton(
979
+ getToolbarLabel(options, "next"),
980
+ getToolbarTitle(options, "next"),
981
+ () => void queue.next(),
982
+ void 0,
983
+ options.icons?.next
984
+ );
985
+ return;
986
+ }
987
+ if (id === "queue" && queue.getLength() > 1) {
988
+ queueLabel = document.createElement("span");
989
+ queueLabel.className = "ofv-toolbar-queue";
990
+ element.append(queueLabel);
991
+ return;
992
+ }
993
+ if (id === "zoom-out" && options.zoom) {
994
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-out");
995
+ return;
996
+ }
997
+ if (id === "zoom-in" && options.zoom) {
998
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-in");
999
+ return;
1000
+ }
1001
+ if (id === "zoom-reset" && options.zoom) {
1002
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "zoom-reset");
1003
+ zoomResetButton = commandButtons[commandButtons.length - 1]?.button;
1004
+ updateZoomLabel();
1005
+ return;
1006
+ }
1007
+ if (id === "rotate-right" && options.rotate) {
1008
+ addCommandButton(id, getToolbarLabel(options, id), getToolbarTitle(options, id), "rotate-right");
1009
+ return;
1010
+ }
1011
+ if (id === "download" && options.download !== false) {
1012
+ addButton(
1013
+ getToolbarLabel(options, id),
1014
+ getToolbarTitle(options, id),
1015
+ () => getContext().download(),
1016
+ void 0,
1017
+ options.icons?.download
1018
+ );
1019
+ return;
1020
+ }
1021
+ if (id === "fullscreen" && options.fullscreen !== false) {
1022
+ addButton(
1023
+ getToolbarLabel(options, id),
1024
+ getToolbarTitle(options, id),
1025
+ () => getContext().fullscreen(),
1026
+ void 0,
1027
+ options.icons?.fullscreen
1028
+ );
1029
+ return;
1030
+ }
1031
+ if (id === "print" && options.print) {
1032
+ addButton(
1033
+ getToolbarLabel(options, id),
1034
+ getToolbarTitle(options, id),
1035
+ () => getContext().print(),
1036
+ void 0,
1037
+ options.icons?.print
1038
+ );
1039
+ return;
1040
+ }
1041
+ if (id === "search" && options.search !== false) {
1042
+ renderSearchControl();
1043
+ return;
1044
+ }
1045
+ };
1046
+ const renderCustomAction = (action) => {
1047
+ const button = addButton(
1048
+ action.label,
1049
+ action.title || action.label,
1050
+ () => void action.onClick(getContext()),
1051
+ action.className,
1052
+ action.icon
1053
+ );
1054
+ button.dataset.ofvToolbarAction = action.id;
1055
+ customButtons.push({ button, action });
1056
+ };
1057
+ const renderSearchControl = () => {
988
1058
  const searchGroup = document.createElement("div");
989
1059
  searchGroup.className = "ofv-toolbar-search";
990
- searchGroup.title = "Search preview text";
1060
+ searchGroup.title = getToolbarTitle(options, "search");
991
1061
  const nextSearchInput = document.createElement("input");
992
1062
  nextSearchInput.type = "search";
993
- nextSearchInput.placeholder = "Search";
994
- nextSearchInput.setAttribute("aria-label", "Search preview text");
1063
+ nextSearchInput.placeholder = getToolbarLabel(options, "search");
1064
+ nextSearchInput.setAttribute("aria-label", getToolbarTitle(options, "search"));
995
1065
  const nextSearchCount = document.createElement("span");
996
1066
  nextSearchCount.className = "ofv-toolbar-search-count";
997
1067
  searchInput = nextSearchInput;
@@ -1004,18 +1074,71 @@ function createToolbar(toolbar, viewport, queue) {
1004
1074
  searchGroup.append(nextSearchInput, nextSearchCount);
1005
1075
  element.append(searchGroup);
1006
1076
  disposers.push(() => nextSearchInput.removeEventListener("input", runSearch));
1077
+ };
1078
+ const renderToolbar = () => {
1079
+ if (options.render) {
1080
+ element.replaceChildren();
1081
+ const customElement = options.render(getContext());
1082
+ if (customElement) {
1083
+ element.append(customElement);
1084
+ }
1085
+ return;
1086
+ }
1087
+ getToolbarOrder(options, queue.getLength()).forEach(renderDefaultAction);
1088
+ getImplicitCustomActions(options).forEach(renderCustomAction);
1089
+ };
1090
+ renderToolbar();
1091
+ const updateCustomButtons = () => {
1092
+ const context = getContext();
1093
+ for (const { button, action } of customButtons) {
1094
+ button.disabled = evaluateToolbarFlag(action.disabled, context);
1095
+ button.hidden = evaluateToolbarFlag(action.hidden, context);
1096
+ }
1097
+ };
1098
+ const resetSearch = () => {
1099
+ search.clear();
1100
+ if (searchInput) {
1101
+ searchInput.value = "";
1102
+ }
1103
+ if (searchCount) {
1104
+ searchCount.textContent = "";
1105
+ }
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
+ );
1007
1122
  }
1123
+ const refreshCustomRender = () => {
1124
+ if (!options.render) {
1125
+ return;
1126
+ }
1127
+ element.replaceChildren();
1128
+ const customElement = options.render(getContext());
1129
+ if (customElement) {
1130
+ element.append(customElement);
1131
+ }
1132
+ };
1008
1133
  return {
1009
1134
  element,
1010
1135
  update(nextFile, index, length) {
1011
1136
  file = nextFile;
1012
- search.clear();
1013
- if (searchInput) {
1014
- searchInput.value = "";
1015
- }
1016
- if (searchCount) {
1017
- searchCount.textContent = "";
1018
- }
1137
+ currentIndex = index;
1138
+ currentLength = length;
1139
+ currentZoom = void 0;
1140
+ updateZoomLabel();
1141
+ resetSearch();
1019
1142
  commandButtons.forEach(({ button }) => {
1020
1143
  button.disabled = true;
1021
1144
  });
@@ -1028,20 +1151,257 @@ function createToolbar(toolbar, viewport, queue) {
1028
1151
  if (nextButton) {
1029
1152
  nextButton.disabled = index >= length - 1;
1030
1153
  }
1154
+ updateCustomButtons();
1155
+ refreshCustomRender();
1031
1156
  },
1032
1157
  setCommandSupport(isSupported) {
1158
+ canRunCommand = isSupported;
1033
1159
  commandButtons.forEach(({ button, command }) => {
1034
1160
  button.disabled = !isSupported(command);
1035
1161
  });
1162
+ updateCustomButtons();
1163
+ refreshCustomRender();
1036
1164
  },
1165
+ getContext,
1166
+ setZoom,
1037
1167
  destroy() {
1038
1168
  search.clear();
1039
1169
  for (const dispose of disposers) {
1040
1170
  dispose();
1041
1171
  }
1172
+ element.replaceChildren();
1042
1173
  }
1043
1174
  };
1044
1175
  }
1176
+ function createToolbarContext({
1177
+ file,
1178
+ index,
1179
+ length,
1180
+ viewport,
1181
+ queue,
1182
+ element,
1183
+ search,
1184
+ canCommand,
1185
+ zoom,
1186
+ setZoom
1187
+ }) {
1188
+ return {
1189
+ file,
1190
+ index,
1191
+ length,
1192
+ viewport,
1193
+ canPrevious: index > 0,
1194
+ canNext: index < length - 1,
1195
+ zoom,
1196
+ zoomLabel: zoom === void 0 ? void 0 : formatToolbarZoom(zoom),
1197
+ async previous() {
1198
+ await queue.previous();
1199
+ },
1200
+ async next() {
1201
+ await queue.next();
1202
+ },
1203
+ command: queue.command,
1204
+ canCommand,
1205
+ setZoom,
1206
+ download() {
1207
+ if (file) {
1208
+ downloadFile(file);
1209
+ }
1210
+ },
1211
+ fullscreen() {
1212
+ void element.parentElement?.requestFullscreen?.();
1213
+ },
1214
+ print() {
1215
+ printPreview(viewport);
1216
+ },
1217
+ search: search.search,
1218
+ clearSearch: search.clear
1219
+ };
1220
+ }
1221
+ var defaultToolbarLabels = {
1222
+ previous: "Prev",
1223
+ next: "Next",
1224
+ queue: "",
1225
+ "zoom-out": "-",
1226
+ "zoom-in": "+",
1227
+ "zoom-reset": "100%",
1228
+ "rotate-right": "Rotate",
1229
+ download: "Download",
1230
+ fullscreen: "Fullscreen",
1231
+ print: "Print",
1232
+ search: "Search"
1233
+ };
1234
+ var defaultToolbarTitles = {
1235
+ previous: "Previous file",
1236
+ next: "Next file",
1237
+ queue: "Current file position",
1238
+ "zoom-out": "Zoom out",
1239
+ "zoom-in": "Zoom in",
1240
+ "zoom-reset": "Reset zoom",
1241
+ "rotate-right": "Rotate right",
1242
+ download: "Download file",
1243
+ fullscreen: "Open preview fullscreen",
1244
+ print: "Print preview",
1245
+ search: "Search preview text"
1246
+ };
1247
+ function getToolbarLabel(options, id) {
1248
+ return options.labels?.[id] ?? defaultToolbarLabels[id];
1249
+ }
1250
+ function getToolbarTitle(options, id) {
1251
+ return options.titles?.[id] ?? options.labels?.[id] ?? defaultToolbarTitles[id];
1252
+ }
1253
+ function formatToolbarZoom(zoom) {
1254
+ return `${Math.round(zoom * 100)}%`;
1255
+ }
1256
+ function getToolbarOrder(options, queueLength) {
1257
+ if (options.order) {
1258
+ return options.order;
1259
+ }
1260
+ const actions = [];
1261
+ if (queueLength > 1) {
1262
+ actions.push("previous", "next", "queue");
1263
+ }
1264
+ if (options.zoom) {
1265
+ actions.push("zoom-out", "zoom-in", "zoom-reset");
1266
+ }
1267
+ if (options.rotate) {
1268
+ actions.push("rotate-right");
1269
+ }
1270
+ if (options.download !== false) {
1271
+ actions.push("download");
1272
+ }
1273
+ if (options.fullscreen !== false) {
1274
+ actions.push("fullscreen");
1275
+ }
1276
+ if (options.print) {
1277
+ actions.push("print");
1278
+ }
1279
+ if (options.search !== false) {
1280
+ actions.push("search");
1281
+ }
1282
+ return actions;
1283
+ }
1284
+ function getImplicitCustomActions(options) {
1285
+ if (options.order || !options.actions) {
1286
+ return [];
1287
+ }
1288
+ return [...options.actions].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1289
+ }
1290
+ function evaluateToolbarFlag(value, context) {
1291
+ return typeof value === "function" ? value(context) : Boolean(value);
1292
+ }
1293
+ function setToolbarButtonContent(button, label, icon) {
1294
+ button.replaceChildren();
1295
+ if (!icon) {
1296
+ button.textContent = label;
1297
+ return;
1298
+ }
1299
+ const iconElement = document.createElement("span");
1300
+ iconElement.className = "ofv-toolbar-icon";
1301
+ iconElement.setAttribute("aria-hidden", "true");
1302
+ if (typeof icon === "string") {
1303
+ iconElement.append(sanitizeToolbarIcon(icon));
1304
+ } else {
1305
+ iconElement.append(icon.cloneNode(true));
1306
+ }
1307
+ const labelElement = document.createElement("span");
1308
+ labelElement.className = "ofv-toolbar-label";
1309
+ labelElement.textContent = label;
1310
+ button.append(iconElement, labelElement);
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
+ }
1402
+ function isBuiltInToolbarAction(id) {
1403
+ return id in defaultToolbarLabels;
1404
+ }
1045
1405
  function createSearchController(root) {
1046
1406
  const markerClass = "ofv-search-match";
1047
1407
  const clear = () => {
@@ -1450,6 +1810,7 @@ function imagePlugin() {
1450
1810
  const updateTransform = () => {
1451
1811
  image.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale}) rotate(${rotation}deg)`;
1452
1812
  zoomLabel.textContent = `${Math.round(scale * 100)}%`;
1813
+ ctx.toolbar?.setZoom(scale);
1453
1814
  };
1454
1815
  const showImageFallback = () => {
1455
1816
  stage.replaceChildren(createImageFallback(ctx.file.name, url));
@@ -1565,6 +1926,7 @@ function imagePlugin() {
1565
1926
  image.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
1566
1927
  },
1567
1928
  destroy() {
1929
+ ctx.toolbar?.setZoom(void 0);
1568
1930
  for (const dispose of disposers) {
1569
1931
  dispose();
1570
1932
  }
@@ -3123,7 +3485,6 @@ var mimeLangMap = {
3123
3485
  };
3124
3486
  var MAX_HIGHLIGHT_CHARS = 18e4;
3125
3487
  var MAX_RENDER_CHARS = 6e5;
3126
- var MONACO_LOADER_KEY = "__OFV_MONACO_LOADER__";
3127
3488
  function loadPrismCss(theme) {
3128
3489
  const lightId = "ofv-prism-css-light";
3129
3490
  const darkId = "ofv-prism-css-dark";
@@ -3347,10 +3708,6 @@ function textPlugin() {
3347
3708
  wrapButton.addEventListener("click", () => {
3348
3709
  const wrapped = wrapper.classList.toggle("is-wrapped");
3349
3710
  wrapButton.setAttribute("aria-pressed", String(wrapped));
3350
- monacoEditor?.updateOptions?.({ wordWrap: wrapped ? "on" : "off" });
3351
- if (fallbackEditor) {
3352
- fallbackEditor.wrap = wrapped ? "soft" : "off";
3353
- }
3354
3711
  });
3355
3712
  const copyButton = document.createElement("button");
3356
3713
  copyButton.type = "button";
@@ -3375,12 +3732,7 @@ function textPlugin() {
3375
3732
  downloadText(ctx.file.name, text);
3376
3733
  status.textContent = "Download ready";
3377
3734
  });
3378
- const editorButton = document.createElement("button");
3379
- editorButton.type = "button";
3380
- editorButton.className = "ofv-code-action";
3381
- editorButton.textContent = "Editor";
3382
- editorButton.setAttribute("aria-pressed", "false");
3383
- actions.append(status, editorButton, wrapButton, copyButton, downloadButton);
3735
+ actions.append(wrapButton, copyButton, downloadButton, status);
3384
3736
  header.append(title, actions);
3385
3737
  const structureSummary = createTextStructureSummary(text, ext, lang, ctx.file.mimeType);
3386
3738
  const body = document.createElement("div");
@@ -3396,93 +3748,6 @@ function textPlugin() {
3396
3748
  code.textContent = codeText;
3397
3749
  pre.appendChild(code);
3398
3750
  body.append(gutter, pre);
3399
- const editorHost = document.createElement("div");
3400
- editorHost.className = "ofv-code-editor";
3401
- editorHost.hidden = true;
3402
- let monacoEditor;
3403
- let monacoModel;
3404
- let fallbackEditor;
3405
- let editorReady = false;
3406
- let editorLoading;
3407
- const showReader = () => {
3408
- editorHost.hidden = true;
3409
- body.hidden = false;
3410
- wrapper.classList.remove("is-editor");
3411
- editorButton.textContent = "Editor";
3412
- editorButton.setAttribute("aria-pressed", "false");
3413
- };
3414
- const showEditor = async () => {
3415
- if (text.length > MAX_RENDER_CHARS) {
3416
- status.textContent = "Editor skipped for large file";
3417
- return;
3418
- }
3419
- if (editorReady) {
3420
- body.hidden = true;
3421
- editorHost.hidden = false;
3422
- wrapper.classList.add("is-editor");
3423
- editorButton.textContent = "Reader";
3424
- editorButton.setAttribute("aria-pressed", "true");
3425
- monacoEditor?.layout?.();
3426
- return;
3427
- }
3428
- if (!editorLoading) {
3429
- editorLoading = (async () => {
3430
- editorButton.disabled = true;
3431
- status.textContent = "Loading editor";
3432
- try {
3433
- const monaco = await loadMonaco();
3434
- if (!monaco.editor?.create) {
3435
- throw new Error("Monaco editor API is unavailable.");
3436
- }
3437
- const monacoLanguage = toMonacoLanguage(ext, lang);
3438
- monaco.editor.setTheme?.(isDark ? "vs-dark" : "vs");
3439
- monacoModel = monaco.editor.createModel?.(text, monacoLanguage);
3440
- monacoEditor = monaco.editor.create(editorHost, {
3441
- ...monacoModel ? { model: monacoModel } : { value: text, language: monacoLanguage },
3442
- automaticLayout: true,
3443
- fontFamily: "var(--ofv-font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace)",
3444
- fontSize: 13,
3445
- lineNumbers: "on",
3446
- minimap: { enabled: text.length <= 8e4 },
3447
- readOnly: true,
3448
- renderLineHighlight: "line",
3449
- scrollBeyondLastLine: false,
3450
- wordWrap: wrapper.classList.contains("is-wrapped") ? "on" : "off"
3451
- });
3452
- editorReady = true;
3453
- status.textContent = "Editor ready";
3454
- body.hidden = true;
3455
- editorHost.hidden = false;
3456
- wrapper.classList.add("is-editor");
3457
- editorButton.textContent = "Reader";
3458
- editorButton.setAttribute("aria-pressed", "true");
3459
- monacoEditor.layout?.();
3460
- } catch (error) {
3461
- console.warn("Monaco editor failed to load; using built-in code editor fallback:", error);
3462
- fallbackEditor = createBasicCodeEditor(text, wrapper.classList.contains("is-wrapped"));
3463
- editorHost.replaceChildren(fallbackEditor);
3464
- editorReady = true;
3465
- status.textContent = "Basic editor";
3466
- body.hidden = true;
3467
- editorHost.hidden = false;
3468
- wrapper.classList.add("is-editor");
3469
- editorButton.textContent = "Reader";
3470
- editorButton.setAttribute("aria-pressed", "true");
3471
- } finally {
3472
- editorButton.disabled = false;
3473
- }
3474
- })();
3475
- }
3476
- await editorLoading;
3477
- };
3478
- editorButton.addEventListener("click", () => {
3479
- if (editorHost.hidden) {
3480
- void showEditor();
3481
- } else {
3482
- showReader();
3483
- status.textContent = "Reader mode";
3484
- }
3485
- });
3486
3751
  wrapper.append(header);
3487
3752
  if (structureSummary) {
3488
3753
  wrapper.append(structureSummary);
@@ -3500,7 +3765,6 @@ function textPlugin() {
3500
3765
  wrapper.append(notice);
3501
3766
  }
3502
3767
  wrapper.appendChild(body);
3503
- wrapper.appendChild(editorHost);
3504
3768
  ctx.viewport.appendChild(wrapper);
3505
3769
  if (shouldHighlight) {
3506
3770
  try {
@@ -3510,12 +3774,7 @@ function textPlugin() {
3510
3774
  }
3511
3775
  }
3512
3776
  return {
3513
- resize() {
3514
- monacoEditor?.layout?.();
3515
- },
3516
3777
  destroy() {
3517
- monacoEditor?.dispose?.();
3518
- monacoModel?.dispose?.();
3519
3778
  wrapper.remove();
3520
3779
  }
3521
3780
  };
@@ -3530,45 +3789,6 @@ function normalizeFileName2(name) {
3530
3789
  const baseName = name.split(/[\\/]/).pop() || name;
3531
3790
  return baseName.toLowerCase();
3532
3791
  }
3533
- async function loadMonaco() {
3534
- const injectedLoader = globalThis[MONACO_LOADER_KEY];
3535
- if (typeof injectedLoader === "function") {
3536
- return await injectedLoader();
3537
- }
3538
- const importer = new Function("specifier", "return import(specifier)");
3539
- return importer("monaco-editor");
3540
- }
3541
- function toMonacoLanguage(ext, prismLanguage) {
3542
- if (ext === "vue") {
3543
- return "html";
3544
- }
3545
- if (ext === "tsx") {
3546
- return "typescript";
3547
- }
3548
- if (ext === "jsx") {
3549
- return "javascript";
3550
- }
3551
- if (prismLanguage === "markup") {
3552
- return ext === "xml" ? "xml" : "html";
3553
- }
3554
- if (prismLanguage === "bash") {
3555
- return "shell";
3556
- }
3557
- if (prismLanguage === "none") {
3558
- return "plaintext";
3559
- }
3560
- return prismLanguage;
3561
- }
3562
- function createBasicCodeEditor(text, wrapped) {
3563
- const textarea = document.createElement("textarea");
3564
- textarea.className = "ofv-code-editor-fallback";
3565
- textarea.readOnly = true;
3566
- textarea.spellcheck = false;
3567
- textarea.wrap = wrapped ? "soft" : "off";
3568
- textarea.value = text;
3569
- textarea.setAttribute("aria-label", "Code editor preview");
3570
- return textarea;
3571
- }
3572
3792
  function createTextFallback(fileName, url) {
3573
3793
  const fallback = document.createElement("div");
3574
3794
  fallback.className = "ofv-fallback";
@@ -3806,7 +4026,13 @@ function pdfPlugin(options = {}) {
3806
4026
  scroller.className = "ofv-pdf ofv-pdf-pages";
3807
4027
  viewer.append(summary, scroller);
3808
4028
  ctx.viewport.append(viewer);
3809
- 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
+ });
3810
4036
  const doc = await documentTask.promise.catch((error) => {
3811
4037
  viewer.remove();
3812
4038
  ctx.viewport.classList.add("ofv-center");
@@ -3842,6 +4068,7 @@ function pdfPlugin(options = {}) {
3842
4068
  let zoomFactor = 1;
3843
4069
  const updateSummary = () => {
3844
4070
  renderPdfSummary(summary, doc.numPages, pagesMeta, ctx.options.fit, zoomFactor);
4071
+ ctx.toolbar?.setZoom(zoomFactor);
3845
4072
  };
3846
4073
  const clearPage = (pageIdx) => {
3847
4074
  const state = pageStates[pageIdx];
@@ -3865,12 +4092,17 @@ function pdfPlugin(options = {}) {
3865
4092
  try {
3866
4093
  const page = await doc.getPage(pageIdx + 1);
3867
4094
  const meta = pagesMeta[pageIdx];
3868
- 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));
3869
4096
  const viewport = page.getViewport({ scale });
4097
+ const outputScale = getPdfOutputScale();
4098
+ const cssWidth = Math.floor(viewport.width);
4099
+ const cssHeight = Math.floor(viewport.height);
3870
4100
  const canvas = document.createElement("canvas");
3871
4101
  canvas.className = "ofv-pdf-page";
3872
- canvas.width = Math.floor(viewport.width);
3873
- 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`;
3874
4106
  const context = canvas.getContext("2d");
3875
4107
  if (!context) {
3876
4108
  throw new Error("Canvas 2D context is not available.");
@@ -3879,7 +4111,8 @@ function pdfPlugin(options = {}) {
3879
4111
  state.canvas = canvas;
3880
4112
  const renderTask = page.render({
3881
4113
  canvasContext: context,
3882
- viewport
4114
+ viewport,
4115
+ transform: outputScale === 1 ? void 0 : [outputScale, 0, 0, outputScale, 0, 0]
3883
4116
  });
3884
4117
  state.renderTask = renderTask;
3885
4118
  await renderTask.promise;
@@ -3887,8 +4120,8 @@ function pdfPlugin(options = {}) {
3887
4120
  const textContent = await page.getTextContent();
3888
4121
  const textLayer = document.createElement("div");
3889
4122
  textLayer.className = "ofv-pdf-text-layer";
3890
- textLayer.style.width = `${Math.floor(viewport.width)}px`;
3891
- textLayer.style.height = `${Math.floor(viewport.height)}px`;
4123
+ textLayer.style.width = `${cssWidth}px`;
4124
+ textLayer.style.height = `${cssHeight}px`;
3892
4125
  state.wrapper.appendChild(textLayer);
3893
4126
  for (const item of textContent.items) {
3894
4127
  if (!("str" in item)) continue;
@@ -3949,7 +4182,7 @@ function pdfPlugin(options = {}) {
3949
4182
  }
3950
4183
  for (let i = 0; i < doc.numPages; i++) {
3951
4184
  const meta = pagesMeta[i];
3952
- 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));
3953
4186
  const w = Math.floor(meta.width * scale);
3954
4187
  const h = Math.floor(meta.height * scale);
3955
4188
  const wrapper = document.createElement("div");
@@ -4004,6 +4237,7 @@ function pdfPlugin(options = {}) {
4004
4237
  }, 120);
4005
4238
  },
4006
4239
  destroy() {
4240
+ ctx.toolbar?.setZoom(void 0);
4007
4241
  window.clearTimeout(resizeTimer);
4008
4242
  observer?.disconnect();
4009
4243
  pageStates.forEach((state) => {
@@ -4022,6 +4256,19 @@ function pdfPlugin(options = {}) {
4022
4256
  }
4023
4257
  };
4024
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
+ }
4025
4272
  function renderPdfSummary(summary, pages, pagesMeta, fit, zoomFactor) {
4026
4273
  summary.replaceChildren();
4027
4274
  appendPdfSummary(summary, "\u9875\u6570", String(pages));
@@ -4781,8 +5028,9 @@ function officePlugin() {
4781
5028
  ctx.viewport.append(panel);
4782
5029
  const extension = resolveFormat(ctx.file, officeMimeFormatMap);
4783
5030
  const arrayBuffer = await readArrayBuffer(ctx.file);
5031
+ let disposeDocxFit;
4784
5032
  if (fileIsDocx(extension)) {
4785
- await renderDocx(panel, arrayBuffer);
5033
+ disposeDocxFit = await renderDocx(panel, arrayBuffer);
4786
5034
  } else if (extension === "rtf") {
4787
5035
  renderPlainDocument(panel, "RTF \u6587\u6863", rtfToText(await readTextFromBuffer(arrayBuffer)));
4788
5036
  } else if (extension === "odt") {
@@ -4807,6 +5055,7 @@ function officePlugin() {
4807
5055
  }
4808
5056
  return {
4809
5057
  destroy() {
5058
+ disposeDocxFit?.();
4810
5059
  panel.remove();
4811
5060
  }
4812
5061
  };
@@ -4840,6 +5089,7 @@ async function renderDocx(panel, arrayBuffer) {
4840
5089
  experimental: true,
4841
5090
  useBase64URL: true
4842
5091
  });
5092
+ return fitDocxPages(content);
4843
5093
  } catch (error) {
4844
5094
  content.replaceChildren();
4845
5095
  const fallbackNote = document.createElement("div");
@@ -4854,6 +5104,74 @@ async function renderDocx(panel, arrayBuffer) {
4854
5104
  }
4855
5105
  console.warn("DOCX layout preview failed, fell back to Mammoth:", error);
4856
5106
  }
5107
+ return () => void 0;
5108
+ }
5109
+ function fitDocxPages(container) {
5110
+ const wrapper = container.querySelector(".ofv-docx-wrapper");
5111
+ if (!wrapper) {
5112
+ return () => void 0;
5113
+ }
5114
+ const update = () => {
5115
+ const frames = ensureDocxPageFrames(wrapper);
5116
+ if (frames.length === 0) {
5117
+ wrapper.style.removeProperty("--ofv-docx-scale");
5118
+ return;
5119
+ }
5120
+ const availableWidth = Math.max(1, container.clientWidth - 48);
5121
+ const pageWidth = Math.max(
5122
+ 1,
5123
+ ...frames.map(({ page }) => {
5124
+ const rectWidth = page.getBoundingClientRect().width;
5125
+ return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
5126
+ })
5127
+ );
5128
+ const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
5129
+ wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
5130
+ wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
5131
+ for (const { frame, page } of frames) {
5132
+ const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
5133
+ if (pageHeight > 0) {
5134
+ frame.style.height = `${Math.ceil(pageHeight * scale)}px`;
5135
+ }
5136
+ }
5137
+ };
5138
+ update();
5139
+ const timers = [0, 80, 240].map((delay) => window.setTimeout(update, delay));
5140
+ if (typeof ResizeObserver === "undefined") {
5141
+ window.addEventListener("resize", update);
5142
+ return () => {
5143
+ timers.forEach((timer) => window.clearTimeout(timer));
5144
+ window.removeEventListener("resize", update);
5145
+ };
5146
+ }
5147
+ const observer = new ResizeObserver(update);
5148
+ observer.observe(container);
5149
+ observer.observe(wrapper);
5150
+ return () => {
5151
+ timers.forEach((timer) => window.clearTimeout(timer));
5152
+ observer.disconnect();
5153
+ };
5154
+ }
5155
+ function ensureDocxPageFrames(wrapper) {
5156
+ const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
5157
+ return pages.map((page) => {
5158
+ const parent = page.parentElement;
5159
+ if (parent?.classList.contains("ofv-docx-page-frame")) {
5160
+ return { frame: parent, page };
5161
+ }
5162
+ const frame = document.createElement("div");
5163
+ frame.className = "ofv-docx-page-frame";
5164
+ page.before(frame);
5165
+ frame.append(page);
5166
+ return { frame, page };
5167
+ });
5168
+ }
5169
+ function parseCssPixelValue(value) {
5170
+ const parsed = Number.parseFloat(value);
5171
+ return Number.isFinite(parsed) ? parsed : 0;
5172
+ }
5173
+ function formatCssNumber(value) {
5174
+ return Number.isFinite(value) ? value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "") : "1";
4857
5175
  }
4858
5176
  async function renderDocxTextFallback(container, arrayBuffer) {
4859
5177
  const article = document.createElement("article");
@@ -4952,7 +5270,7 @@ async function renderSheet(panel, arrayBuffer, extension) {
4952
5270
  workbook = xlsx.read(arrayBuffer, { type: "array" });
4953
5271
  } catch (error) {
4954
5272
  if (isLegacyOfficeBinary(extension)) {
4955
- renderLegacyOfficeBinary(panel, extension, arrayBuffer, normalizeOfficeError(error));
5273
+ renderLegacyOfficeBinary(panel, extension, arrayBuffer, `\u8868\u683C\u89E3\u6790\u5931\u8D25\uFF1A${normalizeOfficeError(error)}`);
4956
5274
  return;
4957
5275
  }
4958
5276
  renderSheetFallback(panel, extension, normalizeOfficeError(error));
@@ -6057,14 +6375,15 @@ function legacyOfficeFormatLabel(extension) {
6057
6375
  function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6058
6376
  const fragments = extractLegacyOfficeText(arrayBuffer);
6059
6377
  panel.replaceChildren();
6060
- const section = createSection("Office \u4E8C\u8FDB\u5236\u57FA\u7840\u9884\u89C8");
6378
+ const section = createSection("Office \u8F6C\u6362\u63D0\u793A");
6379
+ section.classList.add("ofv-office-conversion");
6061
6380
  const format = document.createElement("p");
6062
6381
  const strong = document.createElement("strong");
6063
6382
  strong.textContent = `.${extension}`;
6064
6383
  format.append(
6065
6384
  strong,
6066
6385
  document.createTextNode(
6067
- " \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\u5148\u63D0\u53D6\u53EF\u8BFB\u6587\u672C\u7247\u6BB5\u548C\u7ED3\u6784\u6307\u7EB9\uFF0C\u5B8C\u6574\u7248\u5F0F\u5EFA\u8BAE\u63A5\u5165 LibreOffice/OnlyOffice \u670D\u52A1\u7AEF\u8F6C\u6362\u3002"
6386
+ " \u5C5E\u4E8E\u65E7\u7248 Microsoft Office \u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u6D4F\u89C8\u5668\u5185\u65E0\u6CD5\u9AD8\u4FDD\u771F\u89E3\u6790\uFF1B\u5F53\u524D\u4EC5\u5C55\u793A\u53EF\u4FE1\u6587\u672C\u7247\u6BB5\u548C\u7ED3\u6784\u6307\u7EB9\uFF0C\u5B8C\u6574\u6392\u7248\u5EFA\u8BAE\u63A5\u5165 LibreOffice/OnlyOffice \u670D\u52A1\u7AEF\u8F6C\u6362\u4E3A PDF/HTML\u3002"
6068
6387
  )
6069
6388
  );
6070
6389
  const meta = document.createElement("dl");
@@ -6073,12 +6392,15 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6073
6392
  appendOfficeBinaryMeta(meta, "\u6587\u4EF6\u7ED3\u6784", hasOleSignature(arrayBuffer) ? "\u68C0\u6D4B\u5230 OLE Compound File \u7B7E\u540D" : "\u672A\u68C0\u6D4B\u5230\u6807\u51C6 OLE \u7B7E\u540D\uFF0C\u6309\u539F\u59CB\u4E8C\u8FDB\u5236\u5C1D\u8BD5\u63D0\u53D6");
6074
6393
  appendOfficeBinaryMeta(meta, "\u6587\u672C\u7247\u6BB5", `${fragments.length} \u6BB5`);
6075
6394
  if (parseError) {
6076
- appendOfficeBinaryMeta(meta, "\u8868\u683C\u89E3\u6790", `xlsx \u8BFB\u53D6\u5931\u8D25\uFF0C\u5DF2\u5207\u6362\u4E8C\u8FDB\u5236\u6307\u7EB9\uFF1A${parseError}`);
6395
+ appendOfficeBinaryMeta(meta, "\u89E3\u6790\u72B6\u6001", parseError);
6077
6396
  }
6078
6397
  section.append(format, meta);
6079
6398
  if (fragments.length > 0) {
6080
6399
  const article = document.createElement("article");
6081
- article.className = "ofv-document";
6400
+ article.className = "ofv-document ofv-office-binary-fragments";
6401
+ const heading = document.createElement("h4");
6402
+ heading.textContent = "\u53EF\u8BFB\u6587\u672C\u7247\u6BB5";
6403
+ article.append(heading);
6082
6404
  for (const fragment of fragments.slice(0, 80)) {
6083
6405
  const paragraph = document.createElement("p");
6084
6406
  paragraph.textContent = fragment;
@@ -6087,7 +6409,8 @@ function renderLegacyOfficeBinary(panel, extension, arrayBuffer, parseError) {
6087
6409
  section.append(article);
6088
6410
  } else {
6089
6411
  const empty = document.createElement("p");
6090
- empty.textContent = "\u672A\u63D0\u53D6\u5230\u7A33\u5B9A\u53EF\u8BFB\u6587\u672C\u3002\u8BE5\u6587\u4EF6\u53EF\u80FD\u7ECF\u8FC7\u538B\u7F29\u3001\u52A0\u5BC6\uFF0C\u6216\u9700\u8981\u5B8C\u6574\u4E8C\u8FDB\u5236\u89E3\u6790\u5668\u3002";
6412
+ empty.className = "ofv-office-binary-empty";
6413
+ empty.textContent = "\u672A\u63D0\u53D6\u5230\u7A33\u5B9A\u53EF\u8BFB\u6587\u672C\u3002\u8BE5\u6587\u4EF6\u53EF\u80FD\u7ECF\u8FC7\u538B\u7F29\u3001\u52A0\u5BC6\uFF0C\u6216\u6587\u672C\u7F16\u7801\u65E0\u6CD5\u5728\u6D4F\u89C8\u5668\u7AEF\u53EF\u9760\u8BC6\u522B\uFF1B\u8BF7\u4F7F\u7528\u670D\u52A1\u7AEF LibreOffice/OnlyOffice \u8F6C\u6362\u540E\u9884\u89C8\u3002";
6091
6414
  section.append(empty);
6092
6415
  }
6093
6416
  panel.append(section);
@@ -6120,12 +6443,12 @@ function normalizeOfficeError(error) {
6120
6443
  function extractLegacyOfficeText(arrayBuffer) {
6121
6444
  const bytes = new Uint8Array(arrayBuffer);
6122
6445
  const fragments = [
6123
- ...extractPrintableRuns(bytes),
6124
- ...extractUtf16Runs(bytes)
6125
- ].map((fragment) => normalizeLegacyText(fragment)).filter((fragment) => fragment.length >= 3 && /[\p{L}\p{N}]/u.test(fragment));
6446
+ ...extractPrintableRuns(bytes).map((text) => ({ text, source: "ascii" })),
6447
+ ...extractUtf16Runs(bytes).map((text) => ({ text, source: "utf16" }))
6448
+ ].map(({ text, source }) => ({ text: normalizeLegacyText(text), source })).filter(({ text, source }) => isReadableLegacyTextFragment(text, source));
6126
6449
  const unique = [];
6127
6450
  const seen = /* @__PURE__ */ new Set();
6128
- for (const fragment of fragments) {
6451
+ for (const { text: fragment } of fragments) {
6129
6452
  const key = fragment.toLowerCase();
6130
6453
  if (!seen.has(key)) {
6131
6454
  seen.add(key);
@@ -6134,6 +6457,104 @@ function extractLegacyOfficeText(arrayBuffer) {
6134
6457
  }
6135
6458
  return unique.slice(0, 160);
6136
6459
  }
6460
+ function isReadableLegacyTextFragment(fragment, source) {
6461
+ if (fragment.length > 600) {
6462
+ return false;
6463
+ }
6464
+ if (isLegacyOfficeMetadataNoise(fragment)) {
6465
+ return false;
6466
+ }
6467
+ if (!/[\p{L}\p{N}]/u.test(fragment)) {
6468
+ return false;
6469
+ }
6470
+ const chars = Array.from(fragment);
6471
+ const letters = chars.filter((char) => /\p{L}/u.test(char)).length;
6472
+ const digits = chars.filter((char) => /\p{N}/u.test(char)).length;
6473
+ const spaces = chars.filter((char) => /\s/u.test(char)).length;
6474
+ const asciiLetters = chars.filter((char) => /[A-Za-z]/.test(char)).length;
6475
+ const cjkLetters = chars.filter((char) => /[\u3400-\u9fff]/u.test(char)).length;
6476
+ const punctuation = chars.filter((char) => /[^\p{L}\p{N}\s]/u.test(char)).length;
6477
+ const alphaNumeric = letters + digits;
6478
+ const readableRatio = alphaNumeric / chars.length;
6479
+ const punctuationRatio = punctuation / chars.length;
6480
+ if (fragment.length < 4 || readableRatio < 0.55 || punctuationRatio > 0.24) {
6481
+ return false;
6482
+ }
6483
+ if (/([\p{L}\p{N}])\1{4,}/u.test(fragment)) {
6484
+ return false;
6485
+ }
6486
+ if (cjkLetters >= 2) {
6487
+ const suspiciousCjk = chars.filter((char) => isAsciiBytePairCjk(char)).length;
6488
+ if (suspiciousCjk / cjkLetters > 0.65) {
6489
+ return false;
6490
+ }
6491
+ if (isLikelyCjkHeading(fragment)) {
6492
+ return true;
6493
+ }
6494
+ if (punctuation > 0 && fragment.length < 12) {
6495
+ return false;
6496
+ }
6497
+ return cjkLetters >= 8 || cjkLetters >= 4 && spaces > 0;
6498
+ }
6499
+ if (asciiLetters >= 4) {
6500
+ if (punctuation > 0 && spaces === 0) {
6501
+ return false;
6502
+ }
6503
+ if (source === "ascii" && /^[A-Z]{2,8}$/.test(fragment)) {
6504
+ return false;
6505
+ }
6506
+ if (spaces > 0) {
6507
+ return letters >= 3;
6508
+ }
6509
+ return fragment.length >= 6;
6510
+ }
6511
+ if (spaces > 0 && letters >= 3) {
6512
+ return true;
6513
+ }
6514
+ return false;
6515
+ }
6516
+ function isLegacyOfficeMetadataNoise(fragment) {
6517
+ if (/[$�\uFFFD]/u.test(fragment)) {
6518
+ return true;
6519
+ }
6520
+ if (/^(?:Root Entry|WordDocument|Workbook|Book|SummaryInformation|DocumentSummaryInformation|CompObj|ObjectPool|Data|PowerPoint Document|Pictures)$/i.test(fragment)) {
6521
+ return true;
6522
+ }
6523
+ if (/\.(dotm?|docm?|pptx?|ppsx?|xlsm?|xlsx?)\b/i.test(fragment)) {
6524
+ return true;
6525
+ }
6526
+ if (/^(?:默认段落字体|普通表格|正文|标题|副标题|目录|页眉|页脚|批注|超链接)(?:\s*\d+)?$/.test(fragment)) {
6527
+ return true;
6528
+ }
6529
+ if (/\b(?:Normal|Default|Calibri|Times New Roman|WPS Office|Microsoft Office|KSOP?ProductBuildVer)\b/i.test(fragment)) {
6530
+ return true;
6531
+ }
6532
+ if (/^\d+(?:Table|List|Heading|Title|Style)$/i.test(fragment)) {
6533
+ return true;
6534
+ }
6535
+ if (/[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[})]?/i.test(fragment)) {
6536
+ return true;
6537
+ }
6538
+ if (/^[A-Z_]{3,}$/.test(fragment) || /^[A-Za-z]+(?:Information|Document|Storage|Stream|Table|Data|Pool|Obj|Props)$/i.test(fragment)) {
6539
+ return true;
6540
+ }
6541
+ return false;
6542
+ }
6543
+ function isLikelyCjkHeading(fragment) {
6544
+ return /^(?:标题|第[一二三四五六七八九十\d]+[章节条]|[一二三四五六七八九十\d]+[、..])\s*[\p{L}\p{N}\s-]*$/u.test(fragment);
6545
+ }
6546
+ function isAsciiBytePairCjk(char) {
6547
+ const code = char.codePointAt(0) || 0;
6548
+ if (code < 13312 || code > 40959) {
6549
+ return false;
6550
+ }
6551
+ const low = code & 255;
6552
+ const high = code >> 8;
6553
+ return isPrintableAsciiByte(low) && isPrintableAsciiByte(high);
6554
+ }
6555
+ function isPrintableAsciiByte(value) {
6556
+ return value >= 32 && value <= 126;
6557
+ }
6137
6558
  function extractPrintableRuns(bytes) {
6138
6559
  const fragments = [];
6139
6560
  let current = "";
@@ -6156,6 +6577,13 @@ function extractUtf16Runs(bytes) {
6156
6577
  const fragments = [];
6157
6578
  let current = "";
6158
6579
  for (let index = 0; index < bytes.length - 1; index += 2) {
6580
+ if (looksLikeMisalignedAsciiUtf16(bytes[index], bytes[index + 1])) {
6581
+ if (current.length >= 3) {
6582
+ fragments.push(current);
6583
+ }
6584
+ current = "";
6585
+ continue;
6586
+ }
6159
6587
  const code = bytes[index] | bytes[index + 1] << 8;
6160
6588
  if (code >= 32 && code <= 55295 || code === 9) {
6161
6589
  current += String.fromCharCode(code);
@@ -6171,6 +6599,9 @@ function extractUtf16Runs(bytes) {
6171
6599
  }
6172
6600
  return fragments;
6173
6601
  }
6602
+ function looksLikeMisalignedAsciiUtf16(lowByte, highByte) {
6603
+ return lowByte === 0 && (highByte >= 48 && highByte <= 57 || highByte >= 65 && highByte <= 90 || highByte >= 97 && highByte <= 122);
6604
+ }
6174
6605
  function normalizeLegacyText(value) {
6175
6606
  return value.replace(/[\u0000-\u001f]+/g, " ").replace(/\s+/g, " ").trim();
6176
6607
  }
@@ -6808,18 +7239,45 @@ function archivePlugin() {
6808
7239
  layout.className = "ofv-archive-layout";
6809
7240
  const sidebar = document.createElement("div");
6810
7241
  sidebar.className = "ofv-archive-sidebar";
7242
+ const sidebarPanel = document.createElement("div");
7243
+ sidebarPanel.className = "ofv-archive-sidebar-panel";
6811
7244
  const header = document.createElement("div");
6812
7245
  header.className = "ofv-archive-header";
6813
- header.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
6814
- sidebar.append(header);
7246
+ const sidebarTitle = document.createElement("span");
7247
+ sidebarTitle.className = "ofv-archive-header-title";
7248
+ sidebarTitle.textContent = `\u6587\u4EF6\u5217\u8868 (${archiveEntries.filter((e) => !e.dir).length})`;
7249
+ const sidebarToggle = document.createElement("button");
7250
+ sidebarToggle.className = "ofv-archive-sidebar-toggle";
7251
+ sidebarToggle.type = "button";
7252
+ sidebarToggle.setAttribute("aria-label", "\u5C55\u5F00\u6587\u4EF6\u5217\u8868");
7253
+ sidebarToggle.setAttribute("aria-expanded", "false");
7254
+ sidebarToggle.title = "\u5C55\u5F00\u6587\u4EF6\u5217\u8868";
7255
+ sidebarToggle.textContent = "\u2039";
7256
+ header.append(sidebarToggle, sidebarTitle);
7257
+ sidebarPanel.append(header);
6815
7258
  const tree = document.createElement("div");
6816
7259
  tree.className = "ofv-archive-tree";
6817
- sidebar.append(tree);
7260
+ sidebarPanel.append(tree);
7261
+ sidebar.append(sidebarPanel);
6818
7262
  const mainPanel = document.createElement("div");
6819
7263
  mainPanel.className = "ofv-archive-main";
6820
7264
  layout.append(sidebar, mainPanel);
6821
7265
  panel.append(layout);
6822
7266
  let currentSubInstance = null;
7267
+ const getSidebarViewportWidth = () => ctx.viewport.clientWidth || ctx.size.width;
7268
+ const shouldAutoCollapseSidebar = () => getSidebarViewportWidth() <= 520;
7269
+ const setSidebarCollapsed = (collapsed) => {
7270
+ layout.classList.toggle("is-sidebar-collapsed", collapsed);
7271
+ sidebarToggle.setAttribute("aria-expanded", String(!collapsed));
7272
+ const label = collapsed ? "\u5C55\u5F00\u6587\u4EF6\u5217\u8868" : "\u6536\u8D77\u6587\u4EF6\u5217\u8868";
7273
+ sidebarToggle.setAttribute("aria-label", label);
7274
+ sidebarToggle.title = label;
7275
+ sidebarToggle.textContent = collapsed ? "\u203A" : "\u2039";
7276
+ };
7277
+ setSidebarCollapsed(false);
7278
+ sidebarToggle.addEventListener("click", () => {
7279
+ setSidebarCollapsed(!layout.classList.contains("is-sidebar-collapsed"));
7280
+ });
6823
7281
  const showDefaultSummary = () => {
6824
7282
  mainPanel.replaceChildren();
6825
7283
  const summary = document.createElement("div");
@@ -6859,6 +7317,9 @@ function archivePlugin() {
6859
7317
  if (destroyed) {
6860
7318
  return;
6861
7319
  }
7320
+ if (shouldAutoCollapseSidebar()) {
7321
+ setSidebarCollapsed(true);
7322
+ }
6862
7323
  const token = ++renderToken;
6863
7324
  sidebar.querySelectorAll(".ofv-archive-item").forEach((el) => {
6864
7325
  el.classList.remove("is-active");
@@ -9141,7 +9602,7 @@ function cadPlugin() {
9141
9602
  return { destroy: () => panel.remove() };
9142
9603
  }
9143
9604
  const dxf = await readTextFile(ctx.file);
9144
- const viewer = renderDxf(panel, dxf);
9605
+ const viewer = renderDxf(panel, dxf, ctx);
9145
9606
  return {
9146
9607
  canCommand(command) {
9147
9608
  return viewer.canCommand(command);
@@ -9610,7 +10071,7 @@ function hexPreview(bytes) {
9610
10071
  }
9611
10072
  return rows.join("\n") || "\u65E0\u53EF\u5C55\u793A\u5B57\u8282\u3002";
9612
10073
  }
9613
- function renderDxf(panel, dxf) {
10074
+ function renderDxf(panel, dxf, ctx) {
9614
10075
  const pairs = dxf.split(/\r?\n/).map((line) => line.trim());
9615
10076
  const lines = [];
9616
10077
  const circles = [];
@@ -9788,6 +10249,10 @@ function renderDxf(panel, dxf) {
9788
10249
  note.textContent = `\u5DF2\u63D0\u53D6 LINE ${lines.length} \u4E2A\u3001CIRCLE ${circles.length} \u4E2A\u3001ARC ${arcs.length} \u4E2A\u3001POLYLINE ${polylines.length} \u4E2A\u3001POINT ${points.length} \u4E2A\u3001TEXT ${texts.length} \u4E2A\u3002`;
9789
10250
  section.append(note, svg);
9790
10251
  panel.append(section);
10252
+ const updateToolbarZoom = () => {
10253
+ ctx.toolbar?.setZoom(initialViewBox.width / currentViewBox.width);
10254
+ };
10255
+ updateToolbarZoom();
9791
10256
  return {
9792
10257
  canCommand(command) {
9793
10258
  return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset";
@@ -9802,14 +10267,19 @@ function renderDxf(panel, dxf) {
9802
10267
  currentViewBox.x = centerX - currentViewBox.width / 2;
9803
10268
  currentViewBox.y = centerY - currentViewBox.height / 2;
9804
10269
  applyViewBox();
10270
+ updateToolbarZoom();
9805
10271
  return true;
9806
10272
  }
9807
10273
  if (command === "zoom-reset") {
9808
10274
  currentViewBox = { ...initialViewBox };
9809
10275
  applyViewBox();
10276
+ updateToolbarZoom();
9810
10277
  return true;
9811
10278
  }
9812
10279
  return false;
10280
+ },
10281
+ destroy() {
10282
+ ctx.toolbar?.setZoom(void 0);
9813
10283
  }
9814
10284
  };
9815
10285
  }
@@ -10085,6 +10555,12 @@ function model3dPlugin() {
10085
10555
  renderer.setSize(width, height, false);
10086
10556
  };
10087
10557
  resize(ctx.size);
10558
+ const updateToolbarZoom = () => {
10559
+ const currentDistance = vectorDistance(camera.position, controls.target);
10560
+ const initialDistance = vectorDistance(initialFrame.cameraPosition, initialFrame.target);
10561
+ ctx.toolbar?.setZoom(initialDistance > 0 ? initialDistance / currentDistance : void 0);
10562
+ };
10563
+ updateToolbarZoom();
10088
10564
  return {
10089
10565
  canCommand(command) {
10090
10566
  return command === "zoom-in" || command === "zoom-out" || command === "zoom-reset" || command === "rotate-right" || command === "rotate-left";
@@ -10095,6 +10571,7 @@ function model3dPlugin() {
10095
10571
  camera.position.sub(controls.target).multiplyScalar(factor).add(controls.target);
10096
10572
  camera.updateProjectionMatrix();
10097
10573
  controls.update();
10574
+ updateToolbarZoom();
10098
10575
  return true;
10099
10576
  }
10100
10577
  if (command === "zoom-reset") {
@@ -10104,6 +10581,7 @@ function model3dPlugin() {
10104
10581
  camera.far = initialFrame.far;
10105
10582
  camera.updateProjectionMatrix();
10106
10583
  controls.update();
10584
+ updateToolbarZoom();
10107
10585
  return true;
10108
10586
  }
10109
10587
  if (command === "rotate-right" || command === "rotate-left") {
@@ -10114,6 +10592,7 @@ function model3dPlugin() {
10114
10592
  },
10115
10593
  resize,
10116
10594
  destroy() {
10595
+ ctx.toolbar?.setZoom(void 0);
10117
10596
  window.cancelAnimationFrame(animationFrame);
10118
10597
  controls.dispose();
10119
10598
  renderer.dispose();
@@ -10125,6 +10604,12 @@ function model3dPlugin() {
10125
10604
  }
10126
10605
  };
10127
10606
  }
10607
+ function vectorDistance(a, b) {
10608
+ if (typeof a.distanceTo === "function") {
10609
+ return a.distanceTo(b);
10610
+ }
10611
+ return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
10612
+ }
10128
10613
  function renderModelFallback(ctx, url, isExternal, message) {
10129
10614
  const panel = document.createElement("div");
10130
10615
  panel.className = "ofv-fallback";
@@ -10481,34 +10966,65 @@ function gisPlugin() {
10481
10966
  }
10482
10967
  const wrapper = document.createElement("div");
10483
10968
  wrapper.className = "ofv-gis-viewer";
10484
- wrapper.append(createGisSummary(geojson));
10969
+ const summary = summarizeGeoJson(geojson);
10970
+ wrapper.append(createGisSummary(summary));
10485
10971
  ctx.viewport.appendChild(wrapper);
10486
10972
  const mapContainer = document.createElement("div");
10487
10973
  mapContainer.className = "ofv-map-stage";
10488
10974
  wrapper.appendChild(mapContainer);
10975
+ if (summary.features === 0) {
10976
+ mapContainer.append(createEmptyMapState());
10977
+ } else {
10978
+ mapContainer.append(createMapLegend(summary));
10979
+ }
10489
10980
  const map = Leaflet.map(mapContainer).setView([0, 0], 2);
10490
10981
  Leaflet.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
10491
10982
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
10492
10983
  }).addTo(map);
10493
10984
  const geojsonLayer = Leaflet.geoJSON(geojson, {
10494
10985
  style: () => ({
10495
- color: "#3b82f6",
10986
+ className: "ofv-map-feature",
10987
+ color: "#e11d48",
10496
10988
  weight: 2,
10497
- opacity: 0.8,
10498
- fillColor: "#93c5fd",
10499
- fillOpacity: 0.35
10989
+ opacity: 0.92,
10990
+ fillColor: "#fb923c",
10991
+ fillOpacity: 0.3,
10992
+ lineCap: "round",
10993
+ lineJoin: "round"
10500
10994
  }),
10501
10995
  pointToLayer: (feature, latlng) => {
10502
10996
  return Leaflet.circleMarker(latlng, {
10503
- radius: 6,
10504
- fillColor: "#3b82f6",
10997
+ className: "ofv-map-feature ofv-map-point",
10998
+ radius: 7,
10999
+ fillColor: "#e11d48",
10505
11000
  color: "#ffffff",
10506
- weight: 1.5,
11001
+ weight: 2.5,
10507
11002
  opacity: 1,
10508
- fillOpacity: 0.8
11003
+ fillOpacity: 0.9
10509
11004
  });
10510
11005
  },
10511
11006
  onEachFeature: (feature, layer) => {
11007
+ const label = feature.properties?.name || feature.properties?.title || feature.properties?.label;
11008
+ if (label) {
11009
+ layer.bindTooltip?.(String(label), {
11010
+ className: "ofv-map-tooltip",
11011
+ direction: "top",
11012
+ sticky: true
11013
+ });
11014
+ }
11015
+ layer.on?.({
11016
+ mouseover(event) {
11017
+ event.target?.setStyle?.({
11018
+ weight: 4,
11019
+ opacity: 1,
11020
+ fillOpacity: 0.4
11021
+ });
11022
+ event.target?.bringToFront?.();
11023
+ },
11024
+ mouseout(event) {
11025
+ geojsonLayer.resetStyle?.(event.target);
11026
+ }
11027
+ });
10512
11028
  if (feature.properties) {
10513
11029
  const props = feature.properties;
10514
11030
  const keys = Object.keys(props);
@@ -10543,15 +11059,20 @@ function gisPlugin() {
10543
11059
  const bounds = geojsonLayer.getBounds();
10544
11060
  if (bounds.isValid()) {
10545
11061
  map.fitBounds(bounds, { padding: [20, 20] });
11062
+ map.invalidateSize();
10546
11063
  }
10547
11064
  } catch (e) {
10548
11065
  console.warn("Could not fit bounds for GeoJSON data:", e);
10549
11066
  }
11067
+ const resizeTimers = [0, 80, 240].map((delay) => window.setTimeout(() => {
11068
+ map.invalidateSize();
11069
+ }, delay));
10550
11070
  return {
10551
11071
  resize() {
10552
11072
  map.invalidateSize();
10553
11073
  },
10554
11074
  destroy() {
11075
+ resizeTimers.forEach((timer) => window.clearTimeout(timer));
10555
11076
  map.remove();
10556
11077
  wrapper.remove();
10557
11078
  }
@@ -10576,8 +11097,7 @@ function normalizeGisError(error, fileName) {
10576
11097
  function isGisFallback(value) {
10577
11098
  return typeof value === "object" && value !== null && "fallback" in value;
10578
11099
  }
10579
- function createGisSummary(geojson) {
10580
- const summary = summarizeGeoJson(geojson);
11100
+ function createGisSummary(summary) {
10581
11101
  const bar = document.createElement("div");
10582
11102
  bar.className = "ofv-gis-summary";
10583
11103
  appendSummaryItem(bar, "\u8981\u7D20", String(summary.features));
@@ -10591,6 +11111,26 @@ function createGisSummary(geojson) {
10591
11111
  }
10592
11112
  return bar;
10593
11113
  }
11114
+ function createEmptyMapState() {
11115
+ const empty = document.createElement("div");
11116
+ empty.className = "ofv-map-empty";
11117
+ const title = document.createElement("strong");
11118
+ title.textContent = "\u6682\u65E0\u53EF\u5C55\u793A\u7684\u5730\u56FE\u8981\u7D20";
11119
+ const detail = document.createElement("span");
11120
+ detail.textContent = "GeoJSON \u5DF2\u8BC6\u522B\uFF0C\u4F46 features \u4E3A\u7A7A\u3002";
11121
+ empty.append(title, detail);
11122
+ return empty;
11123
+ }
11124
+ function createMapLegend(summary) {
11125
+ const legend = document.createElement("div");
11126
+ legend.className = "ofv-map-legend";
11127
+ const title = document.createElement("strong");
11128
+ title.textContent = "GeoJSON";
11129
+ const detail = document.createElement("span");
11130
+ detail.textContent = `${summary.features} \u4E2A\u8981\u7D20 \xB7 ${formatGeometryCounts(summary.geometryCounts)}`;
11131
+ legend.append(title, detail);
11132
+ return legend;
11133
+ }
10594
11134
  function appendSummaryItem(parent, label, value) {
10595
11135
  const item = document.createElement("span");
10596
11136
  const key = document.createElement("span");