@file-viewer/renderer-word 3.0.2 → 3.1.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
@@ -36,6 +36,15 @@ Use `@file-viewer/preset-all` when you want the same complete matrix as the offi
36
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
+ ## Text Revisions
40
+
41
+ `options.docx.reviewMode` applies to DOC and DOCX: `all` (default) shows actual
42
+ insertions underlined and deletions struck through; `final` shows revised text,
43
+ and `original` shows text before those edits. Changing this option updates the
44
+ current preview without replacing the file. It does not accept/reject revisions
45
+ or rewrite the original bytes. Text revisions are not a claim of complete
46
+ Word review-history, formatting-change, or move-tracking support.
47
+
39
48
  ## Offline Assets
40
49
 
41
50
  DOCX Worker defaults to viewer assets:
package/README.md CHANGED
@@ -36,6 +36,10 @@ const options = {
36
36
  - 安装 RTF capability 后才使用 `rtf.js`;未安装时会显示精确 CLI 启用命令。ODT / ODP 读取 OpenDocument 包内 `content.xml` 做安全结构预览。
37
37
  - 继续复用 core 的统一搜索、缩放、打印、导出、生命周期和操作能力。
38
38
 
39
+ ## 文本修订
40
+
41
+ `options.docx.reviewMode` 同时适用于 DOC 和 DOCX:`all`(默认)用下划线显示实际插入、用删除线显示实际删除;`final` 显示定稿,`original` 显示修订前文字。同一组件修改该参数后会更新预览,不需要重新选择文件,也不会接受、拒绝修订或重写原始文件。文本修订支持不等于完整支持 Word 审阅历史、格式修订或移动记录。
42
+
39
43
  ## 离线资产
40
44
 
41
45
  DOCX Worker 默认读取 viewer assets 下的:
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A paragraph-relative vertical anchor makes that paragraph an absolute-position
3
+ * containing block. Its horizontal axis can still be relative to the column or
4
+ * page margins: paragraph indentation must not be added to that authored offset.
5
+ * Keep the engine's original CSS length and correct only the containing-block
6
+ * origin. Recompute after fitting/resizing; never accumulate pixel corrections.
7
+ */
8
+ export declare function correctDocxMixedAnchorOrigins(section: HTMLElement): void;
@@ -0,0 +1,60 @@
1
+ const authoredLeft = new WeakMap();
2
+ const number = (value) => Number.parseFloat(value) || 0;
3
+ /**
4
+ * A paragraph-relative vertical anchor makes that paragraph an absolute-position
5
+ * containing block. Its horizontal axis can still be relative to the column or
6
+ * page margins: paragraph indentation must not be added to that authored offset.
7
+ * Keep the engine's original CSS length and correct only the containing-block
8
+ * origin. Recompute after fitting/resizing; never accumulate pixel corrections.
9
+ */
10
+ export function correctDocxMixedAnchorOrigins(section) {
11
+ var _a;
12
+ const view = section.ownerDocument.defaultView;
13
+ if (!view)
14
+ return;
15
+ const pageRect = section.getBoundingClientRect();
16
+ const pageStyle = view.getComputedStyle(section);
17
+ const pageWidth = number(pageStyle.width) + (pageStyle.boxSizing === 'border-box' ? 0 :
18
+ number(pageStyle.paddingLeft) + number(pageStyle.paddingRight) + number(pageStyle.borderLeftWidth) + number(pageStyle.borderRightWidth));
19
+ const scale = pageWidth ? pageRect.width / pageWidth : 0;
20
+ if (!Number.isFinite(scale) || scale <= 0)
21
+ return;
22
+ for (const anchor of section.querySelectorAll('[data-docx-anchor-horizontal="column"], [data-docx-anchor-horizontal="margin"]')) {
23
+ const paragraph = anchor.closest('[data-docx-anchor-context="paragraph"]');
24
+ if (!paragraph || anchor.offsetParent !== paragraph || view.getComputedStyle(anchor).position !== 'absolute' || !anchor.style.left)
25
+ continue;
26
+ // Cell-relative drawings are left to the engine's layoutInCell policy.
27
+ if (paragraph.closest('td, th'))
28
+ continue;
29
+ const root = anchor.dataset.docxAnchorHorizontal === 'margin'
30
+ ? section : paragraph.closest('article, header, footer');
31
+ if (!root || !section.contains(root) && root !== section)
32
+ continue;
33
+ const rootStyle = view.getComputedStyle(root);
34
+ const rootRect = root.getBoundingClientRect();
35
+ let origin = rootRect.left + (number(rootStyle.borderLeftWidth) + number(rootStyle.paddingLeft)) * scale;
36
+ const paragraphStyle = view.getComputedStyle(paragraph);
37
+ const paragraphRect = paragraph.getBoundingClientRect();
38
+ if (root !== section) {
39
+ const count = number(rootStyle.columnCount);
40
+ const width = number(rootStyle.columnWidth);
41
+ if (count > 1 || width > 0) {
42
+ const gap = rootStyle.columnGap === 'normal' ? number(rootStyle.fontSize) : number(rootStyle.columnGap);
43
+ const contentWidth = root.clientWidth - number(rootStyle.paddingLeft) - number(rootStyle.paddingRight);
44
+ const columns = count > 0 ? count : Math.max(1, Math.floor((contentWidth + gap) / (width + gap)));
45
+ const stride = (contentWidth + gap) / columns;
46
+ if (stride > 0) {
47
+ const unindentedLeft = (paragraphRect.left - origin) / scale - number(paragraphStyle.marginLeft);
48
+ origin += Math.round(unindentedLeft / stride) * stride * scale;
49
+ }
50
+ }
51
+ }
52
+ const containingOrigin = paragraphRect.left + number(paragraphStyle.borderLeftWidth) * scale;
53
+ const correction = (origin - containingOrigin) / scale;
54
+ if (!Number.isFinite(correction))
55
+ continue;
56
+ const left = (_a = authoredLeft.get(anchor)) !== null && _a !== void 0 ? _a : anchor.style.left;
57
+ authoredLeft.set(anchor, left);
58
+ anchor.style.left = Math.abs(correction) < 0.01 ? left : `calc(${left} + ${correction.toFixed(4)}px)`;
59
+ }
60
+ }
package/dist/index.d.ts CHANGED
@@ -8,9 +8,10 @@ export declare const renderFileViewerWordDocx: FileRenderHandler<FileViewerRende
8
8
  /**
9
9
  * A surprising number of legacy systems keep the `.doc` suffix after saving
10
10
  * an OOXML document. Route those ZIP-based files through the DOCX engine while
11
- * leaving genuine OLE/CFB `.doc` files on the binary parser.
11
+ * leaving genuine OLE/CFB `.doc` files on the binary parser. Word 2003 XML
12
+ * has its own single-file container and is adapted lazily to the OOXML renderer.
12
13
  */
13
- export declare const resolveFileViewerWordContainer: (buffer: ArrayBuffer) => "openxml" | "binary";
14
+ export declare const resolveFileViewerWordContainer: (buffer: ArrayBuffer) => "openxml" | "wordml" | "binary";
14
15
  export declare const renderFileViewerWordDoc: FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>;
15
16
  export declare const renderFileViewerOpenDocument: FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>;
16
17
  export declare const wordRenderer: FileViewerRendererPlugin<FileRenderHandler<FileViewerRenderedInstance, HTMLDivElement>>;
package/dist/index.js CHANGED
@@ -15,18 +15,33 @@ export const renderFileViewerWordDocx = (buffer, target, _type, context) => impo
15
15
  /**
16
16
  * A surprising number of legacy systems keep the `.doc` suffix after saving
17
17
  * an OOXML document. Route those ZIP-based files through the DOCX engine while
18
- * leaving genuine OLE/CFB `.doc` files on the binary parser.
18
+ * leaving genuine OLE/CFB `.doc` files on the binary parser. Word 2003 XML
19
+ * has its own single-file container and is adapted lazily to the OOXML renderer.
19
20
  */
20
21
  export const resolveFileViewerWordContainer = (buffer) => {
21
22
  if (buffer.byteLength < 2) {
22
23
  return 'binary';
23
24
  }
24
25
  const bytes = new Uint8Array(buffer, 0, 2);
25
- return bytes[0] === 0x50 && bytes[1] === 0x4b ? 'openxml' : 'binary';
26
+ if (bytes[0] === 0x50 && bytes[1] === 0x4b)
27
+ return 'openxml';
28
+ const prefix = new Uint8Array(buffer, 0, Math.min(buffer.byteLength, 8192));
29
+ const utf16le = (bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes[0] === 0x3c && bytes[1] === 0);
30
+ const utf16be = (bytes[0] === 0xfe && bytes[1] === 0xff) || (bytes[0] === 0 && bytes[1] === 0x3c);
31
+ const text = new TextDecoder(utf16le ? 'utf-16le' : utf16be ? 'utf-16be' : 'utf-8').decode(prefix);
32
+ return /<(?:[\w.-]+:)?wordDocument(?:\s|>)/.test(text) &&
33
+ text.includes('http://schemas.microsoft.com/office/word/2003/wordml') ? 'wordml' : 'binary';
34
+ };
35
+ export const renderFileViewerWordDoc = (buffer, target, _type, context) => {
36
+ const container = resolveFileViewerWordContainer(buffer);
37
+ if (container === 'wordml') {
38
+ return Promise.all([import('./wordMl.js'), import('./wordDocx.js')])
39
+ .then(async ([{ convertWordMlToDocx }, { default: renderWordDocx }]) => renderWordDocx(await convertWordMlToDocx(buffer, target), target, context));
40
+ }
41
+ return container === 'openxml'
42
+ ? import('./wordDocx.js').then(({ default: renderWordDocx }) => renderWordDocx(buffer, target, context))
43
+ : import('./wordDoc.js').then(({ default: renderWordDoc }) => renderWordDoc(buffer, target, context));
26
44
  };
27
- export const renderFileViewerWordDoc = (buffer, target, _type, context) => resolveFileViewerWordContainer(buffer) === 'openxml'
28
- ? import('./wordDocx.js').then(({ default: renderWordDocx }) => renderWordDocx(buffer, target, context))
29
- : import('./wordDoc.js').then(({ default: renderWordDoc }) => renderWordDoc(buffer, target, context));
30
45
  export const renderFileViewerOpenDocument = (buffer, target, type, context) => import('./openDocument.js').then(({ default: renderOpenDocument }) => renderOpenDocument(buffer, target, type, context));
31
46
  export const wordRenderer = {
32
47
  id: 'file-viewer-renderer-word',
package/dist/wordDoc.js CHANGED
@@ -368,12 +368,13 @@ 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, _f, _g, _h;
371
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
372
372
  const rendered = await parseMsDocToHtml(buffer, {
373
373
  renderOptions: {
374
+ reviewMode: (_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.reviewMode) !== null && _c !== void 0 ? _c : 'all',
374
375
  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',
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
+ externalLinkPolicy: (_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.externalLinkPolicy) !== null && _f !== void 0 ? _f : 'block',
377
+ externalResourcePolicy: (_j = (_h = (_g = context === null || context === void 0 ? void 0 : context.options) === null || _g === void 0 ? void 0 : _g.docx) === null || _h === void 0 ? void 0 : _h.externalResourcePolicy) !== null && _j !== void 0 ? _j : 'block'
377
378
  }
378
379
  });
379
380
  const targetWindow = target.ownerDocument.defaultView;
@@ -387,13 +388,13 @@ export default async function render(buffer, target, context) {
387
388
  content.append(sanitizeMsDocHtml(wrapAsWordPages(rendered.html), targetWindow));
388
389
  target.replaceChildren(style, ...Array.from(content.childNodes));
389
390
  const disposeResponsive = makeMsDocResponsive(target);
390
- (_g = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _g === void 0 ? void 0 : _g.call(context, {
391
+ (_k = context === null || context === void 0 ? void 0 : context.registerExportAdapter) === null || _k === void 0 ? void 0 : _k.call(context, {
391
392
  includeDocumentStyles: false,
392
393
  getPrintMaskPages: () => Array.from(target.querySelectorAll('.msdoc-page')),
393
394
  printStyle: buildMsDocPrintStyle,
394
395
  toHtml: () => prepareMsDocCloneForExport(target)
395
396
  });
396
- (_h = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _h === void 0 ? void 0 : _h.call(context, {
397
+ (_l = context === null || context === void 0 ? void 0 : context.registerThumbnailAdapter) === null || _l === void 0 ? void 0 : _l.call(context, {
397
398
  getTarget: () => target.querySelector('.msdoc-page') || target
398
399
  });
399
400
  return {
package/dist/wordDocx.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import JSZip from 'jszip';
2
- import { resolveFileViewerDocxWorkerJsZipUrl, resolveFileViewerDocxWorkerUrl, resolveFileViewerRuntimeAssetBaseUrl, } from '@file-viewer/core/assets';
2
+ import { correctDocxMixedAnchorOrigins } from './docxAnchors.js';
3
+ import { DEFAULT_FILE_VIEWER_DOCX_RUNTIME_VERSION, resolveFileViewerDocxWorkerJsZipUrl, resolveFileViewerDocxWorkerUrl, resolveFileViewerRuntimeAssetBaseUrl, } from '@file-viewer/core/assets';
3
4
  import { applyPrintPageSize, buildPrintPageStyle, createFileViewerTranslator, createFileViewerZoomChangeEmitter as createZoomChangeEmitter, formatCssPixels, getElementPrintPageSize, normalizeFileViewerTheme, replaceFileViewerCanvasWithImages, registerFileViewerZoomProvider, resolveFileViewerFitScale, unregisterFileViewerZoomProvider, waitForFileViewerNextPaint, } from '@file-viewer/core';
4
5
  const DOCX_DEFAULT_PAGE_SIZE = {
5
6
  width: 794,
@@ -9,7 +10,6 @@ const DOCX_WORKER_UNSAFE_PROTOCOLS = new Set(['file:', 'about:', 'data:']);
9
10
  const DOCX_MIN_SCALE = 0.24;
10
11
  const DOCX_MAX_SCALE = 3;
11
12
  const DOCX_ZOOM_STEP = 0.15;
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';
@@ -259,9 +259,9 @@ const appendDocxVendorAssetVersion = (url, explicitUrl) => {
259
259
  return url;
260
260
  }
261
261
  if (/[?&]file-viewer-docx=[^&#]*/.test(url)) {
262
- return url.replace(/([?&])file-viewer-docx=[^&#]*/, `$1file-viewer-docx=${DOCX_VENDOR_ASSET_VERSION}`);
262
+ return url.replace(/([?&])file-viewer-docx=[^&#]*/, `$1file-viewer-docx=${DEFAULT_FILE_VIEWER_DOCX_RUNTIME_VERSION}`);
263
263
  }
264
- return `${url}${url.includes('?') ? '&' : '?'}file-viewer-docx=${DOCX_VENDOR_ASSET_VERSION}`;
264
+ return `${url}${url.includes('?') ? '&' : '?'}file-viewer-docx=${DEFAULT_FILE_VIEWER_DOCX_RUNTIME_VERSION}`;
265
265
  };
266
266
  export const applyDocxExternalLinkPolicy = (target, policy) => {
267
267
  if (policy === 'allow') {
@@ -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, _d;
286
+ var _a, _b, _c, _d, _e;
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);
@@ -298,8 +298,12 @@ export const createDocxOptions = (target, context, notifyProgressiveRender) => {
298
298
  const externalResourcePolicy = (_c = docxOptions === null || docxOptions === void 0 ? void 0 : docxOptions.externalResourcePolicy) !== null && _c !== void 0 ? _c : 'block';
299
299
  const options = {
300
300
  useWorker,
301
- breakPages: usePagedLayout,
302
- ignoreLastRenderedPageBreak: (_d = docxOptions === null || docxOptions === void 0 ? void 0 : docxOptions.ignoreLastRenderedPageBreak) !== null && _d !== void 0 ? _d : !usePagedLayout,
301
+ reviewMode: (_d = docxOptions === null || docxOptions === void 0 ? void 0 : docxOptions.reviewMode) !== null && _d !== void 0 ? _d : 'all',
302
+ // Authored page breaks define separate anchor coordinate spaces even in
303
+ // flow mode. Only measured/fixed-height pagination remains opt-in.
304
+ breakPages: true,
305
+ fixedPageHeight: usePagedLayout,
306
+ ignoreLastRenderedPageBreak: (_e = docxOptions === null || docxOptions === void 0 ? void 0 : docxOptions.ignoreLastRenderedPageBreak) !== null && _e !== void 0 ? _e : !usePagedLayout,
303
307
  externalLinkPolicy,
304
308
  externalResourcePolicy,
305
309
  darkMode,
@@ -355,6 +359,8 @@ const isTargetHTMLElement = (value, target) => {
355
359
  return HTMLElementCtor ? value instanceof HTMLElementCtor : value instanceof HTMLElement;
356
360
  };
357
361
  const DOCX_RESPONSIVE_CSS = `
362
+ /* This component has no review balloon rail, so keep all-markup deletions readable inline. */
363
+ .docx-fit-viewer [data-docx-review-enabled="true"][data-docx-review-mode="all"] del[data-docx-change-kind]{display:inline!important;width:auto!important;max-width:none!important;height:auto!important;overflow:visible!important;line-height:inherit!important;color:var(--docx-review-color,#c2410c);text-decoration:line-through;text-decoration-color:var(--docx-review-color,#c2410c)}
358
364
  .docx-fit-viewer {
359
365
  box-sizing: border-box;
360
366
  height: 100%;
@@ -477,6 +483,7 @@ function makeDocxResponsive(target, context) {
477
483
  return;
478
484
  }
479
485
  page.style.transform = 'translateX(-50%)';
486
+ correctDocxMixedAnchorOrigins(page);
480
487
  const pageWidth = page.offsetWidth;
481
488
  const contentHeight = pagedLayout
482
489
  ? page.offsetHeight
@@ -0,0 +1,8 @@
1
+ export declare function decodeWordMlBytes(buffer: ArrayBuffer): string;
2
+ /**
3
+ * Adapt the single-file Word 2003 XML container to the existing OOXML renderer.
4
+ * This is structural conversion, not HTML insertion or remote conversion. Body,
5
+ * styles, table properties, section geometry and VML remain renderer-owned.
6
+ * Inline headers/footers become package parts; only embedded image data is read.
7
+ */
8
+ export declare function convertWordMlToDocx(buffer: ArrayBuffer, target: HTMLElement): Promise<ArrayBuffer>;
package/dist/wordMl.js ADDED
@@ -0,0 +1,211 @@
1
+ import JSZip from 'jszip';
2
+ const WORDML = 'http://schemas.microsoft.com/office/word/2003/wordml';
3
+ const WORD = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
4
+ const REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
5
+ const PACKAGE_REL = 'http://schemas.openxmlformats.org/package/2006/relationships';
6
+ const CONTENT_TYPES = 'http://schemas.openxmlformats.org/package/2006/content-types';
7
+ const AUX = 'http://schemas.microsoft.com/office/word/2003/auxHint';
8
+ const VML = 'urn:schemas-microsoft-com:vml';
9
+ const XMLNS = 'http://www.w3.org/2000/xmlns/';
10
+ const aliases = {
11
+ 'h-ansi': 'hAnsi', fareast: 'eastAsia', 'b-cs': 'bCs', 'i-cs': 'iCs',
12
+ 'sz-cs': 'szCs', 'lang-cs': 'lang', 'panose-1': 'panose1',
13
+ 'line-rule': 'lineRule', 'line-pitch': 'linePitch', 'char-space': 'charSpace',
14
+ 'hanging-chars': 'hangingChars', 'first-line': 'firstLine',
15
+ 'first-line-chars': 'firstLineChars', 'left-chars': 'leftChars', 'right-chars': 'rightChars',
16
+ };
17
+ export function decodeWordMlBytes(buffer) {
18
+ const bytes = new Uint8Array(buffer);
19
+ const utf16le = (bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes[0] === 0x3c && bytes[1] === 0);
20
+ const utf16be = (bytes[0] === 0xfe && bytes[1] === 0xff) || (bytes[0] === 0 && bytes[1] === 0x3c);
21
+ return new TextDecoder(utf16le ? 'utf-16le' : utf16be ? 'utf-16be' : 'utf-8').decode(bytes);
22
+ }
23
+ /**
24
+ * Adapt the single-file Word 2003 XML container to the existing OOXML renderer.
25
+ * This is structural conversion, not HTML insertion or remote conversion. Body,
26
+ * styles, table properties, section geometry and VML remain renderer-owned.
27
+ * Inline headers/footers become package parts; only embedded image data is read.
28
+ */
29
+ export async function convertWordMlToDocx(buffer, target) {
30
+ var _a, _b, _c, _d;
31
+ const source = decodeWordMlBytes(buffer);
32
+ if (/<!DOCTYPE\b|<!ENTITY\b/i.test(source)) {
33
+ throw new Error('Word 2003 XML must not contain a DTD or entity declarations.');
34
+ }
35
+ const view = target.ownerDocument.defaultView;
36
+ const Parser = (_a = view === null || view === void 0 ? void 0 : view.DOMParser) !== null && _a !== void 0 ? _a : globalThis.DOMParser;
37
+ const Serializer = (_b = view === null || view === void 0 ? void 0 : view.XMLSerializer) !== null && _b !== void 0 ? _b : globalThis.XMLSerializer;
38
+ const input = new Parser().parseFromString(source, 'application/xml');
39
+ if (input.getElementsByTagName('parsererror').length || input.documentElement.localName !== 'wordDocument' || input.documentElement.namespaceURI !== WORDML) {
40
+ throw new Error('Invalid Word 2003 XML document.');
41
+ }
42
+ const body = Array.from(input.documentElement.children).find(element => element.namespaceURI === WORDML && element.localName === 'body');
43
+ if (!body)
44
+ throw new Error('Word 2003 XML document has no body.');
45
+ const zip = new JSZip();
46
+ const serializer = new Serializer();
47
+ const overrides = [];
48
+ const images = new Map();
49
+ // Never resolve a WordML image name as a file/HTTP URL. It must have binData.
50
+ for (const binary of Array.from(input.getElementsByTagNameNS(WORDML, 'binData'))) {
51
+ const name = binary.getAttributeNS(WORDML, 'name') || '';
52
+ if (images.has(name))
53
+ continue;
54
+ const extension = (_c = /\.(png|jpe?g|gif|bmp|tiff?|emf|wmf)$/i.exec(name)) === null || _c === void 0 ? void 0 : _c[1].toLowerCase();
55
+ const bytes = ((_d = binary.textContent) === null || _d === void 0 ? void 0 : _d.replace(/\s/g, '')) || '';
56
+ if (name && extension && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(bytes) && bytes) {
57
+ const path = `media/image${images.size + 1}.${extension}`;
58
+ images.set(name, { path, bytes });
59
+ zip.file(`word/${path}`, bytes, { base64: true });
60
+ }
61
+ }
62
+ const createDocument = (name) => {
63
+ const doc = input.implementation.createDocument(WORD, `w:${name}`, null);
64
+ doc.documentElement.setAttributeNS(XMLNS, 'xmlns:r', REL);
65
+ return doc;
66
+ };
67
+ const serialize = (document) => `<?xml version="1.0" encoding="UTF-8"?>${serializer.serializeToString(document)}`;
68
+ const relationships = new Map();
69
+ function relate(part, type, destination) {
70
+ let doc = relationships.get(part);
71
+ if (!doc) {
72
+ doc = input.implementation.createDocument(PACKAGE_REL, 'Relationships', null);
73
+ relationships.set(part, doc);
74
+ }
75
+ const id = `rId${doc.documentElement.children.length + 1}`;
76
+ const item = doc.createElementNS(PACKAGE_REL, 'Relationship');
77
+ item.setAttribute('Id', id);
78
+ item.setAttribute('Type', `${REL}/${type}`);
79
+ item.setAttribute('Target', destination);
80
+ doc.documentElement.appendChild(item);
81
+ return id;
82
+ }
83
+ let headerNumber = 0;
84
+ let footerNumber = 0;
85
+ function appendConverted(node, parent, part, depth = 0) {
86
+ if (depth > 256)
87
+ throw new Error('Word 2003 XML nesting is too deep.');
88
+ const output = parent.ownerDocument;
89
+ if (node.nodeType === 3 || node.nodeType === 4) {
90
+ parent.appendChild(output.createTextNode(node.textContent || ''));
91
+ return;
92
+ }
93
+ if (node.nodeType !== 1)
94
+ return;
95
+ const element = node;
96
+ const isWord = element.namespaceURI === WORDML;
97
+ if (isWord && element.localName === 'binData')
98
+ return;
99
+ if (element.namespaceURI === AUX) {
100
+ // wx:sect and wx:sub-section are grouping hints, not content containers.
101
+ for (const child of Array.from(element.childNodes))
102
+ appendConverted(child, parent, part, depth + 1);
103
+ return;
104
+ }
105
+ if (isWord && (element.localName === 'hdr' || element.localName === 'ftr') && parent.localName === 'sectPr') {
106
+ const header = element.localName === 'hdr';
107
+ const partName = `${header ? 'header' : 'footer'}${header ? ++headerNumber : ++footerNumber}.xml`;
108
+ const document = createDocument(header ? 'hdr' : 'ftr');
109
+ for (const child of Array.from(element.childNodes))
110
+ appendConverted(child, document.documentElement, partName, depth + 1);
111
+ zip.file(`word/${partName}`, serialize(document));
112
+ overrides.push([`/word/${partName}`, `application/vnd.openxmlformats-officedocument.wordprocessingml.${header ? 'header' : 'footer'}+xml`]);
113
+ const reference = output.createElementNS(WORD, `w:${header ? 'header' : 'footer'}Reference`);
114
+ const type = element.getAttributeNS(WORDML, 'type');
115
+ reference.setAttributeNS(WORD, 'w:type', type === 'first' || type === 'even' ? type : 'default');
116
+ reference.setAttributeNS(REL, 'r:id', relate(part, header ? 'header' : 'footer', partName));
117
+ parent.appendChild(reference);
118
+ return;
119
+ }
120
+ const namespace = isWord ? WORD : element.namespaceURI;
121
+ const localName = isWord ? aliases[element.localName] || element.localName : element.localName;
122
+ const name = isWord ? `w:${localName}` : element.nodeName;
123
+ const converted = output.createElementNS(namespace, name);
124
+ for (const attribute of Array.from(element.attributes)) {
125
+ if (attribute.namespaceURI === XMLNS)
126
+ continue;
127
+ // VML image references are replaced with package-local relationships below.
128
+ if (namespace === VML && (localName === 'imagedata' || localName === 'fill') && (attribute.localName === 'src' || attribute.localName === 'href'))
129
+ continue;
130
+ const wordAttribute = attribute.namespaceURI === WORDML;
131
+ const key = wordAttribute ? aliases[attribute.localName] || attribute.localName : attribute.localName;
132
+ let value = attribute.value;
133
+ if (wordAttribute && key === 'hint' && value === 'fareast')
134
+ value = 'eastAsia';
135
+ if (isWord && localName === 'fldChar' && wordAttribute && key === 'fldCharType' && value === 'start')
136
+ value = 'begin';
137
+ converted.setAttributeNS(wordAttribute ? WORD : attribute.namespaceURI, wordAttribute ? `w:${key}` : attribute.name, value);
138
+ }
139
+ if (namespace === VML && (localName === 'imagedata' || localName === 'fill')) {
140
+ const image = images.get(element.getAttribute('src') || '');
141
+ if (image)
142
+ converted.setAttributeNS(REL, 'r:id', relate(part, 'image', image.path));
143
+ }
144
+ parent.appendChild(converted);
145
+ for (const child of Array.from(element.childNodes))
146
+ appendConverted(child, converted, part, depth + 1);
147
+ }
148
+ const document = createDocument('document');
149
+ const outputBody = document.createElementNS(WORD, 'w:body');
150
+ document.documentElement.appendChild(outputBody);
151
+ for (const child of Array.from(body.childNodes))
152
+ appendConverted(child, outputBody, 'document.xml');
153
+ // In OOXML only the final section properties may be a direct body child.
154
+ // Earlier WordML wx:sect properties belong on the preceding paragraph.
155
+ for (const section of Array.from(outputBody.children)) {
156
+ if (section.localName !== 'sectPr' || section === outputBody.lastElementChild)
157
+ continue;
158
+ let paragraph = section.previousElementSibling;
159
+ if (!paragraph || paragraph.localName !== 'p') {
160
+ paragraph = document.createElementNS(WORD, 'w:p');
161
+ outputBody.insertBefore(paragraph, section);
162
+ }
163
+ let properties = Array.from(paragraph.children).find(child => child.localName === 'pPr');
164
+ if (!properties) {
165
+ properties = document.createElementNS(WORD, 'w:pPr');
166
+ paragraph.insertBefore(properties, paragraph.firstChild);
167
+ }
168
+ properties.appendChild(section);
169
+ }
170
+ zip.file('word/document.xml', serialize(document));
171
+ overrides.push(['/word/document.xml', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml']);
172
+ for (const [sourceName, rootName, path, type] of [
173
+ ['styles', 'styles', 'styles.xml', 'styles'],
174
+ ['fonts', 'fonts', 'fontTable.xml', 'fontTable'],
175
+ ['docPr', 'settings', 'settings.xml', 'settings'],
176
+ ]) {
177
+ const element = Array.from(input.documentElement.children).find(child => child.namespaceURI === WORDML && child.localName === sourceName);
178
+ if (!element)
179
+ continue;
180
+ const part = createDocument(rootName);
181
+ for (const child of Array.from(element.childNodes))
182
+ appendConverted(child, part.documentElement, path);
183
+ zip.file(`word/${path}`, serialize(part));
184
+ relate('document.xml', type, path);
185
+ overrides.push([`/word/${path}`, `application/vnd.openxmlformats-officedocument.wordprocessingml.${type}+xml`]);
186
+ }
187
+ for (const [part, relations] of relationships)
188
+ zip.file(`word/_rels/${part}.rels`, serialize(relations));
189
+ const rootRelations = input.implementation.createDocument(PACKAGE_REL, 'Relationships', null);
190
+ const office = rootRelations.createElementNS(PACKAGE_REL, 'Relationship');
191
+ for (const [name, value] of [['Id', 'rId1'], ['Type', `${REL}/officeDocument`], ['Target', 'word/document.xml']])
192
+ office.setAttribute(name, value);
193
+ rootRelations.documentElement.appendChild(office);
194
+ zip.file('_rels/.rels', serialize(rootRelations));
195
+ const types = input.implementation.createDocument(CONTENT_TYPES, 'Types', null);
196
+ const imageTypes = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', bmp: 'image/bmp', tif: 'image/tiff', tiff: 'image/tiff', emf: 'image/x-emf', wmf: 'image/x-wmf' };
197
+ for (const [extension, mime] of [['rels', 'application/vnd.openxmlformats-package.relationships+xml'], ['xml', 'application/xml'], ...Object.entries(imageTypes)]) {
198
+ const entry = types.createElementNS(CONTENT_TYPES, 'Default');
199
+ entry.setAttribute('Extension', extension);
200
+ entry.setAttribute('ContentType', mime);
201
+ types.documentElement.appendChild(entry);
202
+ }
203
+ for (const [path, mime] of overrides) {
204
+ const entry = types.createElementNS(CONTENT_TYPES, 'Override');
205
+ entry.setAttribute('PartName', path);
206
+ entry.setAttribute('ContentType', mime);
207
+ types.documentElement.appendChild(entry);
208
+ }
209
+ zip.file('[Content_Types].xml', serialize(types));
210
+ return zip.generateAsync({ type: 'arraybuffer' });
211
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-word",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone DOCX, DOC, and OpenDocument renderer for File Viewer; RTF parsing is an explicit opt-in capability.",
@@ -58,10 +58,10 @@
58
58
  "LICENSE"
59
59
  ],
60
60
  "dependencies": {
61
- "@file-viewer/core": "3.0.2",
62
- "@file-viewer/doc": "3.0.2",
63
- "@file-viewer/docx": "0.3.29",
64
- "jszip": "^3.10.1"
61
+ "@file-viewer/core": "3.1.0",
62
+ "@file-viewer/doc": "3.1.0",
63
+ "@file-viewer/docx": "0.3.32",
64
+ "jszip": "3.10.2"
65
65
  },
66
66
  "devDependencies": {
67
67
  "rtf.js": "^3.0.9",