@file-viewer/renderer-text 2.4.0 → 3.0.0

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, 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.
3
+ Base code, text, and Markdown renderer package for Flyfish File Viewer. Mermaid, side-by-side patch diff, and Git bundle inspection live in `@file-viewer/capability-mermaid` and `@file-viewer/capability-text-tools`; they are excluded from the standard/full default closure.
4
4
 
5
5
  ## Usage
6
6
 
@@ -34,8 +34,8 @@ const options = {
34
34
  - Code and text preview uses `highlight.js` core with per-language dynamic imports instead of registering every language up front.
35
35
  - 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
36
  - 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`.
37
- - `patch` uses `diff2html` on demand for side-by-side review.
38
- - `bundle` / `bdl` parses Git bundle headers, refs, commit history, file trees, and readable blobs; regular OFS_DELTA / REF_DELTA objects are resolved in the browser, while very large packs or bundles that depend on external prerequisites surface a clear boundary notice.
37
+ - With the text-tools capability installed, `patch` uses `diff2html` for side-by-side review and `bundle` / `bdl` enables Git bundle inspection.
38
+ - With the Mermaid capability installed, fenced Mermaid blocks render as diagrams. Without it, the source stays visible with the exact CLI enablement command.
39
39
  - HTML, XML, Vue, and similar files are escaped and shown as source, never executed.
40
40
  - Markdown uses `marked` for a read-only reading surface with dark/light theme support, table scrolling, and a unified zoom provider.
41
41
  - Markdown no longer falls back to source because of the general large-text threshold. Set `options.text.markdownVirtualizeAboveBytes` only when an application must bound exceptionally large Markdown files.
@@ -43,4 +43,4 @@ const options = {
43
43
 
44
44
  ## Migration Note
45
45
 
46
- `@file-viewer/core` no longer bundles code / markdown renderers and no longer depends directly on `highlight.js`, `marked`, `diff2html`, or git bundle parsing dependencies. Install this package explicitly for code, text, Markdown, patch, or git bundle preview, or use `@file-viewer/preset-all`, which aggregates this renderer automatically.
46
+ Standard/full includes base code, text, and Markdown without installing `diff2html`, `pako`, or Mermaid. Optional uploads show `npx file-viewer-cli add text-tools --write` or `add mermaid-markdown --write`; `preset-all` is only for explicit all/debug use.
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 阅读面、Markdown 内嵌 Mermaid 图、左右比对、bundle 结构查看和统一缩放。
3
+ Flyfish File Viewer 的基础代码、文本和 Markdown renderer 包。Mermaid、patch 左右比对和 Git bundle 检查已拆到 `@file-viewer/capability-mermaid` `@file-viewer/capability-text-tools`,不进入 standard/full 默认闭包。
4
4
 
5
5
  ## 用法
6
6
 
@@ -34,8 +34,8 @@ const options = {
34
34
  - 代码和文本使用 `highlight.js` core + 按语言动态加载,避免一次性注册全部语言。
35
35
  - 代码、文本和超大 Markdown 源码视图默认显示文件类型、索引状态和行数元信息栏;传入 `options.text.toolbar: false` 可隐藏该 renderer 内部栏,不影响 Viewer 的下载、搜索、缩放等全局工具栏。
36
36
  - 普通代码和文本可通过 `options.text.lineNumbers: true` 显示行号;行号不会进入复制内容、搜索结果或无障碍朗读。超大文本保留原有的虚拟行号栏,可显式传 `false` 隐藏。
37
- - `patch` 使用 `diff2html` 按需渲染左右比对视图。
38
- - `bundle` / `bdl` 会解析 Git bundle header、refs、commit 历史、文件树和可读 blob;常规 OFS_DELTA / REF_DELTA 会在浏览器端还原,超大包或依赖外部 prerequisite 的包会给出清晰边界提示。
37
+ - 安装 text-tools capability 后,`patch` 使用 `diff2html` 渲染左右比对视图,`bundle` / `bdl` 才启用 Git bundle 结构检查。
38
+ - 安装 Mermaid capability 后,Markdown 内嵌 Mermaid 图才会渲染;未安装时保留源码并显示精确 CLI 启用命令。
39
39
  - HTML / XML / Vue 等文件按源码方式转义展示,不执行脚本。
40
40
  - Markdown 使用 `marked` 输出只读阅读面,并保留明暗主题、表格滚动和统一缩放 provider。
41
41
  - Markdown 不再因为通用大文本阈值自动退化成源码;如业务必须限制超大 Markdown,可单独设置 `options.text.markdownVirtualizeAboveBytes`。
@@ -43,4 +43,4 @@ const options = {
43
43
 
44
44
  ## 迁移说明
45
45
 
46
- `@file-viewer/core` 已不再内置 code / markdown renderer,也不再直接依赖 `highlight.js`、`marked`、`diff2html` 或 Git bundle 解析依赖。需要代码、文本、Markdown、patch git bundle 预览时,请显式安装本包,或使用会自动聚合本包的 `@file-viewer/preset-all`。
46
+ standard/full 默认包含基础代码、文本和 Markdown,不安装 `diff2html`、`pako` 或 Mermaid。打开可选格式时会提示运行 `npx file-viewer-cli add text-tools --write` `add mermaid-markdown --write`;`preset-all` 仅用于显式全量/调试。
package/dist/gitBundle.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createFileViewerTranslator, createFileViewerZoomChangeEmitter as createZoomChangeEmitter, registerFileViewerZoomProvider, unregisterFileViewerZoomProvider } from '@file-viewer/core';
2
- import { Inflate } from 'pako';
2
+ import { getFileViewerPakoLoader } from './optionalCapabilities.js';
3
+ let Inflate = null;
3
4
  const textDecoder = new TextDecoder('utf-8');
4
5
  const asciiDecoder = new TextDecoder('latin1');
5
6
  const bundleStyle = `
@@ -135,6 +136,8 @@ const readOfsDeltaBase = (bytes, start) => {
135
136
  return { value, offset };
136
137
  };
137
138
  const inflateObject = (bytes, start) => {
139
+ if (!Inflate)
140
+ throw new Error('Git bundle support is opt-in. Run `npx file-viewer-cli add text-tools --write`, then `npx file-viewer-cli install --yes`.');
138
141
  const inflator = new Inflate();
139
142
  let offset = start;
140
143
  while (!inflator.ended && offset < bytes.length) {
@@ -476,6 +479,12 @@ const renderMeta = (documentRef, panel, model, t) => {
476
479
  }
477
480
  };
478
481
  export default async function renderGitBundle(buffer, target, type = 'bundle', context) {
482
+ const loadPako = getFileViewerPakoLoader();
483
+ if (!loadPako) {
484
+ throw new Error('Git bundle support is opt-in. Run `npx file-viewer-cli add text-tools --write`, then `npx file-viewer-cli install --yes`.');
485
+ }
486
+ ;
487
+ ({ Inflate } = await loadPako());
479
488
  const documentRef = target.ownerDocument || document;
480
489
  const t = createFileViewerTranslator(context === null || context === void 0 ? void 0 : context.options);
481
490
  const model = await parseBundleModel(buffer, t);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { type FileRenderHandler, type FileViewerRenderedInstance, type FileViewerRendererPlugin, type RendererDefinition } from '@file-viewer/core';
2
+ export { getFileViewerMermaidLoader, getFileViewerDiffToHtml, getFileViewerPakoLoader, registerFileViewerDiffToHtml, registerFileViewerMermaidLoader, registerFileViewerPakoLoader, } from './optionalCapabilities.js';
3
+ export type { FileViewerMermaidLoader, FileViewerMermaidModule, FileViewerDiffToHtml, FileViewerPakoModule, } from './optionalCapabilities.js';
4
+ export { sanitizeFileViewerRichHtml } from './sanitizeHtml.js';
2
5
  export declare const textRendererDefinitions: RendererDefinition[];
3
6
  export declare const renderFileViewerCode: FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>;
4
7
  export declare const renderFileViewerMarkdown: FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>;
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { DEFAULT_RENDERER_DEFINITIONS, } from '@file-viewer/core';
2
+ export { getFileViewerMermaidLoader, getFileViewerDiffToHtml, getFileViewerPakoLoader, registerFileViewerDiffToHtml, registerFileViewerMermaidLoader, registerFileViewerPakoLoader, } from './optionalCapabilities.js';
3
+ export { sanitizeFileViewerRichHtml } from './sanitizeHtml.js';
2
4
  const textRendererIds = ['code', 'markdown'];
3
5
  const textDefinitions = DEFAULT_RENDERER_DEFINITIONS.filter(definition => textRendererIds.includes(definition.id));
4
6
  if (textDefinitions.length !== textRendererIds.length) {
@@ -1,3 +1,4 @@
1
1
  import { type FileRenderContext, type FileViewerRenderedInstance } from '@file-viewer/core';
2
+ export declare const sanitizeMermaidSvg: (documentRef: Document, svg: string) => SVGSVGElement;
2
3
  export declare const stripMarkdownFrontmatter: (text: string) => string;
3
4
  export default function renderMarkdown(buffer: ArrayBuffer, target: HTMLDivElement, context?: FileRenderContext): Promise<FileViewerRenderedInstance>;
package/dist/markdown.js CHANGED
@@ -1,27 +1,22 @@
1
1
  import { marked } from 'marked';
2
2
  import createDOMPurify from 'dompurify';
3
- import { createFileViewerZoomChangeEmitter as createZoomChangeEmitter, decodeFileViewerTextBuffer, registerFileViewerZoomProvider, resolveFileViewerFitScale, unregisterFileViewerZoomProvider, } from '@file-viewer/core';
4
- const createSafeTextFragment = (documentRef, value) => {
5
- const fragment = documentRef.createDocumentFragment();
6
- fragment.append(documentRef.createTextNode(value));
7
- return fragment;
8
- };
9
- const sanitizeMarkdownHtml = (documentRef, html) => {
3
+ import { createFileViewerZoomChangeEmitter as createZoomChangeEmitter, assertFileViewerMermaidSourceHasNoExternalResources, decodeFileViewerTextBuffer, registerFileViewerZoomProvider, resolveFileViewerFitScale, unregisterFileViewerZoomProvider, sanitizeFileViewerSvgResources, } from '@file-viewer/core';
4
+ import { getFileViewerMermaidLoader } from './optionalCapabilities.js';
5
+ import { sanitizeFileViewerRichHtml } from './sanitizeHtml.js';
6
+ const sanitizeMarkdownHtml = sanitizeFileViewerRichHtml;
7
+ const purifierByDocument = new WeakMap();
8
+ const getMarkdownPurifier = (documentRef) => {
9
+ const cached = purifierByDocument.get(documentRef);
10
+ if (cached)
11
+ return cached;
10
12
  const windowRef = documentRef.defaultView;
11
- if (!windowRef) {
12
- return createSafeTextFragment(documentRef, html);
13
- }
13
+ if (!windowRef)
14
+ return null;
14
15
  const purifier = createDOMPurify(windowRef);
15
- if (!purifier.isSupported) {
16
- return createSafeTextFragment(documentRef, html);
17
- }
18
- return purifier.sanitize(html, {
19
- RETURN_DOM_FRAGMENT: true,
20
- USE_PROFILES: { html: true },
21
- ADD_ATTR: ['target'],
22
- FORBID_TAGS: ['style', 'iframe', 'object', 'embed', 'form'],
23
- FORBID_ATTR: ['style', 'srcdoc'],
24
- });
16
+ if (!purifier.isSupported)
17
+ return null;
18
+ purifierByDocument.set(documentRef, purifier);
19
+ return purifier;
25
20
  };
26
21
  const hardenMarkdownLinks = (root) => {
27
22
  root.querySelectorAll('a[target]').forEach(anchor => {
@@ -108,35 +103,38 @@ const isDarkTheme = (documentRef, theme) => {
108
103
  }
109
104
  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);
110
105
  };
111
- const sanitizeMermaidSvg = (documentRef, svg) => {
112
- var _a;
113
- const Parser = ((_a = documentRef.defaultView) === null || _a === void 0 ? void 0 : _a.DOMParser) || DOMParser;
114
- const parsed = new Parser().parseFromString(svg, 'image/svg+xml');
115
- const parseError = parsed.querySelector('parsererror');
116
- if (parseError) {
117
- throw new Error(parseError.textContent || 'Unable to parse the Mermaid SVG.');
106
+ export const sanitizeMermaidSvg = (documentRef, svg) => {
107
+ const purifier = getMarkdownPurifier(documentRef);
108
+ if (!purifier) {
109
+ throw new Error('Unable to initialize the Mermaid SVG sanitizer.');
118
110
  }
119
- parsed.querySelectorAll('script,iframe,object,embed').forEach(node => node.remove());
120
- parsed.querySelectorAll('*').forEach(node => {
121
- for (const attribute of Array.from(node.attributes)) {
122
- if (/^on/i.test(attribute.name)) {
123
- node.removeAttribute(attribute.name);
124
- }
125
- else if (/^(?:href|xlink:href|src)$/i.test(attribute.name) && /^\s*javascript:/i.test(attribute.value)) {
126
- node.removeAttribute(attribute.name);
127
- }
128
- }
111
+ const fragment = purifier.sanitize(svg, {
112
+ RETURN_DOM_FRAGMENT: true,
113
+ USE_PROFILES: { html: true, svg: true, svgFilters: true },
114
+ FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form'],
115
+ FORBID_ATTR: ['srcdoc'],
129
116
  });
130
- return documentRef.importNode(parsed.documentElement, true);
117
+ const root = fragment.querySelector('svg');
118
+ if (!root) {
119
+ throw new Error('Unable to parse the Mermaid SVG.');
120
+ }
121
+ sanitizeFileViewerSvgResources(fragment);
122
+ return documentRef.importNode(root, true);
131
123
  };
132
124
  const renderMermaidSvg = async (documentRef, source, theme) => {
133
125
  const render = async () => {
134
- const mermaidModule = await import('mermaid');
126
+ assertFileViewerMermaidSourceHasNoExternalResources(source);
127
+ const loadMermaid = getFileViewerMermaidLoader();
128
+ if (!loadMermaid) {
129
+ throw new Error('Mermaid support is opt-in. Run `npx file-viewer-cli add mermaid-markdown --write`, then `npx file-viewer-cli install --yes`.');
130
+ }
131
+ const mermaidModule = await loadMermaid();
135
132
  const mermaid = mermaidModule.default;
136
133
  const id = `file-viewer-markdown-mermaid-${Date.now()}-${mermaidRenderSequence += 1}`;
137
134
  mermaid.initialize({
138
135
  startOnLoad: false,
139
136
  securityLevel: 'strict',
137
+ htmlLabels: false,
140
138
  theme: isDarkTheme(documentRef, theme) ? 'dark' : 'default',
141
139
  });
142
140
  const rendered = await mermaid.render(id, source);
@@ -171,7 +169,9 @@ const renderEmbeddedMermaid = async (article, theme) => {
171
169
  const message = documentRef.createElement('p');
172
170
  message.className = 'markdown-mermaid-error';
173
171
  message.setAttribute('role', 'alert');
174
- message.textContent = 'Mermaid diagram could not be rendered. The source is shown above.';
172
+ message.textContent = error instanceof Error && error.message.includes('npx file-viewer-cli')
173
+ ? error.message
174
+ : 'Mermaid diagram could not be rendered. The source is shown above.';
175
175
  pre.after(message);
176
176
  console.warn('[file-viewer] Unable to render embedded Mermaid diagram.', error);
177
177
  }
@@ -0,0 +1,21 @@
1
+ export type FileViewerMermaidModule = {
2
+ default: {
3
+ initialize(options: Record<string, unknown>): void;
4
+ render(id: string, source: string): Promise<{
5
+ svg: string;
6
+ }>;
7
+ };
8
+ };
9
+ export type FileViewerMermaidLoader = () => Promise<FileViewerMermaidModule>;
10
+ export type FileViewerDiffToHtml = (input: string, options: Record<string, unknown>) => string;
11
+ export type FileViewerPakoModule = typeof import('pako');
12
+ /**
13
+ * Enables embedded Mermaid blocks without making Mermaid part of the base text
14
+ * renderer dependency closure. Capability packs call this once at startup.
15
+ */
16
+ export declare const registerFileViewerMermaidLoader: (loader: FileViewerMermaidLoader | null) => void;
17
+ export declare const getFileViewerMermaidLoader: () => FileViewerMermaidLoader | null;
18
+ export declare const registerFileViewerDiffToHtml: (renderer: FileViewerDiffToHtml | null) => void;
19
+ export declare const getFileViewerDiffToHtml: () => FileViewerDiffToHtml | null;
20
+ export declare const registerFileViewerPakoLoader: (loader: (() => Promise<FileViewerPakoModule>) | null) => void;
21
+ export declare const getFileViewerPakoLoader: () => (() => Promise<FileViewerPakoModule>) | null;
@@ -0,0 +1,19 @@
1
+ let mermaidLoader = null;
2
+ let diffToHtml = null;
3
+ let pakoLoader = null;
4
+ /**
5
+ * Enables embedded Mermaid blocks without making Mermaid part of the base text
6
+ * renderer dependency closure. Capability packs call this once at startup.
7
+ */
8
+ export const registerFileViewerMermaidLoader = (loader) => {
9
+ mermaidLoader = loader;
10
+ };
11
+ export const getFileViewerMermaidLoader = () => mermaidLoader;
12
+ export const registerFileViewerDiffToHtml = (renderer) => {
13
+ diffToHtml = renderer;
14
+ };
15
+ export const getFileViewerDiffToHtml = () => diffToHtml;
16
+ export const registerFileViewerPakoLoader = (loader) => {
17
+ pakoLoader = loader;
18
+ };
19
+ export const getFileViewerPakoLoader = () => pakoLoader;
package/dist/patch.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createFileViewerZoomChangeEmitter as createZoomChangeEmitter, decodeFileViewerTextBuffer, registerFileViewerZoomProvider, unregisterFileViewerZoomProvider } from '@file-viewer/core';
2
- import { html as diffToHtml } from 'diff2html';
2
+ import { getFileViewerDiffToHtml } from './optionalCapabilities.js';
3
+ import { sanitizeFileViewerRichHtml } from './sanitizeHtml.js';
3
4
  const patchStyle = `
4
5
  .patch-viewer{min-height:100%;--patch-bg:#f6f8fa;--patch-surface:#fff;--patch-border:rgba(31,35,40,.12);--patch-text:#24292f;--patch-muted:#57606a;--patch-add:#dafbe1;--patch-del:#ffebe9;--patch-info:#ddf4ff;--patch-font-size:13px;background:var(--patch-bg);color:var(--patch-text);box-sizing:border-box}
5
6
  .patch-toolbar{position:sticky;top:0;z-index:2;display:flex;height:46px;align-items:center;justify-content:space-between;gap:12px;padding:0 16px;border-bottom:1px solid var(--patch-border);background:rgba(255,255,255,.92);backdrop-filter:blur(12px);box-sizing:border-box}
@@ -52,18 +53,6 @@ const countFiles = (text) => {
52
53
  }
53
54
  return ((_a = text.match(/^---\s+/gm)) === null || _a === void 0 ? void 0 : _a.length) || 1;
54
55
  };
55
- const escapeHtml = (value) => {
56
- return value.replace(/[&<>"']/g, char => {
57
- const entities = {
58
- '&': '&amp;',
59
- '<': '&lt;',
60
- '>': '&gt;',
61
- '"': '&quot;',
62
- "'": '&#39;'
63
- };
64
- return entities[char];
65
- });
66
- };
67
56
  export default async function renderPatch(buffer, target, type = 'patch', context) {
68
57
  var _a, _b;
69
58
  const documentRef = target.ownerDocument || document;
@@ -76,16 +65,21 @@ export default async function renderPatch(buffer, target, type = 'patch', contex
76
65
  toolbar.append(createElement(documentRef, 'span', undefined, type.toUpperCase()), createElement(documentRef, 'strong', undefined, `${countFiles(text)} files · side-by-side`));
77
66
  const body = createElement(documentRef, 'div', 'patch-body');
78
67
  try {
79
- body.innerHTML = diffToHtml(text, {
68
+ const diffToHtml = getFileViewerDiffToHtml();
69
+ if (!diffToHtml) {
70
+ throw new Error('Patch side-by-side rendering is opt-in. Run `npx file-viewer-cli add text-tools --write`, then `npx file-viewer-cli install --yes`.');
71
+ }
72
+ const rendered = diffToHtml(text, {
80
73
  drawFileList: true,
81
74
  matching: 'lines',
82
75
  outputFormat: 'side-by-side',
83
76
  renderNothingWhenEmpty: false
84
77
  });
78
+ body.replaceChildren(sanitizeFileViewerRichHtml(documentRef, rendered, { allowSvg: true }));
85
79
  }
86
80
  catch {
87
81
  const fallback = createElement(documentRef, 'pre', 'patch-fallback');
88
- fallback.innerHTML = escapeHtml(text);
82
+ fallback.textContent = text;
89
83
  body.replaceChildren(fallback);
90
84
  }
91
85
  root.append(toolbar, body);
@@ -0,0 +1,5 @@
1
+ export type FileViewerRichHtmlSanitizerOptions = {
2
+ /** Preserve inert SVG markup such as diff icons. Raw Markdown keeps this disabled. */
3
+ allowSvg?: boolean;
4
+ };
5
+ export declare const sanitizeFileViewerRichHtml: (documentRef: Document, html: string, options?: FileViewerRichHtmlSanitizerOptions) => DocumentFragment;
@@ -0,0 +1,47 @@
1
+ import createDOMPurify from 'dompurify';
2
+ import { sanitizeFileViewerSvgResources } from '@file-viewer/core';
3
+ const safeUrl = (value) => {
4
+ var _a, _b;
5
+ // eslint-disable-next-line no-control-regex -- remove URL controls before scheme validation
6
+ const compact = value.trim().replace(/[\u0000-\u0020\u007f-\u009f]/g, '');
7
+ const scheme = (_b = (_a = compact.match(/^([a-z][a-z0-9+.-]*):/i)) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.toLowerCase();
8
+ return !scheme || ['http', 'https', 'mailto', 'tel'].includes(scheme);
9
+ };
10
+ export const sanitizeFileViewerRichHtml = (documentRef, html, options = {}) => {
11
+ const fallback = () => {
12
+ const fragment = documentRef.createDocumentFragment();
13
+ fragment.append(documentRef.createTextNode(html));
14
+ return fragment;
15
+ };
16
+ const windowRef = documentRef.defaultView;
17
+ if (!windowRef)
18
+ return fallback();
19
+ const purifier = createDOMPurify(windowRef);
20
+ if (!purifier.isSupported)
21
+ return fallback();
22
+ const fragment = purifier.sanitize(html, {
23
+ RETURN_DOM_FRAGMENT: true,
24
+ USE_PROFILES: options.allowSvg
25
+ ? { html: true, svg: true, svgFilters: true }
26
+ : { html: true },
27
+ ADD_ATTR: ['target'],
28
+ FORBID_TAGS: ['base', 'embed', 'form', 'iframe', 'object', 'script', 'style', 'template'],
29
+ FORBID_ATTR: ['action', 'formaction', 'srcdoc', 'style'],
30
+ });
31
+ fragment.querySelectorAll('*').forEach(element => {
32
+ for (const attribute of Array.from(element.attributes)) {
33
+ if (/^on/i.test(attribute.name))
34
+ element.removeAttribute(attribute.name);
35
+ }
36
+ });
37
+ fragment.querySelectorAll('a[href]').forEach(anchor => {
38
+ const href = anchor.getAttribute('href') || '';
39
+ if (!safeUrl(href))
40
+ anchor.removeAttribute('href');
41
+ if ((anchor.getAttribute('target') || '').toLowerCase() === '_blank')
42
+ anchor.rel = 'noopener noreferrer';
43
+ });
44
+ if (options.allowSvg)
45
+ sanitizeFileViewerSvgResources(fragment);
46
+ return fragment;
47
+ };
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "../../../ecosystem/capability-manifest.schema.json",
3
+ "schemaVersion": 1,
4
+ "id": "text",
5
+ "packageName": "@file-viewer/renderer-text",
6
+ "rendererIds": ["markdown", "code"],
7
+ "formats": ["md", "markdown", "txt", "json", "js", "mjs", "cjs", "css", "java", "py", "html", "htm", "jsx", "ts", "tsx", "xml", "log", "vue", "yaml", "yml", "ini", "sh", "bash", "sql", "go", "rs", "php", "c", "cpp", "cc", "h", "hpp", "cs", "jsonc", "json5", "ipynb", "toml", "proto", "hcl", "tex", "gv", "http", "react", "rb", "swift", "kt"],
8
+ "assets": { "rendererIds": [] },
9
+ "license": { "spdx": "Apache-2.0", "policy": "permissive" },
10
+ "weight": "standard",
11
+ "profiles": ["lite", "standard", "all"]
12
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-text",
3
- "version": "2.4.0",
3
+ "version": "3.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Standalone code, text, Markdown, patch diff, and Git bundle renderer plugin for File Viewer.",
6
+ "description": "Standalone code, text, and Markdown renderer for File Viewer; Mermaid, patch diff, and Git bundle tools are explicit opt-in capabilities.",
7
7
  "keywords": [
8
8
  "file-viewer",
9
9
  "renderer",
@@ -52,25 +52,30 @@
52
52
  },
53
53
  "files": [
54
54
  "dist",
55
+ "file-viewer.capability.json",
55
56
  "README.md",
56
57
  "README.en.md",
57
58
  "LICENSE"
58
59
  ],
59
60
  "dependencies": {
60
- "@file-viewer/core": "2.4.0",
61
- "diff2html": "^3.4.56",
61
+ "@file-viewer/core": "3.0.0",
62
62
  "highlight.js": "^11.11.1",
63
63
  "marked": "^18.0.5",
64
- "mermaid": "^11.16.1",
65
- "pako": "^2.1.0",
66
64
  "dompurify": "^3.4.13"
67
65
  },
68
66
  "devDependencies": {
67
+ "diff2html": "^3.4.56",
68
+ "mermaid": "^11.16.1",
69
+ "pako": "^2.1.0",
70
+ "jsdom": "^27.4.0",
69
71
  "typescript": "^6.0.3"
70
72
  },
73
+ "fileViewer": {
74
+ "capabilityManifest": "./file-viewer.capability.json"
75
+ },
71
76
  "license": "Apache-2.0",
72
77
  "scripts": {
73
- "build": "tsc -b tsconfig.json",
78
+ "build": "tsc -b tsconfig.json && node --test test/*.test.mjs",
74
79
  "type-check": "tsc -b tsconfig.json"
75
80
  }
76
81
  }