wordDocx.ts 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219
  1. import type { DocxProgressEvent, Options, renderAsync } from '@file-viewer/docx'
  2. import type JSZip from 'jszip'
  3. import {
  4. resolveFileViewerDocxWorkerJsZipUrl,
  5. resolveFileViewerDocxWorkerUrl,
  6. } from '@file-viewer/core/assets'
  7. import {
  8. applyPrintPageSize,
  9. buildPrintPageStyle,
  10. createFileViewerZoomChangeEmitter as createZoomChangeEmitter,
  11. formatCssPixels,
  12. getElementPrintPageSize,
  13. registerFileViewerZoomProvider,
  14. unregisterFileViewerZoomProvider,
  15. type FileRenderContext,
  16. type FileViewerDocxOptions,
  17. type FileViewerRenderedInstance as AppWrapper,
  18. type FileViewerZoomState,
  19. type PrintPageSize,
  20. } from '@file-viewer/core'
  21. const DOCX_DEFAULT_PAGE_SIZE: PrintPageSize = {
  22. width: 794,
  23. height: 1123
  24. }
  25. const DOCX_PROGRESSIVE_BATCH_SIZE = 2
  26. const DOCX_WORKER_TIMEOUT = 5000
  27. const DOCX_WORKER_UNSAFE_PROTOCOLS = new Set(['file:', 'about:', 'data:'])
  28. const DOCX_MIN_SCALE = 0.24
  29. const DOCX_MAX_SCALE = 3
  30. const DOCX_ZOOM_STEP = 0.15
  31. const DOCX_EMU_PER_CSS_PIXEL = 9525
  32. const ZIP_SIGNATURE_PK = 0x504b
  33. type DocxZip = JSZip
  34. interface DocxRelationship {
  35. id: string;
  36. type: string;
  37. target: string;
  38. targetMode?: string;
  39. }
  40. interface DocxImageFallback {
  41. role: 'watermark' | 'ole-preview' | 'vml-image';
  42. key: string;
  43. dataUrl: string;
  44. sourcePath: string;
  45. partPath: string;
  46. title?: string;
  47. style?: string;
  48. width?: number;
  49. height?: number;
  50. paragraphIndex?: number;
  51. }
  52. interface DocxChartSeries {
  53. name: string;
  54. categories: string[];
  55. values: number[];
  56. }
  57. interface DocxChartFallback {
  58. key: string;
  59. title: string;
  60. type: string;
  61. sourcePath: string;
  62. series: DocxChartSeries[];
  63. width?: number;
  64. height?: number;
  65. paragraphIndex?: number;
  66. }
  67. const loadLibrary = (() => {
  68. const loader = {
  69. module: null as null | Promise<{defaultOptions: Options, renderAsync: typeof renderAsync}>,
  70. async load() {
  71. if (!this.module) {
  72. this.module = import('@file-viewer/docx');
  73. }
  74. return this.module;
  75. }
  76. }
  77. return async () => {
  78. return await loader.load();
  79. }
  80. })()
  81. /**
  82. * DOCX / DOCM / DOTX / DOTM are OOXML packages, so a valid file must start
  83. * with a ZIP signature. This catches common enterprise download failures where
  84. * an object-storage XML error page is saved with a `.docx` extension.
  85. */
  86. const assertValidDocxPackage = (buffer: ArrayBuffer) => {
  87. const signature = buffer.byteLength >= 4 ? new DataView(buffer).getUint16(0, false) : 0
  88. if (signature === ZIP_SIGNATURE_PK) {
  89. return
  90. }
  91. throw new Error('文件不是有效的 DOCX/OOXML 压缩包,可能下载不完整或被服务端错误内容替换,请重新上传或检查文件源。')
  92. }
  93. const getTargetWindow = (target: HTMLDivElement) => {
  94. return target.ownerDocument.defaultView
  95. }
  96. const getTargetProtocol = (target: HTMLDivElement) => {
  97. const candidates = [
  98. target.ownerDocument.URL,
  99. getTargetWindow(target)?.location?.href,
  100. globalThis.location?.href
  101. ].filter(Boolean) as string[]
  102. for (const candidate of candidates) {
  103. try {
  104. return new URL(candidate).protocol
  105. } catch {
  106. // Ignore synthetic document URLs created by tests or embedded hosts.
  107. }
  108. }
  109. return ''
  110. }
  111. const shouldUseDocxWorker = (
  112. target: HTMLDivElement,
  113. docxOptions: FileViewerDocxOptions | undefined
  114. ) => {
  115. if (docxOptions?.worker === false) {
  116. return false
  117. }
  118. if (docxOptions?.worker === true) {
  119. return true
  120. }
  121. const WorkerCtor = getTargetWindow(target)?.Worker ?? globalThis.Worker
  122. if (!WorkerCtor) {
  123. return false
  124. }
  125. return !DOCX_WORKER_UNSAFE_PROTOCOLS.has(getTargetProtocol(target))
  126. }
  127. const createDocxOptions = (
  128. target: HTMLDivElement,
  129. context: FileRenderContext | undefined,
  130. notifyProgressiveRender: () => void
  131. ): Partial<Options> => {
  132. const docxOptions = context?.options?.docx
  133. const documentBaseUrl = target.ownerDocument.URL || undefined
  134. const useWorker = shouldUseDocxWorker(target, docxOptions)
  135. const usePagedLayout = docxOptions?.visualPagination === true
  136. const progress = (event: DocxProgressEvent) => {
  137. if (event.phase === 'render' || event.phase === 'layout' || event.phase === 'done') {
  138. notifyProgressiveRender()
  139. }
  140. }
  141. return {
  142. // Word 会写入 autoSpaceDN/autoSpaceDE 等兼容标签;生产预览保持静默,避免 DOCX 调试告警刷屏。
  143. debug: false,
  144. experimental: false,
  145. useWorker,
  146. workerUrl: useWorker
  147. ? resolveFileViewerDocxWorkerUrl(docxOptions, documentBaseUrl)
  148. : undefined,
  149. workerJsZipUrl: useWorker
  150. ? resolveFileViewerDocxWorkerJsZipUrl(docxOptions, documentBaseUrl)
  151. : undefined,
  152. workerFallback: true,
  153. workerTimeout: docxOptions?.workerTimeout ?? DOCX_WORKER_TIMEOUT,
  154. renderPageBatchSize:
  155. docxOptions?.renderPageBatchSize ??
  156. (docxOptions?.progressive === false ? Number.MAX_SAFE_INTEGER : DOCX_PROGRESSIVE_BATCH_SIZE),
  157. renderYieldEveryMs: docxOptions?.renderYieldEveryMs ?? 16,
  158. strictWordCompatibility: docxOptions?.strictWordCompatibility ?? true,
  159. paginationTolerance: docxOptions?.paginationTolerance ?? 2,
  160. breakPages: usePagedLayout,
  161. maxDynamicPaginationPasses:
  162. usePagedLayout
  163. ? docxOptions?.maxDynamicPaginationPasses ?? 1000
  164. : 0,
  165. awaitLayout: docxOptions?.awaitLayout ?? usePagedLayout,
  166. preserveComplexFieldResults: docxOptions?.preserveComplexFieldResults ?? true,
  167. updatePageReferences: docxOptions?.updatePageReferences ?? false,
  168. hideWebHiddenContent: docxOptions?.hideWebHiddenContent ?? false,
  169. ignoreLastRenderedPageBreak: docxOptions?.ignoreLastRenderedPageBreak ?? !usePagedLayout,
  170. progress
  171. }
  172. }
  173. const isTargetHTMLElement = (value: unknown, target: HTMLDivElement): value is HTMLElement => {
  174. const HTMLElementCtor = getTargetWindow(target)?.HTMLElement
  175. return HTMLElementCtor ? value instanceof HTMLElementCtor : value instanceof HTMLElement
  176. }
  177. const shouldPaginateOversizedDocxSections = (context?: FileRenderContext) => {
  178. return context?.options?.docx?.visualPagination === true
  179. }
  180. const DOCX_RESPONSIVE_CSS = `
  181. .docx-fit-viewer {
  182. box-sizing: border-box;
  183. height: 100%;
  184. overflow: auto;
  185. background: #ececec;
  186. }
  187. .docx-fit-viewer .docx-wrapper {
  188. box-sizing: border-box;
  189. min-width: 0 !important;
  190. width: 100% !important;
  191. padding: 24px 14px 40px !important;
  192. background: #e7e9ec !important;
  193. }
  194. .docx-fit-viewer .docx-page-frame {
  195. position: relative;
  196. width: 100%;
  197. min-width: 0;
  198. margin: 0 auto 24px;
  199. overflow: visible;
  200. }
  201. .docx-fit-viewer .docx-flow-frame {
  202. position: relative;
  203. width: 100%;
  204. min-width: 0;
  205. margin: 0 auto 28px;
  206. overflow: visible;
  207. }
  208. .docx-fit-viewer .docx-page-frame > section.docx,
  209. .docx-fit-viewer .docx-flow-frame > section.docx {
  210. position: absolute;
  211. top: 0;
  212. left: 50%;
  213. margin: 0 !important;
  214. background: #ffffff !important;
  215. box-shadow: 0 2px 14px rgba(25, 35, 48, 0.18);
  216. box-sizing: border-box;
  217. color: #111827;
  218. overflow: hidden;
  219. transform-origin: top center;
  220. }
  221. .docx-fit-viewer .docx-flow-frame > section.docx {
  222. height: auto !important;
  223. min-height: 0 !important;
  224. overflow: visible !important;
  225. }
  226. .docx-fit-viewer .docx-page-frame > section.docx > article,
  227. .docx-fit-viewer .docx-flow-frame > section.docx > article {
  228. position: relative;
  229. z-index: 1;
  230. }
  231. .docx-fit-viewer .docx-vml-watermark {
  232. position: absolute;
  233. inset: 0;
  234. z-index: 0;
  235. width: 100%;
  236. height: 100%;
  237. object-fit: cover;
  238. opacity: 0.28;
  239. filter: saturate(0.72) brightness(1.24);
  240. pointer-events: none;
  241. user-select: none;
  242. }
  243. .docx-fit-viewer .docx-vml-fallback,
  244. .docx-fit-viewer .docx-chart-fallback {
  245. display: block;
  246. max-width: 100%;
  247. margin: 12px auto;
  248. break-inside: avoid;
  249. page-break-inside: avoid;
  250. }
  251. .docx-fit-viewer .docx-vml-fallback {
  252. text-align: center;
  253. }
  254. .docx-fit-viewer .docx-vml-fallback img {
  255. display: block;
  256. max-width: 100%;
  257. height: auto;
  258. margin: 0 auto;
  259. }
  260. .docx-fit-viewer .docx-chart-fallback {
  261. box-sizing: border-box;
  262. overflow: hidden;
  263. border: 1px solid #d7dee8;
  264. border-radius: 8px;
  265. background: #ffffff;
  266. box-shadow: 0 1px 6px rgba(15, 23, 42, 0.08);
  267. }
  268. .docx-fit-viewer .docx-chart-fallback svg {
  269. display: block;
  270. width: 100%;
  271. height: auto;
  272. }
  273. `
  274. function getXmlLocalName(node: Element) {
  275. return node.localName || node.tagName.split(':').pop() || node.tagName
  276. }
  277. function getXmlElements(root: ParentNode, localName: string) {
  278. return Array.from(root.querySelectorAll('*')).filter(element => getXmlLocalName(element) === localName)
  279. }
  280. function getFirstXmlElement(root: ParentNode, localName: string) {
  281. return getXmlElements(root, localName)[0]
  282. }
  283. function getXmlAttribute(element: Element, name: string, namespace?: string) {
  284. return (namespace ? element.getAttributeNS(namespace, name) : null)
  285. || element.getAttribute(name)
  286. || element.getAttribute(`r:${name}`)
  287. }
  288. function getXmlText(element: Element | undefined) {
  289. if (!element) {
  290. return ''
  291. }
  292. return (element.textContent || '').replace(/\s+/g, ' ').trim()
  293. }
  294. function getXmlAncestor(element: Element | null, localName: string) {
  295. let current = element?.parentElement || null
  296. while (current) {
  297. if (getXmlLocalName(current) === localName) {
  298. return current
  299. }
  300. current = current.parentElement
  301. }
  302. return null
  303. }
  304. function getParagraphIndex(element: Element) {
  305. const paragraph = getXmlLocalName(element) === 'p' ? element : getXmlAncestor(element, 'p')
  306. if (!paragraph) {
  307. return undefined
  308. }
  309. let index = 0
  310. let sibling = paragraph.previousElementSibling
  311. while (sibling) {
  312. if (getXmlLocalName(sibling) === 'p') {
  313. index += 1
  314. }
  315. sibling = sibling.previousElementSibling
  316. }
  317. return index
  318. }
  319. function parseXml(xml: string, target: HTMLDivElement) {
  320. const Parser = getTargetWindow(target)?.DOMParser || globalThis.DOMParser
  321. if (!Parser) {
  322. return null
  323. }
  324. const doc = new Parser().parseFromString(xml, 'application/xml')
  325. if (doc.querySelector('parsererror')) {
  326. return null
  327. }
  328. return doc
  329. }
  330. function normalizeZipPath(path: string) {
  331. const segments: string[] = []
  332. path.split('/').forEach(segment => {
  333. if (!segment || segment === '.') {
  334. return
  335. }
  336. if (segment === '..') {
  337. segments.pop()
  338. return
  339. }
  340. segments.push(segment)
  341. })
  342. return segments.join('/')
  343. }
  344. function getDirectoryName(path: string) {
  345. const index = path.lastIndexOf('/')
  346. return index >= 0 ? path.slice(0, index) : ''
  347. }
  348. function getRelationshipPath(partPath: string) {
  349. const directory = getDirectoryName(partPath)
  350. const name = partPath.slice(directory ? directory.length + 1 : 0)
  351. return normalizeZipPath(`${directory}/_rels/${name}.rels`)
  352. }
  353. function resolveRelationshipTarget(partPath: string, target: string) {
  354. if (target.startsWith('/')) {
  355. return normalizeZipPath(target.slice(1))
  356. }
  357. return normalizeZipPath(`${getDirectoryName(partPath)}/${target}`)
  358. }
  359. function getMimeType(path: string) {
  360. const extension = path.split('.').pop()?.toLowerCase()
  361. switch (extension) {
  362. case 'png':
  363. return 'image/png'
  364. case 'jpg':
  365. case 'jpeg':
  366. return 'image/jpeg'
  367. case 'gif':
  368. return 'image/gif'
  369. case 'bmp':
  370. return 'image/bmp'
  371. case 'webp':
  372. return 'image/webp'
  373. case 'svg':
  374. return 'image/svg+xml'
  375. default:
  376. return 'application/octet-stream'
  377. }
  378. }
  379. function parseCssLengthToPixels(value: string | undefined) {
  380. if (!value) {
  381. return undefined
  382. }
  383. const match = value.trim().match(/^(-?\d+(?:\.\d+)?)(px|pt|in|cm|mm)?$/i)
  384. if (!match) {
  385. return undefined
  386. }
  387. const amount = Number(match[1])
  388. if (!Number.isFinite(amount) || amount <= 0) {
  389. return undefined
  390. }
  391. const unit = (match[2] || 'px').toLowerCase()
  392. switch (unit) {
  393. case 'pt':
  394. return amount * 96 / 72
  395. case 'in':
  396. return amount * 96
  397. case 'cm':
  398. return amount * 96 / 2.54
  399. case 'mm':
  400. return amount * 96 / 25.4
  401. default:
  402. return amount
  403. }
  404. }
  405. function parseStyleDeclaration(style: string | undefined) {
  406. const declarations = new Map<string, string>()
  407. if (!style) {
  408. return declarations
  409. }
  410. style.split(';').forEach(declaration => {
  411. const separator = declaration.indexOf(':')
  412. if (separator <= 0) {
  413. return
  414. }
  415. declarations.set(
  416. declaration.slice(0, separator).trim().toLowerCase(),
  417. declaration.slice(separator + 1).trim()
  418. )
  419. })
  420. return declarations
  421. }
  422. function parseShapeSize(style: string | undefined) {
  423. const declarations = parseStyleDeclaration(style)
  424. return {
  425. width: parseCssLengthToPixels(declarations.get('width')),
  426. height: parseCssLengthToPixels(declarations.get('height'))
  427. }
  428. }
  429. function getExtentSize(element: Element) {
  430. const inline = getXmlAncestor(element, 'inline') || getXmlAncestor(element, 'anchor')
  431. const extent = inline ? getFirstXmlElement(inline, 'extent') : undefined
  432. const width = Number(extent?.getAttribute('cx'))
  433. const height = Number(extent?.getAttribute('cy'))
  434. return {
  435. width: Number.isFinite(width) && width > 0 ? width / DOCX_EMU_PER_CSS_PIXEL : undefined,
  436. height: Number.isFinite(height) && height > 0 ? height / DOCX_EMU_PER_CSS_PIXEL : undefined
  437. }
  438. }
  439. async function readDocxRelationships(zip: DocxZip, partPath: string, target: HTMLDivElement) {
  440. const relFile = zip.file(getRelationshipPath(partPath))
  441. const relationships = new Map<string, DocxRelationship>()
  442. if (!relFile) {
  443. return relationships
  444. }
  445. const relDoc = parseXml(await relFile.async('text'), target)
  446. if (!relDoc) {
  447. return relationships
  448. }
  449. getXmlElements(relDoc, 'Relationship').forEach(element => {
  450. const id = element.getAttribute('Id')
  451. const relationshipTarget = element.getAttribute('Target')
  452. if (!id || !relationshipTarget) {
  453. return
  454. }
  455. relationships.set(id, {
  456. id,
  457. type: element.getAttribute('Type') || '',
  458. target: relationshipTarget,
  459. targetMode: element.getAttribute('TargetMode') || undefined
  460. })
  461. })
  462. return relationships
  463. }
  464. async function getDataUrlFromZip(zip: DocxZip, sourcePath: string) {
  465. const file = zip.file(sourcePath)
  466. if (!file) {
  467. return undefined
  468. }
  469. const base64 = await file.async('base64')
  470. return `data:${getMimeType(sourcePath)};base64,${base64}`
  471. }
  472. function getDocxPartPaths(zip: DocxZip) {
  473. return Object.keys(zip.files)
  474. .filter(path => path === 'word/document.xml'
  475. || /^word\/header\d+\.xml$/i.test(path)
  476. || /^word\/footer\d+\.xml$/i.test(path))
  477. .sort((left, right) => {
  478. if (left === 'word/document.xml') {
  479. return -1
  480. }
  481. if (right === 'word/document.xml') {
  482. return 1
  483. }
  484. return left.localeCompare(right)
  485. })
  486. }
  487. function inferVmlFallbackRole(partPath: string, shape: Element | null, style: string | undefined) {
  488. const shapeId = `${shape?.getAttribute('id') || ''} ${shape?.getAttribute('o:spid') || ''}`.toLowerCase()
  489. const normalizedStyle = (style || '').toLowerCase()
  490. if (
  491. partPath.includes('/header')
  492. && (shapeId.includes('watermark') || normalizedStyle.includes('z-index:-') || normalizedStyle.includes('mso-position-horizontal:center'))
  493. ) {
  494. return 'watermark' as const
  495. }
  496. if (shape?.getAttribute('o:ole') === 't' || shape?.getAttribute('ole') === 't') {
  497. return 'ole-preview' as const
  498. }
  499. return 'vml-image' as const
  500. }
  501. async function collectDocxVmlFallbacks(zip: DocxZip, target: HTMLDivElement) {
  502. const fallbacks: DocxImageFallback[] = []
  503. const seen = new Set<string>()
  504. for (const partPath of getDocxPartPaths(zip)) {
  505. const partFile = zip.file(partPath)
  506. if (!partFile) {
  507. continue
  508. }
  509. const doc = parseXml(await partFile.async('text'), target)
  510. if (!doc) {
  511. continue
  512. }
  513. const relationships = await readDocxRelationships(zip, partPath, target)
  514. for (const imagedata of getXmlElements(doc, 'imagedata')) {
  515. const relationshipId = getXmlAttribute(
  516. imagedata,
  517. 'id',
  518. 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
  519. )
  520. const relationship = relationshipId ? relationships.get(relationshipId) : undefined
  521. if (!relationship || relationship.targetMode === 'External' || !relationship.type.includes('/image')) {
  522. continue
  523. }
  524. const sourcePath = resolveRelationshipTarget(partPath, relationship.target)
  525. const dataUrl = await getDataUrlFromZip(zip, sourcePath)
  526. if (!dataUrl) {
  527. continue
  528. }
  529. const shape = getXmlAncestor(imagedata, 'shape')
  530. const style = shape?.getAttribute('style') || undefined
  531. const role = inferVmlFallbackRole(partPath, shape, style)
  532. const key = role === 'watermark'
  533. ? `${role}:${sourcePath}`
  534. : `${role}:${partPath}:${sourcePath}:${getParagraphIndex(imagedata) ?? 'end'}`
  535. if (seen.has(key)) {
  536. continue
  537. }
  538. seen.add(key)
  539. fallbacks.push({
  540. role,
  541. key,
  542. dataUrl,
  543. sourcePath,
  544. partPath,
  545. title: imagedata.getAttribute('o:title') || imagedata.getAttribute('title') || undefined,
  546. style,
  547. ...parseShapeSize(style),
  548. paragraphIndex: partPath === 'word/document.xml' ? getParagraphIndex(imagedata) : undefined
  549. })
  550. }
  551. }
  552. return fallbacks
  553. }
  554. function parseCacheValues(element: Element | undefined) {
  555. if (!element) {
  556. return []
  557. }
  558. return getXmlElements(element, 'pt')
  559. .sort((left, right) => Number(left.getAttribute('idx') || 0) - Number(right.getAttribute('idx') || 0))
  560. .map(point => getXmlText(getFirstXmlElement(point, 'v')))
  561. .filter(Boolean)
  562. }
  563. function parseChartSeries(series: Element) {
  564. const name = parseCacheValues(getFirstXmlElement(getFirstXmlElement(series, 'tx') || series, 'strCache'))[0]
  565. || parseCacheValues(getFirstXmlElement(getFirstXmlElement(series, 'tx') || series, 'numCache'))[0]
  566. || 'Series'
  567. const categories = parseCacheValues(getFirstXmlElement(getFirstXmlElement(series, 'cat') || series, 'strCache'))
  568. const numericCategories = parseCacheValues(getFirstXmlElement(getFirstXmlElement(series, 'cat') || series, 'numCache'))
  569. const values = parseCacheValues(getFirstXmlElement(getFirstXmlElement(series, 'val') || series, 'numCache'))
  570. .map(value => Number(value))
  571. .filter(value => Number.isFinite(value))
  572. return {
  573. name,
  574. categories: categories.length ? categories : numericCategories,
  575. values
  576. }
  577. }
  578. function parseChartFallback(chartDoc: XMLDocument, sourcePath: string, chartElement: Element, paragraphIndex?: number) {
  579. const chartTypeElement = ['lineChart', 'barChart', 'pieChart', 'areaChart', 'scatterChart']
  580. .map(type => getXmlElements(chartDoc, type)[0])
  581. .find(Boolean)
  582. const chartType = chartTypeElement ? getXmlLocalName(chartTypeElement) : 'chart'
  583. const title = getXmlText(getFirstXmlElement(getFirstXmlElement(chartDoc, 'title') || chartDoc, 't'))
  584. || sourcePath.split('/').pop() || 'Chart'
  585. const series = (chartTypeElement ? getXmlElements(chartTypeElement, 'ser') : getXmlElements(chartDoc, 'ser'))
  586. .map(parseChartSeries)
  587. .filter(item => item.values.length)
  588. const { width, height } = getExtentSize(chartElement)
  589. if (!series.length) {
  590. return undefined
  591. }
  592. return {
  593. key: `chart:${sourcePath}:${paragraphIndex ?? 'end'}`,
  594. title,
  595. type: chartType,
  596. sourcePath,
  597. series,
  598. width,
  599. height,
  600. paragraphIndex
  601. } satisfies DocxChartFallback
  602. }
  603. async function collectDocxChartFallbacks(zip: DocxZip, target: HTMLDivElement) {
  604. const partPath = 'word/document.xml'
  605. const documentFile = zip.file(partPath)
  606. if (!documentFile) {
  607. return []
  608. }
  609. const documentDoc = parseXml(await documentFile.async('text'), target)
  610. if (!documentDoc) {
  611. return []
  612. }
  613. const relationships = await readDocxRelationships(zip, partPath, target)
  614. const fallbacks: DocxChartFallback[] = []
  615. const seen = new Set<string>()
  616. for (const chart of getXmlElements(documentDoc, 'chart')) {
  617. const relationshipId = getXmlAttribute(
  618. chart,
  619. 'id',
  620. 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
  621. )
  622. const relationship = relationshipId ? relationships.get(relationshipId) : undefined
  623. if (!relationship || relationship.targetMode === 'External' || !relationship.type.includes('/chart')) {
  624. continue
  625. }
  626. const sourcePath = resolveRelationshipTarget(partPath, relationship.target)
  627. const chartFile = zip.file(sourcePath)
  628. if (!chartFile) {
  629. continue
  630. }
  631. const chartDoc = parseXml(await chartFile.async('text'), target)
  632. const paragraphIndex = getParagraphIndex(chart)
  633. const fallback = chartDoc ? parseChartFallback(chartDoc, sourcePath, chart, paragraphIndex) : undefined
  634. if (!fallback || seen.has(fallback.key)) {
  635. continue
  636. }
  637. seen.add(fallback.key)
  638. fallbacks.push(fallback)
  639. }
  640. return fallbacks
  641. }
  642. function escapeHtml(value: string) {
  643. return value
  644. .replace(/&/g, '&amp;')
  645. .replace(/</g, '&lt;')
  646. .replace(/>/g, '&gt;')
  647. .replace(/"/g, '&quot;')
  648. }
  649. function buildChartSvg(chart: DocxChartFallback) {
  650. const width = Math.max(360, Math.round(chart.width || 520))
  651. const height = Math.max(220, Math.round(chart.height || 320))
  652. const padding = { top: 52, right: 30, bottom: 56, left: 54 }
  653. const plotWidth = width - padding.left - padding.right
  654. const plotHeight = height - padding.top - padding.bottom
  655. const allValues = chart.series.flatMap(item => item.values)
  656. const maxValue = Math.max(...allValues, 1)
  657. const minValue = Math.min(...allValues, 0)
  658. const valueSpan = Math.max(maxValue - minValue, 1)
  659. const colors = ['#2563eb', '#10b981', '#f97316', '#8b5cf6', '#ef4444']
  660. const maxPoints = Math.max(...chart.series.map(item => item.values.length), 1)
  661. const pointX = (index: number) => padding.left + (maxPoints === 1 ? plotWidth / 2 : index * plotWidth / (maxPoints - 1))
  662. const pointY = (value: number) => padding.top + plotHeight - ((value - minValue) / valueSpan) * plotHeight
  663. const seriesPaths = chart.series.map((series, seriesIndex) => {
  664. const color = colors[seriesIndex % colors.length]
  665. const points = series.values.map((value, index) => `${pointX(index).toFixed(1)},${pointY(value).toFixed(1)}`).join(' ')
  666. const circles = series.values.map((value, index) => {
  667. const x = pointX(index).toFixed(1)
  668. const y = pointY(value).toFixed(1)
  669. return `<circle cx="${x}" cy="${y}" r="3.5" fill="${color}"><title>${escapeHtml(series.name)}: ${escapeHtml(String(value))}</title></circle>`
  670. }).join('')
  671. return `<polyline points="${points}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>${circles}`
  672. }).join('')
  673. const categories = chart.series.find(item => item.categories.length)?.categories || []
  674. const labels = categories.slice(0, maxPoints).map((label, index) => {
  675. const x = pointX(index)
  676. const shown = label.length > 14 ? `${label.slice(0, 13)}...` : label
  677. return `<text x="${x.toFixed(1)}" y="${height - 22}" text-anchor="middle" fill="#64748b" font-size="11">${escapeHtml(shown)}</text>`
  678. }).join('')
  679. const legend = chart.series.slice(0, 5).map((series, index) => {
  680. const x = padding.left + index * 98
  681. const y = 30
  682. const color = colors[index % colors.length]
  683. return `<g transform="translate(${x} ${y})"><rect width="10" height="10" rx="2" fill="${color}"/><text x="15" y="9" fill="#475569" font-size="11">${escapeHtml(series.name)}</text></g>`
  684. }).join('')
  685. return `<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHtml(chart.title)}">
  686. <rect x="0" y="0" width="${width}" height="${height}" rx="8" fill="#ffffff"/>
  687. <text x="${padding.left}" y="22" fill="#0f172a" font-size="15" font-weight="700">${escapeHtml(chart.title)}</text>
  688. ${legend}
  689. <line x1="${padding.left}" y1="${padding.top + plotHeight}" x2="${padding.left + plotWidth}" y2="${padding.top + plotHeight}" stroke="#cbd5e1"/>
  690. <line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${padding.top + plotHeight}" stroke="#cbd5e1"/>
  691. <text x="${padding.left - 8}" y="${padding.top + 4}" text-anchor="end" fill="#64748b" font-size="11">${escapeHtml(maxValue.toFixed(1))}</text>
  692. <text x="${padding.left - 8}" y="${padding.top + plotHeight}" text-anchor="end" fill="#64748b" font-size="11">${escapeHtml(minValue.toFixed(1))}</text>
  693. ${seriesPaths}
  694. ${labels}
  695. </svg>`
  696. }
  697. function getDocxSections(target: HTMLDivElement) {
  698. return Array.from(target.querySelectorAll<HTMLElement>('section.docx'))
  699. }
  700. function getDocxArticles(target: HTMLDivElement) {
  701. return Array.from(target.querySelectorAll<HTMLElement>('section.docx > article'))
  702. }
  703. function getDocxParagraphAnchor(target: HTMLDivElement, paragraphIndex: number | undefined) {
  704. if (paragraphIndex === undefined) {
  705. return undefined
  706. }
  707. const paragraphs = Array.from(target.querySelectorAll<HTMLElement>('section.docx > article p'))
  708. return paragraphs[Math.min(Math.max(paragraphIndex, 0), Math.max(paragraphs.length - 1, 0))]
  709. }
  710. function insertDocxFallback(target: HTMLDivElement, node: HTMLElement, paragraphIndex: number | undefined) {
  711. const anchor = getDocxParagraphAnchor(target, paragraphIndex)
  712. if (anchor) {
  713. anchor.after(node)
  714. return
  715. }
  716. const article = getDocxArticles(target)[0]
  717. if (article) {
  718. article.appendChild(node)
  719. }
  720. }
  721. function injectDocxImageFallbacks(target: HTMLDivElement, fallbacks: DocxImageFallback[]) {
  722. const sections = getDocxSections(target)
  723. if (!sections.length) {
  724. return
  725. }
  726. fallbacks.forEach(fallback => {
  727. if (fallback.role === 'watermark') {
  728. sections.forEach(section => {
  729. const image = target.ownerDocument.createElement('img')
  730. image.className = 'docx-vml-watermark'
  731. image.src = fallback.dataUrl
  732. image.alt = fallback.title || ''
  733. image.dataset.docxFallback = fallback.key
  734. section.prepend(image)
  735. })
  736. return
  737. }
  738. const figure = target.ownerDocument.createElement('figure')
  739. figure.className = 'docx-vml-fallback'
  740. figure.dataset.docxFallback = fallback.key
  741. if (fallback.width) {
  742. figure.style.width = `${Math.round(fallback.width)}px`
  743. }
  744. const image = target.ownerDocument.createElement('img')
  745. image.src = fallback.dataUrl
  746. image.alt = fallback.title || (fallback.role === 'ole-preview' ? 'Embedded object preview' : 'Document image')
  747. if (fallback.width) {
  748. image.style.width = `${Math.round(fallback.width)}px`
  749. }
  750. if (fallback.height) {
  751. image.style.height = `${Math.round(fallback.height)}px`
  752. }
  753. figure.appendChild(image)
  754. insertDocxFallback(target, figure, fallback.paragraphIndex)
  755. })
  756. }
  757. function injectDocxChartFallbacks(target: HTMLDivElement, fallbacks: DocxChartFallback[]) {
  758. fallbacks.forEach(fallback => {
  759. const figure = target.ownerDocument.createElement('figure')
  760. figure.className = 'docx-chart-fallback'
  761. figure.dataset.docxFallback = fallback.key
  762. if (fallback.width) {
  763. figure.style.width = `${Math.round(fallback.width)}px`
  764. }
  765. figure.innerHTML = buildChartSvg(fallback)
  766. insertDocxFallback(target, figure, fallback.paragraphIndex)
  767. })
  768. }
  769. async function enhanceDocxFallbacks(buffer: ArrayBuffer, target: HTMLDivElement) {
  770. try {
  771. const { default: JSZip } = await import('jszip')
  772. const zip = await JSZip.loadAsync(buffer.slice(0))
  773. const [imageFallbacks, chartFallbacks] = await Promise.all([
  774. collectDocxVmlFallbacks(zip, target),
  775. collectDocxChartFallbacks(zip, target)
  776. ])
  777. if (imageFallbacks.length) {
  778. injectDocxImageFallbacks(target, imageFallbacks)
  779. }
  780. if (chartFallbacks.length) {
  781. injectDocxChartFallbacks(target, chartFallbacks)
  782. }
  783. } catch (error) {
  784. console.warn('[file-viewer] DOCX 兼容增强解析失败,已保留 @file-viewer/docx 原始渲染结果。', error)
  785. }
  786. }
  787. function installResponsiveStyle(target: HTMLDivElement) {
  788. const style = target.ownerDocument.createElement('style')
  789. style.textContent = DOCX_RESPONSIVE_CSS
  790. target.prepend(style)
  791. return style
  792. }
  793. function clonePageShell(section: HTMLElement, article: HTMLElement, pageHeight: number) {
  794. const nextPage = section.cloneNode(false) as HTMLElement
  795. nextPage.innerHTML = ''
  796. nextPage.dataset.docxPaginated = 'true'
  797. nextPage.style.minHeight = `${pageHeight}px`
  798. nextPage.style.height = `${pageHeight}px`
  799. nextPage.style.overflow = 'hidden'
  800. const nextArticle = article.cloneNode(false) as HTMLElement
  801. nextPage.appendChild(nextArticle)
  802. Array.from(section.children).forEach(child => {
  803. if (child !== article) {
  804. nextPage.appendChild(child.cloneNode(true))
  805. }
  806. })
  807. return { page: nextPage, article: nextArticle }
  808. }
  809. function getDocxPageHeight(section: HTMLElement) {
  810. const style = section.ownerDocument.defaultView?.getComputedStyle(section)
  811. const minHeight = style ? parseFloat(style.minHeight) : 0
  812. return Number.isFinite(minHeight) && minHeight > 0 ? minHeight : section.offsetHeight
  813. }
  814. function paginateOversizedSections(target: HTMLDivElement) {
  815. const wrapper = target.querySelector('.docx-wrapper')
  816. if (!wrapper) {
  817. return
  818. }
  819. Array.from(wrapper.children).forEach(child => {
  820. if (!isTargetHTMLElement(child, target) || !child.matches('section.docx')) {
  821. return
  822. }
  823. const article = child.querySelector(':scope > article')
  824. if (!isTargetHTMLElement(article, target)) {
  825. return
  826. }
  827. const pageHeight = getDocxPageHeight(child)
  828. const originalNodes = Array.from(article.childNodes)
  829. if (!pageHeight || originalNodes.length < 2 || child.scrollHeight <= pageHeight * 1.15) {
  830. return
  831. }
  832. // 新 DOCX 引擎会优先使用 Word 保存的分页;这里保留预览层兜底分页,覆盖缺少分页标记的旧文件。
  833. let current = clonePageShell(child, article, pageHeight)
  834. child.before(current.page)
  835. originalNodes.forEach(node => {
  836. current.article.appendChild(node)
  837. if (current.page.scrollHeight <= pageHeight + 1 || current.article.childNodes.length === 1) {
  838. return
  839. }
  840. current.article.removeChild(node)
  841. current = clonePageShell(child, article, pageHeight)
  842. child.before(current.page)
  843. current.article.appendChild(node)
  844. })
  845. child.remove()
  846. })
  847. }
  848. function wrapDocxSections(target: HTMLDivElement, pagedLayout: boolean) {
  849. const wrapper = target.querySelector('.docx-wrapper')
  850. if (!wrapper) {
  851. return []
  852. }
  853. return Array.from(wrapper.children).flatMap(child => {
  854. if (!isTargetHTMLElement(child, target) || !child.matches('section.docx')) {
  855. return []
  856. }
  857. const frame = target.ownerDocument.createElement('div')
  858. frame.className = pagedLayout ? 'docx-page-frame' : 'docx-flow-frame'
  859. child.before(frame)
  860. frame.appendChild(child)
  861. return [frame]
  862. })
  863. }
  864. function makeDocxResponsive(target: HTMLDivElement, context?: FileRenderContext) {
  865. target.classList.add('docx-fit-viewer')
  866. const style = installResponsiveStyle(target)
  867. const pagedLayout = shouldPaginateOversizedDocxSections(context)
  868. if (pagedLayout) {
  869. paginateOversizedSections(target)
  870. }
  871. const frames = wrapDocxSections(target, pagedLayout)
  872. const view = getTargetWindow(target)
  873. const ResizeObserverCtor = view?.ResizeObserver
  874. let resizeFrame = 0
  875. let userZoom = 1
  876. let currentScale = 1
  877. let currentFitScale = 1
  878. const zoomEmitter = createZoomChangeEmitter()
  879. const clampScale = (scale: number) => {
  880. return Math.min(DOCX_MAX_SCALE, Math.max(DOCX_MIN_SCALE, Number(scale.toFixed(2))))
  881. }
  882. const resize = () => {
  883. if (!view) {
  884. return
  885. }
  886. view.cancelAnimationFrame(resizeFrame)
  887. resizeFrame = view.requestAnimationFrame(() => {
  888. let firstScale = 1
  889. frames.forEach(frame => {
  890. const page = frame.firstElementChild
  891. if (!isTargetHTMLElement(page, target)) {
  892. return
  893. }
  894. page.style.transform = 'translateX(-50%)'
  895. const pageWidth = page.offsetWidth
  896. const contentHeight = pagedLayout
  897. ? page.offsetHeight
  898. : Math.max(page.scrollHeight, page.offsetHeight)
  899. if (!pageWidth || !contentHeight) {
  900. return
  901. }
  902. const availableWidth = Math.max(target.clientWidth - 28, 120)
  903. const fitScale = Math.min(1, Math.max(DOCX_MIN_SCALE, availableWidth / pageWidth))
  904. const scale = clampScale(fitScale * userZoom)
  905. firstScale = scale
  906. currentFitScale = fitScale
  907. page.style.transform = `translateX(-50%) scale(${scale})`
  908. frame.style.width = `${Math.ceil(Math.max(pageWidth * scale, target.clientWidth - 28, 120))}px`
  909. frame.style.maxWidth = 'none'
  910. frame.style.height = `${Math.ceil(contentHeight * scale)}px`
  911. })
  912. currentScale = firstScale
  913. zoomEmitter.emit()
  914. })
  915. }
  916. const getZoomState = (): FileViewerZoomState => ({
  917. scale: currentScale,
  918. label: `${Math.round(currentScale * 100)}%`,
  919. canZoomIn: currentScale < DOCX_MAX_SCALE,
  920. canZoomOut: currentScale > DOCX_MIN_SCALE,
  921. canReset: userZoom !== 1,
  922. minScale: DOCX_MIN_SCALE,
  923. maxScale: DOCX_MAX_SCALE
  924. })
  925. const setUserZoom = (nextZoom: number) => {
  926. userZoom = Math.min(6, Math.max(0.2, Number(nextZoom.toFixed(2))))
  927. resize()
  928. return getZoomState()
  929. }
  930. target.dataset.viewerZoomProvider = 'docx'
  931. registerFileViewerZoomProvider(target, {
  932. zoomIn: () => setUserZoom(userZoom + DOCX_ZOOM_STEP),
  933. zoomOut: () => setUserZoom(userZoom - DOCX_ZOOM_STEP),
  934. resetZoom: () => setUserZoom(1),
  935. setZoom: scale => setUserZoom(scale / Math.max(currentFitScale, 0.01)),
  936. getState: getZoomState,
  937. subscribe: zoomEmitter.subscribe
  938. })
  939. const observer = ResizeObserverCtor ? new ResizeObserverCtor(resize) : null
  940. observer?.observe(target)
  941. frames.forEach(frame => {
  942. const page = getDocxPageElement(frame)
  943. if (page) {
  944. observer?.observe(page)
  945. }
  946. })
  947. resize()
  948. return () => {
  949. view?.cancelAnimationFrame(resizeFrame)
  950. observer?.disconnect()
  951. unregisterFileViewerZoomProvider(target)
  952. style.remove()
  953. target.classList.remove('docx-fit-viewer')
  954. }
  955. }
  956. function getDocxPageElement(frame: HTMLElement) {
  957. const page = frame.firstElementChild
  958. const HTMLElementCtor = frame.ownerDocument.defaultView?.HTMLElement
  959. return HTMLElementCtor && page instanceof HTMLElementCtor ? page : null
  960. }
  961. function isDocxFlowFrame(frame: HTMLElement | undefined) {
  962. return !!frame?.classList.contains('docx-flow-frame')
  963. }
  964. function getDocxFramePrintSize(frame: HTMLElement | undefined) {
  965. const page = frame ? getDocxPageElement(frame) : null
  966. if (!page) {
  967. return DOCX_DEFAULT_PAGE_SIZE
  968. }
  969. const size = getElementPrintPageSize(page, DOCX_DEFAULT_PAGE_SIZE)
  970. if (!isDocxFlowFrame(frame)) {
  971. return size
  972. }
  973. return {
  974. width: size.width,
  975. height: Math.max(page.scrollHeight || 0, page.offsetHeight || 0, DOCX_DEFAULT_PAGE_SIZE.height)
  976. }
  977. }
  978. function normalizeDocxPageForPrint(frame: HTMLElement, pageSize: PrintPageSize) {
  979. const flowLayout = isDocxFlowFrame(frame)
  980. const pageWidth = formatCssPixels(pageSize.width)
  981. const pageHeight = formatCssPixels(pageSize.height)
  982. applyPrintPageSize(frame, pageSize, { heightMode: flowLayout ? 'min' : 'fixed' })
  983. frame.style.margin = '0 auto 18px'
  984. const page = getDocxPageElement(frame)
  985. if (!page) {
  986. return
  987. }
  988. page.style.position = 'relative'
  989. page.style.top = 'auto'
  990. page.style.left = 'auto'
  991. page.style.width = pageWidth
  992. page.style.maxWidth = 'none'
  993. page.style.minHeight = flowLayout ? '0' : pageHeight
  994. page.style.height = flowLayout ? 'auto' : pageHeight
  995. page.style.margin = '0 auto'
  996. page.style.transform = 'none'
  997. page.style.transformOrigin = 'top left'
  998. page.style.overflow = flowLayout ? 'visible' : 'hidden'
  999. page.style.boxShadow = 'none'
  1000. }
  1001. function buildDocxPrintStyle(target: HTMLDivElement) {
  1002. const firstFrame = target.querySelector<HTMLElement>('.docx-page-frame, .docx-flow-frame')
  1003. const pageSize = getDocxFramePrintSize(firstFrame || undefined)
  1004. const selector = firstFrame?.classList.contains('docx-flow-frame')
  1005. ? '.viewer-export-content .docx-flow-frame'
  1006. : '.viewer-export-content .docx-page-frame'
  1007. return buildPrintPageStyle({
  1008. selector,
  1009. width: pageSize.width,
  1010. height: firstFrame?.classList.contains('docx-flow-frame')
  1011. ? DOCX_DEFAULT_PAGE_SIZE.height
  1012. : pageSize.height,
  1013. heightMode: firstFrame?.classList.contains('docx-flow-frame') ? 'min' : 'fixed'
  1014. })
  1015. }
  1016. function prepareDocxCloneForExport(target: HTMLDivElement) {
  1017. const liveFrames = Array.from(target.querySelectorAll<HTMLElement>('.docx-page-frame, .docx-flow-frame'))
  1018. const clone = target.cloneNode(true) as HTMLElement
  1019. const printDocument = target.ownerDocument.createElement('div')
  1020. printDocument.className = 'docx-print-document'
  1021. const scopedStyles = Array.from(clone.querySelectorAll('style'))
  1022. .filter(style => !style.textContent?.includes('.docx-fit-viewer'))
  1023. .map(style => style.outerHTML)
  1024. .join('')
  1025. clone.querySelectorAll<HTMLElement>('.docx-page-frame, .docx-flow-frame').forEach((frame, index) => {
  1026. normalizeDocxPageForPrint(frame, getDocxFramePrintSize(liveFrames[index]))
  1027. printDocument.appendChild(frame.cloneNode(true))
  1028. })
  1029. return printDocument.childElementCount ? `${scopedStyles}${printDocument.outerHTML}` : clone.innerHTML
  1030. }
  1031. /**
  1032. * 渲染docx文件
  1033. */
  1034. export default async function(buffer: ArrayBuffer, target: HTMLDivElement, context?: FileRenderContext): Promise<AppWrapper> {
  1035. assertValidDocxPackage(buffer)
  1036. let hasNotifiedProgressiveRender = false
  1037. const notifyProgressiveRender = () => {
  1038. if (hasNotifiedProgressiveRender) {
  1039. return
  1040. }
  1041. hasNotifiedProgressiveRender = true
  1042. context?.onProgressiveRender?.()
  1043. }
  1044. const docxOptions = createDocxOptions(target, context, notifyProgressiveRender)
  1045. const { defaultOptions, renderAsync } = await loadLibrary()
  1046. target.dataset.docxWorker = docxOptions.useWorker ? 'self' : 'false'
  1047. await renderAsync(buffer, target, undefined, {
  1048. ...defaultOptions,
  1049. ...docxOptions
  1050. })
  1051. notifyProgressiveRender()
  1052. await enhanceDocxFallbacks(buffer, target)
  1053. const disposeResponsive = makeDocxResponsive(target, context)
  1054. context?.registerExportAdapter?.({
  1055. includeDocumentStyles: false,
  1056. beforeSnapshot: () => {
  1057. const view = getTargetWindow(target)
  1058. if (view) {
  1059. view.dispatchEvent(new view.Event('resize'))
  1060. }
  1061. },
  1062. printStyle: () => buildDocxPrintStyle(target),
  1063. toHtml: () => prepareDocxCloneForExport(target)
  1064. })
  1065. return {
  1066. $el: target,
  1067. unmount() {
  1068. context?.registerExportAdapter?.(null)
  1069. disposeResponsive()
  1070. delete target.dataset.docxWorker
  1071. target.innerHTML = ''
  1072. }
  1073. }
  1074. }