@file-viewer/renderer-text 2.4.0 → 3.0.1

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.
@@ -0,0 +1,232 @@
1
+ export const DEFAULT_PRETTY_PRINT_MAX_BYTES = 512 * 1024;
2
+ const htmlEmbeddedLanguagePattern = /<(?:script|style)(?:\s|>)/i;
3
+ const htmlScriptPattern = /<script(?:\s|>)/i;
4
+ const htmlStylePattern = /<style(?:\s|>)/i;
5
+ const withHtmlEmbeddedPlugins = (source) => {
6
+ if (!htmlEmbeddedLanguagePattern.test(source)) {
7
+ return ['html'];
8
+ }
9
+ const plugins = ['html'];
10
+ if (htmlScriptPattern.test(source)) {
11
+ plugins.push('babel', 'estree', 'typescript');
12
+ }
13
+ if (htmlStylePattern.test(source)) {
14
+ plugins.push('postcss');
15
+ }
16
+ return plugins;
17
+ };
18
+ const prettierLanguages = {
19
+ cjs: { parser: 'babel', plugins: ['babel', 'estree'] },
20
+ css: { parser: 'css', plugins: ['postcss'] },
21
+ graphql: { parser: 'graphql', plugins: ['graphql'] },
22
+ gql: { parser: 'graphql', plugins: ['graphql'] },
23
+ html: { parser: 'html', plugins: ['html'], resolvePlugins: withHtmlEmbeddedPlugins },
24
+ htm: { parser: 'html', plugins: ['html'], resolvePlugins: withHtmlEmbeddedPlugins },
25
+ ipynb: { parser: 'json', plugins: ['babel', 'estree'] },
26
+ js: { parser: 'babel', plugins: ['babel', 'estree'] },
27
+ json: { parser: 'json', plugins: ['babel', 'estree'] },
28
+ json5: { parser: 'json5', plugins: ['babel', 'estree'] },
29
+ jsonc: { parser: 'jsonc', plugins: ['babel', 'estree'] },
30
+ jsx: { parser: 'babel', plugins: ['babel', 'estree'] },
31
+ markdown: { parser: 'markdown', plugins: ['markdown'] },
32
+ md: { parser: 'markdown', plugins: ['markdown'] },
33
+ mjs: { parser: 'babel', plugins: ['babel', 'estree'] },
34
+ react: { parser: 'babel', plugins: ['babel', 'estree'] },
35
+ ts: { parser: 'typescript', plugins: ['typescript', 'estree'] },
36
+ tsx: { parser: 'typescript', plugins: ['typescript', 'estree'] },
37
+ vue: {
38
+ parser: 'vue',
39
+ plugins: ['html', 'babel', 'estree', 'typescript', 'postcss']
40
+ },
41
+ xml: {
42
+ parser: 'xml',
43
+ plugins: ['xml'],
44
+ options: {
45
+ xmlWhitespaceSensitivity: 'preserve',
46
+ xmlQuoteAttributes: 'preserve',
47
+ xmlSortAttributesByKey: false
48
+ }
49
+ },
50
+ yaml: { parser: 'yaml', plugins: ['yaml'] },
51
+ yml: { parser: 'yaml', plugins: ['yaml'] }
52
+ };
53
+ const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace';
54
+ const hasWhitespaceSensitiveXml = (source) => {
55
+ if (/\bxml:space\s*=\s*(["'])preserve\1/i.test(source)) {
56
+ return true;
57
+ }
58
+ const Parser = globalThis.DOMParser;
59
+ if (typeof Parser !== 'function') {
60
+ // The text renderer itself is browser-facing, but fail closed when this
61
+ // helper is evaluated in a non-DOM runtime.
62
+ return true;
63
+ }
64
+ try {
65
+ const documentRef = new Parser().parseFromString(source, 'application/xml');
66
+ if (documentRef.querySelector('parsererror')) {
67
+ return false;
68
+ }
69
+ const containsMixedContent = (element) => {
70
+ var _a, _b, _c;
71
+ const preservesWhitespace = ((_a = element.getAttributeNS(XML_NAMESPACE, 'space')) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'preserve' ||
72
+ ((_b = element.getAttribute('xml:space')) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'preserve';
73
+ if (preservesWhitespace) {
74
+ return true;
75
+ }
76
+ let hasElementChild = false;
77
+ let hasDirectText = false;
78
+ for (const child of Array.from(element.childNodes)) {
79
+ if (child.nodeType === 1) {
80
+ hasElementChild = true;
81
+ if (containsMixedContent(child)) {
82
+ return true;
83
+ }
84
+ }
85
+ else if ((child.nodeType === 3 || child.nodeType === 4) && ((_c = child.textContent) === null || _c === void 0 ? void 0 : _c.trim())) {
86
+ hasDirectText = true;
87
+ }
88
+ }
89
+ return hasElementChild && hasDirectText;
90
+ };
91
+ return documentRef.documentElement
92
+ ? containsMixedContent(documentRef.documentElement)
93
+ : false;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ };
99
+ const pluginLoaders = {
100
+ babel: () => import('prettier/plugins/babel').then(module => module.default),
101
+ estree: () => import('prettier/plugins/estree').then(module => module.default),
102
+ typescript: () => import('prettier/plugins/typescript').then(module => module.default),
103
+ postcss: () => import('prettier/plugins/postcss').then(module => module.default),
104
+ html: () => import('prettier/plugins/html').then(module => module.default),
105
+ markdown: () => import('prettier/plugins/markdown').then(module => module.default),
106
+ yaml: () => import('prettier/plugins/yaml').then(module => module.default),
107
+ graphql: () => import('prettier/plugins/graphql').then(module => module.default),
108
+ xml: () => import('@prettier/plugin-xml').then(module => module.default)
109
+ };
110
+ const loadPrettierRuntime = async (definition, source) => {
111
+ var _a, _b;
112
+ const prettierPromise = import('prettier/standalone');
113
+ const pluginNames = (_b = (_a = definition.resolvePlugins) === null || _a === void 0 ? void 0 : _a.call(definition, source)) !== null && _b !== void 0 ? _b : definition.plugins;
114
+ const [prettier, ...plugins] = await Promise.all([
115
+ prettierPromise,
116
+ ...pluginNames.map(pluginName => pluginLoaders[pluginName]())
117
+ ]);
118
+ return {
119
+ format: prettier.format,
120
+ plugins
121
+ };
122
+ };
123
+ const utf8ByteLength = (source) => new TextEncoder().encode(source).byteLength;
124
+ export const resolveFileViewerPrettyPrintMaxBytes = (options) => {
125
+ var _a;
126
+ const configured = (_a = options === null || options === void 0 ? void 0 : options.prettyPrintMaxBytes) !== null && _a !== void 0 ? _a : options === null || options === void 0 ? void 0 : options.virtualizeAboveBytes;
127
+ if (!Number.isFinite(configured)) {
128
+ return DEFAULT_PRETTY_PRINT_MAX_BYTES;
129
+ }
130
+ return Math.max(0, Math.trunc(Number(configured)));
131
+ };
132
+ export const supportsFileViewerPrettyPrint = (extension) => {
133
+ return Boolean(prettierLanguages[extension.trim().toLowerCase()]);
134
+ };
135
+ /**
136
+ * Formats a decoded display representation without mutating the source buffer.
137
+ *
138
+ * Parser support and byte limits are resolved before the Prettier runtime or
139
+ * any parser plugin is imported. Failures intentionally return the original
140
+ * source so malformed or unsupported uploads remain previewable.
141
+ */
142
+ export const formatFileViewerTextForDisplay = async (source, extension, options, signal, runtimeLoader = loadPrettierRuntime) => {
143
+ const maxBytes = resolveFileViewerPrettyPrintMaxBytes(options);
144
+ const sourceByteLength = utf8ByteLength(source);
145
+ if ((options === null || options === void 0 ? void 0 : options.prettyPrint) !== true) {
146
+ return { text: source, formatted: false, reason: 'disabled', sourceByteLength, maxBytes };
147
+ }
148
+ const definition = prettierLanguages[extension.trim().toLowerCase()];
149
+ if (!definition) {
150
+ return { text: source, formatted: false, reason: 'unsupported', sourceByteLength, maxBytes };
151
+ }
152
+ if (sourceByteLength > maxBytes) {
153
+ return {
154
+ text: source,
155
+ formatted: false,
156
+ reason: 'too-large',
157
+ parser: definition.parser,
158
+ sourceByteLength,
159
+ maxBytes
160
+ };
161
+ }
162
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
163
+ return {
164
+ text: source,
165
+ formatted: false,
166
+ reason: 'aborted',
167
+ parser: definition.parser,
168
+ sourceByteLength,
169
+ maxBytes
170
+ };
171
+ }
172
+ if (definition.parser === 'xml' && hasWhitespaceSensitiveXml(source)) {
173
+ return {
174
+ text: source,
175
+ formatted: false,
176
+ reason: 'whitespace-sensitive',
177
+ parser: definition.parser,
178
+ sourceByteLength,
179
+ maxBytes
180
+ };
181
+ }
182
+ try {
183
+ const runtime = await runtimeLoader(definition, source);
184
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
185
+ return {
186
+ text: source,
187
+ formatted: false,
188
+ reason: 'aborted',
189
+ parser: definition.parser,
190
+ sourceByteLength,
191
+ maxBytes
192
+ };
193
+ }
194
+ const formatted = await runtime.format(source, {
195
+ parser: definition.parser,
196
+ plugins: runtime.plugins,
197
+ tabWidth: 2,
198
+ useTabs: false,
199
+ printWidth: 80,
200
+ endOfLine: 'lf',
201
+ ...definition.options
202
+ });
203
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
204
+ return {
205
+ text: source,
206
+ formatted: false,
207
+ reason: 'aborted',
208
+ parser: definition.parser,
209
+ sourceByteLength,
210
+ maxBytes
211
+ };
212
+ }
213
+ return {
214
+ text: formatted.replace(/\n$/, ''),
215
+ formatted: true,
216
+ reason: 'formatted',
217
+ parser: definition.parser,
218
+ sourceByteLength,
219
+ maxBytes
220
+ };
221
+ }
222
+ catch {
223
+ return {
224
+ text: source,
225
+ formatted: false,
226
+ reason: 'failed',
227
+ parser: definition.parser,
228
+ sourceByteLength,
229
+ maxBytes
230
+ };
231
+ }
232
+ };
@@ -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.1",
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,32 @@
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.1",
62
+ "@prettier/plugin-xml": "3.4.2",
63
+ "dompurify": "^3.4.14",
62
64
  "highlight.js": "^11.11.1",
63
65
  "marked": "^18.0.5",
64
- "mermaid": "^11.16.1",
65
- "pako": "^2.1.0",
66
- "dompurify": "^3.4.13"
66
+ "prettier": "3.8.3"
67
67
  },
68
68
  "devDependencies": {
69
+ "diff2html": "^3.4.56",
70
+ "mermaid": "^11.17.2",
71
+ "pako": "^2.1.0",
72
+ "jsdom": "^27.4.0",
69
73
  "typescript": "^6.0.3"
70
74
  },
75
+ "fileViewer": {
76
+ "capabilityManifest": "./file-viewer.capability.json"
77
+ },
71
78
  "license": "Apache-2.0",
72
79
  "scripts": {
73
- "build": "tsc -b tsconfig.json",
80
+ "build": "tsc -b tsconfig.json && node --test test/*.test.mjs",
74
81
  "type-check": "tsc -b tsconfig.json"
75
82
  }
76
83
  }