@file-viewer/renderer-word 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
@@ -1,6 +1,6 @@
1
1
  # @file-viewer/renderer-word
2
2
 
3
- Standalone Word and compatible document renderer package for Flyfish File Viewer. It owns the DOCX/DOCM/DOTX/DOTM, legacy DOC/DOT, RTF, ODT, and ODP preview path so `@file-viewer/core` no longer installs Word parsing engines directly.
3
+ Word and OpenDocument renderer package for Flyfish File Viewer. Standard/full includes DOCX/DOCM/DOTX/DOTM, legacy DOC/DOT, ODT, and ODP. RTF is enabled explicitly by `@file-viewer/capability-rtf`.
4
4
 
5
5
  ## Usage
6
6
 
@@ -33,7 +33,7 @@ Use `@file-viewer/preset-all` when you want the same complete matrix as the offi
33
33
 
34
34
  - DOCX / DOCM / DOTX / DOTM use the self-maintained `@file-viewer/docx` engine with Worker parsing, continuous reading layout, cached TOC fields, async batched rendering, and a dark document surface that follows the viewer theme.
35
35
  - DOC / DOT use `@file-viewer/doc` with a Word-like paper surface, zoom, print, and HTML export adapters.
36
- - RTF uses `rtf.js`; ODT / ODP read `content.xml` from OpenDocument packages for safe structure previews.
36
+ - With the RTF capability installed, RTF uses `rtf.js`; without it the viewer shows the exact CLI enablement command. ODT / ODP read `content.xml` from OpenDocument packages for safe structure previews.
37
37
  - The renderer reuses core search, zoom, print, export, lifecycle, and operation APIs.
38
38
 
39
39
  ## Offline Assets
