diff --git a/dist/docx-preview.js b/dist/docx-preview.js index 4835dfcd..f2e0920e 100644 --- a/dist/docx-preview.js +++ b/dist/docx-preview.js @@ -9313,7 +9313,7 @@ section.${c}>ol>li::before { if (this.options.responsive) { styleText += ` .${c}-wrapper { padding: 8px; } -section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing: border-box; } +section.${c}:not([data-docxjs-thumbnail]) { width: auto !important; max-width: 100%; min-width: 0; box-sizing: border-box; } .${c} img { max-width: 100%; height: auto; } .${c} table { max-width: 100%; table-layout: auto; } @media (max-width: 768px) { @@ -11749,6 +11749,16 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing } return null; } + function cloneWithout(root, shouldSkip) { + const clone = root.cloneNode(false); + for (let i = 0; i < root.childNodes.length; i++) { + const child = root.childNodes[i]; + if (!shouldSkip(child)) { + clone.appendChild(cloneWithout(child, shouldSkip)); + } + } + return clone; + } function ensureStyle(doc, className, activeClassName) { const head = doc.head; if (!head) @@ -11765,8 +11775,11 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing margin: 0.5rem auto; cursor: pointer; outline: none; + user-select: none; + -webkit-user-select: none; /* Safari */ } -.${className}-thumbnail:focus-visible .${className}-thumbnail-preview { +.${className}-thumbnail:not(.${activeClassName}):hover .${className}-thumbnail-preview, +.${className}-thumbnail:not(.${activeClassName}):focus-visible .${className}-thumbnail-preview { box-shadow: 0 0 0 2px #4a90e2, 0 0 10px rgba(0, 0, 0, 0.5); } .${className}-thumbnail-preview { @@ -11776,6 +11789,8 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing box-sizing: content-box; border: 2px solid transparent; position: relative; + display: inherit; + flex-direction: inherit; } .${className}-thumbnail-label { font-size: 0.75rem; @@ -11787,6 +11802,20 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing .${activeClassName} .${className}-thumbnail-preview { border-color: #4a90e2; } +.${className}-thumbnail-loading { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} +.${className}-thumbnail-spinner { + width: 18px; + height: 18px; + border: 2px solid rgba(0,0,0,.15); + border-top-color: #4a90e2; + border-radius: 50%; +} `; head.appendChild(style); } @@ -11824,7 +11853,7 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing }]; } const cs = win?.getComputedStyle(section); - if (cs && cs.position === 'static') { + if (cs?.position === 'static') { section.style.position = 'relative'; } const pages = []; @@ -11853,6 +11882,36 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing } return pages; } + function createThumbnailContent(preview, section, topOffset, width, pageWidth) { + const clone = cloneWithout(section, node => node instanceof Element && + node.hasAttribute("data-docxjs-page-anchor")); + clone.setAttribute('aria-hidden', 'true'); + clone.setAttribute('data-docxjs-thumbnail', ''); + clone.removeAttribute('id'); + clone.style.boxShadow = 'none'; + clone.style.margin = '0'; + clone.style.flexShrink = '0'; + let scale = 1; + if (pageWidth > 0) { + clone.style.width = `${pageWidth}px`; + scale = width / pageWidth; + } + const translate = -topOffset * scale; + clone.style.transform = `translateY(${translate}px) scale(${scale})`; + clone.style.transformOrigin = '0 0'; + preview.replaceChildren(clone); + } + function isFullyVisible(thumb, root) { + const thumbRect = thumb.getBoundingClientRect(); + const doc = thumb.ownerDocument; + const viewport = root?.getBoundingClientRect() ?? { + top: 0, + bottom: doc.defaultView?.innerHeight ?? + doc.documentElement.clientHeight, + }; + return (thumbRect.top >= viewport.top && + thumbRect.bottom <= viewport.bottom); + } function renderThumbnails(mainContainer, thumbnailContainer, options) { const width = options?.width ?? 120; const showPageNumbers = options?.showPageNumbers ?? true; @@ -11863,7 +11922,7 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing ensureStyle(mainContainer.ownerDocument, className, activeClassName); thumbnailContainer.innerHTML = ''; const sections = Array.from(mainContainer.querySelectorAll(`section.${className}`)); - const splitterRan = mainContainer.querySelector(`section[${VISUAL_PAGE_MARKER}]`) !== null; + const splitterRan = sections.some(section => section.hasAttribute(VISUAL_PAGE_MARKER)); const pages = []; for (const section of sections) { const sectionPages = splitterRan @@ -11874,8 +11933,94 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing } } const pairs = []; + const renderQueue = []; + let rendering = false; + let cancelScheduledRender; + const scheduleRender = (callback) => { + if ("requestIdleCallback" in globalThis) { + const id = requestIdleCallback(callback, { timeout: 250 }); + return () => cancelIdleCallback(id); + } + const id = requestAnimationFrame(callback); + return () => cancelAnimationFrame(id); + }; + const enqueue = (preview) => { + if (renderQueue.includes(preview)) { + return; + } + renderQueue.push(preview); + processQueue(); + }; + const processQueue = () => { + if (rendering || !renderQueue.length) { + return; + } + rendering = true; + const preview = renderQueue.shift(); + if (!preview.isConnected) { + rendering = false; + processQueue(); + return; + } + const page = pages[Number(preview.dataset.pageIndex)]; + cancelScheduledRender = scheduleRender(() => { + cancelScheduledRender = undefined; + createThumbnailContent(preview, page.section, page.topOffset, width, page.pageWidth); + delete preview.dataset.rendered; + delete preview.dataset.pageIndex; + rendering = false; + processQueue(); + }); + }; + let thumbObserver = null; + const thumbnailScrollRoot = findScrollingAncestor(thumbnailContainer); + const IO = (win ?? globalThis).IntersectionObserver; + if (IO) { + thumbObserver = new IO((entries) => { + for (const entry of entries) { + const preview = entry.target; + const thumb = preview.firstElementChild; + if (!entry.isIntersecting) { + if (thumb) { + thumb.style.display = "none"; + } + continue; + } + if (thumb) { + thumb.style.display = ""; + continue; + } + if (preview.dataset.rendered !== "false") { + continue; + } + const loading = doc.createElement("div"); + loading.className = `${className}-thumbnail-loading`; + const spinner = doc.createElement("div"); + spinner.className = `${className}-thumbnail-spinner`; + spinner.animate([ + { transform: "rotate(0deg)" }, + { transform: "rotate(360deg)" }, + ], { + duration: 800, + iterations: Infinity, + easing: "linear", + }); + loading.appendChild(spinner); + preview.appendChild(loading); + preview.dataset.rendered = ""; + enqueue(preview); + } + }, { + root: thumbnailScrollRoot, + rootMargin: "200px 0px", + threshold: 0 + }); + } + let suppressAutoScroll = false; + const fragment = doc.createDocumentFragment(); + const scrollRoot = findScrollingAncestor(mainContainer); for (let i = 0; i < pages.length; i++) { - const { section, scrollTarget, topOffset, pageWidth, pageHeight } = pages[i]; + const { scrollTarget, pageWidth, pageHeight } = pages[i]; const pageNum = i + 1; const thumb = doc.createElement('div'); thumb.className = `${className}-thumbnail`; @@ -11885,12 +12030,6 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing thumb.dataset.page = String(pageNum); const preview = doc.createElement('div'); preview.className = `${className}-thumbnail-preview`; - const clone = section.cloneNode(true); - clone.setAttribute('aria-hidden', 'true'); - clone.removeAttribute('id'); - clone.style.boxShadow = 'none'; - clone.style.margin = '0'; - clone.style.flexShrink = '0'; let scale = 1; let previewHeight = 0; if (pageWidth > 0) { @@ -11901,10 +12040,8 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing if (previewHeight > 0) { preview.style.height = `${previewHeight}px`; } - const translate = -topOffset * scale; - clone.style.transform = `translateY(${translate}px) scale(${scale})`; - clone.style.transformOrigin = '0 0'; - preview.appendChild(clone); + preview.dataset.pageIndex = String(i); + preview.dataset.rendered = "false"; thumb.appendChild(preview); if (showPageNumbers) { const label = doc.createElement('div'); @@ -11913,40 +12050,93 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing thumb.appendChild(label); } const goTo = () => { + suppressAutoScroll = true; scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; thumb.addEventListener('click', goTo); thumb.addEventListener('keydown', (ev) => { - const ke = ev; - if (ke.key === 'Enter' || ke.key === ' ') { - ke.preventDefault(); + if (ev.key === 'Enter' || ev.key === ' ') { + ev.preventDefault(); goTo(); } }); - thumbnailContainer.appendChild(thumb); + fragment.appendChild(thumb); + thumbObserver?.observe(preview); pairs.push({ scrollTarget, thumb }); } + thumbnailContainer.appendChild(fragment); + const scrollTarget = scrollRoot ?? doc; + let scrollEndTimer; + const onScrollEnd = () => { + suppressAutoScroll = false; + scrollEndTimer = undefined; + }; + const onScroll = () => { + if (scrollEndTimer !== undefined) { + clearTimeout(scrollEndTimer); + } + scrollEndTimer = setTimeout(onScrollEnd, 100); + }; + let removeScrollListener; + if ("onscrollend" in scrollTarget) { + scrollTarget.addEventListener("scrollend", onScrollEnd, { passive: true }); + removeScrollListener = () => scrollTarget.removeEventListener("scrollend", onScrollEnd); + } + else { + scrollTarget.addEventListener("scroll", onScroll, { passive: true }); + removeScrollListener = () => scrollTarget.removeEventListener("scroll", onScroll); + } let observer = null; - const IO = win?.IntersectionObserver ?? - globalThis.IntersectionObserver; + let activeThumbnail = -1; if (IO && pairs.length > 0) { - const scrollRoot = findScrollingAncestor(mainContainer); const visibility = new Map(); + let activeIdx = -1; observer = new IO((entries) => { + let needsUpdate = false; for (const entry of entries) { - visibility.set(entry.target, entry.intersectionRatio); + const r = entry.intersectionRatio; + const target = entry.target; + visibility.set(target, r); + if (needsUpdate) { + continue; + } + const activeTarget = pairs[activeIdx].scrollTarget; + if (activeIdx === -1 || + activeTarget === target || + r > (visibility.get(activeTarget) ?? 0)) { + needsUpdate = true; + } + } + if (!needsUpdate) { + return; } let bestIdx = -1; - let bestRatio = -1; - for (let i = 0; i < pairs.length; i++) { + for (let i = 0, bestRatio = 0; i < pairs.length; i++) { const r = visibility.get(pairs[i].scrollTarget) ?? 0; if (r > bestRatio) { bestRatio = r; bestIdx = i; } } - for (let i = 0; i < pairs.length; i++) { - pairs[i].thumb.classList.toggle(activeClassName, i === bestIdx && bestRatio > 0); + if (bestIdx !== activeIdx) { + if (activeIdx !== -1) { + pairs[activeIdx].thumb.classList.remove(activeClassName); + } + if (bestIdx !== -1) { + pairs[bestIdx].thumb.classList.add(activeClassName); + } + activeIdx = bestIdx; + } + if (bestIdx !== activeThumbnail && bestIdx >= 0) { + activeThumbnail = bestIdx; + const { thumb } = pairs[bestIdx]; + if (!suppressAutoScroll && !isFullyVisible(thumb, thumbnailScrollRoot)) { + thumb.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "nearest" + }); + } } }, { root: scrollRoot, @@ -11958,11 +12148,26 @@ section.${c} { width: auto !important; max-width: 100%; min-width: 0; box-sizing } return { dispose() { + removeScrollListener(); + if (scrollEndTimer !== undefined) { + clearTimeout(scrollEndTimer); + scrollEndTimer = undefined; + } + if (cancelScheduledRender !== undefined) { + cancelScheduledRender(); + cancelScheduledRender = undefined; + } if (observer) { observer.disconnect(); observer = null; } + if (thumbObserver) { + thumbObserver.disconnect(); + thumbObserver = null; + } thumbnailContainer.innerHTML = ''; + renderQueue.length = 0; + rendering = false; for (const section of sections) { section.querySelectorAll('[data-docxjs-page-anchor]').forEach(n => n.remove()); } diff --git a/dist/docx-preview.js.map b/dist/docx-preview.js.map index b907692a..468bbb1f 100644 --- a/dist/docx-preview.js.map +++ b/dist/docx-preview.js.map @@ -1 +1 @@ -{"version":3,"file":"docx-preview.js","sources":["../src/common/relationship.ts","../src/utils.ts","../src/document/common.ts","../src/parser/xml-parser.ts","../src/common/part.ts","../src/font-table/fonts.ts","../src/font-table/font-table.ts","../src/common/content-types.ts","../src/common/open-xml-package.ts","../src/document/document-part.ts","../src/document/border.ts","../src/document/section.ts","../src/document/line-spacing.ts","../src/document/run.ts","../src/document/paragraph.ts","../src/numbering/numbering.ts","../src/numbering/numbering-part.ts","../src/styles/styles-part.ts","../src/document/dom.ts","../src/header-footer/elements.ts","../src/header-footer/parts.ts","../src/document-props/extended-props.ts","../src/document-props/extended-props-part.ts","../src/document-props/core-props.ts","../src/document-props/core-props-part.ts","../src/theme/theme.ts","../src/theme/theme-part.ts","../src/notes/elements.ts","../src/notes/parts.ts","../src/settings/settings.ts","../src/settings/settings-part.ts","../src/document-props/custom-props.ts","../src/document-props/custom-props-part.ts","../src/comments/comments-part.ts","../src/comments/comments-extended-part.ts","../src/drawing/theme.ts","../src/charts/chart-part.ts","../src/charts/chartex-part.ts","../src/smartart/smartart-parts.ts","../src/common/vector-image.ts","../src/word-document.ts","../src/document/bookmarks.ts","../src/vml/vml.ts","../src/comments/elements.ts","../src/document-parser.ts","../src/javascript.ts","../src/fields/instruction.ts","../src/html.ts","../src/drawing/shapes.ts","../src/charts/render.ts","../src/shared-classes.ts","../src/html-renderer.ts","../src/page-break.ts","../src/thumbnails.ts","../src/docx-preview.ts"],"sourcesContent":["import { XmlParser } from \"../parser/xml-parser\";\n\nexport interface Relationship {\n id: string,\n type: RelationshipTypes | string,\n target: string\n targetMode: \"\" | \"External\" | string \n}\n\nexport enum RelationshipTypes {\n OfficeDocument = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\",\n FontTable = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable\",\n Image = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\",\n Numbering = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering\",\n Styles = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\",\n StylesWithEffects = \"http://schemas.microsoft.com/office/2007/relationships/stylesWithEffects\",\n Theme = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme\",\n Settings = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings\",\n WebSettings = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings\",\n Hyperlink = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink\",\n Footnotes = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes\",\n\tEndnotes = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes\",\n Footer = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer\",\n Header = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header\",\n ExtendedProperties = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties\",\n CoreProperties = \"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties\",\n\tCustomProperties = \"http://schemas.openxmlformats.org/package/2006/relationships/metadata/custom-properties\",\n\tComments = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments\",\n CommentsExtended = \"http://schemas.microsoft.com/office/2011/relationships/commentsExtended\",\n AltChunk = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk\",\n Chart = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart\",\n // Modern 2013+ chart parts. Relationship type differs from Chart;\n // loaded as a `ChartExPart` and rendered as a placeholder. See\n // src/charts/chartex-part.ts.\n ChartEx = \"http://schemas.microsoft.com/office/2014/relationships/chartEx\",\n // SmartArt — five related parts referenced from :\n // r:dm → /word/diagrams/data*.xml (data model)\n // r:lo → /word/diagrams/layout*.xml (layout definition)\n // r:qs → /word/diagrams/quickStyle*.xml (quick-style)\n // r:cs → /word/diagrams/colors*.xml (colour transform)\n // A sixth, `diagramDrawing`, may be present on some files and points at\n // an extra cached drawing part that Word uses to persist manual layout\n // overrides. Registering these types makes the package resolver walk\n // their own relationships so any images they embed get preloaded. We\n // do not currently parse the diagram XML itself — the Fallback drawing\n // is what reaches the DOM (see parseSmartArtReference in\n // document-parser.ts). See also TODO.md \"SmartArt ()\".\n DiagramData = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData\",\n DiagramLayout = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout\",\n DiagramQuickStyle = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle\",\n DiagramColors = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors\",\n DiagramDrawing = \"http://schemas.microsoft.com/office/2007/relationships/diagramDrawing\",\n // Glossary document (building blocks / auto-text). A full document part\n // at /word/glossary/document.xml; docxjs parses it with the same\n // pipeline as the main body but does not render it by default.\n GlossaryDocument = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument\",\n // Custom XML data ( targets). One or more parts at\n // /customXml/item*.xml, each with a companion itemProps*.xml rel\n // that carries the ds:itemID (the GUID we match against storeItemID\n // on ).\n CustomXml = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml\",\n CustomXmlProps = \"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps\",\n}\n\nexport function parseRelationships(root: Element, xml: XmlParser): Relationship[] {\n return xml.elements(root).map(e => {\n id: xml.attr(e, \"Id\"),\n type: xml.attr(e, \"Type\"),\n target: xml.attr(e, \"Target\"),\n targetMode: xml.attr(e, \"TargetMode\")\n });\n}","export function escapeClassName(className: string) {\n\treturn className?.replace(/[ .]+/g, '-').replace(/[&]+/g, 'and').toLowerCase();\n}\n\nexport function encloseFontFamily(fontFamily: string): string {\n return /^[^\"'].*\\s.*[^\"']$/.test(fontFamily) ? `'${fontFamily}'` : fontFamily;\n}\n\n/**\n * Returns a CSS font-family value safe to interpolate into a stylesheet or\n * inline style. Strips backslashes and quotes, then wraps the identifier in\n * single quotes. Attacker-controlled DOCX strings can otherwise break out of\n * the property and inject new CSS rules (see SECURITY_REVIEW.md #4).\n * Falls back to `sans-serif` for empty / non-string input.\n */\nexport function sanitizeFontFamily(value: unknown): string {\n if (typeof value !== 'string') return 'sans-serif';\n const cleaned = value.replace(/[\"'\\\\;{}@<>]/g, '').trim();\n if (!cleaned) return 'sans-serif';\n return `'${cleaned}'`;\n}\n\n// Accept only pure hex-digit color values (3, 4, 6, or 8 hex digits). Anything\n// else — including `red}a{}@import …` — is rejected. See SECURITY_REVIEW.md #4.\nconst HEX_COLOR_RE = /^[0-9A-Fa-f]{3,8}$/;\nconst CSS_FN_COLOR_RE = /^(rgb|rgba|hsl|hsla)\\(\\s*[-0-9.,%\\s/deg]+\\s*\\)$/i;\n\n/**\n * Returns a safe CSS color string for a DOCX-derived value, or `null` if the\n * input can't be safely emitted. Accepts bare hex (returned with a leading\n * `#`), `#hex`, and `rgb()/rgba()/hsl()/hsla()` functions with numeric args.\n * See SECURITY_REVIEW.md #4.\n */\nexport function sanitizeCssColor(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const v = value.trim();\n if (!v) return null;\n if (HEX_COLOR_RE.test(v)) return `#${v}`;\n if (v.startsWith('#') && HEX_COLOR_RE.test(v.slice(1))) return v;\n if (CSS_FN_COLOR_RE.test(v)) return v;\n return null;\n}\n\n// CSS-identifier fragments we interpolate into custom property names and\n// selectors. Restricted to alphanum + underscore so attacker strings can't\n// break out of the rule. See SECURITY_REVIEW.md #3, #4.\nconst SAFE_CSS_IDENT_RE = /^[A-Za-z0-9_]+$/;\n\nexport function isSafeCssIdent(value: unknown): value is string {\n return typeof value === 'string' && SAFE_CSS_IDENT_RE.test(value);\n}\n\n/**\n * Escapes a string so it can be embedded inside a CSS string literal wrapped\n * in double quotes (`content: \"...\"`). Backslashes are doubled, and `\"` and\n * newline characters are CSS-escaped. See SECURITY_REVIEW.md #3.\n */\nexport function escapeCssStringContent(value: string): string {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\A ')\n .replace(/\\r/g, '\\\\D ');\n}\n\nexport function splitPath(path: string): [string, string] {\n let si = path.lastIndexOf('/') + 1;\n let folder = si == 0 ? \"\" : path.substring(0, si);\n let fileName = si == 0 ? path : path.substring(si);\n\n return [folder, fileName];\n}\n\nexport function resolvePath(path: string, base: string): string {\n try {\n const prefix = \"http://docx/\";\n const url = new URL(path, prefix + base).toString();\n return url.substring(prefix.length);\n } catch {\n return `${base}${path}`;\n }\n}\n\n// Keys that, if used as map keys on a plain `{}`, can mutate the prototype\n// chain (`__proto__`) or confuse later lookups (`constructor`, `prototype`).\n// DOCX strings feed these maps (comment/footnote/style ids, theme color\n// names), so we filter defensively even when the downstream code happens to\n// be safe. See SECURITY_REVIEW.md #6.\nconst UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nexport function keyBy(array: T[], by: (x: T) => any): Record {\n // `Object.create(null)` produces a null-prototype object: reads like\n // `m['toString']` return `undefined` rather than walking into\n // `Object.prototype`, and writes to `__proto__` set an own property\n // instead of mutating the prototype. Callers use `m[key]` / `for..in`,\n // both of which work identically on null-prototype and plain objects.\n const result: Record = Object.create(null);\n for (const x of array) {\n const k = by(x);\n if (k == null) continue;\n const s = String(k);\n if (UNSAFE_KEYS.has(s)) continue;\n result[s] = x;\n }\n return result;\n}\n\nexport function blobToBase64(blob: Blob): Promise {\n\treturn new Promise((resolve, reject) => {\n\t\tconst reader = new FileReader();\n\t\treader.onloadend = () => resolve(reader.result as string);\n\t\treader.onerror = () => reject();\n\t\treader.readAsDataURL(blob);\n\t});\n}\n\nexport function isObject(item) {\n return item && typeof item === 'object' && !Array.isArray(item);\n}\n\nexport function isString(item: unknown): item is string {\n return typeof item === 'string' || item instanceof String;\n}\n\nexport function mergeDeep(target, ...sources) {\n if (!sources.length)\n return target;\n\n const source = sources.shift();\n\n if (isObject(target) && isObject(source)) {\n for (const key in source) {\n // Skip unsafe keys (`__proto__`, `constructor`, `prototype`) and\n // inherited properties so attacker-controlled DOCX values never\n // poison the prototype chain. See SECURITY_REVIEW.md #6.\n if (UNSAFE_KEYS.has(key)) continue;\n if (!Object.prototype.hasOwnProperty.call(source, key)) continue;\n\n if (isObject(source[key])) {\n const val = target[key] ?? (target[key] = {});\n mergeDeep(val, source[key]);\n } else {\n target[key] = source[key];\n }\n }\n }\n\n return mergeDeep(target, ...sources);\n}\n\nexport function parseCssRules(text: string): Record {\n\tconst result: Record = {};\n\n\tfor (const rule of text.split(';')) {\n\t\tconst [key, val] = rule.split(':');\n\t\tresult[key] = val;\n\t}\n\n\treturn result\n}\n\nexport function formatCssRules(style: Record): string {\n\treturn Object.entries(style).map((k, v) => `${k}: ${v}`).join(';');\n}\n\nexport function asArray(val: T | T[]): T[] {\n\treturn Array.isArray(val) ? val : [val];\n}\n\nexport function clamp(val, min, max) {\n return min > val ? min : (max < val ? max : val);\n}","import { XmlParser } from \"../parser/xml-parser\";\nimport { clamp } from \"../utils\";\n\nexport const ns = {\n wordml: \"http://schemas.openxmlformats.org/wordprocessingml/2006/main\",\n drawingml: \"http://schemas.openxmlformats.org/drawingml/2006/main\",\n picture: \"http://schemas.openxmlformats.org/drawingml/2006/picture\",\n\tcompatibility: \"http://schemas.openxmlformats.org/markup-compatibility/2006\",\n\tmath: \"http://schemas.openxmlformats.org/officeDocument/2006/math\"\n}\n\nexport type LengthType = \"px\" | \"pt\" | \"%\" | \"\";\nexport type Length = string;\n\nexport interface Font {\n name: string;\n family: string;\n}\n\nexport interface CommonProperties {\n fontSize: Length;\n color: string;\n}\n\nexport type LengthUsageType = { mul: number, unit: LengthType, min?: number, max?: number };\n\nexport const LengthUsage: Record = {\n Dxa: { mul: 0.05, unit: \"pt\" }, //twips\n Emu: { mul: 1 / 12700, unit: \"pt\" },\n FontSize: { mul: 0.5, unit: \"pt\" },\n Border: { mul: 0.125, unit: \"pt\", min: 0.25, max: 12 }, //NOTE: http://officeopenxml.com/WPtextBorders.php\n Point: { mul: 1, unit: \"pt\" },\n Percent: { mul: 0.02, unit: \"%\" },\n LineHeight: { mul: 1 / 240, unit: \"\" },\n VmlEmu: { mul: 1 / 12700, unit: \"\" },\n}\n\nexport function convertLength(val: string, usage: LengthUsageType = LengthUsage.Dxa): string {\n //\"simplified\" docx documents use pt's as units\n if (val == null || /.+(p[xt]|[%])$/.test(val)) {\n return val;\n }\n\n var num = parseInt(val) * usage.mul;\n\n if (usage.min && usage.max)\n num = clamp(num, usage.min, usage.max);\n \n\treturn `${num.toFixed(2)}${usage.unit}`;\n}\n\nexport function convertBoolean(v: string, defaultValue = false): boolean {\n switch (v) {\n case \"1\": return true;\n case \"0\": return false;\n case \"on\": return true;\n case \"off\": return false;\n case \"true\": return true;\n case \"false\": return false;\n default: return defaultValue;\n }\n}\n\nexport function convertPercentage(val: string): number {\n return val ? parseInt(val) / 100 : null;\n}\n\nexport function parseCommonProperty(elem: Element, props: CommonProperties, xml: XmlParser): boolean {\n if(elem.namespaceURI != ns.wordml)\n return false;\n\n switch(elem.localName) {\n case \"color\": \n props.color = xml.attr(elem, \"val\");\n break;\n\n case \"sz\":\n props.fontSize = xml.lengthAttr(elem, \"val\", LengthUsage.FontSize);\n break;\n\n default:\n return false;\n }\n\n return true;\n}","import { Length, LengthUsage, LengthUsageType, convertLength, convertBoolean } from \"../document/common\";\n\nexport function parseXmlString(xmlString: string, trimXmlDeclaration: boolean = false): Document {\n if (trimXmlDeclaration)\n xmlString = xmlString.replace(/<[?].*[?]>/, \"\");\n \n xmlString = removeUTF8BOM(xmlString);\n \n const result = new DOMParser().parseFromString(xmlString, \"application/xml\"); \n const errorText = hasXmlParserError(result);\n\n if (errorText)\n throw new Error(errorText);\n\n return result;\n}\n\nfunction hasXmlParserError(doc: Document) {\n return doc.getElementsByTagName(\"parsererror\")[0]?.textContent;\n}\n\nfunction removeUTF8BOM(data: string) {\n return data.charCodeAt(0) === 0xFEFF ? data.substring(1) : data;\n}\n\nexport function serializeXmlString(elem: Node): string {\n return new XMLSerializer().serializeToString(elem);\n}\n\nexport class XmlParser {\n elements(elem: Element, localName: string = null): Element[] {\n const result = [];\n\n for (let i = 0, l = elem.childNodes.length; i < l; i++) {\n let c = elem.childNodes.item(i);\n\n if (c.nodeType == Node.ELEMENT_NODE && (localName == null || (c as Element).localName == localName))\n result.push(c);\n }\n\n return result;\n }\n\n element(elem: Element, localName: string): Element {\n for (let i = 0, l = elem.childNodes.length; i < l; i++) {\n let c = elem.childNodes.item(i);\n\n if (c.nodeType == 1 && (c as Element).localName == localName)\n return c as Element;\n }\n\n return null;\n }\n\n elementAttr(elem: Element, localName: string, attrLocalName: string): string {\n var el = this.element(elem, localName);\n return el ? this.attr(el, attrLocalName) : undefined;\n }\n\n\tattrs(elem: Element) {\n\t\treturn Array.from(elem.attributes);\n\t}\n\n attr(elem: Element, localName: string): string {\n for (let i = 0, l = elem.attributes.length; i < l; i++) {\n let a = elem.attributes.item(i);\n\n if (a.localName == localName)\n return a.value;\n }\n\n return null;\n }\n\n intAttr(node: Element, attrName: string, defaultValue: number = null): number {\n var val = this.attr(node, attrName);\n return val ? parseInt(val) : defaultValue;\n }\n\n\thexAttr(node: Element, attrName: string, defaultValue: number = null): number {\n var val = this.attr(node, attrName);\n return val ? parseInt(val, 16) : defaultValue;\n }\n\n floatAttr(node: Element, attrName: string, defaultValue: number = null): number {\n var val = this.attr(node, attrName);\n return val ? parseFloat(val) : defaultValue;\n }\n\n boolAttr(node: Element, attrName: string, defaultValue: boolean = null) {\n return convertBoolean(this.attr(node, attrName), defaultValue);\n }\n\n lengthAttr(node: Element, attrName: string, usage: LengthUsageType = LengthUsage.Dxa): Length {\n return convertLength(this.attr(node, attrName), usage);\n }\n}\n\nconst globalXmlParser = new XmlParser();\n\nexport default globalXmlParser;","import { serializeXmlString } from \"../parser/xml-parser\";\nimport { OpenXmlPackage } from \"./open-xml-package\";\nimport { Relationship } from \"./relationship\";\n\nexport class Part {\n protected _xmlDocument: Document;\n\n rels: Relationship[];\n\n constructor(protected _package: OpenXmlPackage, public path: string) {\n }\n\n async load(): Promise {\n this.rels = await this._package.loadRelationships(this.path);\n\n const xmlText = await this._package.load(this.path);\n const xmlDoc = this._package.parseXmlDocument(xmlText);\n\n if (this._package.options.keepOrigin) {\n this._xmlDocument = xmlDoc;\n }\n\n this.parseXml(xmlDoc.firstElementChild);\n }\n\n save() {\n this._package.update(this.path, serializeXmlString(this._xmlDocument));\n }\n\n protected parseXml(root: Element) {\n }\n}","import { XmlParser } from \"../parser/xml-parser\";\n\nconst embedFontTypeMap = {\n embedRegular: 'regular',\n embedBold: 'bold',\n embedItalic: 'italic',\n embedBoldItalic: 'boldItalic',\n}\n\nexport interface FontDeclaration {\n name: string,\n altName: string,\n family: string,\n embedFontRefs: EmbedFontRef[];\n}\n\nexport interface EmbedFontRef {\n id: string;\n key: string;\n type: 'regular' | 'bold' | 'italic' | 'boldItalic';\n}\n\nexport function parseFonts(root: Element, xml: XmlParser): FontDeclaration[] {\n return xml.elements(root).map(el => parseFont(el, xml));\n}\n\nexport function parseFont(elem: Element, xml: XmlParser): FontDeclaration {\n let result = {\n name: xml.attr(elem, \"name\"),\n embedFontRefs: []\n };\n\n for (let el of xml.elements(elem)) {\n switch (el.localName) {\n case \"family\":\n result.family = xml.attr(el, \"val\");\n break;\n\n case \"altName\":\n result.altName = xml.attr(el, \"val\");\n break;\n\n case \"embedRegular\":\n case \"embedBold\":\n case \"embedItalic\":\n case \"embedBoldItalic\":\n result.embedFontRefs.push(parseEmbedFontRef(el, xml));\n break;\n }\n }\n\n return result;\n}\n\nexport function parseEmbedFontRef(elem: Element, xml: XmlParser): EmbedFontRef {\n return { \n id: xml.attr(elem, \"id\"), \n key: xml.attr(elem, \"fontKey\"),\n type: embedFontTypeMap[elem.localName]\n };\n}","import { Part } from \"../common/part\";\nimport { FontDeclaration, parseFonts } from \"./fonts\";\n\nexport class FontTablePart extends Part {\n fonts: FontDeclaration[];\n\n parseXml(root: Element) {\n this.fonts = parseFonts(root, this._package.xmlParser);\n }\n}","import { XmlParser } from \"../parser/xml-parser\";\n\nexport interface ContentType {\n extension: string,\n partName: string,\n contentType: string;\n}\n\nexport function parseContentTypes(root: Element, xml: XmlParser): ContentType[] {\n return xml.elements(root).map(e => {\n extension: xml.attr(e, \"Extension\"),\n partName: xml.attr(e, \"PartName\"),\n contentType: xml.attr(e, \"ContentType\")\n });\n}","import JSZip from \"jszip\";\nimport { parseXmlString, XmlParser } from \"../parser/xml-parser\";\nimport { splitPath } from \"../utils\";\nimport { parseRelationships, Relationship } from \"./relationship\";\nimport { ContentType, parseContentTypes } from \"./content-types\";\n\nexport interface OpenXmlPackageOptions {\n trimXmlDeclaration: boolean,\n keepOrigin: boolean,\n}\n\nexport class OpenXmlPackage {\n xmlParser: XmlParser = new XmlParser();\n\n constructor(private _zip: JSZip, public options: OpenXmlPackageOptions) {\n }\n\n get(path: string): any {\n const p = normalizePath(path);\n return this._zip.files[p] ?? this._zip.files[p.replace(/\\//g, '\\\\')];\n }\n\n update(path: string, content: any) {\n this._zip.file(path, content);\n }\n\n static async load(input: Blob | any, options: OpenXmlPackageOptions): Promise {\n const zip = await JSZip.loadAsync(input);\n\t\treturn new OpenXmlPackage(zip, options);\n }\n\n save(type: any = \"blob\"): Promise {\n return this._zip.generateAsync({ type });\n }\n\n load(path: string, type: JSZip.OutputType = \"string\"): Promise {\n return this.get(path)?.async(type) ?? Promise.resolve(null);\n }\n\n async loadRelationships(path: string = null): Promise {\n let relsPath = `_rels/.rels`;\n\n if (path != null) {\n const [f, fn] = splitPath(path);\n relsPath = `${f}_rels/${fn}.rels`;\n }\n\n const txt = await this.load(relsPath);\n\t\treturn txt ? parseRelationships(this.parseXmlDocument(txt).firstElementChild, this.xmlParser) : null;\n }\n\n async loadContentTypes(): Promise {\n const txt = await this.load(\"[Content_Types].xml\");\n\t\treturn txt ? parseContentTypes(this.parseXmlDocument(txt).firstElementChild, this.xmlParser) : [];\n }\n\n /** @internal */\n parseXmlDocument(txt: string): Document {\n return parseXmlString(txt, this.options.trimXmlDeclaration);\n }\n}\n\nfunction normalizePath(path: string) {\n return path.startsWith('/') ? path.substr(1) : path;\n}","import { OpenXmlPackage } from \"../common/open-xml-package\";\nimport { Part } from \"../common/part\";\nimport { DocumentParser } from \"../document-parser\";\nimport { DocumentElement } from \"./document\";\n\nexport class DocumentPart extends Part {\n private _documentParser: DocumentParser;\n\n constructor(pkg: OpenXmlPackage, path: string, parser: DocumentParser) {\n super(pkg, path);\n this._documentParser = parser;\n }\n \n body: DocumentElement\n\n parseXml(root: Element) {\n this.body = this._documentParser.parseDocumentFile(root);\n }\n}","import { XmlParser } from \"../parser/xml-parser\";\nimport { Length, LengthUsage } from \"./common\";\n\nexport interface Border {\n color: string;\n type: string;\n size: Length;\n frame: boolean;\n shadow: boolean;\n offset: Length;\n}\n\nexport interface Borders {\n top: Border;\n left: Border;\n right: Border;\n bottom: Border;\n}\n\nexport function parseBorder(elem: Element, xml: XmlParser): Border {\n return {\n type: xml.attr(elem, \"val\"),\n color: xml.attr(elem, \"color\"),\n size: xml.lengthAttr(elem, \"sz\", LengthUsage.Border),\n offset: xml.lengthAttr(elem, \"space\", LengthUsage.Point),\n frame: xml.boolAttr(elem, 'frame'),\n shadow: xml.boolAttr(elem, 'shadow')\n };\n}\n\nexport function parseBorders(elem: Element, xml: XmlParser): Borders {\n var result = {};\n\n for (let e of xml.elements(elem)) {\n switch (e.localName) {\n case \"left\": result.left = parseBorder(e, xml); break;\n case \"top\": result.top = parseBorder(e, xml); break;\n case \"right\": result.right = parseBorder(e, xml); break;\n case \"bottom\": result.bottom = parseBorder(e, xml); break;\n }\n }\n\n return result;\n}","import globalXmlParser, { XmlParser } from \"../parser/xml-parser\";\nimport { Borders, parseBorders } from \"./border\";\nimport { Length } from \"./common\";\n\nexport interface Column {\n space: Length;\n width: Length;\n}\n\nexport interface Columns {\n space: Length;\n numberOfColumns: number;\n separator: boolean;\n equalWidth: boolean;\n columns: Column[];\n}\n\nexport interface LineNumbering {\n countBy: number;\n start: number;\n distance: Length;\n restart: \"newPage\" | \"newSection\" | \"continuous\" | string;\n}\n\nexport interface DocGrid {\n type: \"default\" | \"lines\" | \"linesAndChars\" | \"snapToChars\" | string;\n /** Line pitch in twentieths of a point (1/20 pt). */\n linePitch: number;\n /** Character pitch in twentieths of a point; parsed but unused for non-default grids. */\n charSpace: number;\n}\n\nexport interface PageSize {\n width: Length, \n height: Length, \n orientation: \"landscape\" | string \n}\n\nexport interface PageNumber {\n start: number;\n chapSep: \"colon\" | \"emDash\" | \"endash\" | \"hyphen\" | \"period\" | string;\n chapStyle: string;\n format: \"none\" | \"cardinalText\" | \"decimal\" | \"decimalEnclosedCircle\" | \"decimalEnclosedFullstop\" \n | \"decimalEnclosedParen\" | \"decimalZero\" | \"lowerLetter\" | \"lowerRoman\"\n | \"ordinalText\" | \"upperLetter\" | \"upperRoman\" | string;\n}\n\nexport interface PageMargins {\n top: Length;\n right: Length;\n bottom: Length;\n left: Length;\n header: Length;\n footer: Length;\n gutter: Length;\n}\n\nexport enum SectionType {\n Continuous = \"continuous\",\n NextPage = \"nextPage\", \n NextColumn = \"nextColumn\",\n EvenPage = \"evenPage\",\n OddPage = \"oddPage\",\n}\n\nexport interface FooterHeaderReference {\n id: string;\n type: string | \"first\" | \"even\" | \"default\";\n}\n\nexport interface SectionProperties {\n type: SectionType | string;\n pageSize: PageSize,\n pageMargins: PageMargins,\n pageBorders: Borders;\n pageNumber: PageNumber;\n columns: Columns;\n footerRefs: FooterHeaderReference[];\n headerRefs: FooterHeaderReference[];\n titlePage: boolean;\n lineNumbering: LineNumbering;\n docGrid: DocGrid;\n mirrorMargins: boolean;\n}\n\nexport function parseSectionProperties(elem: Element, xml: XmlParser = globalXmlParser): SectionProperties {\n var section = {};\n\n for (let e of xml.elements(elem)) {\n switch (e.localName) {\n case \"pgSz\":\n section.pageSize = {\n width: xml.lengthAttr(e, \"w\"),\n height: xml.lengthAttr(e, \"h\"),\n orientation: xml.attr(e, \"orient\")\n }\n break;\n\n case \"type\":\n section.type = xml.attr(e, \"val\");\n break;\n\n case \"pgMar\":\n section.pageMargins = {\n left: xml.lengthAttr(e, \"left\"),\n right: xml.lengthAttr(e, \"right\"),\n top: xml.lengthAttr(e, \"top\"),\n bottom: xml.lengthAttr(e, \"bottom\"),\n header: xml.lengthAttr(e, \"header\"),\n footer: xml.lengthAttr(e, \"footer\"),\n gutter: xml.lengthAttr(e, \"gutter\"),\n };\n break;\n\n case \"cols\":\n section.columns = parseColumns(e, xml);\n break;\n\n case \"headerReference\":\n (section.headerRefs ?? (section.headerRefs = [])).push(parseFooterHeaderReference(e, xml)); \n break;\n \n case \"footerReference\":\n (section.footerRefs ?? (section.footerRefs = [])).push(parseFooterHeaderReference(e, xml)); \n break;\n\n case \"titlePg\":\n section.titlePage = xml.boolAttr(e, \"val\", true);\n break;\n\n case \"pgBorders\":\n section.pageBorders = parseBorders(e, xml);\n break;\n\n case \"pgNumType\":\n section.pageNumber = parsePageNumber(e, xml);\n break;\n\n case \"lnNumType\":\n section.lineNumbering = parseLineNumbering(e, xml);\n break;\n\n case \"docGrid\":\n section.docGrid = parseDocGrid(e, xml);\n break;\n\n case \"mirrorMargins\":\n section.mirrorMargins = xml.boolAttr(e, \"val\", true);\n break;\n }\n }\n\n return section;\n}\n\nfunction parseLineNumbering(elem: Element, xml: XmlParser): LineNumbering {\n return {\n countBy: xml.intAttr(elem, \"countBy\", 1),\n start: xml.intAttr(elem, \"start\", 1),\n distance: xml.lengthAttr(elem, \"distance\"),\n restart: xml.attr(elem, \"restart\") || \"newPage\",\n };\n}\n\nfunction parseDocGrid(elem: Element, xml: XmlParser): DocGrid {\n return {\n type: xml.attr(elem, \"type\") || \"default\",\n linePitch: xml.intAttr(elem, \"linePitch\", 0),\n charSpace: xml.intAttr(elem, \"charSpace\", 0),\n };\n}\n\nfunction parseColumns(elem: Element, xml: XmlParser): Columns {\n return {\n numberOfColumns: xml.intAttr(elem, \"num\"),\n space: xml.lengthAttr(elem, \"space\"),\n separator: xml.boolAttr(elem, \"sep\"),\n equalWidth: xml.boolAttr(elem, \"equalWidth\", true),\n columns: xml.elements(elem, \"col\")\n .map(e => {\n width: xml.lengthAttr(e, \"w\"),\n space: xml.lengthAttr(e, \"space\")\n })\n };\n}\n\nfunction parsePageNumber(elem: Element, xml: XmlParser): PageNumber {\n return {\n chapSep: xml.attr(elem, \"chapSep\"),\n chapStyle: xml.attr(elem, \"chapStyle\"),\n format: xml.attr(elem, \"fmt\"),\n start: xml.intAttr(elem, \"start\")\n };\n}\n\nfunction parseFooterHeaderReference(elem: Element, xml: XmlParser): FooterHeaderReference {\n return {\n id: xml.attr(elem, \"id\"),\n type: xml.attr(elem, \"type\"),\n }\n}","import { XmlParser } from \"../parser/xml-parser\";\nimport { Length } from \"./common\";\n\nexport interface LineSpacing {\n after: Length;\n before: Length;\n line: number;\n lineRule: \"atLeast\" | \"exactly\" | \"auto\";\n}\n\nexport function parseLineSpacing(elem: Element, xml: XmlParser): LineSpacing {\n return {\n before: xml.lengthAttr(elem, \"before\"),\n after: xml.lengthAttr(elem, \"after\"),\n line: xml.intAttr(elem, \"line\"),\n lineRule: xml.attr(elem, \"lineRule\")\n } as LineSpacing;\n}","import { XmlParser } from \"../parser/xml-parser\";\nimport { CommonProperties, parseCommonProperty } from \"./common\";\nimport { OpenXmlElement } from \"./dom\";\n\nexport interface WmlRun extends OpenXmlElement, RunProperties {\n id?: string;\n verticalAlign?: string;\n\tfieldRun?: boolean;\n\t// w:rPr/w:fitText — target width in twips (1/20 pt) and optional id\n\t// grouping consecutive fitText runs. Parsed via parseFloat so no raw\n\t// DOCX string reaches the renderer's CSS sink.\n\tfitText?: { width: number; id?: string };\n\t// w:rPr/w:bdo — explicit bidi override. The parser allowlists the raw\n\t// DOCX val against /^(ltr|rtl)$/; anything else is dropped.\n\tbidiOverride?: \"ltr\" | \"rtl\";\n}\n\nexport interface RunProperties extends CommonProperties {\n\n}\n\nexport function parseRunProperties(elem: Element, xml: XmlParser): RunProperties {\n let result = {};\n\n for(let el of xml.elements(elem)) {\n parseRunProperty(el, result, xml);\n }\n\n return result;\n}\n\nexport function parseRunProperty(elem: Element, props: RunProperties, xml: XmlParser) {\n if (parseCommonProperty(elem, props, xml))\n return true;\n\n return false;\n}","import { OpenXmlElement } from \"./dom\";\nimport { CommonProperties, Length, ns, parseCommonProperty } from \"./common\";\nimport { Borders } from \"./border\";\nimport { parseSectionProperties, SectionProperties } from \"./section\";\nimport { LineSpacing, parseLineSpacing } from \"./line-spacing\";\nimport { XmlParser } from \"../parser/xml-parser\";\nimport { parseRunProperties, RunProperties } from \"./run\";\n\nexport interface WmlParagraph extends OpenXmlElement, ParagraphProperties {\n paraId?: string;\n}\n\nexport interface ParagraphProperties extends CommonProperties {\n sectionProps: SectionProperties;\n tabs: ParagraphTab[];\n numbering: ParagraphNumbering;\n\n border: Borders;\n textAlignment: \"auto\" | \"baseline\" | \"bottom\" | \"center\" | \"top\" | string;\n lineSpacing: LineSpacing;\n keepLines: boolean;\n keepNext: boolean;\n pageBreakBefore: boolean;\n widowControl: boolean;\n outlineLevel: number;\n\tstyleName?: string;\n\n /** Drop-cap variant from w:framePr (`drop` = in-flow, `margin` = in margin). */\n dropCap?: \"drop\" | \"margin\";\n /** How many lines the drop cap spans (w:lines on w:framePr). Default 3. */\n dropCapLines?: number;\n\n runProps: RunProperties;\n}\n\nexport interface ParagraphTab {\n style: \"bar\" | \"center\" | \"clear\" | \"decimal\" | \"end\" | \"num\" | \"start\" | \"left\" | \"right\";\n leader: \"none\" | \"dot\" | \"heavy\" | \"hyphen\" | \"middleDot\" | \"underscore\";\n position: Length;\n}\n\nexport interface ParagraphNumbering {\n id: string;\n level: number;\n}\n\nexport function parseParagraphProperties(elem: Element, xml: XmlParser): ParagraphProperties {\n let result = {};\n\n for(let el of xml.elements(elem)) {\n parseParagraphProperty(el, result, xml);\n }\n\n return result;\n}\n\nexport function parseParagraphProperty(elem: Element, props: ParagraphProperties, xml: XmlParser) {\n if (elem.namespaceURI != ns.wordml)\n return false;\n\n if(parseCommonProperty(elem, props, xml))\n return true;\n\n switch (elem.localName) {\n case \"tabs\":\n props.tabs = parseTabs(elem, xml);\n break;\n\n case \"sectPr\":\n props.sectionProps = parseSectionProperties(elem, xml);\n break;\n\n case \"numPr\":\n props.numbering = parseNumbering(elem, xml);\n break;\n \n case \"spacing\":\n props.lineSpacing = parseLineSpacing(elem, xml);\n return false; // TODO\n break;\n\n case \"textAlignment\":\n props.textAlignment = xml.attr(elem, \"val\");\n return false; //TODO\n break;\n\n case \"keepLines\":\n props.keepLines = xml.boolAttr(elem, \"val\", true);\n break;\n \n case \"keepNext\":\n props.keepNext = xml.boolAttr(elem, \"val\", true);\n break;\n \n case \"pageBreakBefore\":\n props.pageBreakBefore = xml.boolAttr(elem, \"val\", true);\n break;\n\n case \"widowControl\":\n props.widowControl = xml.boolAttr(elem, \"val\", true);\n break;\n\n case \"outlineLvl\":\n props.outlineLevel = xml.intAttr(elem, \"val\");\n break;\n\n case \"pStyle\":\n props.styleName = xml.attr(elem, \"val\");\n break;\n\n case \"rPr\":\n props.runProps = parseRunProperties(elem, xml);\n break;\n \n default:\n return false;\n }\n\n return true;\n}\n\nexport function parseTabs(elem: Element, xml: XmlParser): ParagraphTab[] {\n return xml.elements(elem, \"tab\")\n .map(e => {\n position: xml.lengthAttr(e, \"pos\"),\n leader: xml.attr(e, \"leader\"),\n style: xml.attr(e, \"val\")\n });\n}\n\nexport function parseNumbering(elem: Element, xml: XmlParser): ParagraphNumbering {\n var result = {};\n\n for (let e of xml.elements(elem)) {\n switch (e.localName) {\n case \"numId\":\n result.id = xml.attr(e, \"val\");\n break;\n\n case \"ilvl\":\n result.level = xml.intAttr(e, \"val\");\n break;\n }\n }\n\n return result;\n}","import { NumberingPicBullet } from \"../document/dom\";\nimport { ParagraphProperties, parseParagraphProperties } from \"../document/paragraph\";\nimport { parseRunProperties, RunProperties } from \"../document/run\";\nimport { XmlParser } from \"../parser/xml-parser\";\n\nexport interface NumberingPartProperties {\n numberings: Numbering[];\n abstractNumberings: AbstractNumbering[];\n bulletPictures: NumberingBulletPicture[];\n}\n\nexport interface Numbering {\n id: string;\n abstractId: string;\n overrides: NumberingLevelOverride[];\n}\n\nexport interface NumberingLevelOverride {\n level: number;\n start: number;\n numberingLevel: NumberingLevel;\n}\n\nexport interface AbstractNumbering {\n id: string;\n name: string;\n multiLevelType: \"singleLevel\" | \"multiLevel\" | \"hybridMultilevel\" | string;\n levels: NumberingLevel[];\n numberingStyleLink: string;\n styleLink: string;\n}\n\nexport interface NumberingLevel {\n level: number;\n start: string;\n restart: number;\n format: 'lowerRoman' | 'lowerLetter' | string;\n text: string;\n justification: string;\n bulletPictureId: string;\n paragraphStyle: string;\n paragraphProps: ParagraphProperties;\n runProps: RunProperties;\n}\n\nexport interface NumberingBulletPicture {\n id: string;\n referenceId: string;\n style: string;\n}\n\nexport function parseNumberingPart(elem: Element, xml: XmlParser): NumberingPartProperties {\n let result: NumberingPartProperties = {\n numberings: [],\n abstractNumberings: [],\n bulletPictures: []\n }\n \n for (let e of xml.elements(elem)) {\n switch (e.localName) {\n case \"num\":\n result.numberings.push(parseNumbering(e, xml));\n break;\n case \"abstractNum\":\n result.abstractNumberings.push(parseAbstractNumbering(e, xml));\n break;\n case \"numPicBullet\":\n result.bulletPictures.push(parseNumberingBulletPicture(e, xml));\n break;\n }\n }\n\n return result;\n}\n\nexport function parseNumbering(elem: Element, xml: XmlParser): Numbering {\n let result = {\n id: xml.attr(elem, 'numId'),\n overrides: []\n };\n\n for (let e of xml.elements(elem)) {\n switch (e.localName) {\n case \"abstractNumId\":\n result.abstractId = xml.attr(e, \"val\");\n break;\n case \"lvlOverride\":\n result.overrides.push(parseNumberingLevelOverrride(e, xml));\n break;\n }\n }\n\n return result;\n}\n\nexport function parseAbstractNumbering(elem: Element, xml: XmlParser): AbstractNumbering {\n let result = {\n id: xml.attr(elem, 'abstractNumId'),\n levels: []\n };\n\n for (let e of xml.elements(elem)) {\n switch (e.localName) {\n case \"name\":\n result.name = xml.attr(e, \"val\");\n break;\n case \"multiLevelType\":\n result.multiLevelType = xml.attr(e, \"val\");\n break;\n case \"numStyleLink\":\n result.numberingStyleLink = xml.attr(e, \"val\");\n break;\n case \"styleLink\":\n result.styleLink = xml.attr(e, \"val\");\n break;\n case \"lvl\":\n result.levels.push(parseNumberingLevel(e, xml));\n break;\n }\n }\n\n return result;\n}\n\nexport function parseNumberingLevel(elem: Element, xml: XmlParser): NumberingLevel {\n let result = {\n level: xml.intAttr(elem, 'ilvl')\n };\n\n for (let e of xml.elements(elem)) {\n switch (e.localName) {\n case \"start\":\n result.start = xml.attr(e, \"val\");\n break;\n case \"lvlRestart\":\n result.restart = xml.intAttr(e, \"val\");\n break;\n case \"numFmt\":\n result.format = xml.attr(e, \"val\");\n break;\n case \"lvlText\":\n result.text = xml.attr(e, \"val\");\n break;\n case \"lvlJc\":\n result.justification = xml.attr(e, \"val\");\n break;\n case \"lvlPicBulletId\":\n result.bulletPictureId = xml.attr(e, \"val\");\n break;\n case \"pStyle\":\n result.paragraphStyle = xml.attr(e, \"val\");\n break;\n case \"pPr\":\n result.paragraphProps = parseParagraphProperties(e, xml);\n break;\n case \"rPr\":\n result.runProps = parseRunProperties(e, xml);\n break;\n }\n }\n\n return result;\n}\n\nexport function parseNumberingLevelOverrride(elem: Element, xml: XmlParser): NumberingLevelOverride {\n let result = {\n level: xml.intAttr(elem, 'ilvl')\n };\n\n for (let e of xml.elements(elem)) {\n switch (e.localName) {\n case \"startOverride\":\n result.start = xml.intAttr(e, \"val\");\n break;\n case \"lvl\":\n result.numberingLevel = parseNumberingLevel(e, xml);\n break;\n }\n }\n\n return result;\n}\n\nexport function parseNumberingBulletPicture(elem: Element, xml: XmlParser): NumberingBulletPicture {\n //TODO\n var pict = xml.element(elem, \"pict\");\n var shape = pict && xml.element(pict, \"shape\");\n var imagedata = shape && xml.element(shape, \"imagedata\");\n\n return imagedata ? {\n id: xml.attr(elem, \"numPicBulletId\"),\n referenceId: xml.attr(imagedata, \"id\"),\n style: xml.attr(shape, \"style\")\n } : null;\n}","import { OpenXmlPackage } from \"../common/open-xml-package\";\nimport { Part } from \"../common/part\";\nimport { DocumentParser } from \"../document-parser\";\nimport { IDomNumbering } from \"../document/dom\";\nimport { AbstractNumbering, Numbering, NumberingBulletPicture, NumberingPartProperties, parseNumberingPart } from \"./numbering\";\n\nexport class NumberingPart extends Part implements NumberingPartProperties {\n private _documentParser: DocumentParser;\n\n constructor(pkg: OpenXmlPackage, path: string, parser: DocumentParser) {\n super(pkg, path);\n this._documentParser = parser;\n }\n\n numberings: Numbering[];\n abstractNumberings: AbstractNumbering[];\n bulletPictures: NumberingBulletPicture[];\n \n domNumberings: IDomNumbering[];\n\n parseXml(root: Element) {\n Object.assign(this, parseNumberingPart(root, this._package.xmlParser));\n this.domNumberings = this._documentParser.parseNumberingFile(root); \n }\n}","import { OpenXmlPackage } from \"../common/open-xml-package\";\nimport { Part } from \"../common/part\";\nimport { DocumentParser } from \"../document-parser\";\nimport { IDomStyle } from \"../document/style\";\n\nexport class StylesPart extends Part {\n styles: IDomStyle[];\n\n private _documentParser: DocumentParser;\n\n constructor(pkg: OpenXmlPackage, path: string, parser: DocumentParser) {\n super(pkg, path);\n this._documentParser = parser;\n }\n\n parseXml(root: Element) {\n this.styles = this._documentParser.parseStylesFile(root); \n }\n}","export enum DomType {\n Document = \"document\",\n Paragraph = \"paragraph\",\n Run = \"run\",\n Break = \"break\",\n NoBreakHyphen = \"noBreakHyphen\",\n Table = \"table\",\n Row = \"row\",\n Cell = \"cell\",\n Hyperlink = \"hyperlink\",\n SmartTag = \"smartTag\",\n Drawing = \"drawing\",\n Image = \"image\",\n DrawingShape = \"drawingShape\",\n DrawingGroup = \"drawingGroup\",\n Text = \"text\",\n Tab = \"tab\",\n Symbol = \"symbol\",\n BookmarkStart = \"bookmarkStart\",\n BookmarkEnd = \"bookmarkEnd\",\n Footer = \"footer\",\n Header = \"header\",\n FootnoteReference = \"footnoteReference\", \n\tEndnoteReference = \"endnoteReference\",\n Footnote = \"footnote\",\n Endnote = \"endnote\",\n SimpleField = \"simpleField\",\n ComplexField = \"complexField\",\n Instruction = \"instruction\",\n\tVmlPicture = \"vmlPicture\",\n\tMmlMath = \"mmlMath\",\n\tMmlMathParagraph = \"mmlMathParagraph\",\n\tMmlFraction = \"mmlFraction\",\n\tMmlFunction = \"mmlFunction\",\n\tMmlFunctionName = \"mmlFunctionName\",\n\tMmlNumerator = \"mmlNumerator\",\n\tMmlDenominator = \"mmlDenominator\",\n\tMmlRadical = \"mmlRadical\",\n\tMmlBase = \"mmlBase\",\n\tMmlDegree = \"mmlDegree\",\n\tMmlSuperscript = \"mmlSuperscript\",\n\tMmlSubscript = \"mmlSubscript\",\n\tMmlPreSubSuper = \"mmlPreSubSuper\",\n\tMmlSubArgument = \"mmlSubArgument\",\n\tMmlSuperArgument = \"mmlSuperArgument\",\n\tMmlNary = \"mmlNary\",\n\tMmlDelimiter = \"mmlDelimiter\",\n\tMmlRun = \"mmlRun\",\n\tMmlEquationArray = \"mmlEquationArray\",\n\tMmlLimit = \"mmlLimit\",\n\tMmlLimitLower = \"mmlLimitLower\",\n\tMmlMatrix = \"mmlMatrix\",\n\tMmlMatrixRow = \"mmlMatrixRow\",\n\tMmlBox = \"mmlBox\",\n\tMmlBar = \"mmlBar\",\n\tMmlGroupChar = \"mmlGroupChar\",\n\tMmlAccent = \"mmlAccent\",\n\tMmlBorderBox = \"mmlBorderBox\",\n\tMmlSubSuperscript = \"mmlSubSuperscript\",\n\tMmlPhantom = \"mmlPhantom\",\n\tMmlGroup = \"mmlGroup\",\n\tVmlElement = \"vmlElement\",\n\tInserted = \"inserted\",\n\tDeleted = \"deleted\",\n\tDeletedText = \"deletedText\",\n\tMoveFrom = \"moveFrom\",\n\tMoveTo = \"moveTo\",\n\tComment = \"comment\",\n\tCommentReference = \"commentReference\",\n\tCommentRangeStart = \"commentRangeStart\",\n\tCommentRangeEnd = \"commentRangeEnd\",\n AltChunk = \"altChunk\",\n Sdt = \"sdt\",\n // East-Asian / bidi typography (see document-parser.ts parseRuby,\n // parseFitText, parseBidiOverride). Ruby wraps rubyBase and rt children;\n // the renderer emits native HTML . FitText wraps a run with a\n // target width in twips. BidiOverride wraps content in .\n Ruby = \"ruby\",\n RubyBase = \"rubyBase\",\n RubyText = \"rubyText\",\n FitText = \"fitText\",\n BidiOverride = \"bidiOverride\",\n Chart = \"chart\",\n // Modern 2013+ chart parts (sunburst / waterfall / funnel / treemap /\n // histogram / pareto / box-whisker). Rendered as a labelled\n // placeholder rather than a real SVG chart. See\n // src/charts/chartex-part.ts and the renderChartEx branch of\n // html-renderer.ts.\n ChartEx = \"chartEx\",\n // SmartArt reference that could not be rendered as its Fallback\n // drawing (either no mc:Fallback existed or its content was\n // unrecognised). Rendered as a labelled placeholder with the\n // layout URN carried in a data attribute. See\n // parseSmartArtReference in document-parser.ts.\n SmartArt = \"smartArt\",\n // — legacy embedded OLE content (Equation.3,\n // Excel spreadsheets, generic packaged files). Rendered as a styled\n // placeholder span; the embedded payload itself is not decoded. The\n // ProgID is carried via an allowlisted dataset attribute so it's safe\n // to interpolate into the DOM. See parseOleObject in document-parser.ts.\n OleObject = \"oleObject\"\n}\n\nexport interface WmlOleObject extends OpenXmlElement {\n progId?: string;\n shapeId?: string;\n objectType?: string;\n}\n\n// Structured Document Tag (content control). Parsed from w:sdt when\n// w:sdtPr contains a w:alias or w:tag — otherwise parseSdt unwraps\n// directly to sdtContent children (unless a typed form control is\n// detected below, which also forces a wrapper). See parseSdt in\n// document-parser.ts and the DomType.Sdt branch of renderElement in\n// html-renderer.ts.\nexport interface WmlSdt extends OpenXmlElement {\n sdtAlias?: string;\n sdtTag?: string;\n sdtControl?: SdtControl;\n // Parsed on . The xpath is DOCX-derived and\n // must only be evaluated against a custom XML part document (never the\n // rendered HTML document) and must pass the XPath safety allowlist in\n // html-renderer.ts before reaching document.evaluate.\n dataBinding?: SdtDataBinding;\n}\n\nexport interface SdtDataBinding {\n xpath: string;\n storeItemID?: string;\n prefixMappings?: string;\n}\n\n// Typed form-control metadata extracted from w:sdtPr. All DOCX-derived\n// string fields (displayText, value, format, fullDate) are attacker\n// controlled — they must only reach the DOM via setAttribute /\n// textContent, never innerHTML or className interpolation.\nexport type SdtControl =\n | SdtCheckboxControl\n | SdtDropdownControl\n | SdtDateControl\n | SdtPictureControl\n | SdtGalleryControl;\n\nexport interface SdtCheckboxControl {\n type: \"checkbox\";\n checked: boolean;\n checkedChar?: number;\n uncheckedChar?: number;\n}\n\nexport interface SdtDropdownItem {\n displayText: string;\n value: string;\n}\n\nexport interface SdtDropdownControl {\n // Shared shape for w:dropDownList and w:comboBox — the only rendering\n // difference Word draws between them (editable combo vs strict list)\n // doesn't apply in a read-only HTML view, so both emit