@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.
- package/README.en.md +11 -4
- package/README.md +11 -4
- package/dist/code.d.ts +3 -5
- package/dist/code.js +176 -35
- package/dist/codeStyle.d.ts +1 -1
- package/dist/codeStyle.js +18 -4
- package/dist/gitBundle.js +10 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/largeText.js +170 -16
- 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/prettyPrint.d.ts +35 -0
- package/dist/prettyPrint.js +232 -0
- package/dist/sanitizeHtml.d.ts +5 -0
- package/dist/sanitizeHtml.js +47 -0
- package/file-viewer.capability.json +12 -0
- package/package.json +15 -8
|
@@ -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": "
|
|
3
|
+
"version": "3.0.1",
|
|
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,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": "
|
|
61
|
-
"
|
|
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
|
-
"
|
|
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
|
}
|