@@ -56,6 +56,10 @@ const options = {
56
56
 
57
57
  DOCX dark rendering follows `options.theme` by default: `dark` enables it, `light` disables it, and `system` follows the browser color scheme. Pass `options.docx.darkMode: true / false` when the host app needs a fixed result.
58
58
 
59
+ DOC, DOCX, and RTF external links and HTTP(S) image relationships are blocked by default. Enable them explicitly with `options.docx.externalLinkPolicy: 'allow'` and `options.docx.externalResourcePolicy: 'allow'`; links are still limited to HTTP(S), `mailto:`, `tel:`, and safe relative addresses. Unknown and protocol-relative schemes are always rejected. Embedded images, internal bookmarks, and local `data:`/`blob:` image resources remain available.
60
+
61
+ The standard renderer sanitizes the DOM produced by `rtf.js` before mounting it. Custom RTF integrations that consume HTML directly should first call `sanitizeFileViewerRtfHtml(document, markup, options)`; it uses the same link policy and DOMPurify boundary as the standard mount path. A strict Trusted Types CSP should allow the `file-viewer-document-sanitizer` policy name.
62
+
59
63
  ## Migration
60
64
 
61
65
  `@file-viewer/core` no longer depends on `@file-viewer/docx`, `@file-viewer/doc`, `rtf.js`, `linkedom`, or `@xmldom/xmldom` directly. Install and pass this renderer for full Word preview, or use `@file-viewer/preset-all`.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @file-viewer/renderer-word
2
2
 
3
- Flyfish File Viewer 的独立 Word / 兼容文档 renderer 包。它承接 DOCX/DOCM/DOTX/DOTM、旧版 DOC/DOT、RTF、ODT/ODP 等文档预览链路,让 `@file-viewer/core` 不再直接安装 Word 解析与渲染依赖。
3
+ Flyfish File Viewer Word / OpenDocument renderer 包。standard/full 默认承接 DOCX/DOCM/DOTX/DOTM、旧版 DOC/DOTODT/ODP;RTF `@file-viewer/capability-rtf` 显式启用。
4
4
 
5
5
  ## 用法
6
6
 
@@ -33,7 +33,7 @@ const options = {
33
33
 
34
34
  - DOCX / DOCM / DOTX / DOTM 使用自研 `@file-viewer/docx`,默认 Worker 解析、连续流式阅读、目录字段缓存、异步分批渲染,并跟随 viewer 主题启用暗黑文档面。
35
35
  - DOC / DOT 使用 `@file-viewer/doc`,并套用 Word 风格纸张阅读面、缩放、打印和 HTML 导出适配。
36
- - RTF 使用 `rtf.js`,ODT / ODP 读取 OpenDocument 包内 `content.xml` 做安全结构预览。
36
+ - 安装 RTF capability 后才使用 `rtf.js`;未安装时会显示精确 CLI 启用命令。ODT / ODP 读取 OpenDocument 包内 `content.xml` 做安全结构预览。
37
37
  - 继续复用 core 的统一搜索、缩放、打印、导出、生命周期和操作能力。
38
38
 
39
39
  ## 离线资产
@@ -56,6 +56,10 @@ const options = {
56
56
 
57
57
  DOCX 暗黑渲染默认由 `options.theme` 决定:`dark` 开启、`light` 关闭、`system` 跟随浏览器系统主题。需要业务固定效果时可传 `options.docx.darkMode: true / false`。
58
58
 
59
+ DOC、DOCX 与 RTF 的外部链接及 HTTP(S) 图片关系默认阻断。只有显式设置 `options.docx.externalLinkPolicy: 'allow'` 和 `options.docx.externalResourcePolicy: 'allow'` 才会启用;链接仍只接受 HTTP(S)、`mailto:`、`tel:` 和安全相对地址,未知协议与协议相对地址始终拒绝。内嵌图片、内部书签以及本地 `data:`/`blob:` 图片资源不受影响。
60
+
61
+ 标准 renderer 会在挂载前净化 `rtf.js` 产生的 DOM。自定义 RTF 集成若直接消费 HTML,应先调用 `sanitizeFileViewerRtfHtml(document, markup, options)`;该边界与标准挂载使用同一链接策略和 DOMPurify 配置。严格 Trusted Types CSP 需允许 `file-viewer-document-sanitizer` 策略名。
62
+
59
63
  ## 迁移说明
60
64
 
61
65
  `@file-viewer/core` 已不再直接依赖 `@file-viewer/docx`、`@file-viewer/doc`、`rtf.js`、`linkedom` 或 `@xmldom/xmldom`。需要 Word 完整预览时,请安装本包并通过 `renderers` 传入,或使用 `@file-viewer/preset-all`。
package/dist/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { type FileRenderHandler, type FileViewerRenderedInstance, type FileViewerRendererPlugin, type RendererDefinition } from '@file-viewer/core';
2
+ export { getFileViewerRtfLoader, registerFileViewerRtfLoader, } from './optionalCapabilities.js';
3
+ export type { FileViewerRtfLoader, FileViewerRtfModule, } from './optionalCapabilities.js';
4
+ export { createFileViewerRtfHyperlinkContainer, isSafeFileViewerRtfHyperlink, sanitizeFileViewerRtfElement, sanitizeFileViewerRtfHtml, sanitizeFileViewerRtfHyperlink, } from './sanitizeRtf.js';
5
+ export type { FileViewerRtfExternalPolicy, FileViewerRtfSanitizeOptions, } from './sanitizeRtf.js';
2
6
  export declare const wordRendererDefinitions: RendererDefinition[];
3
7
  export declare const renderFileViewerWordDocx: FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>;
4
8
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { DEFAULT_RENDERER_DEFINITIONS, } from '@file-viewer/core';
2
+ export { getFileViewerRtfLoader, registerFileViewerRtfLoader, } from './optionalCapabilities.js';
3
+ export { createFileViewerRtfHyperlinkContainer, isSafeFileViewerRtfHyperlink, sanitizeFileViewerRtfElement, sanitizeFileViewerRtfHtml, sanitizeFileViewerRtfHyperlink, } from './sanitizeRtf.js';
2
4
  const wordRendererIds = [
3
5
  'office-word-openxml',
4
6
  'office-word-binary',
@@ -1,5 +1,7 @@
1
1
  import JSZip from 'jszip';
2
2
  import { createFileViewerTranslator, } from '@file-viewer/core';
3
+ import { getFileViewerRtfLoader } from './optionalCapabilities.js';
4
+ import { createFileViewerRtfHyperlinkContainer, sanitizeFileViewerRtfElement, } from './sanitizeRtf.js';
3
5
  const openDocumentStyle = `
4
6
  .odf-viewer{min-height:100%;padding:28px;overflow:auto;background:var(--file-viewer-render-surface-background,#dfe5eb);box-sizing:border-box}
5
7
  .odf-shell{width:min(100%,980px);margin:0 auto}
@@ -12,8 +14,8 @@ const openDocumentStyle = `
12
14
  @media (prefers-color-scheme:dark){[data-viewer-theme='system'] .odf-viewer,[data-viewer-theme='system'] .flyfish-rtf-viewer{color-scheme:dark;background:var(--file-viewer-render-surface-background,#0d1117);color:#e6edf3}[data-viewer-theme='system'] .odf-page,[data-viewer-theme='system'] .flyfish-rtf-paper{border:1px solid rgba(139,148,158,.24);background:#161b22;color:#e6edf3;box-shadow:0 18px 44px rgba(0,0,0,.34)}}
13
15
  @media (max-width:720px){.odf-viewer,.flyfish-rtf-viewer{padding:14px}.odf-page{padding:28px 24px}.flyfish-rtf-paper{padding:36px 28px}}
14
16
  `;
15
- const createStyle = () => {
16
- const style = document.createElement('style');
17
+ const createStyle = (documentRef = document) => {
18
+ const style = documentRef.createElement('style');
17
19
  style.textContent = openDocumentStyle;
18
20
  return style;
19
21
  };
@@ -73,23 +75,35 @@ const renderOdfPages = (pages) => {
73
75
  return root;
74
76
  };
75
77
  const resolveRtfJs = async () => {
76
- const rtfModule = await import('rtf.js/dist/RTFJS.bundle.js');
78
+ const loadRtf = getFileViewerRtfLoader();
79
+ if (!loadRtf) {
80
+ throw new Error('RTF support is opt-in. Run `npx file-viewer-cli add rtf --write`, then `npx file-viewer-cli install --yes`.');
81
+ }
82
+ const rtfModule = await loadRtf();
77
83
  return rtfModule.RTFJS || rtfModule.default || rtfModule;
78
84
  };
79
85
  const renderRtf = async (buffer, target, context) => {
80
- var _a, _b;
86
+ var _a, _b, _c, _d, _e, _f, _g, _h;
81
87
  const RTFJS = await resolveRtfJs();
82
- (_a = RTFJS.loggingEnabled) === null || _a === void 0 ? void 0 : _a.call(RTFJS, false);
83
- const doc = new RTFJS.Document(buffer, {});
88
+ const documentRef = target.ownerDocument || document;
89
+ const externalLinkPolicy = (_c = (_b = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.docx) === null || _b === void 0 ? void 0 : _b.externalLinkPolicy) !== null && _c !== void 0 ? _c : 'block';
90
+ const externalResourcePolicy = (_f = (_e = (_d = context === null || context === void 0 ? void 0 : context.options) === null || _d === void 0 ? void 0 : _d.docx) === null || _e === void 0 ? void 0 : _e.externalResourcePolicy) !== null && _f !== void 0 ? _f : 'block';
91
+ (_g = RTFJS.loggingEnabled) === null || _g === void 0 ? void 0 : _g.call(RTFJS, false);
92
+ const doc = new RTFJS.Document(buffer, {
93
+ onHyperlink: (create, hyperlink) => createFileViewerRtfHyperlinkContainer(documentRef, create, hyperlink, externalLinkPolicy),
94
+ });
84
95
  const elements = await doc.render();
85
- const stage = document.createElement('div');
96
+ const stage = documentRef.createElement('div');
86
97
  stage.className = 'flyfish-rtf-viewer';
87
- const paper = document.createElement('article');
98
+ const paper = documentRef.createElement('article');
88
99
  paper.className = 'flyfish-rtf-paper';
89
- elements.forEach((element) => paper.appendChild(element));
100
+ elements.forEach((element) => paper.append(sanitizeFileViewerRtfElement(documentRef, element, {
101
+ externalLinkPolicy,
102
+ externalResourcePolicy,
103
+ })));
90
104
  stage.appendChild(paper);
91
- target.replaceChildren(createStyle(), stage);
92
- (_b = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _b === void 0 ? void 0 : _b.call(context, { getTarget: () => paper });
105
+ target.replaceChildren(createStyle(documentRef), stage);
106
+ (_h = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _h === void 0 ? void 0 : _h.call(context, { getTarget: () => paper });
93
107
  return {
94
108
  $el: target,
95
109
  unmount() {
@@ -0,0 +1,7 @@
1
+ export type FileViewerRtfModule = {
2
+ RTFJS?: any;
3
+ default?: any;
4
+ };
5
+ export type FileViewerRtfLoader = () => Promise<FileViewerRtfModule>;
6
+ export declare const registerFileViewerRtfLoader: (loader: FileViewerRtfLoader | null) => void;
7
+ export declare const getFileViewerRtfLoader: () => FileViewerRtfLoader | null;
@@ -0,0 +1,5 @@
1
+ let rtfLoader = null;
2
+ export const registerFileViewerRtfLoader = (loader) => {
3
+ rtfLoader = loader;
4
+ };
5
+ export const getFileViewerRtfLoader = () => rtfLoader;
@@ -0,0 +1,25 @@
1
+ export type FileViewerRtfExternalPolicy = 'allow' | 'block';
2
+ export interface FileViewerRtfSanitizeOptions {
3
+ externalLinkPolicy?: FileViewerRtfExternalPolicy;
4
+ externalResourcePolicy?: FileViewerRtfExternalPolicy;
5
+ }
6
+ /**
7
+ * Canonicalizes an RTF field hyperlink through the same policy used by DOC.
8
+ * Word's `HYPERLINK \\l "bookmark"` field syntax is converted to a fragment
9
+ * before external navigation is considered, so bookmarks remain available
10
+ * even when the default external-link policy is `block`.
11
+ */
12
+ export declare const sanitizeFileViewerRtfHyperlink: (value: string, policy?: FileViewerRtfExternalPolicy) => string | null;
13
+ export declare const isSafeFileViewerRtfHyperlink: (value: string, policy?: FileViewerRtfExternalPolicy) => boolean;
14
+ export declare const createFileViewerRtfHyperlinkContainer: (documentRef: Document, create: () => HTMLElement, hyperlink: {
15
+ url: () => string;
16
+ }, policy?: FileViewerRtfExternalPolicy) => {
17
+ element: HTMLSpanElement;
18
+ content: HTMLSpanElement;
19
+ };
20
+ export declare const sanitizeFileViewerRtfElement: (documentRef: Document, element: HTMLElement, options?: FileViewerRtfSanitizeOptions) => DocumentFragment;
21
+ /**
22
+ * Safe serialization boundary for integrations that consume renderer markup
23
+ * directly instead of mounting through the standard File Viewer handler.
24
+ */
25
+ export declare const sanitizeFileViewerRtfHtml: (documentRef: Document, markup: string, options?: FileViewerRtfSanitizeOptions) => string;
@@ -0,0 +1,149 @@
1
+ import { sanitizeFileViewerDocumentMarkup, sanitizeMsDocLinkHref } from '@file-viewer/doc';
2
+ // eslint-disable-next-line no-control-regex -- remove URL controls before scheme validation
3
+ const compactUrl = (value) => value.trim().replace(/[\u0000-\u0020\u007f-\u009f]/g, '');
4
+ const schemeOf = (value) => {
5
+ var _a, _b;
6
+ return (_b = (_a = compactUrl(value)
7
+ .match(/^([a-z][a-z0-9+.-]*):/i)) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.toLowerCase();
8
+ };
9
+ const normalizeRtfBookmarkHref = (value) => {
10
+ const match = String(value !== null && value !== void 0 ? value : '')
11
+ .trim()
12
+ .match(/^\\l\s+(?:"([^"]+)"|([^\s"]+))\s*$/i);
13
+ if (!match)
14
+ return null;
15
+ const bookmark = (match[1] || match[2] || '').trim();
16
+ return /^[A-Za-z_][A-Za-z0-9_]{0,39}$/.test(bookmark) ? `#${bookmark}` : null;
17
+ };
18
+ /**
19
+ * Canonicalizes an RTF field hyperlink through the same policy used by DOC.
20
+ * Word's `HYPERLINK \\l "bookmark"` field syntax is converted to a fragment
21
+ * before external navigation is considered, so bookmarks remain available
22
+ * even when the default external-link policy is `block`.
23
+ */
24
+ export const sanitizeFileViewerRtfHyperlink = (value, policy = 'block') => {
25
+ const bookmarkHref = normalizeRtfBookmarkHref(value);
26
+ return sanitizeMsDocLinkHref(bookmarkHref || value, policy);
27
+ };
28
+ export const isSafeFileViewerRtfHyperlink = (value, policy = 'allow') => sanitizeFileViewerRtfHyperlink(value, policy) !== null;
29
+ const sanitizeResourceUrl = (value, policy = 'block') => {
30
+ const compact = compactUrl(value);
31
+ if (compact.startsWith('#'))
32
+ return compact;
33
+ if (/^data:image\/(?:png|gif|jpe?g|webp);base64,/i.test(compact))
34
+ return compact;
35
+ const scheme = schemeOf(compact);
36
+ if (scheme === 'blob')
37
+ return compact;
38
+ if (policy !== 'allow')
39
+ return null;
40
+ if (scheme === 'http' || scheme === 'https')
41
+ return compact;
42
+ if (!scheme && !compact.startsWith('//') && !compact.includes('\\'))
43
+ return compact;
44
+ return null;
45
+ };
46
+ const decodeCssEscapes = (value) => value
47
+ .replace(/\/\*[\s\S]*?\*\//g, '')
48
+ .replace(/\\([0-9a-f]{1,6})\s?/gi, (_match, digits) => {
49
+ const codePoint = Number.parseInt(digits, 16);
50
+ return codePoint > 0 && codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : '';
51
+ })
52
+ .replace(/\\([^\r\n\f])/g, '$1');
53
+ const safeStyle = (value) => !/(?:expression\s*\(|(?:url|src|image|image-set|cross-fade)\s*\(|@import|behavior\s*:|-moz-binding)/i.test(decodeCssEscapes(value));
54
+ const isResourceHrefElement = (node) => ['image', 'use'].includes(node.localName.toLowerCase());
55
+ export const createFileViewerRtfHyperlinkContainer = (documentRef, create, hyperlink, policy = 'block') => {
56
+ const href = sanitizeFileViewerRtfHyperlink(hyperlink.url(), policy);
57
+ if (!href) {
58
+ const span = documentRef.createElement('span');
59
+ span.dataset.fileViewerBlockedLink = 'true';
60
+ span.setAttribute('aria-disabled', 'true');
61
+ return { element: span, content: span };
62
+ }
63
+ const anchor = create();
64
+ anchor.setAttribute('href', href);
65
+ if ((anchor.getAttribute('target') || '').toLowerCase() === '_blank')
66
+ anchor.setAttribute('rel', 'noopener noreferrer');
67
+ return { element: anchor, content: anchor };
68
+ };
69
+ const fallbackFragment = (documentRef, text) => {
70
+ const fragment = documentRef.createDocumentFragment();
71
+ fragment.append(documentRef.createTextNode(text));
72
+ return fragment;
73
+ };
74
+ const sanitizeFileViewerRtfMarkup = (documentRef, markup, fallbackText, options = {}) => {
75
+ const fallback = () => {
76
+ return fallbackFragment(documentRef, fallbackText);
77
+ };
78
+ const windowRef = documentRef.defaultView;
79
+ if (!windowRef)
80
+ return fallback();
81
+ const fragment = sanitizeFileViewerDocumentMarkup(markup, windowRef, {
82
+ RETURN_DOM_FRAGMENT: true,
83
+ USE_PROFILES: { html: true, svg: true, svgFilters: true },
84
+ ADD_ATTR: ['target'],
85
+ FORBID_TAGS: [
86
+ 'base',
87
+ 'embed',
88
+ 'foreignObject',
89
+ 'form',
90
+ 'iframe',
91
+ 'object',
92
+ 'script',
93
+ 'style',
94
+ 'template'
95
+ ],
96
+ FORBID_ATTR: ['action', 'formaction', 'srcdoc']
97
+ });
98
+ if ((fragment === null || fragment === void 0 ? void 0 : fragment.nodeType) !== 11)
99
+ return fallback();
100
+ fragment.querySelectorAll('*').forEach((node) => {
101
+ var _a, _b, _c;
102
+ for (const attribute of Array.from(node.attributes)) {
103
+ if (/^on/i.test(attribute.name))
104
+ node.removeAttribute(attribute.name);
105
+ else if (attribute.name.toLowerCase() === 'style' && !safeStyle(attribute.value))
106
+ node.removeAttribute(attribute.name);
107
+ else if (/^(?:href|xlink:href)$/i.test(attribute.name)) {
108
+ const href = isResourceHrefElement(node)
109
+ ? sanitizeResourceUrl(attribute.value, (_a = options.externalResourcePolicy) !== null && _a !== void 0 ? _a : 'block')
110
+ : sanitizeFileViewerRtfHyperlink(attribute.value, (_b = options.externalLinkPolicy) !== null && _b !== void 0 ? _b : 'block');
111
+ if (href)
112
+ node.setAttribute(attribute.name, href);
113
+ else
114
+ node.removeAttribute(attribute.name);
115
+ }
116
+ else if (/^src$/i.test(attribute.name)) {
117
+ const source = sanitizeResourceUrl(attribute.value, (_c = options.externalResourcePolicy) !== null && _c !== void 0 ? _c : 'block');
118
+ if (source)
119
+ node.setAttribute(attribute.name, source);
120
+ else
121
+ node.removeAttribute(attribute.name);
122
+ }
123
+ else if (/^(?:background|poster|srcset)$/i.test(attribute.name)) {
124
+ node.removeAttribute(attribute.name);
125
+ }
126
+ else if (/^ping$/i.test(attribute.name)) {
127
+ node.removeAttribute(attribute.name);
128
+ }
129
+ }
130
+ if (node instanceof windowRef.HTMLAnchorElement) {
131
+ if ((node.getAttribute('href') || '').startsWith('#'))
132
+ node.removeAttribute('target');
133
+ else if ((node.getAttribute('target') || '').toLowerCase() === '_blank') {
134
+ node.setAttribute('rel', 'noopener noreferrer');
135
+ }
136
+ }
137
+ });
138
+ return fragment;
139
+ };
140
+ export const sanitizeFileViewerRtfElement = (documentRef, element, options = {}) => sanitizeFileViewerRtfMarkup(documentRef, element.outerHTML, element.textContent || '', options);
141
+ /**
142
+ * Safe serialization boundary for integrations that consume renderer markup
143
+ * directly instead of mounting through the standard File Viewer handler.
144
+ */
145
+ export const sanitizeFileViewerRtfHtml = (documentRef, markup, options = {}) => {
146
+ const container = documentRef.createElement('div');
147
+ container.append(sanitizeFileViewerRtfMarkup(documentRef, markup, markup, options));
148
+ return container.innerHTML;
149
+ };
package/dist/wordDoc.js CHANGED
@@ -368,11 +368,12 @@ function makeMsDocResponsive(target) {
368
368
  * 渲染 doc 文件
369
369
  */
370
370
  export default async function render(buffer, target, context) {
371
- var _a, _b, _c, _d, _e;
371
+ var _a, _b, _c, _d, _e, _f, _g, _h;
372
372
  const rendered = await parseMsDocToHtml(buffer, {
373
373
  renderOptions: {
374
374
  css: `${defaultMsDocCss()}\n${WORD_PAGE_CSS}`,
375
- externalLinkPolicy: (_c = (_b = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.docx) === null || _b === void 0 ? void 0 : _b.externalLinkPolicy) !== null && _c !== void 0 ? _c : 'block'
375
+ externalLinkPolicy: (_c = (_b = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.docx) === null || _b === void 0 ? void 0 : _b.externalLinkPolicy) !== null && _c !== void 0 ? _c : 'block',
376
+ externalResourcePolicy: (_f = (_e = (_d = context === null || context === void 0 ? void 0 : context.options) === null || _d === void 0 ? void 0 : _d.docx) === null || _e === void 0 ? void 0 : _e.externalResourcePolicy) !== null && _f !== void 0 ? _f : 'block'
376
377
  }
377
378
  });
378
379
  const targetWindow = target.ownerDocument.defaultView;
@@ -386,13 +387,13 @@ export default async function render(buffer, target, context) {
386
387
  content.append(sanitizeMsDocHtml(wrapAsWordPages(rendered.html), targetWindow));
387
388
  target.replaceChildren(style, ...Array.from(content.childNodes));
388
389
  const disposeResponsive = makeMsDocResponsive(target);
389
- (_d = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _d === void 0 ? void 0 : _d.call(context, {
390
+ (_g = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _g === void 0 ? void 0 : _g.call(context, {
390
391
  includeDocumentStyles: false,
391
392
  getPrintMaskPages: () => Array.from(target.querySelectorAll('.msdoc-page')),
392
393
  printStyle: buildMsDocPrintStyle,
393
394
  toHtml: () => prepareMsDocCloneForExport(target)
394
395
  });
395
- (_e = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _e === void 0 ? void 0 : _e.call(context, {
396
+ (_h = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _h === void 0 ? void 0 : _h.call(context, {
396
397
  getTarget: () => target.querySelector('.msdoc-page') || target
397
398
  });
398
399
  return {
@@ -402,7 +403,7 @@ export default async function render(buffer, target, context) {
402
403
  (_a = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _a === void 0 ? void 0 : _a.call(context, null);
403
404
  (_b = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _b === void 0 ? void 0 : _b.call(context, null);
404
405
  disposeResponsive();
405
- target.innerHTML = '';
406
+ target.replaceChildren();
406
407
  }
407
408
  };
408
409
  }
@@ -2,8 +2,10 @@ import type { Options, renderAsync } from '@file-viewer/docx';
2
2
  import { type FileRenderContext, type FileViewerDocxOptions, type FileViewerRenderedInstance as AppWrapper } from '@file-viewer/core';
3
3
  type DocxRenderAsync = typeof renderAsync;
4
4
  type DocxExternalLinkPolicy = NonNullable<FileViewerDocxOptions['externalLinkPolicy']>;
5
+ type DocxExternalResourcePolicy = NonNullable<FileViewerDocxOptions['externalResourcePolicy']>;
5
6
  type DocxRenderOptions = Partial<Options> & {
6
7
  externalLinkPolicy: DocxExternalLinkPolicy;
8
+ externalResourcePolicy: DocxExternalResourcePolicy;
7
9
  };
8
10
  export declare const isMissingDocxHeaderFooterRootError: (error: unknown) => boolean;
9
11
  /**
package/dist/wordDocx.js CHANGED
@@ -9,7 +9,7 @@ const DOCX_WORKER_UNSAFE_PROTOCOLS = new Set(['file:', 'about:', 'data:']);
9
9
  const DOCX_MIN_SCALE = 0.24;
10
10
  const DOCX_MAX_SCALE = 3;
11
11
  const DOCX_ZOOM_STEP = 0.15;
12
- const DOCX_VENDOR_ASSET_VERSION = '0.3.27';
12
+ const DOCX_VENDOR_ASSET_VERSION = '0.3.28';
13
13
  const ZIP_SIGNATURE_PK = 0x504b;
14
14
  const WORDPROCESSINGML_NAMESPACE = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
15
15
  const OFFICE_RELATIONSHIP_NAMESPACE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
@@ -74,7 +74,7 @@ export const renderDocxWithHeaderFooterFallback = async (render, buffer, target,
74
74
  if (!isMissingDocxHeaderFooterRootError(error)) {
75
75
  throw error;
76
76
  }
77
- target.innerHTML = '';
77
+ target.replaceChildren();
78
78
  await render(buffer, target, undefined, {
79
79
  ...options,
80
80
  renderHeaders: false,
@@ -283,7 +283,7 @@ export const applyDocxExternalLinkPolicy = (target, policy) => {
283
283
  return blocked;
284
284
  };
285
285
  export const createDocxOptions = (target, context, notifyProgressiveRender) => {
286
- var _a, _b, _c;
286
+ var _a, _b, _c, _d;
287
287
  const docxOptions = (_a = context === null || context === void 0 ? void 0 : context.options) === null || _a === void 0 ? void 0 : _a.docx;
288
288
  const documentBaseUrl = resolveFileViewerRuntimeAssetBaseUrl(target.ownerDocument);
289
289
  const useWorker = shouldUseDocxWorker(target, docxOptions);
@@ -295,11 +295,13 @@ export const createDocxOptions = (target, context, notifyProgressiveRender) => {
295
295
  }
296
296
  };
297
297
  const externalLinkPolicy = (_b = docxOptions === null || docxOptions === void 0 ? void 0 : docxOptions.externalLinkPolicy) !== null && _b !== void 0 ? _b : 'block';
298
+ const externalResourcePolicy = (_c = docxOptions === null || docxOptions === void 0 ? void 0 : docxOptions.externalResourcePolicy) !== null && _c !== void 0 ? _c : 'block';
298
299
  const options = {
299
300
  useWorker,
300
301
  breakPages: usePagedLayout,
301
- ignoreLastRenderedPageBreak: (_c = docxOptions === null || docxOptions === void 0 ? void 0 : docxOptions.ignoreLastRenderedPageBreak) !== null && _c !== void 0 ? _c : !usePagedLayout,
302
+ ignoreLastRenderedPageBreak: (_d = docxOptions === null || docxOptions === void 0 ? void 0 : docxOptions.ignoreLastRenderedPageBreak) !== null && _d !== void 0 ? _d : !usePagedLayout,
302
303
  externalLinkPolicy,
304
+ externalResourcePolicy,
303
305
  darkMode,
304
306
  progress: event => {
305
307
  if (event.phase === 'render' || event.phase === 'layout' || event.phase === 'done') {
@@ -700,7 +702,7 @@ function prepareDocxCloneForExport(target) {
700
702
  export default async function (buffer, target, context) {
701
703
  var _a, _b;
702
704
  assertValidDocxPackage(buffer, context);
703
- target.innerHTML = '';
705
+ target.replaceChildren();
704
706
  let hasNotifiedProgressiveRender = false;
705
707
  const notifyProgressiveRender = () => {
706
708
  var _a;
@@ -753,7 +755,7 @@ export default async function (buffer, target, context) {
753
755
  delete target.dataset.docxDarkMode;
754
756
  delete target.dataset.docxHeaderFooterFallback;
755
757
  delete target.dataset.docxPageBackground;
756
- target.innerHTML = '';
758
+ target.replaceChildren();
757
759
  }
758
760
  };
759
761
  }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "../../../ecosystem/capability-manifest.schema.json",
3
+ "schemaVersion": 1,
4
+ "id": "word",
5
+ "packageName": "@file-viewer/renderer-word",
6
+ "rendererIds": ["office-word-openxml", "office-word-binary", "open-document"],
7
+ "formats": ["docx", "docm", "dotx", "dotm", "doc", "dot", "odt", "odp"],
8
+ "assets": {
9
+ "rendererIds": ["office-word-openxml"],
10
+ "packageName": "@file-viewer/assets-standard",
11
+ "bin": "file-viewer-assets-standard",
12
+ "apiExport": "installFileViewerStandardAssets",
13
+ "target": "public/file-viewer",
14
+ "copyGroups": ["office-word-openxml"],
15
+ "copyMode": "profile-pack",
16
+ "receiptFilename": "file-viewer-assets-standard.receipt.json",
17
+ "notice": "DOCX Worker and its offline ZIP runtime."
18
+ },
19
+ "license": { "spdx": "Apache-2.0", "policy": "permissive" },
20
+ "weight": "standard",
21
+ "profiles": ["standard", "office", "all"]
22
+ }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-word",
3
- "version": "2.4.0",
3
+ "version": "3.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Standalone Word and compatible document renderer plugin for File Viewer powered by @file-viewer/docx, @file-viewer/doc, and RTF/ODF parsing.",
6
+ "description": "Standalone DOCX, DOC, and OpenDocument renderer for File Viewer; RTF parsing is an explicit opt-in capability.",
7
7
  "keywords": [
8
8
  "file-viewer",
9
9
  "renderer",
@@ -52,23 +52,29 @@
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
- "@file-viewer/doc": "2.4.0",
62
- "@file-viewer/docx": "^0.3.27",
63
- "jszip": "^3.10.1",
64
- "rtf.js": "^3.0.9"
61
+ "@file-viewer/core": "3.0.0",
62
+ "@file-viewer/doc": "3.0.0",
63
+ "@file-viewer/docx": "0.3.28",
64
+ "jszip": "^3.10.1"
65
65
  },
66
66
  "devDependencies": {
67
+ "rtf.js": "^3.0.9",
68
+ "jsdom": "^27.4.0",
67
69
  "typescript": "^6.0.3"
68
70
  },
71
+ "fileViewer": {
72
+ "capabilityManifest": "./file-viewer.capability.json"
73
+ },
69
74
  "license": "Apache-2.0",
70
75
  "scripts": {
71
- "build": "tsc -b tsconfig.json",
72
- "type-check": "tsc -b tsconfig.json"
76
+ "build": "tsc -b tsconfig.json && node --test test/*.test.mjs",
77
+ "type-check": "tsc -b tsconfig.json",
78
+ "verify:rtf-security": "npm exec --yes --package playwright@1.61.1 -- node scripts/verify-rtf-link-policy.mjs"
73
79
  }
74
80
  }