@file-viewer/renderer-text 3.0.2 → 3.1.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
@@ -43,7 +43,8 @@ const options = {
43
43
  - The legacy `*-full` script-tag IIFE assets do not bundle Prettier, so `prettyPrint` falls back to the original source there. Use the ESM integration (standard component packages or `@file-viewer/preset-*`) for formatted previews.
44
44
  - With the text-tools capability installed, `patch` uses `diff2html` for side-by-side review and `bundle` / `bdl` enables Git bundle inspection.
45
45
  - With the Mermaid capability installed, fenced Mermaid blocks render as diagrams. Without it, the source stays visible with the exact CLI enablement command.
46
- - HTML, XML, Vue, and similar files are escaped and shown as source, never executed.
46
+ - HTML/HTM opens a static page preview with a source-view toggle. `options.text.htmlView: 'source'` starts with the original source. Inline CSS and embedded images are preserved; scripts, forms, external navigation, and external resource requests are blocked by sanitization, CSP, and an opaque sandbox. This is not a website runtime.
47
+ - XML, Vue, and similar files remain escaped source previews. HTML source supports the same highlighting, formatting, and large-text virtualization options.
47
48
  - Markdown uses `marked` for a read-only reading surface with dark/light theme support, table scrolling, and a unified zoom provider.
48
49
  - 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.
49
50
  - Does not depend on any online service or public CDN, making it suitable for intranet logs, configs, snippets, README files, and knowledge-base attachments.
package/README.md CHANGED
@@ -43,7 +43,8 @@ const options = {
43
43
  - 历史 `*-full` 包的 script 标签 IIFE 资源不打包 Prettier,该路径下 `prettyPrint` 无错误回退到原始源码;需要格式化预览时使用 ESM 集成(标准组件包或 `@file-viewer/preset-*`)。
44
44
  - 安装 text-tools capability 后,`patch` 使用 `diff2html` 渲染左右比对视图,`bundle` / `bdl` 才启用 Git bundle 结构检查。
45
45
  - 安装 Mermaid capability 后,Markdown 内嵌 Mermaid 图才会渲染;未安装时保留源码并显示精确 CLI 启用命令。
46
- - HTML / XML / Vue 等文件按源码方式转义展示,不执行脚本。
46
+ - HTML / HTM 默认显示静态页面,提供页面与源码切换;`options.text.htmlView: 'source'` 可默认查看原始源码。页面保留内联 CSS 和内嵌图片,通过净化、CSP 和独立沙箱阻止脚本、表单、外链跳转和外部资源请求,不用于运行完整网站。
47
+ - XML / Vue 等仍按源码转义展示。HTML 源码视图继续支持高亮、格式化和大文本虚拟化。
47
48
  - Markdown 使用 `marked` 输出只读阅读面,并保留明暗主题、表格滚动和统一缩放 provider。
48
49
  - Markdown 不再因为通用大文本阈值自动退化成源码;如业务必须限制超大 Markdown,可单独设置 `options.text.markdownVirtualizeAboveBytes`。
49
50
  - 不绑定任何在线服务或公共 CDN,适合内网日志、配置、代码片段、README 和知识库附件预览。
package/dist/html.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type FileRenderContext, type FileViewerRenderedInstance } from '@file-viewer/core';
2
+ export declare function createHtmlPreviewDocument(documentRef: Document, source: string): string;
3
+ export default function renderHtml(buffer: ArrayBuffer, target: HTMLDivElement, type?: string, context?: FileRenderContext): Promise<FileViewerRenderedInstance>;
package/dist/html.js ADDED
@@ -0,0 +1,143 @@
1
+ import createDOMPurify, {} from 'dompurify';
2
+ import { createFileViewerTranslator, decodeFileViewerTextBuffer, disposeFileViewerRendered } from '@file-viewer/core';
3
+ import renderCode from './code.js';
4
+ const previewPolicy = "default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
5
+ export function createHtmlPreviewDocument(documentRef, source) {
6
+ const windowRef = documentRef.defaultView;
7
+ if (!windowRef)
8
+ throw new Error('HTML preview requires a browser document');
9
+ const purifier = createDOMPurify(windowRef);
10
+ const sanitized = purifier.sanitize(source, {
11
+ WHOLE_DOCUMENT: true,
12
+ USE_PROFILES: { html: true },
13
+ ADD_TAGS: ['style'],
14
+ FORBID_TAGS: [
15
+ 'script',
16
+ 'base',
17
+ 'meta',
18
+ 'link',
19
+ 'iframe',
20
+ 'frame',
21
+ 'frameset',
22
+ 'object',
23
+ 'embed',
24
+ 'form',
25
+ 'template'
26
+ ],
27
+ FORBID_ATTR: ['srcdoc', 'action', 'formaction', 'target', 'ping', 'srcset']
28
+ });
29
+ const preview = new windowRef.DOMParser().parseFromString(sanitized, 'text/html');
30
+ for (const element of preview.querySelectorAll('*')) {
31
+ for (const attribute of [...element.attributes]) {
32
+ const name = attribute.name.toLowerCase();
33
+ if (name.startsWith('on'))
34
+ element.removeAttribute(attribute.name);
35
+ if (name === 'href' && !attribute.value.trim().startsWith('#'))
36
+ element.removeAttribute(attribute.name);
37
+ if (name === 'src' &&
38
+ !/^data:image\/(?:png|jpeg|gif|webp|avif|bmp|x-icon|svg\+xml)[;,]/i.test(attribute.value.trim())) {
39
+ element.removeAttribute(attribute.name);
40
+ }
41
+ }
42
+ }
43
+ // CSP is first, before any retained inline CSS. The containing iframe also has
44
+ // an opaque origin and no sandbox permissions, even if sanitization regresses.
45
+ const policy = preview.createElement('meta');
46
+ policy.httpEquiv = 'Content-Security-Policy';
47
+ policy.content = previewPolicy;
48
+ const viewport = preview.createElement('meta');
49
+ viewport.name = 'viewport';
50
+ viewport.content = 'width=device-width, initial-scale=1';
51
+ preview.head.prepend(policy, viewport);
52
+ return `<!doctype html>\n${preview.documentElement.outerHTML}`;
53
+ }
54
+ export default async function renderHtml(buffer, target, type, context) {
55
+ var _a, _b, _c, _d;
56
+ const documentRef = target.ownerDocument;
57
+ const t = createFileViewerTranslator(context === null || context === void 0 ? void 0 : context.options);
58
+ const root = documentRef.createElement('div');
59
+ root.className = 'html-viewer';
60
+ const style = documentRef.createElement('style');
61
+ style.textContent = `
62
+ .html-viewer{height:100%;min-height:0;display:flex;flex-direction:column}
63
+ .html-viewer-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid #dbe2e8;background:var(--file-viewer-panel,#f8fafc);color:var(--file-viewer-text,#172033)}
64
+ .html-viewer-toolbar button{border:1px solid #cbd5e1;border-radius:6px;padding:5px 10px;background:transparent;color:inherit;cursor:pointer;min-height:36px}
65
+ .html-viewer-toolbar button[aria-pressed="true"]{border-color:#0f766e;background:#0f766e;color:white}
66
+ .html-viewer-hint{font-size:12px;flex:1 1 160px}
67
+ .html-preview-frame,.html-source-view{flex:1;width:100%;min-height:0;border:0}
68
+ .html-preview-frame{background:white}
69
+ .html-source-view{overflow:auto}
70
+ .html-viewer [hidden]{display:none!important}
71
+ @media(max-width:600px){.html-viewer-toolbar button{min-height:44px}}
72
+ `;
73
+ const toolbar = documentRef.createElement('div');
74
+ toolbar.className = 'html-viewer-toolbar';
75
+ const previewButton = documentRef.createElement('button');
76
+ previewButton.type = 'button';
77
+ previewButton.textContent = t('text.html.preview');
78
+ previewButton.dataset.htmlView = 'preview';
79
+ const sourceButton = documentRef.createElement('button');
80
+ sourceButton.type = 'button';
81
+ sourceButton.textContent = t('text.html.source');
82
+ sourceButton.dataset.htmlView = 'source';
83
+ const hint = documentRef.createElement('span');
84
+ hint.className = 'html-viewer-hint';
85
+ hint.textContent = t('text.html.safePreview');
86
+ toolbar.append(previewButton, sourceButton, hint);
87
+ const frame = documentRef.createElement('iframe');
88
+ frame.className = 'html-preview-frame';
89
+ frame.title = t('text.html.preview');
90
+ frame.setAttribute('sandbox', '');
91
+ frame.referrerPolicy = 'no-referrer';
92
+ const sourceTarget = documentRef.createElement('div');
93
+ sourceTarget.className = 'html-source-view';
94
+ if (((_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.toolbar) !== false)
95
+ root.append(toolbar);
96
+ root.append(frame, sourceTarget);
97
+ target.replaceChildren(style, root);
98
+ let sourceInstance;
99
+ let sourcePending;
100
+ let disposed = false;
101
+ const select = async (view) => {
102
+ var _a, _b;
103
+ if (disposed)
104
+ return;
105
+ root.dataset.htmlView = view;
106
+ previewButton.setAttribute('aria-pressed', String(view === 'preview'));
107
+ sourceButton.setAttribute('aria-pressed', String(view === 'source'));
108
+ frame.hidden = view !== 'preview';
109
+ sourceTarget.hidden = view !== 'source';
110
+ hint.hidden = view !== 'preview';
111
+ if (view === 'preview' && !frame.hasAttribute('srcdoc')) {
112
+ frame.srcdoc = createHtmlPreviewDocument(documentRef, decodeFileViewerTextBuffer(buffer, (_b = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.text) === null || _b === void 0 ? void 0 : _b.encoding).text);
113
+ }
114
+ if (view === 'source' && !sourcePending) {
115
+ sourcePending = renderCode(buffer, sourceTarget, type, context).then((instance) => {
116
+ if (disposed)
117
+ disposeFileViewerRendered(instance);
118
+ else
119
+ sourceInstance = instance;
120
+ });
121
+ }
122
+ await sourcePending;
123
+ };
124
+ const showPreview = () => {
125
+ void select('preview');
126
+ };
127
+ const showSource = () => {
128
+ void select('source');
129
+ };
130
+ previewButton.addEventListener('click', showPreview);
131
+ sourceButton.addEventListener('click', showSource);
132
+ await select(((_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.htmlView) || 'preview');
133
+ return {
134
+ $el: target,
135
+ unmount() {
136
+ disposed = true;
137
+ previewButton.removeEventListener('click', showPreview);
138
+ sourceButton.removeEventListener('click', showSource);
139
+ disposeFileViewerRendered(sourceInstance);
140
+ target.replaceChildren();
141
+ }
142
+ };
143
+ }
package/dist/index.js CHANGED
@@ -7,7 +7,9 @@ if (textDefinitions.length !== textRendererIds.length) {
7
7
  throw new Error('@file-viewer/renderer-text could not locate the shared code/markdown format definitions.');
8
8
  }
9
9
  export const textRendererDefinitions = textDefinitions;
10
- export const renderFileViewerCode = (buffer, target, type, context) => import('./code.js').then(({ default: renderCode }) => renderCode(buffer, target, type, context));
10
+ export const renderFileViewerCode = (buffer, target, type, context) => /^(?:html|htm)$/i.test(type || '')
11
+ ? import('./html.js').then(({ default: renderHtml }) => renderHtml(buffer, target, type, context))
12
+ : import('./code.js').then(({ default: renderCode }) => renderCode(buffer, target, type, context));
11
13
  export const renderFileViewerMarkdown = (buffer, target, type, context) => import('./largeText.js').then(async ({ default: renderLargeText, shouldVirtualizeMarkdownBuffer }) => {
12
14
  if (shouldVirtualizeMarkdownBuffer(buffer, context)) {
13
15
  return renderLargeText(buffer, target, type || 'md', context);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-text",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone code, text, and Markdown renderer for File Viewer; Mermaid, patch diff, and Git bundle tools are explicit opt-in capabilities.",
@@ -58,11 +58,11 @@
58
58
  "LICENSE"
59
59
  ],
60
60
  "dependencies": {
61
- "@file-viewer/core": "3.0.2",
61
+ "@file-viewer/core": "3.1.0",
62
62
  "@prettier/plugin-xml": "3.4.2",
63
- "dompurify": "^3.4.14",
63
+ "dompurify": "3.4.15",
64
64
  "highlight.js": "^11.11.1",
65
- "marked": "^18.0.5",
65
+ "marked": "^18.0.12",
66
66
  "prettier": "3.9.6"
67
67
  },
68
68
  "devDependencies": {