@file-viewer/doc 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 +4 -0
- package/README.md +3 -0
- package/dist/core/utils.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/msdoc/fkp.js +2 -1
- package/dist/msdoc/html-word.js +151 -17
- package/dist/msdoc/objects.d.ts +1 -0
- package/dist/msdoc/objects.js +9 -2
- package/dist/render/html.js +58 -8
- package/dist/types.d.ts +2 -0
- package/dist/viewer.d.ts +7 -0
- package/dist/viewer.js +60 -3
- package/package.json +4 -3
package/README.en.md
CHANGED
|
@@ -21,6 +21,10 @@ const rendered = renderMsDoc(parsed)
|
|
|
21
21
|
container.innerHTML = `<style>${rendered.css}</style><div class="msdoc-root">${rendered.html}</div>`
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
External links and linked images are blocked by default. Trusted integrations
|
|
25
|
+
can opt in separately with `externalLinkPolicy: 'allow'` and
|
|
26
|
+
`externalResourcePolicy: 'allow'`; embedded images and bookmarks keep working.
|
|
27
|
+
|
|
24
28
|
The package also exports `parseMsDocToHtml`, `mountMsDoc`, `createMsDocViewer`, and `MsDocWorkerClient`.
|
|
25
29
|
|
|
26
30
|
## Scope
|
package/README.md
CHANGED
package/dist/core/utils.js
CHANGED
|
@@ -48,7 +48,8 @@ export function slugify(text) {
|
|
|
48
48
|
return String(text ?? '')
|
|
49
49
|
.toLowerCase()
|
|
50
50
|
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
|
51
|
-
.replace(
|
|
51
|
+
.replace(/^-/, '')
|
|
52
|
+
.replace(/-$/, '') || 'item';
|
|
52
53
|
}
|
|
53
54
|
export function shallowEqual(a, b) {
|
|
54
55
|
if (a === b)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { parseMsDoc } from './msdoc/parser.js';
|
|
2
2
|
export { renderMsDoc, defaultMsDocCss, sanitizeMsDocLinkHref } from './render/html.js';
|
|
3
|
-
export { createMsDocViewer, mountMsDoc, parseMsDocToHtml, sanitizeMsDocHtml } from './viewer.js';
|
|
3
|
+
export { createMsDocViewer, mountMsDoc, parseMsDocToHtml, sanitizeFileViewerDocumentMarkup, sanitizeMsDocHtml, } from './viewer.js';
|
|
4
4
|
export { MsDocWorkerClient } from './worker-client.js';
|
|
5
5
|
export type * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { parseMsDoc } from './msdoc/parser.js';
|
|
2
2
|
export { renderMsDoc, defaultMsDocCss, sanitizeMsDocLinkHref } from './render/html.js';
|
|
3
|
-
export { createMsDocViewer, mountMsDoc, parseMsDocToHtml, sanitizeMsDocHtml } from './viewer.js';
|
|
3
|
+
export { createMsDocViewer, mountMsDoc, parseMsDocToHtml, sanitizeFileViewerDocumentMarkup, sanitizeMsDocHtml, } from './viewer.js';
|
|
4
4
|
export { MsDocWorkerClient } from './worker-client.js';
|
package/dist/msdoc/fkp.js
CHANGED
|
@@ -170,7 +170,8 @@ export function readPapxProperties(wordBytes, offset, dataBytes) {
|
|
|
170
170
|
bodyStart = offset + 2;
|
|
171
171
|
}
|
|
172
172
|
else {
|
|
173
|
-
|
|
173
|
+
// PapxInFkp.cb counts 2-byte words; the payload occupies (2 * cb) - 1 bytes.
|
|
174
|
+
bodySize = cb * 2 - 1;
|
|
174
175
|
bodyStart = offset + 1;
|
|
175
176
|
}
|
|
176
177
|
if (bodySize < 2 || !reader.ensure(bodyStart, Math.max(2, bodySize))) {
|
package/dist/msdoc/html-word.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { charPropsToState, paraPropsToState } from './properties.js';
|
|
2
2
|
const HTML_PREFIX_RE = /^(?:<!doctype\s+html\b|<html\b|<body\b)/i;
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const HTML_BREAK_TAGS = new Set(['br']);
|
|
4
|
+
const HTML_PARAGRAPH_TAGS = new Set(['div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'pre', 'blockquote', 'tr']);
|
|
5
|
+
const HTML_BLOCK_TAGS = new Set(['div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'pre', 'blockquote', 'tr', 'table']);
|
|
6
|
+
const HTML_CELL_TAGS = new Set(['td', 'th']);
|
|
7
|
+
const HTML_SKIPPED_CONTENT_TAGS = new Set(['script', 'style', 'noscript', 'iframe', 'object', 'embed', 'svg']);
|
|
5
8
|
function asciiPreview(bytes, length = 1024) {
|
|
6
9
|
return Array.from(bytes.subarray(0, Math.min(length, bytes.length)), byte => byte === 0 ? '' : String.fromCharCode(byte)).join('');
|
|
7
10
|
}
|
|
@@ -63,34 +66,165 @@ function decodeEntities(value) {
|
|
|
63
66
|
return named[entity.toLowerCase()] ?? match;
|
|
64
67
|
});
|
|
65
68
|
}
|
|
69
|
+
function findHtmlTagEnd(value, start) {
|
|
70
|
+
let quote = '';
|
|
71
|
+
for (let index = start + 1; index < value.length; index += 1) {
|
|
72
|
+
const character = value[index] || '';
|
|
73
|
+
if (quote) {
|
|
74
|
+
if (character === quote)
|
|
75
|
+
quote = '';
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (character === '"' || character === "'") {
|
|
79
|
+
quote = character;
|
|
80
|
+
}
|
|
81
|
+
else if (character === '>') {
|
|
82
|
+
return index;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
function readHtmlTag(value, start, end) {
|
|
88
|
+
let cursor = start + 1;
|
|
89
|
+
while (cursor < end && /\s/.test(value[cursor] || ''))
|
|
90
|
+
cursor += 1;
|
|
91
|
+
const closing = value[cursor] === '/';
|
|
92
|
+
if (closing)
|
|
93
|
+
cursor += 1;
|
|
94
|
+
while (cursor < end && /\s/.test(value[cursor] || ''))
|
|
95
|
+
cursor += 1;
|
|
96
|
+
const nameStart = cursor;
|
|
97
|
+
while (cursor < end && /[A-Za-z0-9]/.test(value[cursor] || ''))
|
|
98
|
+
cursor += 1;
|
|
99
|
+
return {
|
|
100
|
+
closing,
|
|
101
|
+
name: value.slice(nameStart, cursor).toLowerCase(),
|
|
102
|
+
attributes: closing ? '' : value.slice(cursor, end),
|
|
103
|
+
selfClosing: !closing && /\/\s*$/.test(value.slice(cursor, end)),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function skipHtmlElementContent(value, lowerValue, tagName, start) {
|
|
107
|
+
const closingPrefix = `</${tagName}`;
|
|
108
|
+
let cursor = start;
|
|
109
|
+
while (cursor < value.length) {
|
|
110
|
+
const closingStart = lowerValue.indexOf(closingPrefix, cursor);
|
|
111
|
+
if (closingStart < 0)
|
|
112
|
+
return value.length;
|
|
113
|
+
const boundary = lowerValue[closingStart + closingPrefix.length] || '';
|
|
114
|
+
if (boundary && !/[\s>]/.test(boundary)) {
|
|
115
|
+
cursor = closingStart + closingPrefix.length;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const closingEnd = findHtmlTagEnd(value, closingStart);
|
|
119
|
+
return closingEnd < 0 ? value.length : closingEnd + 1;
|
|
120
|
+
}
|
|
121
|
+
return value.length;
|
|
122
|
+
}
|
|
123
|
+
function htmlFragmentToText(fragment) {
|
|
124
|
+
const chunks = [];
|
|
125
|
+
const lowerFragment = fragment.toLowerCase();
|
|
126
|
+
let cursor = 0;
|
|
127
|
+
while (cursor < fragment.length) {
|
|
128
|
+
const tagStart = fragment.indexOf('<', cursor);
|
|
129
|
+
if (tagStart < 0) {
|
|
130
|
+
chunks.push(fragment.slice(cursor));
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
chunks.push(fragment.slice(cursor, tagStart));
|
|
134
|
+
if (fragment.startsWith('<!--', tagStart)) {
|
|
135
|
+
const commentEnd = fragment.indexOf('-->', tagStart + 4);
|
|
136
|
+
cursor = commentEnd < 0 ? fragment.length : commentEnd + 3;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const tagEnd = findHtmlTagEnd(fragment, tagStart);
|
|
140
|
+
if (tagEnd < 0) {
|
|
141
|
+
chunks.push(fragment.slice(tagStart));
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
const tag = readHtmlTag(fragment, tagStart, tagEnd);
|
|
145
|
+
if (!tag.closing && HTML_SKIPPED_CONTENT_TAGS.has(tag.name)) {
|
|
146
|
+
cursor = skipHtmlElementContent(fragment, lowerFragment, tag.name, tagEnd + 1);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (HTML_BREAK_TAGS.has(tag.name) || (tag.closing && HTML_BLOCK_TAGS.has(tag.name))) {
|
|
150
|
+
chunks.push('\n');
|
|
151
|
+
}
|
|
152
|
+
else if (HTML_CELL_TAGS.has(tag.name)) {
|
|
153
|
+
chunks.push('\t');
|
|
154
|
+
}
|
|
155
|
+
cursor = tagEnd + 1;
|
|
156
|
+
}
|
|
157
|
+
return chunks.join('').replace(/\u0000/g, '');
|
|
158
|
+
}
|
|
66
159
|
function fragmentToText(fragment) {
|
|
67
|
-
return decodeEntities(fragment
|
|
68
|
-
.replace(UNSAFE_CONTENT_RE, '')
|
|
69
|
-
.replace(/<!--[\s\S]*?-->/g, '')
|
|
70
|
-
.replace(/<br\s*\/?>/gi, '\n')
|
|
71
|
-
.replace(/<\/(?:div|p|h[1-6]|li|pre|blockquote|tr|table)>/gi, '\n')
|
|
72
|
-
.replace(/<\/?(?:td|th)\b[^>]*>/gi, '\t')
|
|
73
|
-
.replace(/<[^>]+>/g, '')
|
|
74
|
-
.replace(/\u0000/g, ''))
|
|
160
|
+
return decodeEntities(htmlFragmentToText(fragment))
|
|
75
161
|
.replace(/\r\n?/g, '\n')
|
|
76
162
|
.replace(/[\t\f\v ]+/g, ' ')
|
|
77
163
|
.replace(/ *\n */g, '\n')
|
|
78
164
|
.trim();
|
|
79
165
|
}
|
|
80
166
|
function extractParagraphs(html) {
|
|
81
|
-
const safe = html.replace(UNSAFE_CONTENT_RE, '').replace(/<!--[\s\S]*?-->/g, '');
|
|
82
167
|
const paragraphs = [];
|
|
83
|
-
|
|
84
|
-
|
|
168
|
+
const lowerHtml = html.toLowerCase();
|
|
169
|
+
let active = null;
|
|
170
|
+
let cursor = 0;
|
|
171
|
+
const appendParagraph = (attributes, content) => {
|
|
85
172
|
const text = fragmentToText(content);
|
|
86
|
-
if (
|
|
173
|
+
if (text)
|
|
174
|
+
paragraphs.push({ text, attributes, content });
|
|
175
|
+
};
|
|
176
|
+
while (cursor < html.length) {
|
|
177
|
+
const tagStart = html.indexOf('<', cursor);
|
|
178
|
+
if (tagStart < 0)
|
|
179
|
+
break;
|
|
180
|
+
if (html.startsWith('<!--', tagStart)) {
|
|
181
|
+
const commentEnd = html.indexOf('-->', tagStart + 4);
|
|
182
|
+
cursor = commentEnd < 0 ? html.length : commentEnd + 3;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const tagEnd = findHtmlTagEnd(html, tagStart);
|
|
186
|
+
if (tagEnd < 0)
|
|
187
|
+
break;
|
|
188
|
+
const tag = readHtmlTag(html, tagStart, tagEnd);
|
|
189
|
+
if (!tag.closing && HTML_SKIPPED_CONTENT_TAGS.has(tag.name)) {
|
|
190
|
+
cursor = skipHtmlElementContent(html, lowerHtml, tag.name, tagEnd + 1);
|
|
87
191
|
continue;
|
|
88
|
-
|
|
192
|
+
}
|
|
193
|
+
if (HTML_PARAGRAPH_TAGS.has(tag.name) && !tag.selfClosing) {
|
|
194
|
+
if (!tag.closing) {
|
|
195
|
+
if (!active) {
|
|
196
|
+
active = {
|
|
197
|
+
attributes: tag.attributes,
|
|
198
|
+
contentStart: tagEnd + 1,
|
|
199
|
+
name: tag.name,
|
|
200
|
+
sameNameDepth: 0,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
else if (active.name === tag.name) {
|
|
204
|
+
active.sameNameDepth += 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (active?.name === tag.name) {
|
|
208
|
+
if (active.sameNameDepth > 0) {
|
|
209
|
+
active.sameNameDepth -= 1;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
appendParagraph(active.attributes, html.slice(active.contentStart, tagStart));
|
|
213
|
+
active = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
cursor = tagEnd + 1;
|
|
218
|
+
}
|
|
219
|
+
// Malformed HTML saved by old Word versions is common. Preserve its visible
|
|
220
|
+
// tail as a paragraph without restarting a regex search at every opening tag.
|
|
221
|
+
if (active) {
|
|
222
|
+
appendParagraph(active.attributes, html.slice(active.contentStart));
|
|
89
223
|
}
|
|
90
224
|
if (paragraphs.length)
|
|
91
225
|
return paragraphs;
|
|
92
|
-
const text = fragmentToText(
|
|
93
|
-
return text ? [{ text, attributes: '', content:
|
|
226
|
+
const text = fragmentToText(html);
|
|
227
|
+
return text ? [{ text, attributes: '', content: html }] : [];
|
|
94
228
|
}
|
|
95
229
|
function readStyle(attributes) {
|
|
96
230
|
return /\bstyle\s*=\s*(["'])([\s\S]*?)\1/i.exec(attributes)?.[2] || '';
|
package/dist/msdoc/objects.d.ts
CHANGED
package/dist/msdoc/objects.js
CHANGED
|
@@ -36,6 +36,13 @@ function isPrintableAnsi(bytes) {
|
|
|
36
36
|
}
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
|
+
export function trimTrailingNullCharacters(value) {
|
|
40
|
+
let end = value.length;
|
|
41
|
+
while (end > 0 && value.charCodeAt(end - 1) === 0) {
|
|
42
|
+
end -= 1;
|
|
43
|
+
}
|
|
44
|
+
return end === value.length ? value : value.slice(0, end);
|
|
45
|
+
}
|
|
39
46
|
function isProbablyPicturePath(value) {
|
|
40
47
|
const normalized = value.trim();
|
|
41
48
|
if (!normalized)
|
|
@@ -217,7 +224,7 @@ function readOptionalPictureName(pictureChunk, picf) {
|
|
|
217
224
|
const raw = pictureChunk.subarray(offset + 1, offset + 1 + length);
|
|
218
225
|
if (!isPrintableAnsi(raw))
|
|
219
226
|
return null;
|
|
220
|
-
const value = new TextDecoder('windows-1252').decode(raw)
|
|
227
|
+
const value = trimTrailingNullCharacters(new TextDecoder('windows-1252').decode(raw));
|
|
221
228
|
if (!isProbablyPicturePath(value)) {
|
|
222
229
|
if (picf.mm !== MM_SHAPEFILE)
|
|
223
230
|
return null;
|
|
@@ -322,7 +329,7 @@ function extractEmbeddedBlipFromFbse(bytes, offset, header) {
|
|
|
322
329
|
if (!candidate)
|
|
323
330
|
return null;
|
|
324
331
|
const fbseName = cbName > 0 && nameOffset + cbName <= payloadEnd
|
|
325
|
-
? new TextDecoder('utf-16le').decode(bytes.subarray(nameOffset, nameOffset + Math.max(0, cbName - 2)))
|
|
332
|
+
? trimTrailingNullCharacters(new TextDecoder('utf-16le').decode(bytes.subarray(nameOffset, nameOffset + Math.max(0, cbName - 2))))
|
|
326
333
|
: '';
|
|
327
334
|
return {
|
|
328
335
|
...candidate,
|
package/dist/render/html.js
CHANGED
|
@@ -50,12 +50,12 @@ export function sanitizeMsDocLinkHref(href, policy = 'block') {
|
|
|
50
50
|
return null;
|
|
51
51
|
if (/^(?:https?:|mailto:|tel:)/i.test(value))
|
|
52
52
|
return value;
|
|
53
|
-
if (value.startsWith('//') || value.
|
|
53
|
+
if (value.startsWith('//') || value.includes('\\'))
|
|
54
54
|
return null;
|
|
55
|
-
if (/^(?:\/|\.\/|\.\.\/)/.test(value) && !value.startsWith('//')
|
|
55
|
+
if (/^(?:\/|\.\/|\.\.\/)/.test(value) && !value.startsWith('//')) {
|
|
56
56
|
return value;
|
|
57
57
|
}
|
|
58
|
-
if (!/^[a-z][a-z\d+.-]*:/i.test(value)
|
|
58
|
+
if (!/^[a-z][a-z\d+.-]*:/i.test(value))
|
|
59
59
|
return value;
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
@@ -263,13 +263,61 @@ function applyInlineImageDisplaySize(style, asset) {
|
|
|
263
263
|
if (heightPx)
|
|
264
264
|
style.height = `${heightPx}px`;
|
|
265
265
|
}
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
const SAFE_RASTER_IMAGE_DATA_URL = /^data:image\/(?:avif|bmp|gif|jpeg|png|webp|x-icon);base64,[a-z\d+/=]+$/i;
|
|
267
|
+
function decodeBase64Text(value) {
|
|
268
|
+
try {
|
|
269
|
+
const bufferCtor = globalThis.Buffer;
|
|
270
|
+
if (bufferCtor)
|
|
271
|
+
return bufferCtor.from(value, 'base64').toString('utf8');
|
|
272
|
+
if (typeof atob !== 'function')
|
|
273
|
+
return null;
|
|
274
|
+
const binary = atob(value);
|
|
275
|
+
const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
|
|
276
|
+
return new TextDecoder().decode(bytes);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function isSafeSvgImageDataUrl(value) {
|
|
283
|
+
const match = /^data:image\/svg\+xml;base64,([a-z\d+/=]+)$/i.exec(value);
|
|
284
|
+
if (!match || match[1].length > 8 * 1024 * 1024)
|
|
285
|
+
return false;
|
|
286
|
+
const svg = decodeBase64Text(match[1]);
|
|
287
|
+
if (!svg || !/^\s*<svg(?:\s|>)/i.test(svg))
|
|
288
|
+
return false;
|
|
289
|
+
if (/<\/?(?:script|style|foreignObject|iframe|object|embed|form|link)\b/i.test(svg))
|
|
290
|
+
return false;
|
|
291
|
+
if (/<!\s*(?:doctype|entity)\b/i.test(svg) || /\son[a-z\d_-]+\s*=/i.test(svg))
|
|
292
|
+
return false;
|
|
293
|
+
if (svg.includes('\\') || svg.includes('/*') || /@import\b/i.test(svg))
|
|
294
|
+
return false;
|
|
295
|
+
if (/url\s*\(\s*(?!["']?#[A-Za-z0-9_.:-]+["']?\s*\))/i.test(svg))
|
|
296
|
+
return false;
|
|
297
|
+
for (const resource of svg.matchAll(/\s(?:href|xlink:href|src)\s*=\s*(["'])([\s\S]*?)\1/gi)) {
|
|
298
|
+
const target = String(resource[2] || '').replace(/[\u0000-\u0020\u007f-\u009f]/g, '');
|
|
299
|
+
if (!/^#[A-Za-z0-9_.:-]+$/.test(target) && !SAFE_RASTER_IMAGE_DATA_URL.test(target))
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
function sanitizeImageSource(src, policy, allowGeneratedVector = false) {
|
|
305
|
+
const value = String(src || '').replace(/[\u0000-\u0020\u007f-\u009f]/g, '').trim();
|
|
268
306
|
if (!value)
|
|
269
307
|
return null;
|
|
270
|
-
if (
|
|
308
|
+
if (SAFE_RASTER_IMAGE_DATA_URL.test(value))
|
|
271
309
|
return value;
|
|
272
|
-
if (
|
|
310
|
+
if (allowGeneratedVector && isSafeSvgImageDataUrl(value))
|
|
311
|
+
return value;
|
|
312
|
+
if (/^blob:/i.test(value))
|
|
313
|
+
return value;
|
|
314
|
+
if (policy !== 'allow')
|
|
315
|
+
return null;
|
|
316
|
+
if (/^https?:/i.test(value))
|
|
317
|
+
return value;
|
|
318
|
+
if (value.startsWith('//') || value.startsWith('\\'))
|
|
319
|
+
return null;
|
|
320
|
+
if (!/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith('\\'))
|
|
273
321
|
return value;
|
|
274
322
|
return null;
|
|
275
323
|
}
|
|
@@ -298,7 +346,8 @@ function sanitizeAssetHref(href, context) {
|
|
|
298
346
|
return null;
|
|
299
347
|
}
|
|
300
348
|
function renderImageNode(node, context) {
|
|
301
|
-
const
|
|
349
|
+
const allowGeneratedVector = node.asset.meta?.vectorConverted === true;
|
|
350
|
+
const src = sanitizeImageSource(node.asset.sourceUrl, context.externalResourcePolicy, allowGeneratedVector) || sanitizeImageSource(node.asset.dataUrl, context.externalResourcePolicy, allowGeneratedVector);
|
|
302
351
|
const baseStyle = inlineStyleToCss(node.style);
|
|
303
352
|
const displaySize = inlineImageDisplaySizePx(node.asset);
|
|
304
353
|
if (displaySize.widthPx || displaySize.heightPx) {
|
|
@@ -489,6 +538,7 @@ export function renderMsDoc(parsed, options = {}) {
|
|
|
489
538
|
const css = options.css ?? defaultMsDocCss();
|
|
490
539
|
const context = {
|
|
491
540
|
externalLinkPolicy: options.externalLinkPolicy ?? 'block',
|
|
541
|
+
externalResourcePolicy: options.externalResourcePolicy ?? 'block',
|
|
492
542
|
};
|
|
493
543
|
const html = parsed.blocks.map((block) => {
|
|
494
544
|
if (block.type === 'paragraph')
|
package/dist/types.d.ts
CHANGED
|
@@ -619,6 +619,8 @@ export interface MsDocRenderOptions {
|
|
|
619
619
|
css?: string;
|
|
620
620
|
/** External links are blocked by default; internal document bookmarks remain active. */
|
|
621
621
|
externalLinkPolicy?: 'allow' | 'block';
|
|
622
|
+
/** Linked image resources are blocked by default; embedded data/blob images remain available. */
|
|
623
|
+
externalResourcePolicy?: 'allow' | 'block';
|
|
622
624
|
}
|
|
623
625
|
export interface MsDocMeta {
|
|
624
626
|
fib: {
|
package/dist/viewer.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import type { Config } from 'dompurify';
|
|
1
2
|
import type { MsDocParseToHtmlOptions, MsDocRenderResult, MsDocViewer, MsDocViewerConfig, ViewerInput } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared DOC/RTF DOMPurify boundary. A per-window purifier and Trusted Types
|
|
5
|
+
* policy avoid duplicate policy creation when several Word formats are opened
|
|
6
|
+
* in the same host document.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeFileViewerDocumentMarkup(html: string, targetWindow: Window, config: Config): unknown | null;
|
|
2
9
|
export declare function sanitizeMsDocHtml(html: string, targetWindow: Window): DocumentFragment;
|
|
3
10
|
export declare function mountMsDoc(container: HTMLElement, rendered: MsDocRenderResult): HTMLElement;
|
|
4
11
|
export declare function parseMsDocToHtml(input: ViewerInput, options?: MsDocParseToHtmlOptions): Promise<MsDocRenderResult>;
|
package/dist/viewer.js
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
import createDOMPurify from 'dompurify';
|
|
2
2
|
import { parseMsDoc } from './msdoc/parser.js';
|
|
3
3
|
import { renderMsDoc } from './render/html.js';
|
|
4
|
+
const purifierByWindow = new WeakMap();
|
|
5
|
+
const trustedTypesPolicyByWindow = new WeakMap();
|
|
6
|
+
const getDocumentPurifier = (targetWindow) => {
|
|
7
|
+
const cached = purifierByWindow.get(targetWindow);
|
|
8
|
+
if (cached)
|
|
9
|
+
return cached;
|
|
10
|
+
const purifier = createDOMPurify(targetWindow);
|
|
11
|
+
if (!purifier.isSupported)
|
|
12
|
+
return null;
|
|
13
|
+
purifierByWindow.set(targetWindow, purifier);
|
|
14
|
+
return purifier;
|
|
15
|
+
};
|
|
16
|
+
const getDocumentTrustedTypesPolicy = (targetWindow) => {
|
|
17
|
+
const cached = trustedTypesPolicyByWindow.get(targetWindow);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
const trustedTypes = targetWindow.trustedTypes;
|
|
21
|
+
if (!trustedTypes?.createPolicy)
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const policy = trustedTypes.createPolicy('file-viewer-document-sanitizer', {
|
|
25
|
+
createHTML: value => value,
|
|
26
|
+
createScriptURL: value => value,
|
|
27
|
+
});
|
|
28
|
+
trustedTypesPolicyByWindow.set(targetWindow, policy);
|
|
29
|
+
return policy;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Shared DOC/RTF DOMPurify boundary. A per-window purifier and Trusted Types
|
|
37
|
+
* policy avoid duplicate policy creation when several Word formats are opened
|
|
38
|
+
* in the same host document.
|
|
39
|
+
*/
|
|
40
|
+
export function sanitizeFileViewerDocumentMarkup(html, targetWindow, config) {
|
|
41
|
+
const purifier = getDocumentPurifier(targetWindow);
|
|
42
|
+
if (!purifier)
|
|
43
|
+
return null;
|
|
44
|
+
const policy = getDocumentTrustedTypesPolicy(targetWindow);
|
|
45
|
+
try {
|
|
46
|
+
return purifier.sanitize(html, {
|
|
47
|
+
...config,
|
|
48
|
+
...(policy ? { TRUSTED_TYPES_POLICY: policy } : {}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// If a host CSP forbids this policy name, callers fall back to a text node
|
|
53
|
+
// rather than mounting unsanitized markup or failing the whole preview.
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
4
57
|
async function normalizeInput(input) {
|
|
5
58
|
if (input instanceof ArrayBuffer)
|
|
6
59
|
return input;
|
|
@@ -21,13 +74,17 @@ async function normalizeInput(input) {
|
|
|
21
74
|
throw new TypeError('Unsupported input type');
|
|
22
75
|
}
|
|
23
76
|
export function sanitizeMsDocHtml(html, targetWindow) {
|
|
24
|
-
const
|
|
25
|
-
return purifier.sanitize(html, {
|
|
77
|
+
const sanitized = sanitizeFileViewerDocumentMarkup(html, targetWindow, {
|
|
26
78
|
RETURN_DOM_FRAGMENT: true,
|
|
27
79
|
USE_PROFILES: { html: true },
|
|
28
80
|
FORBID_TAGS: ['base', 'embed', 'form', 'iframe', 'object', 'script', 'style', 'template'],
|
|
29
81
|
FORBID_ATTR: ['action', 'formaction', 'srcdoc'],
|
|
30
82
|
});
|
|
83
|
+
if (sanitized?.nodeType === 11)
|
|
84
|
+
return sanitized;
|
|
85
|
+
const fallback = targetWindow.document.createDocumentFragment();
|
|
86
|
+
fallback.append(targetWindow.document.createTextNode(html));
|
|
87
|
+
return fallback;
|
|
31
88
|
}
|
|
32
89
|
export function mountMsDoc(container, rendered) {
|
|
33
90
|
if (!container)
|
|
@@ -77,7 +134,7 @@ export function createMsDocViewer(container, config = {}) {
|
|
|
77
134
|
return mountMsDoc(container, rendered);
|
|
78
135
|
},
|
|
79
136
|
clear() {
|
|
80
|
-
container.
|
|
137
|
+
container.replaceChildren();
|
|
81
138
|
current = null;
|
|
82
139
|
},
|
|
83
140
|
destroy() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@file-viewer/doc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser-native legacy MS-DOC (.doc) parser and HTML renderer extracted from the public msdoc-viewer 0.2.0 line.",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"LICENSE"
|
|
63
63
|
],
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"dompurify": "^3.4.
|
|
65
|
+
"dompurify": "^3.4.14"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"typescript": "^6.0.3"
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"verify:github-34": "pnpm build && node scripts/verify-github-34.mjs",
|
|
76
76
|
"verify:github-87": "pnpm build && node scripts/verify-github-87.mjs",
|
|
77
77
|
"verify:github-171": "pnpm build && node scripts/verify-github-171.mjs",
|
|
78
|
-
"verify:github-192": "pnpm build && node scripts/verify-github-192.mjs"
|
|
78
|
+
"verify:github-192": "pnpm build && node scripts/verify-github-192.mjs",
|
|
79
|
+
"verify:github-236": "pnpm build && node scripts/verify-github-236.mjs"
|
|
79
80
|
}
|
|
80
81
|
}
|