@file-viewer/renderer-text 2.1.24 → 2.1.26

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
@@ -1,6 +1,6 @@
1
1
  # @file-viewer/renderer-text
2
2
 
3
- Standalone code, text, Markdown, patch, and git bundle renderer package for Flyfish File Viewer. It handles source highlighting, Markdown reading surfaces, side-by-side patch diffs, git bundle inspection, and unified zoom for `.txt`, `.json`, `.ts`, `.vue`, `.log`, `.md`, `.markdown`, `.patch`, `.bundle`, and other code delivery formats.
3
+ Standalone code, text, Markdown, patch, and git bundle renderer package for Flyfish File Viewer. It handles source highlighting, Markdown reading surfaces, embedded Mermaid diagrams in Markdown, side-by-side patch diffs, git bundle inspection, and unified zoom for `.txt`, `.json`, `.ts`, `.vue`, `.log`, `.md`, `.markdown`, `.patch`, `.bundle`, and other code delivery formats.
4
4
 
5
5
  ## Usage
6
6
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @file-viewer/renderer-text
2
2
 
3
- Flyfish File Viewer 的独立代码、文本、Markdown、patch 和 git bundle renderer 包。它负责 `.txt`、`.json`、`.ts`、`.vue`、`.log`、`.md`、`.markdown`、`.patch`、`.bundle` 等文本与代码交付文件的源码高亮、Markdown 阅读面、左右比对、bundle 结构查看和统一缩放。
3
+ Flyfish File Viewer 的独立代码、文本、Markdown、patch 和 git bundle renderer 包。它负责 `.txt`、`.json`、`.ts`、`.vue`、`.log`、`.md`、`.markdown`、`.patch`、`.bundle` 等文本与代码交付文件的源码高亮、Markdown 阅读面、Markdown 内嵌 Mermaid 图、左右比对、bundle 结构查看和统一缩放。
4
4
 
5
5
  ## 用法
6
6
 
