@file-viewer/doc 2.4.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 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
@@ -109,6 +109,9 @@ document.getElementById('app')!.innerHTML = `
109
109
  `;
110
110
  ```
111
111
 
112
+ 外部链接和关联图片默认阻断。确需访问可信外部资源时,应分别显式设置
113
+ `externalLinkPolicy: 'allow'` 和 `externalResourcePolicy: 'allow'`;内嵌图片及书签不受影响。
114
+
112
115
  ### 2)使用内置 Viewer
113
116
 
114
117
  ```ts
@@ -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(/^-+|-+$/g, '') || 'item';
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';
@@ -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 BLOCK_RE = /<(div|p|h[1-6]|li|pre|blockquote|tr)\b([^>]*)>([\s\S]*?)<\/\1\s*>/gi;
4
- const UNSAFE_CONTENT_RE = /<(script|style|noscript|iframe|object|embed|svg)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
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
- for (const match of safe.matchAll(BLOCK_RE)) {
84
- const content = match[3] || '';
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 (!text)
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
- paragraphs.push({ text, attributes: match[2] || '', content });
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(safe);
93
- return text ? [{ text, attributes: '', content: safe }] : [];
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] || '';
@@ -6,6 +6,7 @@ export interface OfficeArtRecordHeader {
6
6
  recLen: number;
7
7
  size: number;
8
8
  }
9
+ export declare function trimTrailingNullCharacters(value: string): string;
9
10
  export declare function detectImageSegment(bytes: Uint8Array): {
10
11
  mime: string;
11
12
  start: number;
@@ -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).replace(/\0+$/g, '');
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))).replace(/\0+$/g, '')
332
+ ? trimTrailingNullCharacters(new TextDecoder('utf-16le').decode(bytes.subarray(nameOffset, nameOffset + Math.max(0, cbName - 2))))
326
333
  : '';
327
334
  return {
328
335
  ...candidate,
@@ -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.startsWith('\\'))
53
+ if (value.startsWith('//') || value.includes('\\'))
54
54
  return null;
55
- if (/^(?:\/|\.\/|\.\.\/)/.test(value) && !value.startsWith('//') && !value.startsWith('\\\\')) {
55
+ if (/^(?:\/|\.\/|\.\.\/)/.test(value) && !value.startsWith('//')) {
56
56
  return value;
57
57
  }
58
- if (!/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith('\\'))
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
- function sanitizeImageSource(src) {
267
- const value = String(src || '').trim();
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 (/^data:image\//i.test(value))
308
+ if (SAFE_RASTER_IMAGE_DATA_URL.test(value))
271
309
  return value;
272
- if (/^(?:https?:|blob:)/i.test(value))
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 src = sanitizeImageSource(node.asset.sourceUrl) || sanitizeImageSource(node.asset.dataUrl);
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 purifier = createDOMPurify(targetWindow);
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.innerHTML = '';
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": "2.4.0",
3
+ "version": "3.0.0",
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.",