@file-viewer/renderer-text 2.3.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 +4 -4
- package/README.md +4 -4
- package/dist/gitBundle.js +10 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/markdown.d.ts +1 -0
- package/dist/markdown.js +40 -40
- package/dist/optionalCapabilities.d.ts +21 -0
- package/dist/optionalCapabilities.js +19 -0
- package/dist/patch.js +9 -15
- package/dist/sanitizeHtml.d.ts +5 -0
- package/dist/sanitizeHtml.js +47 -0
- package/file-viewer.capability.json +12 -0
- package/package.json +12 -7
package/README.en.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @file-viewer/renderer-text
|
|
2
2
|
|
|
3
|
-
|
|
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`
|
|
38
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
38
|
-
-
|
|
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
|
-
|
|
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 {
|
|
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) {
|
package/dist/markdown.d.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
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
|
|
13
|
-
}
|
|
13
|
+
if (!windowRef)
|
|
14
|
+
return null;
|
|
14
15
|
const purifier = createDOMPurify(windowRef);
|
|
15
|
-
if (!purifier.isSupported)
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
return purifier
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
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
|
-
'&': '&',
|
|
59
|
-
'<': '<',
|
|
60
|
-
'>': '>',
|
|
61
|
-
'"': '"',
|
|
62
|
-
"'": '''
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "Standalone code, text, Markdown, patch diff, and Git bundle
|
|
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": "
|
|
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
|
}
|