package/dist/code.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { createFileViewerTranslator, createFileViewerZoomChangeEmitter as createZoomChangeEmitter, readFileViewerText as readText, registerFileViewerZoomProvider, unregisterFileViewerZoomProvider } from '@file-viewer/core';
2
+ import { codeStyle } from './codeStyle.js';
3
+ import renderLargeText, { shouldVirtualizeTextBuffer } from './largeText.js';
2
4
  const languageMap = {
3
5
  bash: 'bash',
4
6
  c: 'cpp',
@@ -77,22 +79,6 @@ const languageLoaders = {
77
79
  xml: () => import('highlight.js/lib/languages/xml'),
78
80
  yaml: () => import('highlight.js/lib/languages/yaml')
79
81
  };
80
- const codeStyle = `
81
- .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}
82
- .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}
83
- .code-toolbar span,.code-toolbar strong{color:var(--code-muted);font-size:12px;font-weight:700;letter-spacing:0}
84
- .code-area{display:block;min-width:min-content;margin:0;padding:18px 20px 28px;overflow:auto;background:transparent;box-sizing:border-box}
85
- .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}
86
- .code-area .hljs-comment,.code-area .hljs-quote{color:var(--code-comment)}
87
- .code-area .hljs-keyword,.code-area .hljs-selector-tag,.code-area .hljs-subst{color:var(--code-keyword)}
88
- .code-area .hljs-string,.code-area .hljs-doctag,.code-area .hljs-regexp{color:var(--code-string)}
89
- .code-area .hljs-title,.code-area .hljs-section,.code-area .hljs-selector-id{color:var(--code-title);font-weight:700}
90
- .code-area .hljs-number,.code-area .hljs-literal,.code-area .hljs-variable,.code-area .hljs-template-variable{color:var(--code-number)}
91
- .code-area .hljs-attr,.code-area .hljs-attribute,.code-area .hljs-name,.code-area .hljs-selector-class{color:var(--code-attr)}
92
- .code-area .hljs-built_in,.code-area .hljs-type,.code-area .hljs-class .hljs-title{color:var(--code-built-in)}
93
- [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}
94
- @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}}
95
- `;
96
82
  let highlighterPromise = null;
97
83
  const registeredLanguages = new Set();
98
84
  const createElement = (tagName, className, text) => {
@@ -163,6 +149,11 @@ export default async function renderText(buffer, target, type, context) {
163
149
  const t = createFileViewerTranslator(context === null || context === void 0 ? void 0 : context.options);
164
150
  const extension = type || 'txt';
165
151
  const normalizedExtension = extension.trim().toLowerCase();
152
+ if (normalizedExtension !== 'bundle' &&
153
+ normalizedExtension !== 'bdl' &&
154
+ shouldVirtualizeTextBuffer(buffer, context)) {
155
+ return renderLargeText(buffer, target, extension, context);
156
+ }
166
157
  if (normalizedExtension === 'patch') {
167
158
  const { default: renderPatch } = await import('./patch.js');
168
159
  return renderPatch(buffer, target, extension, context);
@@ -0,0 +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 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";
@@ -0,0 +1,16 @@
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}
4
+ .code-toolbar span,.code-toolbar strong{color:var(--code-muted);font-size:12px;font-weight:700;letter-spacing:0}
5
+ .code-area{display:block;min-width:min-content;margin:0;padding:18px 20px 28px;overflow:auto;background:transparent;box-sizing:border-box}
6
+ .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}
7
+ .code-area .hljs-comment,.code-area .hljs-quote{color:var(--code-comment)}
8
+ .code-area .hljs-keyword,.code-area .hljs-selector-tag,.code-area .hljs-subst{color:var(--code-keyword)}
9
+ .code-area .hljs-string,.code-area .hljs-doctag,.code-area .hljs-regexp{color:var(--code-string)}
10
+ .code-area .hljs-title,.code-area .hljs-section,.code-area .hljs-selector-id{color:var(--code-title);font-weight:700}
11
+ .code-area .hljs-number,.code-area .hljs-literal,.code-area .hljs-variable,.code-area .hljs-template-variable{color:var(--code-number)}
12
+ .code-area .hljs-attr,.code-area .hljs-attribute,.code-area .hljs-name,.code-area .hljs-selector-class{color:var(--code-attr)}
13
+ .code-area .hljs-built_in,.code-area .hljs-type,.code-area .hljs-class .hljs-title{color:var(--code-built-in)}
14
+ [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}
15
+ @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}}
16
+ `;
package/dist/index.js CHANGED
@@ -6,7 +6,13 @@ if (textDefinitions.length !== textRendererIds.length) {
6
6
  }
7
7
  export const textRendererDefinitions = textDefinitions;
8
8
  export const renderFileViewerCode = (buffer, target, type, context) => import('./code.js').then(({ default: renderCode }) => renderCode(buffer, target, type, context));
9
- export const renderFileViewerMarkdown = (buffer, target) => import('./markdown.js').then(({ default: renderMarkdown }) => renderMarkdown(buffer, target));
9
+ export const renderFileViewerMarkdown = (buffer, target, type, context) => import('./largeText.js').then(async ({ default: renderLargeText, shouldVirtualizeTextBuffer }) => {
10
+ if (shouldVirtualizeTextBuffer(buffer, context)) {
11
+ return renderLargeText(buffer, target, type || 'md', context);
12
+ }
13
+ const { default: renderMarkdown } = await import('./markdown.js');
14
+ return renderMarkdown(buffer, target, context);
15
+ });
10
16
  export const textRenderer = {
11
17
  id: 'file-viewer-renderer-text',
12
18
  label: 'Flyfish File Viewer text renderer',
@@ -0,0 +1,6 @@
1
+ import { type FileRenderContext, type FileViewerRenderedInstance } from '@file-viewer/core';
2
+ export declare const DEFAULT_LARGE_TEXT_THRESHOLD_BYTES: number;
3
+ export declare const DEFAULT_LARGE_TEXT_LINE_SEGMENT_BYTES: number;
4
+ export declare const DEFAULT_LARGE_TEXT_OVERSCAN_LINES = 12;
5
+ export declare const shouldVirtualizeTextBuffer: (buffer: ArrayBuffer, context?: FileRenderContext) => boolean;
6
+ export default function renderLargeText(buffer: ArrayBuffer, target: HTMLDivElement, type?: string, context?: FileRenderContext): Promise<FileViewerRenderedInstance>;
@@ -0,0 +1,580 @@
1
+ import { DEFAULT_FILE_VIEWER_SEARCH_MAX_MATCHES, createEmptyFileViewerSearchState, createFileViewerTranslator, createFileViewerZoomChangeEmitter as createZoomChangeEmitter, normalizeFileViewerSearchOptions, registerFileViewerSearchProvider, registerFileViewerZoomProvider, unregisterFileViewerSearchProvider, unregisterFileViewerZoomProvider } from '@file-viewer/core';
2
+ import { codeStyle } from './codeStyle.js';
3
+ export const DEFAULT_LARGE_TEXT_THRESHOLD_BYTES = 512 * 1024;
4
+ export const DEFAULT_LARGE_TEXT_LINE_SEGMENT_BYTES = 16 * 1024;
5
+ export const DEFAULT_LARGE_TEXT_OVERSCAN_LINES = 12;
6
+ const LARGE_TEXT_LINE_CHECKPOINT_STRIDE = 256;
7
+ const LARGE_TEXT_INDEX_YIELD_BYTES = 4 * 1024 * 1024;
8
+ const LARGE_TEXT_SEARCH_CHUNK_BYTES = 256 * 1024;
9
+ const LARGE_TEXT_MAX_SCROLL_HEIGHT = 8000000;
10
+ const LARGE_TEXT_BASE_LINE_HEIGHT = 22.1;
11
+ const clamp = (value, minimum, maximum) => {
12
+ return Number.isFinite(value)
13
+ ? Math.max(minimum, Math.min(maximum, value))
14
+ : minimum;
15
+ };
16
+ const clampZoom = (value) => {
17
+ return Math.min(2.6, Math.max(0.6, Number(value.toFixed(2))));
18
+ };
19
+ const getWindow = (target) => target.ownerDocument.defaultView;
20
+ const nextBrowserTurn = (target) => {
21
+ const view = getWindow(target);
22
+ return new Promise(resolve => {
23
+ if (view === null || view === void 0 ? void 0 : view.setTimeout) {
24
+ view.setTimeout(resolve, 0);
25
+ return;
26
+ }
27
+ setTimeout(resolve, 0);
28
+ });
29
+ };
30
+ const isUtf8ContinuationByte = (value) => (value & 0xc0) === 0x80;
31
+ const alignUtf8Start = (bytes, offset, limit) => {
32
+ let nextOffset = clamp(offset, 0, limit);
33
+ while (nextOffset < limit && isUtf8ContinuationByte(bytes[nextOffset])) {
34
+ nextOffset += 1;
35
+ }
36
+ return nextOffset;
37
+ };
38
+ const alignUtf8End = (bytes, offset, limit) => {
39
+ let nextOffset = clamp(offset, 0, limit);
40
+ while (nextOffset < limit && isUtf8ContinuationByte(bytes[nextOffset])) {
41
+ nextOffset += 1;
42
+ }
43
+ return nextOffset;
44
+ };
45
+ const isLineBreak = (bytes, offset) => {
46
+ const byte = bytes[offset];
47
+ if (byte === 13) {
48
+ return bytes[offset + 1] === 10 ? 2 : 1;
49
+ }
50
+ return byte === 10 ? 1 : 0;
51
+ };
52
+ const buildLargeTextIndex = async (bytes, target, onProgress) => {
53
+ const checkpoints = [0];
54
+ let lineIndex = 0;
55
+ let offset = 0;
56
+ let nextYieldOffset = LARGE_TEXT_INDEX_YIELD_BYTES;
57
+ while (offset < bytes.byteLength) {
58
+ const lineBreakSize = isLineBreak(bytes, offset);
59
+ if (lineBreakSize) {
60
+ offset += lineBreakSize;
61
+ lineIndex += 1;
62
+ if (lineIndex % LARGE_TEXT_LINE_CHECKPOINT_STRIDE === 0) {
63
+ checkpoints.push(offset);
64
+ }
65
+ }
66
+ else {
67
+ offset += 1;
68
+ }
69
+ if (offset >= nextYieldOffset) {
70
+ onProgress(Math.min(99, Math.round((offset / Math.max(1, bytes.byteLength)) * 100)));
71
+ nextYieldOffset = offset + LARGE_TEXT_INDEX_YIELD_BYTES;
72
+ await nextBrowserTurn(target);
73
+ }
74
+ }
75
+ onProgress(100);
76
+ return {
77
+ bytes,
78
+ checkpoints,
79
+ lineCount: lineIndex + 1
80
+ };
81
+ };
82
+ const locateLargeTextLineStart = (index, requestedLine) => {
83
+ var _a;
84
+ const lineIndex = clamp(Math.trunc(requestedLine), 0, index.lineCount - 1);
85
+ const checkpointIndex = Math.floor(lineIndex / LARGE_TEXT_LINE_CHECKPOINT_STRIDE);
86
+ let currentLine = checkpointIndex * LARGE_TEXT_LINE_CHECKPOINT_STRIDE;
87
+ let offset = (_a = index.checkpoints[checkpointIndex]) !== null && _a !== void 0 ? _a : 0;
88
+ while (currentLine < lineIndex && offset < index.bytes.byteLength) {
89
+ const lineBreakSize = isLineBreak(index.bytes, offset);
90
+ offset += lineBreakSize || 1;
91
+ if (lineBreakSize) {
92
+ currentLine += 1;
93
+ }
94
+ }
95
+ return offset;
96
+ };
97
+ const readLargeTextLineBounds = (index, startLine, count) => {
98
+ const lines = [];
99
+ let lineIndex = clamp(Math.trunc(startLine), 0, index.lineCount - 1);
100
+ let offset = locateLargeTextLineStart(index, lineIndex);
101
+ while (lines.length < count && lineIndex < index.lineCount) {
102
+ const start = offset;
103
+ while (offset < index.bytes.byteLength && !isLineBreak(index.bytes, offset)) {
104
+ offset += 1;
105
+ }
106
+ lines.push({ lineIndex, start, end: offset });
107
+ const lineBreakSize = isLineBreak(index.bytes, offset);
108
+ offset += lineBreakSize;
109
+ lineIndex += 1;
110
+ }
111
+ return lines;
112
+ };
113
+ const findLargeTextLineAtOffset = (index, requestedOffset) => {
114
+ var _a, _b;
115
+ const offset = clamp(Math.trunc(requestedOffset), 0, index.bytes.byteLength);
116
+ let low = 0;
117
+ let high = index.checkpoints.length - 1;
118
+ while (low < high) {
119
+ const middle = Math.ceil((low + high) / 2);
120
+ if (((_a = index.checkpoints[middle]) !== null && _a !== void 0 ? _a : 0) <= offset) {
121
+ low = middle;
122
+ }
123
+ else {
124
+ high = middle - 1;
125
+ }
126
+ }
127
+ let lineIndex = low * LARGE_TEXT_LINE_CHECKPOINT_STRIDE;
128
+ let cursor = (_b = index.checkpoints[low]) !== null && _b !== void 0 ? _b : 0;
129
+ while (cursor < offset && cursor < index.bytes.byteLength) {
130
+ const lineBreakSize = isLineBreak(index.bytes, cursor);
131
+ cursor += lineBreakSize || 1;
132
+ if (lineBreakSize) {
133
+ lineIndex += 1;
134
+ }
135
+ }
136
+ return clamp(lineIndex, 0, index.lineCount - 1);
137
+ };
138
+ const decodeLargeTextSegment = (index, line, segmentIndex, segmentBytes) => {
139
+ const segmentCount = Math.max(1, Math.ceil((line.end - line.start) / segmentBytes));
140
+ const normalizedSegment = clamp(Math.trunc(segmentIndex), 0, segmentCount - 1);
141
+ const rawStart = line.start + (normalizedSegment * segmentBytes);
142
+ const rawEnd = Math.min(line.end, rawStart + segmentBytes);
143
+ const start = alignUtf8Start(index.bytes, rawStart, line.end);
144
+ const end = alignUtf8End(index.bytes, rawEnd, line.end);
145
+ return {
146
+ text: new TextDecoder('utf-8').decode(index.bytes.subarray(start, end)),
147
+ segmentCount,
148
+ segmentIndex: normalizedSegment
149
+ };
150
+ };
151
+ const formatLargeNumber = (value) => {
152
+ try {
153
+ return new Intl.NumberFormat().format(value);
154
+ }
155
+ catch {
156
+ return String(value);
157
+ }
158
+ };
159
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
160
+ const createLargeTextSearchRegExp = (query, options) => {
161
+ const escaped = escapeRegExp(query);
162
+ return new RegExp(options.wholeWord ? `\\b${escaped}\\b` : escaped, options.caseSensitive ? 'g' : 'gi');
163
+ };
164
+ const cloneLargeTextSearchState = (state) => ({
165
+ ...state,
166
+ current: state.current ? { ...state.current } : null,
167
+ matches: state.matches.map(match => ({ ...match }))
168
+ });
169
+ export const shouldVirtualizeTextBuffer = (buffer, context) => {
170
+ var _a, _b;
171
+ const configured = (_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.virtualizeAboveBytes;
172
+ const threshold = Number.isFinite(configured)
173
+ ? Math.max(0, Number(configured))
174
+ : DEFAULT_LARGE_TEXT_THRESHOLD_BYTES;
175
+ return buffer.byteLength > threshold;
176
+ };
177
+ const largeTextStyle = `
178
+ .code-viewer--virtual{height:100%;min-height:240px;display:flex;flex-direction:column;overflow:hidden}
179
+ .code-viewer--virtual .code-toolbar{flex:0 0 42px}
180
+ .code-toolbar-meta{display:inline-flex;min-width:0;align-items:center;justify-content:flex-end;gap:10px;white-space:nowrap}
181
+ .code-toolbar-meta span{overflow:hidden;text-overflow:ellipsis}
182
+ .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)}
183
+ .code-virtual-spacer{position:relative;min-width:100%}
184
+ .code-virtual-window{position:absolute;top:0;left:0;min-width:100%;will-change:transform}
185
+ .code-virtual-line{display:flex;height:var(--code-line-height,22.1px);min-width:max-content;align-items:stretch;color:var(--code-text);font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono',monospace;font-size:var(--code-font-size,13px);line-height:var(--code-line-height,22.1px);white-space:pre;contain:layout paint style}
186
+ .code-virtual-line--match{background:rgba(255,215,0,.18)}
187
+ .code-virtual-number{position:sticky;left:0;z-index:1;display:inline-block;width:var(--code-line-number-width,7ch);flex:0 0 var(--code-line-number-width,7ch);padding:0 1.25ch 0 .75ch;border-right:1px solid var(--code-border);background:var(--code-bg);color:var(--code-muted);text-align:right;user-select:none;box-sizing:border-box}
188
+ .code-virtual-content{display:inline-block;padding:0 18px;white-space:pre}
189
+ .code-virtual-content mark{border-radius:2px;background:#ffd54f;color:#1f2328}
190
+ .code-line-segments{position:sticky;left:var(--code-line-number-width,7ch);z-index:1;display:inline-flex;height:100%;align-items:center;gap:2px;padding:0 4px;border-right:1px solid var(--code-border);background:var(--code-toolbar-bg)}
191
+ .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}
192
+ .code-line-segments button:disabled{cursor:not-allowed;opacity:.4}
193
+ .code-line-segments span{min-width:64px;color:var(--code-muted);font-size:11px;line-height:1;text-align:center}
194
+ `;
195
+ export default async function renderLargeText(buffer, target, type = 'txt', context) {
196
+ var _a, _b, _c, _d, _e, _f, _g;
197
+ const t = createFileViewerTranslator(context === null || context === void 0 ? void 0 : context.options);
198
+ const documentRef = target.ownerDocument;
199
+ const bytes = new Uint8Array(buffer);
200
+ const configuredSegmentBytes = (_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.maxRenderedLineBytes;
201
+ const segmentBytes = Number.isFinite(configuredSegmentBytes)
202
+ ? clamp(Math.trunc(Number(configuredSegmentBytes)), 1024, 256 * 1024)
203
+ : DEFAULT_LARGE_TEXT_LINE_SEGMENT_BYTES;
204
+ const configuredOverscan = (_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.virtualOverscanLines;
205
+ const overscan = Number.isFinite(configuredOverscan)
206
+ ? clamp(Math.trunc(Number(configuredOverscan)), 2, 100)
207
+ : DEFAULT_LARGE_TEXT_OVERSCAN_LINES;
208
+ let disposed = false;
209
+ let zoom = 1;
210
+ let scheduledFrame = 0;
211
+ let lastWindowStart = -1;
212
+ let activeLine = -1;
213
+ let searchGeneration = 0;
214
+ const lineSegments = new Map();
215
+ const zoomEmitter = createZoomChangeEmitter();
216
+ const style = documentRef.createElement('style');
217
+ style.textContent = `${codeStyle}\n${largeTextStyle}`;
218
+ const root = documentRef.createElement('div');
219
+ root.className = 'code-viewer code-viewer--virtual';
220
+ root.dataset.viewerZoomProvider = 'code';
221
+ root.dataset.viewerSearchProvider = 'code-virtual';
222
+ const toolbar = documentRef.createElement('div');
223
+ toolbar.className = 'code-toolbar';
224
+ const extensionLabel = documentRef.createElement('span');
225
+ extensionLabel.textContent = type.toUpperCase();
226
+ const toolbarMeta = documentRef.createElement('div');
227
+ toolbarMeta.className = 'code-toolbar-meta';
228
+ const status = documentRef.createElement('span');
229
+ const lineSummary = documentRef.createElement('strong');
230
+ status.textContent = t('text.code.indexingLargeFile', { progress: 0 });
231
+ toolbarMeta.append(status, lineSummary);
232
+ toolbar.append(extensionLabel, toolbarMeta);
233
+ root.append(toolbar);
234
+ target.replaceChildren(style, root);
235
+ (_e = context === null || context === void 0 ? void 0 : context.onProgressiveRender) === null || _e === void 0 ? void 0 : _e.call(context);
236
+ const index = await buildLargeTextIndex(bytes, target, progress => {
237
+ if (!disposed) {
238
+ status.textContent = t('text.code.indexingLargeFile', { progress });
239
+ }
240
+ });
241
+ if (disposed) {
242
+ return { $el: target, unmount() { } };
243
+ }
244
+ status.textContent = t('text.code.virtualized');
245
+ root.dataset.totalLines = String(index.lineCount);
246
+ lineSummary.textContent = `${formatLargeNumber(index.lineCount)} lines`;
247
+ root.style.setProperty('--code-line-number-width', `${Math.max(6, String(index.lineCount).length + 2)}ch`);
248
+ const viewport = documentRef.createElement('div');
249
+ viewport.className = 'code-virtual-scroll';
250
+ viewport.dataset.viewerScrollContainer = 'true';
251
+ viewport.tabIndex = 0;
252
+ const spacer = documentRef.createElement('div');
253
+ spacer.className = 'code-virtual-spacer';
254
+ const windowElement = documentRef.createElement('div');
255
+ windowElement.className = 'code-virtual-window';
256
+ spacer.append(windowElement);
257
+ viewport.append(spacer);
258
+ root.append(viewport);
259
+ const getLineHeight = () => LARGE_TEXT_BASE_LINE_HEIGHT * zoom;
260
+ const getViewportHeight = () => Math.max(240, viewport.clientHeight || 600);
261
+ const getWindowLineCount = () => Math.min(index.lineCount, Math.ceil(getViewportHeight() / getLineHeight()) + (overscan * 2) + 2);
262
+ const getSpacerHeight = () => Math.min(LARGE_TEXT_MAX_SCROLL_HEIGHT, Math.max(getViewportHeight(), index.lineCount * getLineHeight()));
263
+ const usesCappedScrollHeight = () => index.lineCount * getLineHeight() > LARGE_TEXT_MAX_SCROLL_HEIGHT;
264
+ const updateSpacerHeight = () => {
265
+ root.style.setProperty('--code-font-size', `${13 * zoom}px`);
266
+ root.style.setProperty('--code-line-height', `${getLineHeight()}px`);
267
+ spacer.style.height = `${getSpacerHeight()}px`;
268
+ };
269
+ const getFirstVisibleLine = () => {
270
+ if (!usesCappedScrollHeight()) {
271
+ return clamp(Math.floor(viewport.scrollTop / getLineHeight()), 0, index.lineCount - 1);
272
+ }
273
+ const maxScrollTop = Math.max(1, getSpacerHeight() - getViewportHeight());
274
+ return clamp(Math.round((viewport.scrollTop / maxScrollTop) * (index.lineCount - 1)), 0, index.lineCount - 1);
275
+ };
276
+ const getWindowOffset = (startLine, renderedLineCount) => {
277
+ if (!usesCappedScrollHeight()) {
278
+ return startLine * getLineHeight();
279
+ }
280
+ const maxStart = Math.max(1, index.lineCount - renderedLineCount);
281
+ const maxOffset = Math.max(0, getSpacerHeight() - (renderedLineCount * getLineHeight()));
282
+ return (startLine / maxStart) * maxOffset;
283
+ };
284
+ const appendHighlightedContent = (content, text, query) => {
285
+ if (!query) {
286
+ content.textContent = text || ' ';
287
+ return;
288
+ }
289
+ const position = text.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
290
+ if (position < 0) {
291
+ content.textContent = text || ' ';
292
+ return;
293
+ }
294
+ content.append(documentRef.createTextNode(text.slice(0, position)), Object.assign(documentRef.createElement('mark'), { textContent: text.slice(position, position + query.length) }), documentRef.createTextNode(text.slice(position + query.length)));
295
+ };
296
+ let searchState = createEmptyFileViewerSearchState();
297
+ const renderWindow = (force = false) => {
298
+ var _a;
299
+ if (disposed) {
300
+ return;
301
+ }
302
+ const firstVisibleLine = getFirstVisibleLine();
303
+ const visibleCount = getWindowLineCount();
304
+ const startLine = clamp(firstVisibleLine - overscan, 0, Math.max(0, index.lineCount - visibleCount));
305
+ if (!force && startLine === lastWindowStart) {
306
+ return;
307
+ }
308
+ lastWindowStart = startLine;
309
+ const lines = readLargeTextLineBounds(index, startLine, visibleCount);
310
+ const fragment = documentRef.createDocumentFragment();
311
+ for (const line of lines) {
312
+ const row = documentRef.createElement('div');
313
+ row.className = 'code-virtual-line';
314
+ row.dataset.line = String(line.lineIndex + 1);
315
+ if (line.lineIndex === activeLine) {
316
+ row.classList.add('code-virtual-line--match');
317
+ }
318
+ const number = documentRef.createElement('span');
319
+ number.className = 'code-virtual-number';
320
+ number.textContent = String(line.lineIndex + 1);
321
+ row.append(number);
322
+ const currentSegment = (_a = lineSegments.get(line.lineIndex)) !== null && _a !== void 0 ? _a : 0;
323
+ const decoded = decodeLargeTextSegment(index, line, currentSegment, segmentBytes);
324
+ if (decoded.segmentCount > 1) {
325
+ const segments = documentRef.createElement('span');
326
+ segments.className = 'code-line-segments';
327
+ const actions = [
328
+ ['first', '⇤', t('text.code.firstSegment')],
329
+ ['previous', '‹', t('text.code.previousSegment')],
330
+ ['next', '›', t('text.code.nextSegment')],
331
+ ['last', '⇥', t('text.code.lastSegment')]
332
+ ];
333
+ for (const [action, label, title] of actions.slice(0, 2)) {
334
+ const button = documentRef.createElement('button');
335
+ button.type = 'button';
336
+ button.dataset.segmentAction = action;
337
+ button.dataset.lineIndex = String(line.lineIndex);
338
+ button.title = title;
339
+ button.setAttribute('aria-label', title);
340
+ button.textContent = label;
341
+ button.disabled = decoded.segmentIndex === 0;
342
+ segments.append(button);
343
+ }
344
+ const segmentLabel = documentRef.createElement('span');
345
+ segmentLabel.textContent = `${decoded.segmentIndex + 1}/${decoded.segmentCount}`;
346
+ segments.append(segmentLabel);
347
+ for (const [action, label, title] of actions.slice(2)) {
348
+ const button = documentRef.createElement('button');
349
+ button.type = 'button';
350
+ button.dataset.segmentAction = action;
351
+ button.dataset.lineIndex = String(line.lineIndex);
352
+ button.title = title;
353
+ button.setAttribute('aria-label', title);
354
+ button.textContent = label;
355
+ button.disabled = decoded.segmentIndex === decoded.segmentCount - 1;
356
+ segments.append(button);
357
+ }
358
+ row.append(segments);
359
+ }
360
+ const content = documentRef.createElement('span');
361
+ content.className = 'code-virtual-content';
362
+ appendHighlightedContent(content, decoded.text, line.lineIndex === activeLine ? searchState.query : '');
363
+ row.append(content);
364
+ fragment.append(row);
365
+ }
366
+ windowElement.replaceChildren(fragment);
367
+ windowElement.style.transform = `translateY(${getWindowOffset(startLine, lines.length)}px)`;
368
+ };
369
+ const scheduleRender = () => {
370
+ var _a, _b;
371
+ if (scheduledFrame || disposed) {
372
+ return;
373
+ }
374
+ const view = getWindow(target);
375
+ if (view === null || view === void 0 ? void 0 : view.requestAnimationFrame) {
376
+ scheduledFrame = view.requestAnimationFrame(() => {
377
+ scheduledFrame = 0;
378
+ renderWindow();
379
+ });
380
+ return;
381
+ }
382
+ scheduledFrame = Number((_b = (_a = view === null || view === void 0 ? void 0 : view.setTimeout) === null || _a === void 0 ? void 0 : _a.call(view, () => {
383
+ scheduledFrame = 0;
384
+ renderWindow();
385
+ }, 0)) !== null && _b !== void 0 ? _b : setTimeout(() => {
386
+ scheduledFrame = 0;
387
+ renderWindow();
388
+ }, 0));
389
+ };
390
+ const scrollToLine = (requestedLine) => {
391
+ const lineIndex = clamp(Math.trunc(requestedLine), 0, index.lineCount - 1);
392
+ if (usesCappedScrollHeight()) {
393
+ const maxScrollTop = Math.max(0, getSpacerHeight() - getViewportHeight());
394
+ viewport.scrollTop = index.lineCount > 1
395
+ ? (lineIndex / (index.lineCount - 1)) * maxScrollTop
396
+ : 0;
397
+ }
398
+ else {
399
+ viewport.scrollTop = lineIndex * getLineHeight();
400
+ }
401
+ lastWindowStart = -1;
402
+ renderWindow(true);
403
+ };
404
+ const setActiveSearchMatch = (requestedIndex) => {
405
+ const matches = searchState.matches;
406
+ if (!matches.length) {
407
+ activeLine = -1;
408
+ searchState.currentIndex = -1;
409
+ searchState.current = null;
410
+ renderWindow(true);
411
+ return cloneLargeTextSearchState(searchState);
412
+ }
413
+ const currentIndex = ((requestedIndex % matches.length) + matches.length) % matches.length;
414
+ const match = matches[currentIndex];
415
+ searchState.currentIndex = currentIndex;
416
+ searchState.current = match;
417
+ activeLine = match.lineIndex;
418
+ const lineStart = locateLargeTextLineStart(index, match.lineIndex);
419
+ lineSegments.set(match.lineIndex, Math.max(0, Math.floor((match.byteOffset - lineStart) / segmentBytes)));
420
+ scrollToLine(match.lineIndex);
421
+ return cloneLargeTextSearchState(searchState);
422
+ };
423
+ const clearSearch = () => {
424
+ searchGeneration += 1;
425
+ searchState = createEmptyFileViewerSearchState();
426
+ activeLine = -1;
427
+ renderWindow(true);
428
+ return cloneLargeTextSearchState(searchState);
429
+ };
430
+ const searchLargeText = async (rawQuery, rawOptions) => {
431
+ const query = rawQuery.replace(/\s+/g, ' ').trim();
432
+ const options = normalizeFileViewerSearchOptions(rawOptions);
433
+ if (!query || options.enabled === false) {
434
+ return clearSearch();
435
+ }
436
+ const generation = searchGeneration + 1;
437
+ searchGeneration = generation;
438
+ const matches = [];
439
+ const maxMatches = Math.max(1, options.maxMatches || DEFAULT_FILE_VIEWER_SEARCH_MAX_MATCHES);
440
+ const expression = createLargeTextSearchRegExp(query, options);
441
+ const encoder = new TextEncoder();
442
+ const encodedQueryBytes = encoder.encode(query).byteLength;
443
+ const overlap = clamp(encodedQueryBytes * 2, 256, 64 * 1024);
444
+ for (let primaryStart = 0; primaryStart < index.bytes.byteLength && matches.length < maxMatches;) {
445
+ const primaryEnd = Math.min(index.bytes.byteLength, primaryStart + LARGE_TEXT_SEARCH_CHUNK_BYTES);
446
+ const decodeStart = alignUtf8Start(index.bytes, Math.max(0, primaryStart - overlap), index.bytes.byteLength);
447
+ const decodeEnd = alignUtf8End(index.bytes, Math.min(index.bytes.byteLength, primaryEnd + overlap), index.bytes.byteLength);
448
+ const text = new TextDecoder('utf-8').decode(index.bytes.subarray(decodeStart, decodeEnd));
449
+ let lastCharacterOffset = 0;
450
+ let byteCursor = decodeStart;
451
+ expression.lastIndex = 0;
452
+ let match;
453
+ while ((match = expression.exec(text)) && matches.length < maxMatches) {
454
+ if (!match[0]) {
455
+ expression.lastIndex += 1;
456
+ continue;
457
+ }
458
+ byteCursor += encoder.encode(text.slice(lastCharacterOffset, match.index)).byteLength;
459
+ const matchByteOffset = byteCursor;
460
+ byteCursor += encoder.encode(match[0]).byteLength;
461
+ lastCharacterOffset = match.index + match[0].length;
462
+ if (matchByteOffset < primaryStart || matchByteOffset >= primaryEnd) {
463
+ continue;
464
+ }
465
+ const lineIndex = findLargeTextLineAtOffset(index, matchByteOffset);
466
+ matches.push({
467
+ id: `code-virtual-search-${matches.length + 1}`,
468
+ index: matches.length,
469
+ text: match[0],
470
+ anchor: null,
471
+ line: lineIndex + 1,
472
+ byteOffset: matchByteOffset,
473
+ lineIndex
474
+ });
475
+ }
476
+ primaryStart = primaryEnd;
477
+ await nextBrowserTurn(target);
478
+ if (disposed || generation !== searchGeneration) {
479
+ return cloneLargeTextSearchState(searchState);
480
+ }
481
+ }
482
+ searchState = {
483
+ query,
484
+ total: matches.length,
485
+ currentIndex: matches.length ? 0 : -1,
486
+ current: matches[0] || null,
487
+ matches
488
+ };
489
+ return matches.length ? setActiveSearchMatch(0) : cloneLargeTextSearchState(searchState);
490
+ };
491
+ const getZoomState = () => ({
492
+ scale: zoom,
493
+ label: `${Math.round(zoom * 100)}%`,
494
+ canZoomIn: zoom < 2.6,
495
+ canZoomOut: zoom > 0.6,
496
+ canReset: zoom !== 1,
497
+ minScale: 0.6,
498
+ maxScale: 2.6
499
+ });
500
+ const setZoom = (scale) => {
501
+ const firstVisibleLine = getFirstVisibleLine();
502
+ zoom = clampZoom(scale);
503
+ updateSpacerHeight();
504
+ scrollToLine(firstVisibleLine);
505
+ zoomEmitter.emit();
506
+ return getZoomState();
507
+ };
508
+ registerFileViewerSearchProvider(root, {
509
+ search: searchLargeText,
510
+ next: () => setActiveSearchMatch(searchState.currentIndex + 1),
511
+ previous: () => setActiveSearchMatch(searchState.currentIndex - 1),
512
+ clear: clearSearch,
513
+ getState: () => cloneLargeTextSearchState(searchState)
514
+ });
515
+ registerFileViewerZoomProvider(root, {
516
+ zoomIn: () => setZoom(zoom + 0.1),
517
+ zoomOut: () => setZoom(zoom - 0.1),
518
+ resetZoom: () => setZoom(1),
519
+ setZoom,
520
+ getState: getZoomState,
521
+ subscribe: zoomEmitter.subscribe
522
+ });
523
+ (_f = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _f === void 0 ? void 0 : _f.call(context, { print: false, exportHtml: false });
524
+ viewport.addEventListener('scroll', scheduleRender, { passive: true });
525
+ viewport.addEventListener('click', event => {
526
+ var _a, _b;
527
+ const button = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('button[data-segment-action]');
528
+ if (!button) {
529
+ return;
530
+ }
531
+ const lineIndex = Number(button.dataset.lineIndex);
532
+ const line = readLargeTextLineBounds(index, lineIndex, 1)[0];
533
+ if (!line) {
534
+ return;
535
+ }
536
+ const segmentCount = Math.max(1, Math.ceil((line.end - line.start) / segmentBytes));
537
+ const current = (_b = lineSegments.get(lineIndex)) !== null && _b !== void 0 ? _b : 0;
538
+ const action = button.dataset.segmentAction;
539
+ const next = action === 'first'
540
+ ? 0
541
+ : action === 'last'
542
+ ? segmentCount - 1
543
+ : action === 'previous'
544
+ ? current - 1
545
+ : current + 1;
546
+ lineSegments.set(lineIndex, clamp(next, 0, segmentCount - 1));
547
+ renderWindow(true);
548
+ });
549
+ const ResizeObserverCtor = (_g = getWindow(target)) === null || _g === void 0 ? void 0 : _g.ResizeObserver;
550
+ const resizeObserver = ResizeObserverCtor
551
+ ? new ResizeObserverCtor(() => {
552
+ updateSpacerHeight();
553
+ renderWindow(true);
554
+ })
555
+ : null;
556
+ resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.observe(viewport);
557
+ updateSpacerHeight();
558
+ renderWindow(true);
559
+ return {
560
+ $el: target,
561
+ unmount() {
562
+ var _a, _b;
563
+ disposed = true;
564
+ searchGeneration += 1;
565
+ const view = getWindow(target);
566
+ if (scheduledFrame && (view === null || view === void 0 ? void 0 : view.cancelAnimationFrame)) {
567
+ view.cancelAnimationFrame(scheduledFrame);
568
+ }
569
+ else if (scheduledFrame) {
570
+ (_a = view === null || view === void 0 ? void 0 : view.clearTimeout) === null || _a === void 0 ? void 0 : _a.call(view, scheduledFrame);
571
+ }
572
+ resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect();
573
+ viewport.removeEventListener('scroll', scheduleRender);
574
+ unregisterFileViewerSearchProvider(root);
575
+ unregisterFileViewerZoomProvider(root);
576
+ (_b = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _b === void 0 ? void 0 : _b.call(context, null);
577
+ target.replaceChildren();
578
+ }
579
+ };
580
+ }
@@ -1,3 +1,3 @@
1
- import { type FileViewerRenderedInstance } from '@file-viewer/core';
1
+ import { type FileRenderContext, type FileViewerRenderedInstance } from '@file-viewer/core';
2
2
  export declare const stripMarkdownFrontmatter: (text: string) => string;
3
- export default function renderMarkdown(buffer: ArrayBuffer, target: HTMLDivElement): Promise<FileViewerRenderedInstance>;
3
+ export default function renderMarkdown(buffer: ArrayBuffer, target: HTMLDivElement, context?: FileRenderContext): Promise<FileViewerRenderedInstance>;
package/dist/markdown.js CHANGED
@@ -17,6 +17,10 @@ const markdownStyle = `
17
17
  .markdown-body tr{background:var(--bgColor-default);border-top:1px solid var(--borderColor-muted)}
18
18
  .markdown-body tr:nth-child(2n){background:var(--bgColor-muted)}
19
19
  .markdown-body blockquote{padding:0 1em;color:var(--fgColor-muted);border-left:.25em solid var(--borderColor-default)}
20
+ .markdown-mermaid{display:flex;justify-content:center;width:100%;margin:0 0 16px;overflow:auto;border:1px solid var(--borderColor-muted);border-radius:10px;padding:16px;box-sizing:border-box;background:var(--bgColor-default)}
21
+ .markdown-mermaid svg{display:block;max-width:100%;height:auto;margin:auto}
22
+ .markdown-mermaid-error{margin:-8px 0 16px;color:#cf222e;font-size:.875em}
23
+ [data-viewer-theme='dark'] .markdown-mermaid-error{color:#ff7b72}
20
24
  [data-viewer-theme='dark'] .markdown-viewer{background:#101820}
21
25
  [data-viewer-theme='dark'] .markdown-body{color-scheme:dark;--bgColor-default:#0d1117;--bgColor-muted:#151b23;--bgColor-neutral-muted:#656c7633;--borderColor-default:#3d444d;--borderColor-muted:#3d444db3;--borderColor-neutral-muted:#3d444db3;--fgColor-default:#f0f6fc;--fgColor-muted:#9198a1;--fgColor-accent:#4493f8;background:var(--bgColor-default);border-color:rgba(139,148,158,.26);color:var(--fgColor-default);box-shadow:0 24px 56px rgba(0,0,0,.38)}
22
26
  @media (max-width:767px){.markdown-viewer{padding:14px 10px 28px}.markdown-body{padding:22px 18px;border-radius:10px}}
@@ -35,6 +39,87 @@ const applyMarkdownZoom = (host, zoom) => {
35
39
  host.style.setProperty('--markdown-padding', `${45 * zoom}px`);
36
40
  host.style.setProperty('--markdown-font-size', `${16 * zoom}px`);
37
41
  };
42
+ let mermaidRenderSequence = 0;
43
+ let mermaidRenderQueue = Promise.resolve();
44
+ const isDarkTheme = (documentRef, theme) => {
45
+ var _a, _b;
46
+ if (theme === 'dark') {
47
+ return true;
48
+ }
49
+ if (theme === 'light') {
50
+ return false;
51
+ }
52
+ return Boolean((_b = (_a = documentRef.defaultView) === null || _a === void 0 ? void 0 : _a.matchMedia) === null || _b === void 0 ? void 0 : _b.call(_a, '(prefers-color-scheme: dark)').matches);
53
+ };
54
+ const sanitizeMermaidSvg = (documentRef, svg) => {
55
+ var _a;
56
+ const Parser = ((_a = documentRef.defaultView) === null || _a === void 0 ? void 0 : _a.DOMParser) || DOMParser;
57
+ const parsed = new Parser().parseFromString(svg, 'image/svg+xml');
58
+ const parseError = parsed.querySelector('parsererror');
59
+ if (parseError) {
60
+ throw new Error(parseError.textContent || 'Unable to parse the Mermaid SVG.');
61
+ }
62
+ parsed.querySelectorAll('script,iframe,object,embed').forEach(node => node.remove());
63
+ parsed.querySelectorAll('*').forEach(node => {
64
+ for (const attribute of Array.from(node.attributes)) {
65
+ if (/^on/i.test(attribute.name)) {
66
+ node.removeAttribute(attribute.name);
67
+ }
68
+ else if (/^(?:href|xlink:href|src)$/i.test(attribute.name) && /^\s*javascript:/i.test(attribute.value)) {
69
+ node.removeAttribute(attribute.name);
70
+ }
71
+ }
72
+ });
73
+ return documentRef.importNode(parsed.documentElement, true);
74
+ };
75
+ const renderMermaidSvg = async (documentRef, source, theme) => {
76
+ const render = async () => {
77
+ const mermaidModule = await import('mermaid');
78
+ const mermaid = mermaidModule.default;
79
+ const id = `file-viewer-markdown-mermaid-${Date.now()}-${mermaidRenderSequence += 1}`;
80
+ mermaid.initialize({
81
+ startOnLoad: false,
82
+ securityLevel: 'strict',
83
+ theme: isDarkTheme(documentRef, theme) ? 'dark' : 'default',
84
+ });
85
+ const rendered = await mermaid.render(id, source);
86
+ return sanitizeMermaidSvg(documentRef, rendered.svg);
87
+ };
88
+ const result = mermaidRenderQueue.then(render, render);
89
+ mermaidRenderQueue = result.then(() => undefined, () => undefined);
90
+ return result;
91
+ };
92
+ const renderEmbeddedMermaid = async (article, theme) => {
93
+ const documentRef = article.ownerDocument;
94
+ const mermaidBlocks = Array.from(article.querySelectorAll('pre > code')).filter(code => /(?:^|\s)(?:language|lang)-mermaid(?:\s|$)/i.test(code.className));
95
+ for (const code of mermaidBlocks) {
96
+ const pre = code.parentElement;
97
+ if (!pre) {
98
+ continue;
99
+ }
100
+ const source = code.textContent || '';
101
+ try {
102
+ const svg = await renderMermaidSvg(documentRef, source, theme);
103
+ svg.classList.add('markdown-mermaid-svg');
104
+ svg.setAttribute('role', 'img');
105
+ svg.setAttribute('aria-label', 'Mermaid diagram preview');
106
+ const shell = documentRef.createElement('div');
107
+ shell.className = 'markdown-mermaid';
108
+ shell.dataset.markdownMermaid = 'rendered';
109
+ shell.appendChild(svg);
110
+ pre.replaceWith(shell);
111
+ }
112
+ catch (error) {
113
+ pre.classList.add('markdown-mermaid-source-error');
114
+ const message = documentRef.createElement('p');
115
+ message.className = 'markdown-mermaid-error';
116
+ message.setAttribute('role', 'alert');
117
+ message.textContent = 'Mermaid diagram could not be rendered. The source is shown above.';
118
+ pre.after(message);
119
+ console.warn('[file-viewer] Unable to render embedded Mermaid diagram.', error);
120
+ }
121
+ }
122
+ };
38
123
  export const stripMarkdownFrontmatter = (text) => {
39
124
  const input = text.replace(/^\uFEFF/, '');
40
125
  if (!input.startsWith('---')) {
@@ -53,7 +138,8 @@ export const stripMarkdownFrontmatter = (text) => {
53
138
  }
54
139
  return input.slice(match.index + match[0].length).replace(/^\r?\n/, '');
55
140
  };
56
- export default async function renderMarkdown(buffer, target) {
141
+ export default async function renderMarkdown(buffer, target, context) {
142
+ var _a;
57
143
  const text = await readText(buffer);
58
144
  let zoom = 1;
59
145
  const zoomEmitter = createZoomChangeEmitter();
@@ -66,6 +152,7 @@ export default async function renderMarkdown(buffer, target) {
66
152
  applyMarkdownZoom(root, zoom);
67
153
  root.append(article);
68
154
  target.replaceChildren(createStyle(), root);
155
+ await renderEmbeddedMermaid(article, (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.theme);
69
156
  const getZoomState = () => ({
70
157
  scale: zoom,
71
158
  label: `${Math.round(zoom * 100)}%`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-text",
3
- "version": "2.1.24",
3
+ "version": "2.1.26",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone code, text, Markdown, patch diff, and Git bundle renderer plugin for Flyfish File Viewer.",
@@ -57,10 +57,11 @@
57
57
  "LICENSE"
58
58
  ],
59
59
  "dependencies": {
60
- "@file-viewer/core": "2.1.24",
60
+ "@file-viewer/core": "2.1.26",
61
61
  "diff2html": "^3.4.56",
62
62
  "highlight.js": "^11.11.1",
63
63
  "marked": "^18.0.5",
64
+ "mermaid": "^11.15.0",
64
65
  "pako": "^2.1.0"
65
66
  },
66
67
  "devDependencies": {