@file-viewer/renderer-text 3.0.0 → 3.0.2

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/README.en.md CHANGED
@@ -13,6 +13,9 @@ const options = {
13
13
  renderers: textRenderer,
14
14
  text: {
15
15
  lineNumbers: true,
16
+ wrapLongLines: true,
17
+ prettyPrint: true,
18
+ prettyPrintMaxBytes: 512 * 1024,
16
19
  },
17
20
  }
18
21
  ```
@@ -34,6 +37,10 @@ const options = {
34
37
  - Code and text preview uses `highlight.js` core with per-language dynamic imports instead of registering every language up front.
35
38
  - Code, text, and virtualized Markdown source views show their file type, indexing status, and line-count metadata bar by default. Set `options.text.toolbar: false` to hide this renderer-local bar without hiding the viewer-level download, search, or zoom toolbar.
36
39
  - Regular code and text previews can show a line-number gutter with `options.text.lineNumbers: true`. The gutter is excluded from copied source, search matches, and assistive reading. Virtual large-text views keep their existing gutter unless it is explicitly set to `false`.
40
+ - `options.text.wrapLongLines: true` changes layout only and never inserts source newlines. Regular previews keep one gutter entry per logical line; large files remain bounded in the virtual window while wrapping to the available width.
41
+ - `options.text.prettyPrint: true` lazily loads Prettier and only the parser plugins needed by a supported structured format. It formats a display copy, labels the toolbar as a formatted preview, and provides a switch back to the original source. JSON/JSONC/JSON5, JavaScript/TypeScript, HTML/Vue, CSS, YAML, Markdown, GraphQL, and XML share this path.
42
+ - `prettyPrintMaxBytes` limits only Prettier and defaults to the effective `virtualizeAboveBytes` value (512 KiB when omitted). Oversized, malformed, and unsupported inputs fall back to the original source, which continues through the existing regular or virtual renderer. XML uses a conservative whitespace-preserving mode; mixed content and `xml:space="preserve"` stay on the original source path.
43
+ - The legacy `*-full` script-tag IIFE assets do not bundle Prettier, so `prettyPrint` falls back to the original source there. Use the ESM integration (standard component packages or `@file-viewer/preset-*`) for formatted previews.
37
44
  - With the text-tools capability installed, `patch` uses `diff2html` for side-by-side review and `bundle` / `bdl` enables Git bundle inspection.
38
45
  - With the Mermaid capability installed, fenced Mermaid blocks render as diagrams. Without it, the source stays visible with the exact CLI enablement command.
39
46
  - HTML, XML, Vue, and similar files are escaped and shown as source, never executed.
package/README.md CHANGED
@@ -13,6 +13,9 @@ const options = {
13
13
  renderers: textRenderer,
14
14
  text: {
15
15
  lineNumbers: true,
16
+ wrapLongLines: true,
17
+ prettyPrint: true,
18
+ prettyPrintMaxBytes: 512 * 1024,
16
19
  },
17
20
  }
18
21
  ```
@@ -34,6 +37,10 @@ const options = {
34
37
  - 代码和文本使用 `highlight.js` core + 按语言动态加载,避免一次性注册全部语言。
35
38
  - 代码、文本和超大 Markdown 源码视图默认显示文件类型、索引状态和行数元信息栏;传入 `options.text.toolbar: false` 可隐藏该 renderer 内部栏,不影响 Viewer 的下载、搜索、缩放等全局工具栏。
36
39
  - 普通代码和文本可通过 `options.text.lineNumbers: true` 显示行号;行号不会进入复制内容、搜索结果或无障碍朗读。超大文本保留原有的虚拟行号栏,可显式传 `false` 隐藏。
40
+ - `options.text.wrapLongLines: true` 仅改变布局,不向源码插入换行。普通预览会按逻辑行维护行号,超大文本仍使用有界虚拟窗口并按可用宽度换行。
41
+ - `options.text.prettyPrint: true` 会在支持的结构化文本上按需加载 Prettier 与对应 parser,仅格式化显示副本;工具栏会标明“格式化预览”,并可切回原始源码。JSON/JSONC/JSON5、JavaScript/TypeScript、HTML/Vue、CSS、YAML、Markdown、GraphQL 和 XML 共用同一路径。
42
+ - `prettyPrintMaxBytes` 只限制 Prettier,默认继承 `virtualizeAboveBytes`(未配置时为 512 KiB)。超限、语法错误或不支持的格式直接回退原始源码,之后仍由普通或虚拟文本 renderer 决定展示方式。XML 使用保守的 whitespace-preserving 模式;检测到混合内容或 `xml:space="preserve"` 时直接保留原文。
43
+ - 历史 `*-full` 包的 script 标签 IIFE 资源不打包 Prettier,该路径下 `prettyPrint` 无错误回退到原始源码;需要格式化预览时使用 ESM 集成(标准组件包或 `@file-viewer/preset-*`)。
37
44
  - 安装 text-tools capability 后,`patch` 使用 `diff2html` 渲染左右比对视图,`bundle` / `bdl` 才启用 Git bundle 结构检查。
38
45
  - 安装 Mermaid capability 后,Markdown 内嵌 Mermaid 图才会渲染;未安装时保留源码并显示精确 CLI 启用命令。
39
46
  - HTML / XML / Vue 等文件按源码方式转义展示,不执行脚本。
package/dist/code.d.ts CHANGED
@@ -2,10 +2,8 @@ import { type FileRenderContext, type FileViewerRenderedInstance } from '@file-v
2
2
  /**
3
3
  * Framework-neutral text/code renderer.
4
4
  *
5
- * highlight.js core and language definitions are loaded lazily by format. HTML
6
- * and XML are highlighted as escaped source text, never executed as real DOM.
7
- * @param buffer 文本二进制内容
8
- * @param target 目标
9
- * @param type 文件扩展名,用于选择 highlight.js 语言
5
+ * highlight.js core, Prettier standalone, and parser definitions are loaded
6
+ * lazily by format. HTML and XML are always mounted as escaped source text and
7
+ * never executed as real DOM.
10
8
  */
11
9
  export default function renderText(buffer: ArrayBuffer, target: HTMLDivElement, type?: string, context?: FileRenderContext): Promise<FileViewerRenderedInstance>;
package/dist/code.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createFileViewerTranslator, createFileViewerZoomChangeEmitter as createZoomChangeEmitter, decodeFileViewerTextBuffer, registerFileViewerZoomProvider, unregisterFileViewerZoomProvider } from '@file-viewer/core';
2
2
  import { codeStyle } from './codeStyle.js';
3
3
  import renderLargeText, { shouldVirtualizeTextBuffer } from './largeText.js';
