@file-viewer/doc 2.3.0 → 2.3.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/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/render/html.d.ts +8 -0
- package/dist/render/html.js +97 -37
- package/dist/types.d.ts +2 -0
- package/dist/viewer.d.ts +1 -0
- package/dist/viewer.js +20 -1
- package/package.json +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { parseMsDoc } from './msdoc/parser.js';
|
|
2
|
-
export { renderMsDoc, defaultMsDocCss } from './render/html.js';
|
|
3
|
-
export { createMsDocViewer, mountMsDoc, parseMsDocToHtml } from './viewer.js';
|
|
2
|
+
export { renderMsDoc, defaultMsDocCss, sanitizeMsDocLinkHref } from './render/html.js';
|
|
3
|
+
export { createMsDocViewer, mountMsDoc, parseMsDocToHtml, 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
|
-
export { renderMsDoc, defaultMsDocCss } from './render/html.js';
|
|
3
|
-
export { createMsDocViewer, mountMsDoc, parseMsDocToHtml } from './viewer.js';
|
|
2
|
+
export { renderMsDoc, defaultMsDocCss, sanitizeMsDocLinkHref } from './render/html.js';
|
|
3
|
+
export { createMsDocViewer, mountMsDoc, parseMsDocToHtml, sanitizeMsDocHtml } from './viewer.js';
|
|
4
4
|
export { MsDocWorkerClient } from './worker-client.js';
|
package/dist/render/html.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { MsDocParseResult, MsDocRenderOptions, MsDocRenderResult } from '../types.js';
|
|
2
|
+
type ExternalLinkPolicy = NonNullable<MsDocRenderOptions['externalLinkPolicy']>;
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes a document-provided hyperlink before it reaches generated HTML.
|
|
5
|
+
* Internal bookmarks always remain available. External navigation is opt-in
|
|
6
|
+
* and is still restricted to browser-safe schemes and relative paths.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeMsDocLinkHref(href: string | undefined | null, policy?: ExternalLinkPolicy): string | null;
|
|
2
9
|
export declare function defaultMsDocCss(): string;
|
|
3
10
|
/**
|
|
4
11
|
* Converts the parsed AST into HTML and a companion CSS string.
|
|
@@ -6,3 +13,4 @@ export declare function defaultMsDocCss(): string;
|
|
|
6
13
|
* to customize styles or consume the AST directly.
|
|
7
14
|
*/
|
|
8
15
|
export declare function renderMsDoc(parsed: MsDocParseResult, options?: MsDocRenderOptions): MsDocRenderResult;
|
|
16
|
+
export {};
|
package/dist/render/html.js
CHANGED
|
@@ -22,9 +22,51 @@ const COLOR_INDEX_MAP = {
|
|
|
22
22
|
function styleObjectToCss(style) {
|
|
23
23
|
return Object.entries(style)
|
|
24
24
|
.filter(([, value]) => value != null && value !== '')
|
|
25
|
-
.map(([key, value]) => `${key}:${value}`)
|
|
25
|
+
.map(([key, value]) => `${key}:${escapeHtml(value)}`)
|
|
26
26
|
.join(';');
|
|
27
27
|
}
|
|
28
|
+
function quoteCssString(value) {
|
|
29
|
+
const escaped = String(value ?? '')
|
|
30
|
+
.replace(/\\/g, '\\\\')
|
|
31
|
+
.replace(/'/g, "\\'")
|
|
32
|
+
.replace(/[\n\r\f]/g, ' ')
|
|
33
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ');
|
|
34
|
+
return `'${escaped}'`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalizes a document-provided hyperlink before it reaches generated HTML.
|
|
38
|
+
* Internal bookmarks always remain available. External navigation is opt-in
|
|
39
|
+
* and is still restricted to browser-safe schemes and relative paths.
|
|
40
|
+
*/
|
|
41
|
+
export function sanitizeMsDocLinkHref(href, policy = 'block') {
|
|
42
|
+
const value = String(href ?? '')
|
|
43
|
+
.replace(/[\u0000-\u0020\u007f-\u009f]/g, '')
|
|
44
|
+
.trim();
|
|
45
|
+
if (!value)
|
|
46
|
+
return null;
|
|
47
|
+
if (value.startsWith('#'))
|
|
48
|
+
return value;
|
|
49
|
+
if (policy !== 'allow')
|
|
50
|
+
return null;
|
|
51
|
+
if (/^(?:https?:|mailto:|tel:)/i.test(value))
|
|
52
|
+
return value;
|
|
53
|
+
if (value.startsWith('//') || value.startsWith('\\'))
|
|
54
|
+
return null;
|
|
55
|
+
if (/^(?:\/|\.\/|\.\.\/)/.test(value) && !value.startsWith('//') && !value.startsWith('\\\\')) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (!/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith('\\'))
|
|
59
|
+
return value;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function renderLink(content, href) {
|
|
63
|
+
if (!href)
|
|
64
|
+
return content;
|
|
65
|
+
const escapedHref = escapeHtml(href);
|
|
66
|
+
if (href.startsWith('#'))
|
|
67
|
+
return `<a class="msdoc-link" href="${escapedHref}">${content}</a>`;
|
|
68
|
+
return `<a class="msdoc-link" href="${escapedHref}" target="_blank" rel="noreferrer noopener">${content}</a>`;
|
|
69
|
+
}
|
|
28
70
|
function borderToCss(border) {
|
|
29
71
|
if (!border || border.nil)
|
|
30
72
|
return null;
|
|
@@ -167,7 +209,7 @@ function inlineStyleToCss(styleState) {
|
|
|
167
209
|
if (styleState.fontSizeHalfPoints)
|
|
168
210
|
style['font-size'] = `${styleState.fontSizeHalfPoints / 2}pt`;
|
|
169
211
|
if (styleState.fontFamily)
|
|
170
|
-
style['font-family'] =
|
|
212
|
+
style['font-family'] = `${quoteCssString(styleState.fontFamily)},sans-serif`;
|
|
171
213
|
if (styleState.colorIndex && COLOR_INDEX_MAP[styleState.colorIndex])
|
|
172
214
|
style.color = COLOR_INDEX_MAP[styleState.colorIndex];
|
|
173
215
|
const highlightIndex = typeof styleState.highlight === 'number' ? styleState.highlight : styleState.highlight?.index;
|
|
@@ -196,16 +238,13 @@ function inlineStyleToCss(styleState) {
|
|
|
196
238
|
style.direction = 'rtl';
|
|
197
239
|
return style;
|
|
198
240
|
}
|
|
199
|
-
function renderTextNode(node) {
|
|
241
|
+
function renderTextNode(node, context) {
|
|
200
242
|
const content = escapeHtml(node.text);
|
|
201
243
|
const inlineStyle = inlineStyleToCss(node.style);
|
|
202
244
|
inlineStyle['white-space'] = 'break-spaces';
|
|
203
245
|
const style = styleObjectToCss(inlineStyle);
|
|
204
246
|
const inner = `<span${style ? ` style="${style}"` : ''}>${content}</span>`;
|
|
205
|
-
|
|
206
|
-
return `<a class="msdoc-link" href="${escapeHtml(node.href)}" target="_blank" rel="noreferrer noopener">${inner}</a>`;
|
|
207
|
-
}
|
|
208
|
-
return inner;
|
|
247
|
+
return renderLink(inner, sanitizeMsDocLinkHref(node.href, context.externalLinkPolicy));
|
|
209
248
|
}
|
|
210
249
|
function inlineImageDisplaySizePx(asset) {
|
|
211
250
|
const widthPx = twipsToPx(asset.meta?.displayWidthTwips);
|
|
@@ -234,15 +273,31 @@ function sanitizeImageSource(src) {
|
|
|
234
273
|
return value;
|
|
235
274
|
return null;
|
|
236
275
|
}
|
|
237
|
-
function
|
|
238
|
-
const value = String(href
|
|
276
|
+
function sanitizeDownloadHref(href) {
|
|
277
|
+
const value = String(href ?? '').trim();
|
|
239
278
|
if (!value)
|
|
240
279
|
return null;
|
|
241
|
-
|
|
280
|
+
const dataUrl = /^data:([^;,]+);base64,([a-z\d+/=\s]+)$/i.exec(value);
|
|
281
|
+
if (!dataUrl)
|
|
282
|
+
return null;
|
|
283
|
+
const mimeType = dataUrl[1].trim().toLowerCase();
|
|
284
|
+
if (/^(?:text\/(?:html|javascript)|application\/(?:javascript|xhtml\+xml|xml)|image\/svg\+xml)$/.test(mimeType)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
if (mimeType.endsWith('+xml'))
|
|
288
|
+
return null;
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
function sanitizeAssetHref(href, context) {
|
|
292
|
+
const downloadHref = sanitizeDownloadHref(href);
|
|
293
|
+
if (downloadHref)
|
|
294
|
+
return downloadHref;
|
|
295
|
+
const value = sanitizeMsDocLinkHref(href, context.externalLinkPolicy);
|
|
296
|
+
if (value && !value.startsWith('#'))
|
|
242
297
|
return value;
|
|
243
298
|
return null;
|
|
244
299
|
}
|
|
245
|
-
function renderImageNode(node) {
|
|
300
|
+
function renderImageNode(node, context) {
|
|
246
301
|
const src = sanitizeImageSource(node.asset.sourceUrl) || sanitizeImageSource(node.asset.dataUrl);
|
|
247
302
|
const baseStyle = inlineStyleToCss(node.style);
|
|
248
303
|
const displaySize = inlineImageDisplaySizePx(node.asset);
|
|
@@ -257,38 +312,33 @@ function renderImageNode(node) {
|
|
|
257
312
|
baseStyle.height = 'auto';
|
|
258
313
|
}
|
|
259
314
|
if (!src || node.asset.displayable === false) {
|
|
260
|
-
const fallbackHref = sanitizeAssetHref(node.asset.dataUrl) || sanitizeAssetHref(node.asset.sourceUrl);
|
|
315
|
+
const fallbackHref = sanitizeAssetHref(node.asset.dataUrl, context) || sanitizeAssetHref(node.asset.sourceUrl, context);
|
|
261
316
|
const label = escapeHtml(String(node.asset.meta?.linkedPath || node.asset.mime || 'image'));
|
|
262
317
|
const inner = fallbackHref
|
|
263
318
|
? `<a class="msdoc-attachment msdoc-image-fallback" href="${escapeHtml(fallbackHref)}" target="_blank" rel="noreferrer noopener">🖼 ${label}</a>`
|
|
264
319
|
: `<span class="msdoc-image-fallback">🖼 ${label}</span>`;
|
|
265
|
-
|
|
266
|
-
return `<a class="msdoc-link" href="${escapeHtml(node.href)}" target="_blank" rel="noreferrer noopener">${inner}</a>`;
|
|
267
|
-
}
|
|
268
|
-
return inner;
|
|
320
|
+
return renderLink(inner, sanitizeMsDocLinkHref(node.href, context.externalLinkPolicy));
|
|
269
321
|
}
|
|
270
322
|
const img = `<img class="msdoc-image" src="${escapeHtml(src)}" alt="" style="${styleObjectToCss(baseStyle)}">`;
|
|
271
|
-
|
|
272
|
-
return `<a class="msdoc-link" href="${escapeHtml(node.href)}" target="_blank" rel="noreferrer noopener">${img}</a>`;
|
|
273
|
-
}
|
|
274
|
-
return img;
|
|
323
|
+
return renderLink(img, sanitizeMsDocLinkHref(node.href, context.externalLinkPolicy));
|
|
275
324
|
}
|
|
276
|
-
function renderAttachmentNode(node) {
|
|
325
|
+
function renderAttachmentNode(node, context) {
|
|
277
326
|
const label = escapeHtml(node.asset.name || 'embedded-file');
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
327
|
+
const downloadHref = sanitizeDownloadHref(node.asset.dataUrl);
|
|
328
|
+
const inner = downloadHref
|
|
329
|
+
? `<a class="msdoc-attachment" href="${escapeHtml(downloadHref)}" download="${label}">📎 ${label}</a>`
|
|
330
|
+
: `<span class="msdoc-attachment">📎 ${label}</span>`;
|
|
331
|
+
const href = sanitizeMsDocLinkHref(node.href, context.externalLinkPolicy);
|
|
332
|
+
return href ? `<span class="msdoc-inline-group">${inner}${renderLink('↗', href)}</span>` : inner;
|
|
283
333
|
}
|
|
284
|
-
function renderInlineNodes(nodes) {
|
|
334
|
+
function renderInlineNodes(nodes, context) {
|
|
285
335
|
return nodes.map((node) => {
|
|
286
336
|
if (node.type === 'text')
|
|
287
|
-
return renderTextNode(node);
|
|
337
|
+
return renderTextNode(node, context);
|
|
288
338
|
if (node.type === 'image')
|
|
289
|
-
return renderImageNode(node);
|
|
339
|
+
return renderImageNode(node, context);
|
|
290
340
|
if (node.type === 'attachment')
|
|
291
|
-
return renderAttachmentNode(node);
|
|
341
|
+
return renderAttachmentNode(node, context);
|
|
292
342
|
if (node.type === 'lineBreak')
|
|
293
343
|
return '<br>';
|
|
294
344
|
if (node.type === 'pageBreak')
|
|
@@ -296,10 +346,10 @@ function renderInlineNodes(nodes) {
|
|
|
296
346
|
return '';
|
|
297
347
|
}).join('');
|
|
298
348
|
}
|
|
299
|
-
function renderParagraphBlock(block, options = {}) {
|
|
349
|
+
function renderParagraphBlock(block, context, options = {}) {
|
|
300
350
|
const tag = options.inline ? 'div' : 'p';
|
|
301
351
|
const style = styleObjectToCss(paragraphStyleToCss(block.paraState));
|
|
302
|
-
const body = renderInlineNodes(block.inlines || []);
|
|
352
|
+
const body = renderInlineNodes(block.inlines || [], context);
|
|
303
353
|
const empty = body || '<br>';
|
|
304
354
|
const classNames = ['msdoc-paragraph'];
|
|
305
355
|
if (block.styleName)
|
|
@@ -380,7 +430,7 @@ function tableStyle(block) {
|
|
|
380
430
|
style['table-layout'] = 'fixed';
|
|
381
431
|
return style;
|
|
382
432
|
}
|
|
383
|
-
function renderTableBlock(block) {
|
|
433
|
+
function renderTableBlock(block, context) {
|
|
384
434
|
const rows = block.rows.map((row) => {
|
|
385
435
|
const rowHeight = row.state?.rowHeight ? twipsToPx(Math.abs(row.state.rowHeight)) : null;
|
|
386
436
|
const rowStyle = rowHeight ? ` style="height:${rowHeight}px"` : '';
|
|
@@ -393,7 +443,7 @@ function renderTableBlock(block) {
|
|
|
393
443
|
if ((cell.rowspan ?? 1) > 1)
|
|
394
444
|
attrs.push(` rowspan="${cell.rowspan}"`);
|
|
395
445
|
const style = styleObjectToCss(cellStyle(cell));
|
|
396
|
-
const body = cell.paragraphs.map((paragraph) => renderParagraphBlock(paragraph, { inline: true })).join('');
|
|
446
|
+
const body = cell.paragraphs.map((paragraph) => renderParagraphBlock(paragraph, context, { inline: true })).join('');
|
|
397
447
|
return `<td class="msdoc-cell"${attrs.join('')}${style ? ` style="${style}"` : ''}>${body || '<div class="msdoc-paragraph"><br></div>'}</td>`;
|
|
398
448
|
})
|
|
399
449
|
.join('');
|
|
@@ -402,7 +452,14 @@ function renderTableBlock(block) {
|
|
|
402
452
|
return `<table class="msdoc-table msdoc-table-depth-${block.depth}" style="${styleObjectToCss(tableStyle(block))}"><tbody>${rows}</tbody></table>`;
|
|
403
453
|
}
|
|
404
454
|
function renderAttachmentsBlock(block) {
|
|
405
|
-
const items = block.items.map((item) =>
|
|
455
|
+
const items = block.items.map((item) => {
|
|
456
|
+
const label = escapeHtml(item.name || 'embedded-file');
|
|
457
|
+
const downloadHref = sanitizeDownloadHref(item.dataUrl);
|
|
458
|
+
const content = downloadHref
|
|
459
|
+
? `<a class="msdoc-attachment" href="${escapeHtml(downloadHref)}" download="${label}">📎 ${label}</a>`
|
|
460
|
+
: `<span class="msdoc-attachment">📎 ${label}</span>`;
|
|
461
|
+
return `<li>${content}</li>`;
|
|
462
|
+
}).join('');
|
|
406
463
|
return `<section class="msdoc-attachments"><div class="msdoc-attachments-title">Embedded attachments</div><ul>${items}</ul></section>`;
|
|
407
464
|
}
|
|
408
465
|
export function defaultMsDocCss() {
|
|
@@ -430,11 +487,14 @@ export function defaultMsDocCss() {
|
|
|
430
487
|
*/
|
|
431
488
|
export function renderMsDoc(parsed, options = {}) {
|
|
432
489
|
const css = options.css ?? defaultMsDocCss();
|
|
490
|
+
const context = {
|
|
491
|
+
externalLinkPolicy: options.externalLinkPolicy ?? 'block',
|
|
492
|
+
};
|
|
433
493
|
const html = parsed.blocks.map((block) => {
|
|
434
494
|
if (block.type === 'paragraph')
|
|
435
|
-
return renderParagraphBlock(block);
|
|
495
|
+
return renderParagraphBlock(block, context);
|
|
436
496
|
if (block.type === 'table')
|
|
437
|
-
return renderTableBlock(block);
|
|
497
|
+
return renderTableBlock(block, context);
|
|
438
498
|
if (block.type === 'attachments')
|
|
439
499
|
return renderAttachmentsBlock(block);
|
|
440
500
|
return '';
|
package/dist/types.d.ts
CHANGED
|
@@ -617,6 +617,8 @@ export interface MsDocParseOptions {
|
|
|
617
617
|
}
|
|
618
618
|
export interface MsDocRenderOptions {
|
|
619
619
|
css?: string;
|
|
620
|
+
/** External links are blocked by default; internal document bookmarks remain active. */
|
|
621
|
+
externalLinkPolicy?: 'allow' | 'block';
|
|
620
622
|
}
|
|
621
623
|
export interface MsDocMeta {
|
|
622
624
|
fib: {
|
package/dist/viewer.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MsDocParseToHtmlOptions, MsDocRenderResult, MsDocViewer, MsDocViewerConfig, ViewerInput } from './types.js';
|
|
2
|
+
export declare function sanitizeMsDocHtml(html: string, targetWindow: Window): DocumentFragment;
|
|
2
3
|
export declare function mountMsDoc(container: HTMLElement, rendered: MsDocRenderResult): HTMLElement;
|
|
3
4
|
export declare function parseMsDocToHtml(input: ViewerInput, options?: MsDocParseToHtmlOptions): Promise<MsDocRenderResult>;
|
|
4
5
|
/**
|
package/dist/viewer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import createDOMPurify from 'dompurify';
|
|
1
2
|
import { parseMsDoc } from './msdoc/parser.js';
|
|
2
3
|
import { renderMsDoc } from './render/html.js';
|
|
3
4
|
async function normalizeInput(input) {
|
|
@@ -19,10 +20,28 @@ async function normalizeInput(input) {
|
|
|
19
20
|
}
|
|
20
21
|
throw new TypeError('Unsupported input type');
|
|
21
22
|
}
|
|
23
|
+
export function sanitizeMsDocHtml(html, targetWindow) {
|
|
24
|
+
const purifier = createDOMPurify(targetWindow);
|
|
25
|
+
return purifier.sanitize(html, {
|
|
26
|
+
RETURN_DOM_FRAGMENT: true,
|
|
27
|
+
USE_PROFILES: { html: true },
|
|
28
|
+
FORBID_TAGS: ['base', 'embed', 'form', 'iframe', 'object', 'script', 'style', 'template'],
|
|
29
|
+
FORBID_ATTR: ['action', 'formaction', 'srcdoc'],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
22
32
|
export function mountMsDoc(container, rendered) {
|
|
23
33
|
if (!container)
|
|
24
34
|
throw new Error('A container element is required');
|
|
25
|
-
|
|
35
|
+
const targetWindow = container.ownerDocument.defaultView;
|
|
36
|
+
if (!targetWindow)
|
|
37
|
+
throw new Error('The container must belong to a browser document');
|
|
38
|
+
const style = container.ownerDocument.createElement('style');
|
|
39
|
+
style.dataset.msdoc = '';
|
|
40
|
+
style.textContent = rendered.css;
|
|
41
|
+
const root = container.ownerDocument.createElement('div');
|
|
42
|
+
root.className = 'msdoc-root';
|
|
43
|
+
root.append(sanitizeMsDocHtml(rendered.html, targetWindow));
|
|
44
|
+
container.replaceChildren(style, root);
|
|
26
45
|
return container;
|
|
27
46
|
}
|
|
28
47
|
export async function parseMsDocToHtml(input, options = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@file-viewer/doc",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.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.",
|
|
@@ -61,6 +61,9 @@
|
|
|
61
61
|
"README.en.md",
|
|
62
62
|
"LICENSE"
|
|
63
63
|
],
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"dompurify": "^3.4.13"
|
|
66
|
+
},
|
|
64
67
|
"devDependencies": {
|
|
65
68
|
"typescript": "^6.0.3"
|
|
66
69
|
},
|