4
+ import { formatFileViewerTextForDisplay, resolveFileViewerPrettyPrintMaxBytes, supportsFileViewerPrettyPrint } from './prettyPrint.js';
4
5
  const languageMap = {
5
6
  bash: 'bash',
6
7
  c: 'cpp',
@@ -15,6 +16,8 @@ const languageMap = {
15
16
  bdl: 'plaintext',
16
17
  gv: 'plaintext',
17
18
  go: 'go',
19
+ graphql: 'graphql',
20
+ gql: 'graphql',
18
21
  h: 'cpp',
19
22
  hcl: 'plaintext',
20
23
  hpp: 'cpp',
@@ -60,6 +63,7 @@ const languageLoaders = {
60
63
  css: () => import('highlight.js/lib/languages/css'),
61
64
  diff: () => import('highlight.js/lib/languages/diff'),
62
65
  go: () => import('highlight.js/lib/languages/go'),
66
+ graphql: () => import('highlight.js/lib/languages/graphql'),
63
67
  http: () => import('highlight.js/lib/languages/http'),
64
68
  ini: () => import('highlight.js/lib/languages/ini'),
65
69
  java: () => import('highlight.js/lib/languages/java'),
@@ -81,8 +85,8 @@ const languageLoaders = {
81
85
  };
82
86
  let highlighterPromise = null;
83
87
  const registeredLanguages = new Set();
84
- const createElement = (tagName, className, text) => {
85
- const element = document.createElement(tagName);
88
+ const createElement = (documentRef, tagName, className, text) => {
89
+ const element = documentRef.createElement(tagName);
86
90
  if (className) {
87
91
  element.className = className;
88
92
  }
@@ -91,8 +95,8 @@ const createElement = (tagName, className, text) => {
91
95
  }
92
96
  return element;
93
97
  };
94
- const createStyle = () => {
95
- const style = document.createElement('style');
98
+ const createStyle = (documentRef) => {
99
+ const style = documentRef.createElement('style');
96
100
  style.textContent = codeStyle;
97
101
  return style;
98
102
  };
@@ -139,21 +143,122 @@ const lineCountOf = (value) => {
139
143
  const createLineNumberText = (lineCount) => {
140
144
  return Array.from({ length: lineCount }, (_, index) => String(index + 1)).join('\n');
141
145
  };
146
+ const createWrappedSourceLine = (documentRef, lineNumber) => {
147
+ const row = createElement(documentRef, 'span', 'code-source-line');
148
+ row.dataset.line = String(lineNumber);
149
+ const number = createElement(documentRef, 'span', 'code-source-line-number', String(lineNumber));
150
+ number.setAttribute('aria-hidden', 'true');
151
+ const content = createElement(documentRef, 'span', 'code-source-line-content');
152
+ row.append(number, content);
153
+ return { row, content };
154
+ };
155
+ /**
156
+ * Splits trusted highlight.js markup into logical source-line wrappers while
157
+ * recreating active token spans after every newline. This keeps multiline
158
+ * comments and strings highlighted without coupling visual wrapping to source
159
+ * line numbers.
160
+ */
161
+ const mountWrappedHighlightedLines = (code, highlightedHtml, expectedLineCount) => {
162
+ const documentRef = code.ownerDocument;
163
+ const staging = createElement(documentRef, 'span');
164
+ staging.innerHTML = highlightedHtml;
165
+ const lines = [createWrappedSourceLine(documentRef, 1)];
166
+ let lineIndex = 0;
167
+ const activeElements = [];
168
+ let activeClones = [];
169
+ const currentContent = () => lines[lineIndex].content;
170
+ const currentParent = () => { var _a; return (_a = activeClones[activeClones.length - 1]) !== null && _a !== void 0 ? _a : currentContent(); };
171
+ const continueOnNextLine = () => {
172
+ lineIndex += 1;
173
+ lines.push(createWrappedSourceLine(documentRef, lineIndex + 1));
174
+ let parent = currentContent();
175
+ activeClones = activeElements.map(element => {
176
+ const clone = element.cloneNode(false);
177
+ parent.append(clone);
178
+ parent = clone;
179
+ return clone;
180
+ });
181
+ };
182
+ const visit = (node) => {
183
+ var _a;
184
+ if (node.nodeType === 3) {
185
+ const parts = ((_a = node.textContent) !== null && _a !== void 0 ? _a : '').split(/\r\n|\r|\n/);
186
+ parts.forEach((part, index) => {
187
+ if (part) {
188
+ currentParent().append(documentRef.createTextNode(part));
189
+ }
190
+ if (index < parts.length - 1) {
191
+ continueOnNextLine();
192
+ }
193
+ });
194
+ return;
195
+ }
196
+ if (node.nodeType !== 1) {
197
+ return;
198
+ }
199
+ const element = node;
200
+ const clone = element.cloneNode(false);
201
+ currentParent().append(clone);
202
+ activeElements.push(element);
203
+ activeClones.push(clone);
204
+ Array.from(element.childNodes).forEach(visit);
205
+ activeElements.pop();
206
+ activeClones.pop();
207
+ };
208
+ Array.from(staging.childNodes).forEach(visit);
209
+ while (lines.length < expectedLineCount) {
210
+ lines.push(createWrappedSourceLine(documentRef, lines.length + 1));
211
+ }
212
+ code.replaceChildren(...lines.map(line => line.row));
213
+ };
214
+ const mountCodeMarkup = (pre, code, highlightedHtml, lineCount, showLineNumbers, wrapLongLines) => {
215
+ code.replaceChildren();
216
+ if (showLineNumbers && wrapLongLines) {
217
+ pre.className = 'code-area code-area--wrapped-line-numbers';
218
+ pre.replaceChildren(code);
219
+ mountWrappedHighlightedLines(code, highlightedHtml, lineCount);
220
+ return;
221
+ }
222
+ pre.className = showLineNumbers ? 'code-area code-area--line-numbers' : 'code-area';
223
+ if (showLineNumbers) {
224
+ const gutter = createElement(pre.ownerDocument, 'span', 'code-line-numbers', createLineNumberText(lineCount));
225
+ gutter.setAttribute('aria-hidden', 'true');
226
+ pre.replaceChildren(gutter, code);
227
+ }
228
+ else {
229
+ pre.replaceChildren(code);
230
+ }
231
+ code.innerHTML = highlightedHtml;
232
+ };
233
+ const canPossiblyFitDecodedPrettyPrintLimit = (buffer, maxBytes) => {
234
+ // UTF-16 ASCII is the widest supported source encoding relative to its
235
+ // decoded UTF-8 representation. This fast guard avoids decoding enormous
236
+ // structured files that cannot possibly fit the configured decoded limit.
237
+ return buffer.byteLength <= (maxBytes * 2) + 4;
238
+ };
142
239
  /**
143
240
  * Framework-neutral text/code renderer.
144
241
  *
145
- * highlight.js core and language definitions are loaded lazily by format. HTML
146
- * and XML are highlighted as escaped source text, never executed as real DOM.
147
- * @param buffer 文本二进制内容
148
- * @param target 目标
149
- * @param type 文件扩展名,用于选择 highlight.js 语言
242
+ * highlight.js core, Prettier standalone, and parser definitions are loaded
243
+ * lazily by format. HTML and XML are always mounted as escaped source text and
244
+ * never executed as real DOM.
150
245
  */
151
246
  export default async function renderText(buffer, target, type, context) {
152
- var _a, _b, _c, _d, _e, _f;
247
+ var _a, _b;
153
248
  const t = createFileViewerTranslator(context === null || context === void 0 ? void 0 : context.options);
154
249
  const extension = type || 'txt';
155
250
  const normalizedExtension = extension.trim().toLowerCase();
156
- if (normalizedExtension !== 'bundle' &&
251
+ const textOptions = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.text;
252
+ let sourceText;
253
+ let prettyPrintResult;
254
+ if ((textOptions === null || textOptions === void 0 ? void 0 : textOptions.prettyPrint) === true &&
255
+ supportsFileViewerPrettyPrint(normalizedExtension) &&
256
+ canPossiblyFitDecodedPrettyPrintLimit(buffer, resolveFileViewerPrettyPrintMaxBytes(textOptions))) {
257
+ sourceText = decodeFileViewerTextBuffer(buffer, textOptions.encoding).text;
258
+ prettyPrintResult = await formatFileViewerTextForDisplay(sourceText, normalizedExtension, textOptions, context === null || context === void 0 ? void 0 : context.signal);
259
+ }
260
+ if (!(prettyPrintResult === null || prettyPrintResult === void 0 ? void 0 : prettyPrintResult.formatted) &&
261
+ normalizedExtension !== 'bundle' &&
157
262
  normalizedExtension !== 'bdl' &&
158
263
  shouldVirtualizeTextBuffer(buffer, context)) {
159
264
  return renderLargeText(buffer, target, extension, context);
@@ -166,58 +271,92 @@ export default async function renderText(buffer, target, type, context) {
166
271
  const { default: renderGitBundle } = await import('./gitBundle.js');
167
272
  return renderGitBundle(buffer, target, extension, context);
168
273
  }
169
- const text = decodeFileViewerTextBuffer(buffer, (_b = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.text) === null || _b === void 0 ? void 0 : _b.encoding).text;
274
+ const originalText = sourceText !== null && sourceText !== void 0 ? sourceText : decodeFileViewerTextBuffer(buffer, textOptions === null || textOptions === void 0 ? void 0 : textOptions.encoding).text;
275
+ const formattedText = (prettyPrintResult === null || prettyPrintResult === void 0 ? void 0 : prettyPrintResult.formatted) ? prettyPrintResult.text : null;
170
276
  const language = resolveLanguage(extension);
171
- const lineCount = lineCountOf(text);
172
- const showToolbar = ((_d = (_c = context === null || context === void 0 ? void 0 : context.options) === null || _c === void 0 ? void 0 : _c.text) === null || _d === void 0 ? void 0 : _d.toolbar) !== false;
173
- const showLineNumbers = ((_f = (_e = context === null || context === void 0 ? void 0 : context.options) === null || _e === void 0 ? void 0 : _e.text) === null || _f === void 0 ? void 0 : _f.lineNumbers) === true;
277
+ const showToolbar = (textOptions === null || textOptions === void 0 ? void 0 : textOptions.toolbar) !== false;
278
+ const showLineNumbers = (textOptions === null || textOptions === void 0 ? void 0 : textOptions.lineNumbers) === true;
279
+ const wrapLongLines = (textOptions === null || textOptions === void 0 ? void 0 : textOptions.wrapLongLines) === true;
280
+ const documentRef = target.ownerDocument;
174
281
  let disposed = false;
175
282
  let zoom = 1;
283
+ let showingFormatted = formattedText !== null;
284
+ let renderGeneration = 0;
176
285
  const zoomEmitter = createZoomChangeEmitter();
177
- const root = createElement('div', 'code-viewer');
286
+ const root = createElement(documentRef, 'div', wrapLongLines ? 'code-viewer code-viewer--wrap-lines' : 'code-viewer');
178
287
  root.dataset.viewerZoomProvider = 'code';
179
288
  root.dataset.textToolbar = String(showToolbar);
180
289
  root.dataset.lineNumbers = String(showLineNumbers);
181
- const toolbar = createElement('div', 'code-toolbar');
182
- toolbar.append(createElement('span', '', extension.toUpperCase()), createElement('strong', '', `${lineCount} lines`));
183
- const pre = createElement('pre', showLineNumbers ? 'code-area code-area--line-numbers' : 'code-area');
184
- const code = createElement('code', `hljs language-${language}`);
185
- code.innerHTML = language === 'plaintext'
186
- ? escapeHtml(text)
187
- : t('text.code.loadingHighlight');
188
- if (showLineNumbers) {
189
- const gutter = createElement('span', 'code-line-numbers', createLineNumberText(lineCount));
190
- gutter.setAttribute('aria-hidden', 'true');
191
- pre.append(gutter);
192
- }
290
+ root.dataset.wrapLongLines = String(wrapLongLines);
291
+ root.dataset.prettyPrint = (_b = prettyPrintResult === null || prettyPrintResult === void 0 ? void 0 : prettyPrintResult.reason) !== null && _b !== void 0 ? _b : 'disabled';
292
+ const toolbar = createElement(documentRef, 'div', 'code-toolbar');
293
+ const extensionLabel = createElement(documentRef, 'span', '', extension.toUpperCase());
294
+ const toolbarMeta = createElement(documentRef, 'div', 'code-toolbar-meta');
295
+ const representationStatus = createElement(documentRef, 'span', 'code-format-status');
296
+ const representationToggle = createElement(documentRef, 'button', 'code-format-toggle');
297
+ representationToggle.type = 'button';
298
+ const lineSummary = createElement(documentRef, 'strong');
299
+ toolbarMeta.append(representationStatus, representationToggle, lineSummary);
300
+ toolbar.append(extensionLabel, toolbarMeta);
301
+ const pre = createElement(documentRef, 'pre', 'code-area');
302
+ const code = createElement(documentRef, 'code', `hljs language-${language}`);
193
303
  pre.append(code);
194
304
  if (showToolbar) {
195
305
  root.append(toolbar);
196
306
  }
197
307
  root.append(pre);
198
308
  root.style.setProperty('--code-font-size', `${13 * zoom}px`);
199
- target.replaceChildren(createStyle(), root);
200
- const updateHighlighted = async () => {
309
+ target.replaceChildren(createStyle(documentRef), root);
310
+ const currentText = () => showingFormatted && formattedText !== null ? formattedText : originalText;
311
+ const syncRepresentationControls = (lineCount) => {
312
+ const formatted = showingFormatted && formattedText !== null;
313
+ root.dataset.textRepresentation = formatted ? 'formatted' : 'source';
314
+ lineSummary.textContent = `${lineCount} lines`;
315
+ representationStatus.hidden = !formatted;
316
+ representationStatus.textContent = formatted ? t('text.code.formattedPreview') : '';
317
+ representationToggle.hidden = formattedText === null;
318
+ representationToggle.textContent = formatted
319
+ ? t('text.code.showOriginal')
320
+ : t('text.code.showFormatted');
321
+ representationToggle.setAttribute('aria-pressed', String(formatted));
322
+ };
323
+ const renderCurrentRepresentation = async () => {
324
+ const generation = renderGeneration + 1;
325
+ renderGeneration = generation;
326
+ const text = currentText();
327
+ const lineCount = lineCountOf(text);
328
+ syncRepresentationControls(lineCount);
201
329
  if (language === 'plaintext') {
330
+ mountCodeMarkup(pre, code, escapeHtml(text), lineCount, showLineNumbers, wrapLongLines);
202
331
  return;
203
332
  }
333
+ code.textContent = t('text.code.loadingHighlight');
204
334
  try {
205
335
  const hljs = await loadHighlighter();
206
336
  const hasLanguage = await registerLanguageOnce(hljs, language);
207
- if (disposed) {
337
+ if (disposed || generation !== renderGeneration) {
208
338
  return;
209
339
  }
210
- code.innerHTML = hasLanguage
340
+ const highlighted = hasLanguage
211
341
  ? hljs.highlight(text, { language, ignoreIllegals: true }).value
212
342
  : escapeHtml(text);
343
+ mountCodeMarkup(pre, code, highlighted, lineCount, showLineNumbers, wrapLongLines);
213
344
  }
214
345
  catch {
215
- if (!disposed) {
216
- code.innerHTML = escapeHtml(text);
346
+ if (!disposed && generation === renderGeneration) {
347
+ mountCodeMarkup(pre, code, escapeHtml(text), lineCount, showLineNumbers, wrapLongLines);
217
348
  }
218
349
  }
219
350
  };
220
- void updateHighlighted();
351
+ const toggleRepresentation = () => {
352
+ if (formattedText === null) {
353
+ return;
354
+ }
355
+ showingFormatted = !showingFormatted;
356
+ void renderCurrentRepresentation();
357
+ };
358
+ representationToggle.addEventListener('click', toggleRepresentation);
359
+ await renderCurrentRepresentation();
221
360
  const getZoomState = () => ({
222
361
  scale: zoom,
223
362
  label: `${Math.round(zoom * 100)}%`,
@@ -245,6 +384,8 @@ export default async function renderText(buffer, target, type, context) {
245
384
  $el: target,
246
385
  unmount() {
247
386
  disposed = true;
387
+ renderGeneration += 1;
388
+ representationToggle.removeEventListener('click', toggleRepresentation);
248
389
  unregisterFileViewerZoomProvider(root);
249
390
  target.replaceChildren();
250
391
  }
@@ -1 +1 @@
1
- export declare const codeStyle = "\n.code-viewer{min-height:100%;--code-bg:#f6f8fa;--code-toolbar-bg:rgba(255,255,255,.92);--code-border:rgba(31,35,40,.12);--code-text:#24292f;--code-muted:#57606a;--code-keyword:#cf222e;--code-title:#8250df;--code-string:#0a3069;--code-number:#0550ae;--code-comment:#6e7781;--code-attr:#953800;--code-built-in:#116329;background:var(--code-bg);color:var(--code-text);box-sizing:border-box}\n.code-toolbar{position:sticky;top:0;z-index:1;display:flex;height:42px;align-items:center;justify-content:space-between;gap:16px;padding:0 16px;border-bottom:1px solid var(--code-border);background:var(--code-toolbar-bg);backdrop-filter:blur(12px);box-sizing:border-box}\n.code-toolbar span,.code-toolbar strong{color:var(--code-muted);font-size:12px;font-weight:700;letter-spacing:0}\n.code-area{display:block;min-width:min-content;margin:0;padding:18px 20px 28px;overflow:auto;background:transparent;box-sizing:border-box}\n.code-area--line-numbers{display:grid;grid-template-columns:max-content max-content;padding:18px 0 28px}\n.code-line-numbers{position:sticky;left:0;z-index:1;display:block;min-width:4ch;padding:0 12px 0 16px;border-right:1px solid var(--code-border);background:var(--code-bg);color:var(--code-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;font-size:var(--code-font-size,13px);line-height:1.7;text-align:right;white-space:pre;user-select:none;box-sizing:border-box}\n.code-area.code-area--line-numbers code{padding:0 20px}\n.code-area code{display:block;padding:0;overflow:visible;background:transparent;color:inherit;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;font-size:var(--code-font-size,13px);line-height:1.7;tab-size:2;white-space:pre}\n.code-area .hljs-comment,.code-area .hljs-quote{color:var(--code-comment)}\n.code-area .hljs-keyword,.code-area .hljs-selector-tag,.code-area .hljs-subst{color:var(--code-keyword)}\n.code-area .hljs-string,.code-area .hljs-doctag,.code-area .hljs-regexp{color:var(--code-string)}\n.code-area .hljs-title,.code-area .hljs-section,.code-area .hljs-selector-id{color:var(--code-title);font-weight:700}\n.code-area .hljs-number,.code-area .hljs-literal,.code-area .hljs-variable,.code-area .hljs-template-variable{color:var(--code-number)}\n.code-area .hljs-attr,.code-area .hljs-attribute,.code-area .hljs-name,.code-area .hljs-selector-class{color:var(--code-attr)}\n.code-area .hljs-built_in,.code-area .hljs-type,.code-area .hljs-class .hljs-title{color:var(--code-built-in)}\n[data-viewer-theme='dark'] .code-viewer{--code-bg:#0d1117;--code-toolbar-bg:rgba(13,17,23,.92);--code-border:rgba(139,148,158,.24);--code-text:#e6edf3;--code-muted:#8b949e;--code-keyword:#ff7b72;--code-title:#d2a8ff;--code-string:#a5d6ff;--code-number:#79c0ff;--code-comment:#8b949e;--code-attr:#ffa657;--code-built-in:#7ee787}\n@media (prefers-color-scheme:dark){[data-viewer-theme='system'] .code-viewer{--code-bg:#0d1117;--code-toolbar-bg:rgba(13,17,23,.92);--code-border:rgba(139,148,158,.24);--code-text:#e6edf3;--code-muted:#8b949e;--code-keyword:#ff7b72;--code-title:#d2a8ff;--code-string:#a5d6ff;--code-number:#79c0ff;--code-comment:#8b949e;--code-attr:#ffa657;--code-built-in:#7ee787}}\n";
1
+ export declare const codeStyle = "\n.code-viewer{min-height:100%;--code-bg:#f6f8fa;--code-toolbar-bg:rgba(255,255,255,.92);--code-border:rgba(31,35,40,.12);--code-text:#24292f;--code-muted:#57606a;--code-keyword:#cf222e;--code-title:#8250df;--code-string:#0a3069;--code-number:#0550ae;--code-comment:#6e7781;--code-attr:#953800;--code-built-in:#116329;--code-accent:#0969da;--code-accent-border:rgba(9,105,218,.28);--code-accent-soft:rgba(9,105,218,.1);background:var(--code-bg);color:var(--code-text);box-sizing:border-box}\n.code-toolbar{position:sticky;top:0;z-index:2;display:flex;height:42px;align-items:center;justify-content:space-between;gap:16px;padding:0 16px;border-bottom:1px solid var(--code-border);background:var(--code-toolbar-bg);backdrop-filter:blur(12px);box-sizing:border-box}\n.code-toolbar span,.code-toolbar strong{color:var(--code-muted);font-size:12px;font-weight:700;letter-spacing:0}\n.code-toolbar-meta{display:inline-flex;min-width:0;align-items:center;justify-content:flex-end;gap:10px;white-space:nowrap}\n.code-toolbar-meta>span{overflow:hidden;text-overflow:ellipsis}\n.code-format-status{display:inline-flex;align-items:center;min-height:22px;padding:0 8px;border:1px solid var(--code-accent-border);border-radius:999px;background:var(--code-accent-soft);color:var(--code-accent)!important;font-size:11px!important}\n.code-format-toggle{min-height:26px;padding:0 9px;border:1px solid var(--code-border);border-radius:6px;background:var(--code-bg);color:var(--code-text);font:600 11px/1.2 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;cursor:pointer}\n.code-format-toggle:hover{border-color:var(--code-accent);color:var(--code-accent)}\n.code-format-toggle:focus-visible{outline:2px solid var(--code-accent);outline-offset:2px}\n.code-area{display:block;min-width:min-content;margin:0;padding:18px 20px 28px;overflow:auto;background:transparent;box-sizing:border-box}\n.code-area--line-numbers{display:grid;grid-template-columns:max-content max-content;padding:18px 0 28px}\n.code-line-numbers{position:sticky;left:0;z-index:1;display:block;min-width:4ch;padding:0 12px 0 16px;border-right:1px solid var(--code-border);background:var(--code-bg);color:var(--code-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;font-size:var(--code-font-size,13px);line-height:1.7;text-align:right;white-space:pre;user-select:none;box-sizing:border-box}\n.code-area.code-area--line-numbers code{padding:0 20px}\n.code-area code{display:block;padding:0;overflow:visible;background:transparent;color:inherit;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;font-size:var(--code-font-size,13px);line-height:1.7;tab-size:2;white-space:pre}\n.code-viewer--wrap-lines .code-area{width:100%;min-width:0;overflow-x:hidden}\n.code-viewer--wrap-lines .code-area code{min-width:0;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}\n.code-area--wrapped-line-numbers{display:block;padding:18px 0 28px}\n.code-area--wrapped-line-numbers code{width:100%;padding:0;white-space:normal!important}\n.code-source-line{display:grid;width:100%;min-width:0;grid-template-columns:max-content minmax(0,1fr);align-items:stretch;min-height:1.7em}\n.code-source-line-number{position:sticky;left:0;z-index:1;display:block;min-width:4ch;padding:0 12px 0 16px;border-right:1px solid var(--code-border);background:var(--code-bg);color:var(--code-muted);font:inherit;line-height:inherit;text-align:right;white-space:pre;user-select:none;box-sizing:border-box}\n.code-source-line-content{display:block;min-width:0;padding:0 20px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box}\n.code-source-line-content:empty::after{content:' '}\n.code-area .hljs-comment,.code-area .hljs-quote{color:var(--code-comment)}\n.code-area .hljs-keyword,.code-area .hljs-selector-tag,.code-area .hljs-subst{color:var(--code-keyword)}\n.code-area .hljs-string,.code-area .hljs-doctag,.code-area .hljs-regexp{color:var(--code-string)}\n.code-area .hljs-title,.code-area .hljs-section,.code-area .hljs-selector-id{color:var(--code-title);font-weight:700}\n.code-area .hljs-number,.code-area .hljs-literal,.code-area .hljs-variable,.code-area .hljs-template-variable{color:var(--code-number)}\n.code-area .hljs-attr,.code-area .hljs-attribute,.code-area .hljs-name,.code-area .hljs-selector-class{color:var(--code-attr)}\n.code-area .hljs-built_in,.code-area .hljs-type,.code-area .hljs-class .hljs-title{color:var(--code-built-in)}\n[data-viewer-theme='dark'] .code-viewer{--code-bg:#0d1117;--code-toolbar-bg:rgba(13,17,23,.92);--code-border:rgba(139,148,158,.24);--code-text:#e6edf3;--code-muted:#8b949e;--code-keyword:#ff7b72;--code-title:#d2a8ff;--code-string:#a5d6ff;--code-number:#79c0ff;--code-comment:#8b949e;--code-attr:#ffa657;--code-built-in:#7ee787;--code-accent:#58a6ff;--code-accent-border:rgba(88,166,255,.32);--code-accent-soft:rgba(56,139,253,.15)}\n@media (prefers-color-scheme:dark){[data-viewer-theme='system'] .code-viewer{--code-bg:#0d1117;--code-toolbar-bg:rgba(13,17,23,.92);--code-border:rgba(139,148,158,.24);--code-text:#e6edf3;--code-muted:#8b949e;--code-keyword:#ff7b72;--code-title:#d2a8ff;--code-string:#a5d6ff;--code-number:#79c0ff;--code-comment:#8b949e;--code-attr:#ffa657;--code-built-in:#7ee787;--code-accent:#58a6ff;--code-accent-border:rgba(88,166,255,.32);--code-accent-soft:rgba(56,139,253,.15)}}\n";
package/dist/codeStyle.js CHANGED
@@ -1,12 +1,26 @@
1
1
  export const codeStyle = `
2
- .code-viewer{min-height:100%;--code-bg:#f6f8fa;--code-toolbar-bg:rgba(255,255,255,.92);--code-border:rgba(31,35,40,.12);--code-text:#24292f;--code-muted:#57606a;--code-keyword:#cf222e;--code-title:#8250df;--code-string:#0a3069;--code-number:#0550ae;--code-comment:#6e7781;--code-attr:#953800;--code-built-in:#116329;background:var(--code-bg);color:var(--code-text);box-sizing:border-box}
3
- .code-toolbar{position:sticky;top:0;z-index:1;display:flex;height:42px;align-items:center;justify-content:space-between;gap:16px;padding:0 16px;border-bottom:1px solid var(--code-border);background:var(--code-toolbar-bg);backdrop-filter:blur(12px);box-sizing:border-box}
2
+ .code-viewer{min-height:100%;--code-bg:#f6f8fa;--code-toolbar-bg:rgba(255,255,255,.92);--code-border:rgba(31,35,40,.12);--code-text:#24292f;--code-muted:#57606a;--code-keyword:#cf222e;--code-title:#8250df;--code-string:#0a3069;--code-number:#0550ae;--code-comment:#6e7781;--code-attr:#953800;--code-built-in:#116329;--code-accent:#0969da;--code-accent-border:rgba(9,105,218,.28);--code-accent-soft:rgba(9,105,218,.1);background:var(--code-bg);color:var(--code-text);box-sizing:border-box}
3
+ .code-toolbar{position:sticky;top:0;z-index:2;display:flex;height:42px;align-items:center;justify-content:space-between;gap:16px;padding:0 16px;border-bottom:1px solid var(--code-border);background:var(--code-toolbar-bg);backdrop-filter:blur(12px);box-sizing:border-box}
4
4
  .code-toolbar span,.code-toolbar strong{color:var(--code-muted);font-size:12px;font-weight:700;letter-spacing:0}
5
+ .code-toolbar-meta{display:inline-flex;min-width:0;align-items:center;justify-content:flex-end;gap:10px;white-space:nowrap}
6
+ .code-toolbar-meta>span{overflow:hidden;text-overflow:ellipsis}
7
+ .code-format-status{display:inline-flex;align-items:center;min-height:22px;padding:0 8px;border:1px solid var(--code-accent-border);border-radius:999px;background:var(--code-accent-soft);color:var(--code-accent)!important;font-size:11px!important}
8
+ .code-format-toggle{min-height:26px;padding:0 9px;border:1px solid var(--code-border);border-radius:6px;background:var(--code-bg);color:var(--code-text);font:600 11px/1.2 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;cursor:pointer}
9
+ .code-format-toggle:hover{border-color:var(--code-accent);color:var(--code-accent)}
10
+ .code-format-toggle:focus-visible{outline:2px solid var(--code-accent);outline-offset:2px}
5
11
  .code-area{display:block;min-width:min-content;margin:0;padding:18px 20px 28px;overflow:auto;background:transparent;box-sizing:border-box}
6
12
  .code-area--line-numbers{display:grid;grid-template-columns:max-content max-content;padding:18px 0 28px}
7
13
  .code-line-numbers{position:sticky;left:0;z-index:1;display:block;min-width:4ch;padding:0 12px 0 16px;border-right:1px solid var(--code-border);background:var(--code-bg);color:var(--code-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;font-size:var(--code-font-size,13px);line-height:1.7;text-align:right;white-space:pre;user-select:none;box-sizing:border-box}
8
14
  .code-area.code-area--line-numbers code{padding:0 20px}
9
15
  .code-area code{display:block;padding:0;overflow:visible;background:transparent;color:inherit;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;font-size:var(--code-font-size,13px);line-height:1.7;tab-size:2;white-space:pre}
16
+ .code-viewer--wrap-lines .code-area{width:100%;min-width:0;overflow-x:hidden}
17
+ .code-viewer--wrap-lines .code-area code{min-width:0;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}
18
+ .code-area--wrapped-line-numbers{display:block;padding:18px 0 28px}
19
+ .code-area--wrapped-line-numbers code{width:100%;padding:0;white-space:normal!important}
20
+ .code-source-line{display:grid;width:100%;min-width:0;grid-template-columns:max-content minmax(0,1fr);align-items:stretch;min-height:1.7em}
21
+ .code-source-line-number{position:sticky;left:0;z-index:1;display:block;min-width:4ch;padding:0 12px 0 16px;border-right:1px solid var(--code-border);background:var(--code-bg);color:var(--code-muted);font:inherit;line-height:inherit;text-align:right;white-space:pre;user-select:none;box-sizing:border-box}
22
+ .code-source-line-content{display:block;min-width:0;padding:0 20px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box}
23
+ .code-source-line-content:empty::after{content:' '}
10
24
  .code-area .hljs-comment,.code-area .hljs-quote{color:var(--code-comment)}
11
25
  .code-area .hljs-keyword,.code-area .hljs-selector-tag,.code-area .hljs-subst{color:var(--code-keyword)}
12
26
  .code-area .hljs-string,.code-area .hljs-doctag,.code-area .hljs-regexp{color:var(--code-string)}
@@ -14,6 +28,6 @@ export const codeStyle = `
14
28
  .code-area .hljs-number,.code-area .hljs-literal,.code-area .hljs-variable,.code-area .hljs-template-variable{color:var(--code-number)}
15
29
  .code-area .hljs-attr,.code-area .hljs-attribute,.code-area .hljs-name,.code-area .hljs-selector-class{color:var(--code-attr)}
16
30
  .code-area .hljs-built_in,.code-area .hljs-type,.code-area .hljs-class .hljs-title{color:var(--code-built-in)}
17
- [data-viewer-theme='dark'] .code-viewer{--code-bg:#0d1117;--code-toolbar-bg:rgba(13,17,23,.92);--code-border:rgba(139,148,158,.24);--code-text:#e6edf3;--code-muted:#8b949e;--code-keyword:#ff7b72;--code-title:#d2a8ff;--code-string:#a5d6ff;--code-number:#79c0ff;--code-comment:#8b949e;--code-attr:#ffa657;--code-built-in:#7ee787}
18
- @media (prefers-color-scheme:dark){[data-viewer-theme='system'] .code-viewer{--code-bg:#0d1117;--code-toolbar-bg:rgba(13,17,23,.92);--code-border:rgba(139,148,158,.24);--code-text:#e6edf3;--code-muted:#8b949e;--code-keyword:#ff7b72;--code-title:#d2a8ff;--code-string:#a5d6ff;--code-number:#79c0ff;--code-comment:#8b949e;--code-attr:#ffa657;--code-built-in:#7ee787}}
31
+ [data-viewer-theme='dark'] .code-viewer{--code-bg:#0d1117;--code-toolbar-bg:rgba(13,17,23,.92);--code-border:rgba(139,148,158,.24);--code-text:#e6edf3;--code-muted:#8b949e;--code-keyword:#ff7b72;--code-title:#d2a8ff;--code-string:#a5d6ff;--code-number:#79c0ff;--code-comment:#8b949e;--code-attr:#ffa657;--code-built-in:#7ee787;--code-accent:#58a6ff;--code-accent-border:rgba(88,166,255,.32);--code-accent-soft:rgba(56,139,253,.15)}
32
+ @media (prefers-color-scheme:dark){[data-viewer-theme='system'] .code-viewer{--code-bg:#0d1117;--code-toolbar-bg:rgba(13,17,23,.92);--code-border:rgba(139,148,158,.24);--code-text:#e6edf3;--code-muted:#8b949e;--code-keyword:#ff7b72;--code-title:#d2a8ff;--code-string:#a5d6ff;--code-number:#79c0ff;--code-comment:#8b949e;--code-attr:#ffa657;--code-built-in:#7ee787;--code-accent:#58a6ff;--code-accent-border:rgba(88,166,255,.32);--code-accent-soft:rgba(56,139,253,.15)}}
19
33
  `;
package/dist/largeText.js CHANGED
@@ -8,6 +8,7 @@ const LARGE_TEXT_INDEX_YIELD_BYTES = 4 * 1024 * 1024;
8
8
  const LARGE_TEXT_SEARCH_CHUNK_BYTES = 256 * 1024;
9
9
  const LARGE_TEXT_MAX_SCROLL_HEIGHT = 8000000;
10
10
  const LARGE_TEXT_BASE_LINE_HEIGHT = 22.1;
11
+ const LARGE_TEXT_MEASUREMENT_BLOCK_LINES = 256;
11
12
  const clamp = (value, minimum, maximum) => {
12
13
  return Number.isFinite(value)
13
14
  ? Math.max(minimum, Math.min(maximum, value))
@@ -234,8 +235,6 @@ export const shouldVirtualizeMarkdownBuffer = (buffer, context) => {
234
235
  const largeTextStyle = `
235
236
  .code-viewer--virtual{height:100%;min-height:240px;display:flex;flex-direction:column;overflow:hidden}
236
237
  .code-viewer--virtual .code-toolbar{flex:0 0 42px}
237
- .code-toolbar-meta{display:inline-flex;min-width:0;align-items:center;justify-content:flex-end;gap:10px;white-space:nowrap}
238
- .code-toolbar-meta span{overflow:hidden;text-overflow:ellipsis}
239
238
  .code-virtual-scroll{position:relative;flex:1 1 auto;min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable;contain:strict;background:var(--code-bg)}
240
239
  .code-virtual-spacer{position:relative;min-width:100%}
241
240
  .code-virtual-window{position:absolute;top:0;left:0;min-width:100%;will-change:transform}
@@ -248,9 +247,15 @@ const largeTextStyle = `
248
247
  .code-line-segments button{width:22px;height:18px;padding:0;border:1px solid var(--code-border);border-radius:4px;background:var(--code-bg);color:var(--code-muted);font:700 11px/1 ui-monospace,SFMono-Regular,Menlo,monospace;cursor:pointer}
249
248
  .code-line-segments button:disabled{cursor:not-allowed;opacity:.4}
250
249
  .code-line-segments span{min-width:64px;color:var(--code-muted);font-size:11px;line-height:1;text-align:center}
250
+ .code-viewer--virtual.code-viewer--wrap-lines .code-virtual-scroll{overflow-x:hidden}
251
+ .code-viewer--virtual.code-viewer--wrap-lines .code-virtual-spacer,.code-viewer--virtual.code-viewer--wrap-lines .code-virtual-window{width:100%;min-width:0}
252
+ .code-viewer--virtual.code-viewer--wrap-lines .code-virtual-line{width:100%;height:auto;min-height:var(--code-line-height,22.1px);min-width:0;align-items:flex-start;white-space:normal;contain:layout paint style}
253
+ .code-viewer--virtual.code-viewer--wrap-lines .code-virtual-number{align-self:stretch}
254
+ .code-viewer--virtual.code-viewer--wrap-lines .code-line-segments{flex:0 0 auto;align-self:stretch;height:auto}
255
+ .code-viewer--virtual.code-viewer--wrap-lines .code-virtual-content{display:block;min-width:0;flex:1 1 auto;padding:0 18px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}
251
256
  `;
252
257
  export default async function renderLargeText(buffer, target, type = 'txt', context) {
253
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
258
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
254
259
  const t = createFileViewerTranslator(context === null || context === void 0 ? void 0 : context.options);
255
260
  const documentRef = target.ownerDocument;
256
261
  const sourceBytes = new Uint8Array(buffer);
@@ -268,9 +273,11 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
268
273
  // Undefined preserves the large-text renderer's pre-option behavior. An
269
274
  // explicit boolean has the same meaning in both regular and virtual views.
270
275
  const showLineNumbers = ((_k = (_j = context === null || context === void 0 ? void 0 : context.options) === null || _j === void 0 ? void 0 : _j.text) === null || _k === void 0 ? void 0 : _k.lineNumbers) !== false;
276
+ const wrapLongLines = ((_m = (_l = context === null || context === void 0 ? void 0 : context.options) === null || _l === void 0 ? void 0 : _l.text) === null || _m === void 0 ? void 0 : _m.wrapLongLines) === true;
271
277
  let disposed = false;
272
278
  let zoom = 1;
273
279
  let scheduledFrame = 0;
280
+ let measurementFrame = 0;
274
281
  let lastWindowStart = -1;
275
282
  let activeLine = -1;
276
283
  let searchGeneration = 0;
@@ -279,13 +286,17 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
279
286
  const style = documentRef.createElement('style');
280
287
  style.textContent = `${codeStyle}\n${largeTextStyle}`;
281
288
  const root = documentRef.createElement('div');
282
- root.className = showLineNumbers
283
- ? 'code-viewer code-viewer--virtual code-viewer--line-numbers'
284
- : 'code-viewer code-viewer--virtual';
289
+ root.className = [
290
+ 'code-viewer',
291
+ 'code-viewer--virtual',
292
+ showLineNumbers ? 'code-viewer--line-numbers' : '',
293
+ wrapLongLines ? 'code-viewer--wrap-lines' : ''
294
+ ].filter(Boolean).join(' ');
285
295
  root.dataset.viewerZoomProvider = 'code';
286
296
  root.dataset.viewerSearchProvider = 'code-virtual';
287
297
  root.dataset.textToolbar = String(showToolbar);
288
298
  root.dataset.lineNumbers = String(showLineNumbers);
299
+ root.dataset.wrapLongLines = String(wrapLongLines);
289
300
  root.dataset.textEncoding = source.encoding;
290
301
  const toolbar = documentRef.createElement('div');
291
302
  toolbar.className = 'code-toolbar';
@@ -302,7 +313,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
302
313
  root.append(toolbar);
303
314
  }
304
315
  target.replaceChildren(style, root);
305
- (_l = context === null || context === void 0 ? void 0 : context.onProgressiveRender) === null || _l === void 0 ? void 0 : _l.call(context);
316
+ (_o = context === null || context === void 0 ? void 0 : context.onProgressiveRender) === null || _o === void 0 ? void 0 : _o.call(context);
306
317
  const index = await buildLargeTextIndex(bytes, source.encoding, target, progress => {
307
318
  if (!disposed) {
308
319
  status.textContent = t('text.code.indexingLargeFile', { progress });
@@ -329,29 +340,154 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
329
340
  root.append(viewport);
330
341
  const getLineHeight = () => LARGE_TEXT_BASE_LINE_HEIGHT * zoom;
331
342
  const getViewportHeight = () => Math.max(240, viewport.clientHeight || 600);
343
+ const measuredLineHeights = new Map();
344
+ const measuredLineHeightsByBlock = new Map();
345
+ const heightCorrectionTree = new Float64Array(Math.ceil(index.lineCount / LARGE_TEXT_MEASUREMENT_BLOCK_LINES) + 1);
346
+ const updateHeightCorrection = (blockIndex, delta) => {
347
+ for (let treeIndex = blockIndex + 1; treeIndex < heightCorrectionTree.length; treeIndex += treeIndex & -treeIndex) {
348
+ heightCorrectionTree[treeIndex] += delta;
349
+ }
350
+ };
351
+ const getHeightCorrectionBeforeBlock = (blockIndex) => {
352
+ let correction = 0;
353
+ for (let treeIndex = blockIndex; treeIndex > 0; treeIndex -= treeIndex & -treeIndex) {
354
+ correction += heightCorrectionTree[treeIndex];
355
+ }
356
+ return correction;
357
+ };
358
+ const getOffsetForLine = (requestedLine) => {
359
+ const lineIndex = clamp(Math.trunc(requestedLine), 0, index.lineCount);
360
+ if (!wrapLongLines || lineIndex === 0) {
361
+ return lineIndex * getLineHeight();
362
+ }
363
+ const blockIndex = Math.floor(lineIndex / LARGE_TEXT_MEASUREMENT_BLOCK_LINES);
364
+ let correction = getHeightCorrectionBeforeBlock(blockIndex);
365
+ const blockMeasurements = measuredLineHeightsByBlock.get(blockIndex);
366
+ if (blockMeasurements) {
367
+ for (const [measuredLine, height] of blockMeasurements) {
368
+ if (measuredLine >= lineIndex) {
369
+ continue;
370
+ }
371
+ correction += height - getLineHeight();
372
+ }
373
+ }
374
+ return (lineIndex * getLineHeight()) + correction;
375
+ };
376
+ const getTotalContentHeight = () => getOffsetForLine(index.lineCount);
332
377
  const getWindowLineCount = () => Math.min(index.lineCount, Math.ceil(getViewportHeight() / getLineHeight()) + (overscan * 2) + 2);
333
- const getSpacerHeight = () => Math.min(LARGE_TEXT_MAX_SCROLL_HEIGHT, Math.max(getViewportHeight(), index.lineCount * getLineHeight()));
334
- const usesCappedScrollHeight = () => index.lineCount * getLineHeight() > LARGE_TEXT_MAX_SCROLL_HEIGHT;
378
+ const getSpacerHeight = () => Math.min(LARGE_TEXT_MAX_SCROLL_HEIGHT, Math.max(getViewportHeight(), getTotalContentHeight()));
379
+ const usesCappedScrollHeight = () => getTotalContentHeight() > LARGE_TEXT_MAX_SCROLL_HEIGHT;
335
380
  const updateSpacerHeight = () => {
336
381
  root.style.setProperty('--code-font-size', `${13 * zoom}px`);
337
382
  root.style.setProperty('--code-line-height', `${getLineHeight()}px`);
338
383
  spacer.style.height = `${getSpacerHeight()}px`;
339
384
  };
385
+ const getLineAtOffset = (requestedOffset) => {
386
+ const offset = clamp(requestedOffset, 0, Math.max(0, getTotalContentHeight() - 1));
387
+ let low = 0;
388
+ let high = Math.max(0, index.lineCount - 1);
389
+ while (low < high) {
390
+ const middle = Math.ceil((low + high) / 2);
391
+ if (getOffsetForLine(middle) <= offset) {
392
+ low = middle;
393
+ }
394
+ else {
395
+ high = middle - 1;
396
+ }
397
+ }
398
+ return low;
399
+ };
340
400
  const getFirstVisibleLine = () => {
341
401
  if (!usesCappedScrollHeight()) {
342
- return clamp(Math.floor(viewport.scrollTop / getLineHeight()), 0, index.lineCount - 1);
402
+ return wrapLongLines
403
+ ? getLineAtOffset(viewport.scrollTop)
404
+ : clamp(Math.floor(viewport.scrollTop / getLineHeight()), 0, index.lineCount - 1);
343
405
  }
344
406
  const maxScrollTop = Math.max(1, getSpacerHeight() - getViewportHeight());
345
407
  return clamp(Math.round((viewport.scrollTop / maxScrollTop) * (index.lineCount - 1)), 0, index.lineCount - 1);
346
408
  };
347
409
  const getWindowOffset = (startLine, renderedLineCount) => {
348
410
  if (!usesCappedScrollHeight()) {
349
- return startLine * getLineHeight();
411
+ return getOffsetForLine(startLine);
350
412
  }
351
413
  const maxStart = Math.max(1, index.lineCount - renderedLineCount);
352
414
  const maxOffset = Math.max(0, getSpacerHeight() - (renderedLineCount * getLineHeight()));
353
415
  return (startLine / maxStart) * maxOffset;
354
416
  };
417
+ const setMeasuredLineHeight = (lineIndex, measuredHeight) => {
418
+ var _a;
419
+ const nextHeight = Math.max(getLineHeight(), measuredHeight);
420
+ const previousHeight = (_a = measuredLineHeights.get(lineIndex)) !== null && _a !== void 0 ? _a : getLineHeight();
421
+ if (Math.abs(nextHeight - previousHeight) < 0.5) {
422
+ return false;
423
+ }
424
+ const blockIndex = Math.floor(lineIndex / LARGE_TEXT_MEASUREMENT_BLOCK_LINES);
425
+ measuredLineHeights.set(lineIndex, nextHeight);
426
+ let blockMeasurements = measuredLineHeightsByBlock.get(blockIndex);
427
+ if (!blockMeasurements) {
428
+ blockMeasurements = new Map();
429
+ measuredLineHeightsByBlock.set(blockIndex, blockMeasurements);
430
+ }
431
+ blockMeasurements.set(lineIndex, nextHeight);
432
+ updateHeightCorrection(blockIndex, nextHeight - previousHeight);
433
+ return true;
434
+ };
435
+ const clearMeasuredLineHeights = () => {
436
+ measuredLineHeights.clear();
437
+ measuredLineHeightsByBlock.clear();
438
+ heightCorrectionTree.fill(0);
439
+ };
440
+ const scheduleWrappedMeasurement = (startLine, renderedLineCount) => {
441
+ var _a, _b, _c;
442
+ if (!wrapLongLines || disposed || renderedLineCount === 0) {
443
+ return;
444
+ }
445
+ const view = getWindow(target);
446
+ if (measurementFrame && (view === null || view === void 0 ? void 0 : view.cancelAnimationFrame)) {
447
+ view.cancelAnimationFrame(measurementFrame);
448
+ }
449
+ else if (measurementFrame) {
450
+ (_a = view === null || view === void 0 ? void 0 : view.clearTimeout) === null || _a === void 0 ? void 0 : _a.call(view, measurementFrame);
451
+ }
452
+ const measure = () => {
453
+ measurementFrame = 0;
454
+ if (disposed) {
455
+ return;
456
+ }
457
+ const anchorLine = getFirstVisibleLine();
458
+ const anchorOffset = usesCappedScrollHeight()
459
+ ? 0
460
+ : viewport.scrollTop - getOffsetForLine(anchorLine);
461
+ let changed = false;
462
+ const rows = Array.from(windowElement.querySelectorAll('.code-virtual-line'));
463
+ for (const row of rows) {
464
+ const lineIndex = Number(row.dataset.line) - 1;
465
+ if (!Number.isInteger(lineIndex) || lineIndex < 0) {
466
+ continue;
467
+ }
468
+ const measuredHeight = row.getBoundingClientRect().height || row.offsetHeight || 0;
469
+ if (measuredHeight) {
470
+ changed = setMeasuredLineHeight(lineIndex, measuredHeight) || changed;
471
+ }
472
+ }
473
+ if (!changed) {
474
+ return;
475
+ }
476
+ updateSpacerHeight();
477
+ if (!usesCappedScrollHeight()) {
478
+ viewport.scrollTop = getOffsetForLine(anchorLine) + anchorOffset;
479
+ }
480
+ windowElement.style.transform = `translateY(${getWindowOffset(startLine, renderedLineCount)}px)`;
481
+ lastWindowStart = -1;
482
+ scheduleRender();
483
+ };
484
+ if (view === null || view === void 0 ? void 0 : view.requestAnimationFrame) {
485
+ measurementFrame = view.requestAnimationFrame(measure);
486
+ }
487
+ else {
488
+ measurementFrame = Number((_c = (_b = view === null || view === void 0 ? void 0 : view.setTimeout) === null || _b === void 0 ? void 0 : _b.call(view, measure, 0)) !== null && _c !== void 0 ? _c : setTimeout(measure, 0));
489
+ }
490
+ };
355
491
  const appendHighlightedContent = (content, text, query) => {
356
492
  if (!query) {
357
493
  content.textContent = text || ' ';
@@ -383,6 +519,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
383
519
  const row = documentRef.createElement('div');
384
520
  row.className = 'code-virtual-line';
385
521
  row.dataset.line = String(line.lineIndex + 1);
522
+ row.dataset.logicalLine = String(line.lineIndex + 1);
386
523
  if (line.lineIndex === activeLine) {
387
524
  row.classList.add('code-virtual-line--match');
388
525
  }
@@ -439,6 +576,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
439
576
  }
440
577
  windowElement.replaceChildren(fragment);
441
578
  windowElement.style.transform = `translateY(${getWindowOffset(startLine, lines.length)}px)`;
579
+ scheduleWrappedMeasurement(startLine, lines.length);
442
580
  };
443
581
  const scheduleRender = () => {
444
582
  var _a, _b;
@@ -470,7 +608,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
470
608
  : 0;
471
609
  }
472
610
  else {
473
- viewport.scrollTop = lineIndex * getLineHeight();
611
+ viewport.scrollTop = getOffsetForLine(lineIndex);
474
612
  }
475
613
  lastWindowStart = -1;
476
614
  renderWindow(true);
@@ -592,6 +730,9 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
592
730
  const setZoom = (scale) => {
593
731
  const firstVisibleLine = getFirstVisibleLine();
594
732
  zoom = clampZoom(scale);
733
+ if (wrapLongLines) {
734
+ clearMeasuredLineHeights();
735
+ }
595
736
  updateSpacerHeight();
596
737
  scrollToLine(firstVisibleLine);
597
738
  zoomEmitter.emit();
@@ -612,7 +753,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
612
753
  getState: getZoomState,
613
754
  subscribe: zoomEmitter.subscribe
614
755
  });
615
- (_m = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _m === void 0 ? void 0 : _m.call(context, { print: false, exportHtml: false });
756
+ (_p = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _p === void 0 ? void 0 : _p.call(context, { print: false, exportHtml: false });
616
757
  viewport.addEventListener('scroll', scheduleRender, { passive: true });
617
758
  viewport.addEventListener('click', event => {
618
759
  var _a, _b;
@@ -638,9 +779,16 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
638
779
  lineSegments.set(lineIndex, clamp(next, 0, segmentCount - 1));
639
780
  renderWindow(true);
640
781
  });
641
- const ResizeObserverCtor = (_o = getWindow(target)) === null || _o === void 0 ? void 0 : _o.ResizeObserver;
782
+ const ResizeObserverCtor = (_q = getWindow(target)) === null || _q === void 0 ? void 0 : _q.ResizeObserver;
642
783
  const resizeObserver = ResizeObserverCtor
643
784
  ? new ResizeObserverCtor(() => {
785
+ if (wrapLongLines) {
786
+ const firstVisibleLine = getFirstVisibleLine();
787
+ clearMeasuredLineHeights();
788
+ updateSpacerHeight();
789
+ scrollToLine(firstVisibleLine);
790
+ return;
791
+ }
644
792
  updateSpacerHeight();
645
793
  renderWindow(true);
646
794
  })
@@ -651,7 +799,7 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
651
799
  return {
652
800
  $el: target,
653
801
  unmount() {
654
- var _a, _b;
802
+ var _a, _b, _c;
655
803
  disposed = true;
656
804
  searchGeneration += 1;
657
805
  const view = getWindow(target);
@@ -661,11 +809,17 @@ export default async function renderLargeText(buffer, target, type = 'txt', cont
661
809
  else if (scheduledFrame) {
662
810
  (_a = view === null || view === void 0 ? void 0 : view.clearTimeout) === null || _a === void 0 ? void 0 : _a.call(view, scheduledFrame);
663
811
  }
812
+ if (measurementFrame && (view === null || view === void 0 ? void 0 : view.cancelAnimationFrame)) {
813
+ view.cancelAnimationFrame(measurementFrame);
814
+ }
815
+ else if (measurementFrame) {
816
+ (_b = view === null || view === void 0 ? void 0 : view.clearTimeout) === null || _b === void 0 ? void 0 : _b.call(view, measurementFrame);
817
+ }
664
818
  resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect();
665
819
  viewport.removeEventListener('scroll', scheduleRender);
666
820
  unregisterFileViewerSearchProvider(root);
667
821
  unregisterFileViewerZoomProvider(root);
668
- (_b = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _b === void 0 ? void 0 : _b.call(context, null);
822
+ (_c = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _c === void 0 ? void 0 : _c.call(context, null);
669
823
  target.replaceChildren();
670
824
  }
671
825
  };
@@ -0,0 +1,35 @@
1
+ import type { FileViewerTextOptions } from '@file-viewer/core';
2
+ export declare const DEFAULT_PRETTY_PRINT_MAX_BYTES: number;
3
+ export type FileViewerPrettyPrintReason = 'formatted' | 'disabled' | 'unsupported' | 'too-large' | 'whitespace-sensitive' | 'failed' | 'aborted';
4
+ export interface FileViewerPrettyPrintResult {
5
+ text: string;
6
+ formatted: boolean;
7
+ reason: FileViewerPrettyPrintReason;
8
+ parser?: string;
9
+ sourceByteLength: number;
10
+ maxBytes: number;
11
+ }
12
+ type PrettierPlugin = Record<string, unknown>;
13
+ type PrettierRuntime = {
14
+ format: (source: string, options: Record<string, unknown>) => string | Promise<string>;
15
+ plugins: PrettierPlugin[];
16
+ };
17
+ type PrettierPluginName = 'babel' | 'estree' | 'typescript' | 'postcss' | 'html' | 'markdown' | 'yaml' | 'graphql' | 'xml';
18
+ interface PrettierLanguageDefinition {
19
+ parser: string;
20
+ plugins: readonly PrettierPluginName[];
21
+ resolvePlugins?: (source: string) => readonly PrettierPluginName[];
22
+ options?: Readonly<Record<string, unknown>>;
23
+ }
24
+ export type FileViewerPrettierRuntimeLoader = (definition: PrettierLanguageDefinition, source: string) => Promise<PrettierRuntime>;
25
+ export declare const resolveFileViewerPrettyPrintMaxBytes: (options?: FileViewerTextOptions) => number;
26
+ export declare const supportsFileViewerPrettyPrint: (extension: string) => boolean;
27
+ /**
28
+ * Formats a decoded display representation without mutating the source buffer.
29
+ *
30
+ * Parser support and byte limits are resolved before the Prettier runtime or
31
+ * any parser plugin is imported. Failures intentionally return the original
32
+ * source so malformed or unsupported uploads remain previewable.
33
+ */
34
+ export declare const formatFileViewerTextForDisplay: (source: string, extension: string, options?: FileViewerTextOptions, signal?: AbortSignal, runtimeLoader?: FileViewerPrettierRuntimeLoader) => Promise<FileViewerPrettyPrintResult>;
35
+ export {};
@@ -0,0 +1,232 @@
1
+ export const DEFAULT_PRETTY_PRINT_MAX_BYTES = 512 * 1024;
2
+ const htmlEmbeddedLanguagePattern = /<(?:script|style)(?:\s|>)/i;
3
+ const htmlScriptPattern = /<script(?:\s|>)/i;
4
+ const htmlStylePattern = /<style(?:\s|>)/i;
5
+ const withHtmlEmbeddedPlugins = (source) => {
6
+ if (!htmlEmbeddedLanguagePattern.test(source)) {
7
+ return ['html'];
8
+ }
9
+ const plugins = ['html'];
10
+ if (htmlScriptPattern.test(source)) {
11
+ plugins.push('babel', 'estree', 'typescript');
12
+ }
13
+ if (htmlStylePattern.test(source)) {
14
+ plugins.push('postcss');
15
+ }
16
+ return plugins;
17
+ };
18
+ const prettierLanguages = {
19
+ cjs: { parser: 'babel', plugins: ['babel', 'estree'] },
20
+ css: { parser: 'css', plugins: ['postcss'] },
21
+ graphql: { parser: 'graphql', plugins: ['graphql'] },
22
+ gql: { parser: 'graphql', plugins: ['graphql'] },
23
+ html: { parser: 'html', plugins: ['html'], resolvePlugins: withHtmlEmbeddedPlugins },
24
+ htm: { parser: 'html', plugins: ['html'], resolvePlugins: withHtmlEmbeddedPlugins },
25
+ ipynb: { parser: 'json', plugins: ['babel', 'estree'] },
26
+ js: { parser: 'babel', plugins: ['babel', 'estree'] },
27
+ json: { parser: 'json', plugins: ['babel', 'estree'] },
28
+ json5: { parser: 'json5', plugins: ['babel', 'estree'] },
29
+ jsonc: { parser: 'jsonc', plugins: ['babel', 'estree'] },
30
+ jsx: { parser: 'babel', plugins: ['babel', 'estree'] },
31
+ markdown: { parser: 'markdown', plugins: ['markdown'] },
32
+ md: { parser: 'markdown', plugins: ['markdown'] },
33
+ mjs: { parser: 'babel', plugins: ['babel', 'estree'] },
34
+ react: { parser: 'babel', plugins: ['babel', 'estree'] },
35
+ ts: { parser: 'typescript', plugins: ['typescript', 'estree'] },
36
+ tsx: { parser: 'typescript', plugins: ['typescript', 'estree'] },
37
+ vue: {
38
+ parser: 'vue',
39
+ plugins: ['html', 'babel', 'estree', 'typescript', 'postcss']
40
+ },
41
+ xml: {
42
+ parser: 'xml',
43
+ plugins: ['xml'],
44
+ options: {
45
+ xmlWhitespaceSensitivity: 'preserve',
46
+ xmlQuoteAttributes: 'preserve',
47
+ xmlSortAttributesByKey: false
48
+ }
49
+ },
50
+ yaml: { parser: 'yaml', plugins: ['yaml'] },
51
+ yml: { parser: 'yaml', plugins: ['yaml'] }
52
+ };
53
+ const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace';
54
+ const hasWhitespaceSensitiveXml = (source) => {
55
+ if (/\bxml:space\s*=\s*(["'])preserve\1/i.test(source)) {
56
+ return true;
57
+ }
58
+ const Parser = globalThis.DOMParser;
59
+ if (typeof Parser !== 'function') {
60
+ // The text renderer itself is browser-facing, but fail closed when this
61
+ // helper is evaluated in a non-DOM runtime.
62
+ return true;
63
+ }
64
+ try {
65
+ const documentRef = new Parser().parseFromString(source, 'application/xml');
66
+ if (documentRef.querySelector('parsererror')) {
67
+ return false;
68
+ }
69
+ const containsMixedContent = (element) => {
70
+ var _a, _b, _c;
71
+ const preservesWhitespace = ((_a = element.getAttributeNS(XML_NAMESPACE, 'space')) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'preserve' ||
72
+ ((_b = element.getAttribute('xml:space')) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'preserve';
73
+ if (preservesWhitespace) {
74
+ return true;
75
+ }
76
+ let hasElementChild = false;
77
+ let hasDirectText = false;
78
+ for (const child of Array.from(element.childNodes)) {
79
+ if (child.nodeType === 1) {
80
+ hasElementChild = true;
81
+ if (containsMixedContent(child)) {
82
+ return true;
83
+ }
84
+ }
85
+ else if ((child.nodeType === 3 || child.nodeType === 4) && ((_c = child.textContent) === null || _c === void 0 ? void 0 : _c.trim())) {
86
+ hasDirectText = true;
87
+ }
88
+ }
89
+ return hasElementChild && hasDirectText;
90
+ };
91
+ return documentRef.documentElement
92
+ ? containsMixedContent(documentRef.documentElement)
93
+ : false;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ };
99
+ const pluginLoaders = {
100
+ babel: () => import('prettier/plugins/babel').then(module => module.default),
101
+ estree: () => import('prettier/plugins/estree').then(module => module.default),
102
+ typescript: () => import('prettier/plugins/typescript').then(module => module.default),
103
+ postcss: () => import('prettier/plugins/postcss').then(module => module.default),
104
+ html: () => import('prettier/plugins/html').then(module => module.default),
105
+ markdown: () => import('prettier/plugins/markdown').then(module => module.default),
106
+ yaml: () => import('prettier/plugins/yaml').then(module => module.default),
107
+ graphql: () => import('prettier/plugins/graphql').then(module => module.default),
108
+ xml: () => import('@prettier/plugin-xml').then(module => module.default)
109
+ };
110
+ const loadPrettierRuntime = async (definition, source) => {
111
+ var _a, _b;
112
+ const prettierPromise = import('prettier/standalone');
113
+ const pluginNames = (_b = (_a = definition.resolvePlugins) === null || _a === void 0 ? void 0 : _a.call(definition, source)) !== null && _b !== void 0 ? _b : definition.plugins;
114
+ const [prettier, ...plugins] = await Promise.all([
115
+ prettierPromise,
116
+ ...pluginNames.map(pluginName => pluginLoaders[pluginName]())
117
+ ]);
118
+ return {
119
+ format: prettier.format,
120
+ plugins
121
+ };
122
+ };
123
+ const utf8ByteLength = (source) => new TextEncoder().encode(source).byteLength;
124
+ export const resolveFileViewerPrettyPrintMaxBytes = (options) => {
125
+ var _a;
126
+ const configured = (_a = options === null || options === void 0 ? void 0 : options.prettyPrintMaxBytes) !== null && _a !== void 0 ? _a : options === null || options === void 0 ? void 0 : options.virtualizeAboveBytes;
127
+ if (!Number.isFinite(configured)) {
128
+ return DEFAULT_PRETTY_PRINT_MAX_BYTES;
129
+ }
130
+ return Math.max(0, Math.trunc(Number(configured)));
131
+ };
132
+ export const supportsFileViewerPrettyPrint = (extension) => {
133
+ return Boolean(prettierLanguages[extension.trim().toLowerCase()]);
134
+ };
135
+ /**
136
+ * Formats a decoded display representation without mutating the source buffer.
137
+ *
138
+ * Parser support and byte limits are resolved before the Prettier runtime or
139
+ * any parser plugin is imported. Failures intentionally return the original
140
+ * source so malformed or unsupported uploads remain previewable.
141
+ */
142
+ export const formatFileViewerTextForDisplay = async (source, extension, options, signal, runtimeLoader = loadPrettierRuntime) => {
143
+ const maxBytes = resolveFileViewerPrettyPrintMaxBytes(options);
144
+ const sourceByteLength = utf8ByteLength(source);
145
+ if ((options === null || options === void 0 ? void 0 : options.prettyPrint) !== true) {
146
+ return { text: source, formatted: false, reason: 'disabled', sourceByteLength, maxBytes };
147
+ }
148
+ const definition = prettierLanguages[extension.trim().toLowerCase()];
149
+ if (!definition) {
150
+ return { text: source, formatted: false, reason: 'unsupported', sourceByteLength, maxBytes };
151
+ }
152
+ if (sourceByteLength > maxBytes) {
153
+ return {
154
+ text: source,
155
+ formatted: false,
156
+ reason: 'too-large',
157
+ parser: definition.parser,
158
+ sourceByteLength,
159
+ maxBytes
160
+ };
161
+ }
162
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
163
+ return {
164
+ text: source,
165
+ formatted: false,
166
+ reason: 'aborted',
167
+ parser: definition.parser,
168
+ sourceByteLength,
169
+ maxBytes
170
+ };
171
+ }
172
+ if (definition.parser === 'xml' && hasWhitespaceSensitiveXml(source)) {
173
+ return {
174
+ text: source,
175
+ formatted: false,
176
+ reason: 'whitespace-sensitive',
177
+ parser: definition.parser,
178
+ sourceByteLength,
179
+ maxBytes
180
+ };
181
+ }
182
+ try {
183
+ const runtime = await runtimeLoader(definition, source);
184
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
185
+ return {
186
+ text: source,
187
+ formatted: false,
188
+ reason: 'aborted',
189
+ parser: definition.parser,
190
+ sourceByteLength,
191
+ maxBytes
192
+ };
193
+ }
194
+ const formatted = await runtime.format(source, {
195
+ parser: definition.parser,
196
+ plugins: runtime.plugins,
197
+ tabWidth: 2,
198
+ useTabs: false,
199
+ printWidth: 80,
200
+ endOfLine: 'lf',
201
+ ...definition.options
202
+ });
203
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
204
+ return {
205
+ text: source,
206
+ formatted: false,
207
+ reason: 'aborted',
208
+ parser: definition.parser,
209
+ sourceByteLength,
210
+ maxBytes
211
+ };
212
+ }
213
+ return {
214
+ text: formatted.replace(/\n$/, ''),
215
+ formatted: true,
216
+ reason: 'formatted',
217
+ parser: definition.parser,
218
+ sourceByteLength,
219
+ maxBytes
220
+ };
221
+ }
222
+ catch {
223
+ return {
224
+ text: source,
225
+ formatted: false,
226
+ reason: 'failed',
227
+ parser: definition.parser,
228
+ sourceByteLength,
229
+ maxBytes
230
+ };
231
+ }
232
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-text",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone code, text, and Markdown renderer for File Viewer; Mermaid, patch diff, and Git bundle tools are explicit opt-in capabilities.",
@@ -58,15 +58,17 @@
58
58
  "LICENSE"
59
59
  ],
60
60
  "dependencies": {
61
- "@file-viewer/core": "3.0.0",
61
+ "@file-viewer/core": "3.0.2",
62
+ "@prettier/plugin-xml": "3.4.2",
63
+ "dompurify": "^3.4.14",
62
64
  "highlight.js": "^11.11.1",
63
65
  "marked": "^18.0.5",
64
- "dompurify": "^3.4.13"
66
+ "prettier": "3.9.6"
65
67
  },
66
68
  "devDependencies": {
67
69
  "diff2html": "^3.4.56",
68
- "mermaid": "^11.16.1",
69
- "pako": "^2.1.0",
70
+ "mermaid": "^11.17.2",
71
+ "pako": "^2.2.0",
70
72
  "jsdom": "^27.4.0",
71
73
  "typescript": "^6.0.3"
72
74
  },