@docen/export-docx 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1 +1,1417 @@
1
- import{ExternalHyperlink as Mt,TextRun as v,ImageRun as q,Paragraph as d,HeadingLevel as b,TableCell as X,TableRow as St,Table as J,PageBreak as Ht,TableOfContents as Pt,Document as Nt,Packer as Rt,convertInchesToTwip as E,AlignmentType as K,LevelFormat as Q}from"docx";import{imageMeta as V}from"image-meta";import{ofetch as $t}from"ofetch";const Y=96;function T(t){return Math.round(t*96/1440)}function zt(t){return`${T(t)}px`}function f(t){return Math.round(t*(1440/96))}function Z(t){return Math.round(t/(914400/96))}function Wt(t){return Math.round(t*(914400/96))}function Ot(t){const e=parseInt(t,10);if(!isNaN(e))return Z(e)}const jt=/^([\d.]+)(px|pt|em|rem|%|)?$/,_t={px:1,pt:1.333,em:16,rem:16,"%":.16};function g(t){if(!t)return 0;t=t.trim();const e=t.match(jt);if(!e)return 0;const n=parseFloat(e[1]);if(isNaN(n))return 0;const r=e[2]||"px",o=_t[r]??1;return Math.round(n*o)}const Gt=/^([\d.]+)(in|mm|cm|pt|pc|pi)$/,Ut={in:1,mm:1/25.4,cm:1/2.54,pt:1/72,pc:1/6,pi:1/6};function I(t){if(typeof t=="number")return t;const e=t.match(Gt);if(e){const r=parseFloat(e[1]),o=e[2],a=Ut[o];return a!==void 0?r*a:r}const n=parseFloat(t);return isNaN(n)?6.5:n}function P(t){if(typeof t=="number")return t;const e=I(t);return Math.round(e*96)}const tt={black:"#000000",white:"#FFFFFF",red:"#FF0000",green:"#008000",blue:"#0000FF",yellow:"#FFFF00",orange:"#FFA500",purple:"#800080",pink:"#FFC0CB",brown:"#A52A2A",gray:"#808080",grey:"#808080",cyan:"#00FFFF",magenta:"#FF00FF",lime:"#00FF00",navy:"#000080",teal:"#008080",maroon:"#800000",olive:"#808000",silver:"#C0C0C0",gold:"#FFD700",indigo:"#4B0082",violet:"#EE82EE",aqua:"#00FFFF",fuchsia:"#FF00FF",darkblue:"#00008B",darkcyan:"#008B8B",darkgrey:"#A9A9A9",darkgreen:"#006400",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkviolet:"#9400D3",lightblue:"#ADD8E6",lightcyan:"#E0FFFF",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1",lightyellow:"#FFFFE0"};function N(t){if(t)return t.startsWith("#")?t:tt[t.toLowerCase()]||t}function qt(t){return t.startsWith("#")?t:`#${t}`}function Xt(t,e){if(!t.children)return null;for(const n of t.children)if(n.type==="element"&&n.name===e)return n;return null}function et(t,e){if(!t.children)return null;for(const n of t.children)if(n.type==="element"){if(n.name===e)return n;const r=et(n,e);if(r)return r}return null}function nt(t,e){const n=[];if(!t.children)return n;for(const r of t.children)r.type==="element"&&(r.name===e&&n.push(r),n.push(...nt(r,e)));return n}function Jt(t,e){const n=t[e];if(!n)return;const r=parseInt(n,10);if(!isNaN(r))return n}const rt=(t,e)=>t?typeof t=="number"?t:Math.round(I(t)*1440):e;function ot(t){if(!t?.sections?.length)return T(11906-1440*2);const e=t.sections[0];if(!e.properties?.page)return T(11906-1440*2);const n=e.properties.page;let r=11906;if(n.size?.width){const p=n.size.width;r=typeof p=="number"?p:Math.round(I(p)*1440)}const o=n.margin,a=rt(o?.left,1440),i=rt(o?.right,1440),c=r-a-i;return Math.max(T(c),Y)}const at=6.5*96,it={jpg:"jpeg",jpeg:"jpeg",png:"png",gif:"gif",bmp:"bmp",tiff:"tiff"},Kt={jpeg:"jpg",jpg:"jpg",png:"png",gif:"gif",bmp:"bmp"};function st(t){if(!t)return"png";const e=t.toLowerCase();return Kt[e]??"png"}function R(t){if(t.startsWith("data:")){const e=t.match(/data:image\/(\w+);/);if(e){const n=e[1].toLowerCase();return it[n]||"png"}}else{const e=t.split(".").pop()?.toLowerCase();if(e)return it[e]||"png"}return"png"}const ct=(t,e=at)=>{if(!t.width||!t.height)return{width:e,height:Math.round(e*.75)};if(t.width<=e)return{width:t.width,height:t.height};const n=e/t.width;return{width:e,height:Math.round(t.height*n)}};function Qt(){return{horizontalPosition:{relative:"page",align:"center"},verticalPosition:{relative:"page",align:"top"},lockAnchor:!0,behindDocument:!1,inFrontOfText:!1}}function lt(t,e,n){if(t.attrs?.width!==void 0&&t.attrs?.width!==null)return t.attrs.width;const r=n!==void 0?P(n):void 0;return e?.width&&e?.height?ct(e,r).width:r||at}function dt(t,e,n,r){if(t.attrs?.height!==void 0&&t.attrs?.height!==null)return t.attrs.height;const o=r!==void 0?P(r):void 0;return n?.width&&n?.height?ct(n,o).height:Math.round(e*.75)}async function ut(t){try{const e=await(await $t(t,{responseType:"blob"})).bytes();let n;try{n=V(e)}catch(r){console.warn("Failed to extract image metadata:",r),n={width:void 0,height:void 0,type:R(t)||"png",orientation:void 0}}return{data:e,meta:n}}catch(e){throw console.warn(`Failed to fetch image from ${t}:`,e),e}}const $=(t,e)=>{if(!e)return t;let n={...t};return(e.indentLeft||e.indentRight||e.indentFirstLine)&&(n={...n,indent:{...e.indentLeft&&{left:f(g(e.indentLeft))},...e.indentRight&&{right:f(g(e.indentRight))},...e.indentFirstLine&&{firstLine:f(g(e.indentFirstLine))}}}),(e.spacingBefore||e.spacingAfter)&&(n={...n,spacing:{...e.spacingBefore&&{before:f(g(e.spacingBefore))},...e.spacingAfter&&{after:f(g(e.spacingAfter))}}}),e.textAlign&&(n={...n,alignment:{left:"left",right:"right",center:"center",justify:"both"}[e.textAlign]}),n};function x(t){const e=t.marks?.some(s=>s.type==="bold"),n=t.marks?.some(s=>s.type==="italic"),r=t.marks?.some(s=>s.type==="underline"),o=t.marks?.some(s=>s.type==="strike"),a=t.marks?.some(s=>s.type==="code"),i=t.marks?.some(s=>s.type==="subscript"),c=t.marks?.some(s=>s.type==="superscript"),p=t.marks?.find(s=>s.type==="link"),l=t.marks?.find(s=>s.type==="textStyle"),u=t.marks?.some(s=>s.type==="highlight"),m=N(l?.attrs?.color),h=N(l?.attrs?.backgroundColor);let F;if(l?.attrs?.fontSize){const s=l.attrs.fontSize;if(s.endsWith("px")){const A=parseFloat(s);isNaN(A)||(F=Math.round(A*1.5))}}let y;a?y="Consolas":l?.attrs?.fontFamily&&(y=l.attrs.fontFamily);const w={text:t.text||"",bold:e||void 0,italics:n||void 0,underline:r?{}:void 0,strike:o||void 0,font:y,size:F,subScript:i||void 0,superScript:c||void 0,color:m,shading:h?{fill:h}:void 0,highlight:u?"yellow":void 0};return p?.attrs?.href?new Mt({children:[new v({...w,style:"Hyperlink"})],link:p.attrs.href}):new v(w)}function k(t){const e={text:"",break:1};if(t)for(const n of t)switch(n.type){case"bold":e.bold=!0;break;case"italic":e.italics=!0;break;case"underline":e.underline={};break;case"strike":e.strike=!0;break;case"textStyle":n.attrs?.color&&(e.color=n.attrs.color);break}return new v(e)}function z(t=[]){return t.flatMap(e=>e.type==="text"?[x(e)]:e.type==="hardBreak"?[k(e.marks)]:[])}async function W(t,e){const n=l=>{const u=l||R(t.attrs?.src||"");return st(u)};let r,o;try{const l=t.attrs?.src||"";if(l.startsWith("http")){const u=await ut(l);r=u.data,o=u.meta}else if(l.startsWith("data:")){const u=l.split(",")[1];if(!u)throw new Error("Invalid data URL: missing base64 data");const m=atob(u);r=Uint8Array.from(m,h=>h.charCodeAt(0));try{o=V(r)}catch{o={type:"png",width:void 0,height:void 0,orientation:void 0}}}else throw new Error(`Unsupported image source format: ${l.substring(0,20)}...`)}catch(l){return console.warn("Failed to process image:",l),new q({type:"png",data:new Uint8Array(0),transformation:{width:100,height:100},altText:{name:t.attrs?.alt||"Failed to load image"}})}const a=lt(t,o,e?.maxWidth),i=dt(t,a,o,e?.maxWidth),c={width:a,height:i};t.attrs?.rotation!==void 0&&(c.rotation=t.attrs.rotation);const p={type:n(o.type),data:r,transformation:c,altText:{name:t.attrs?.alt||"",description:void 0,title:t.attrs?.title||void 0},...t.attrs?.floating&&{floating:t.attrs.floating},...t.attrs?.outline&&{outline:t.attrs.outline}};return new q(p)}async function C(t,e){const{options:n,image:r}=e||{},o=[];for(const i of t.content||[])if(i.type==="text")o.push(x(i));else if(i.type==="hardBreak")o.push(k(i.marks));else if(i.type==="image"){const c=await W(i,{maxWidth:r?.maxWidth});o.push(c)}let a={children:o};return n&&(a={...a,...n}),t.attrs&&(a=$(a,t.attrs)),new d(a)}function pt(t){const e=t?.attrs?.level,n=z(t.content).filter(a=>a!==void 0),r={1:b.HEADING_1,2:b.HEADING_2,3:b.HEADING_3,4:b.HEADING_4,5:b.HEADING_5,6:b.HEADING_6};let o={children:n,heading:r[e]};return t.attrs&&(o=$(o,t.attrs)),new d(o)}function ht(t){return t.content?t.content.map(e=>{if(e.type==="paragraph"){const n=e.content?.flatMap(r=>r.type==="text"?x(r):r.type==="hardBreak"?k(r.marks):[])||[];return new d({children:n,indent:{left:720},border:{left:{style:"single"}}})}return new d({})}):[]}function M(t){if(!t)return;const e={solid:"single",dashed:"dashed",dotted:"dotted",double:"double",none:"none"},n=t.style&&e[t.style]||"single",r=t.color?.replace("#","")||"auto",o=t.width?t.width*6:4;return{color:r,size:o,style:n}}async function ft(t,e){const{options:n}=e;let r=n?.cell?.paragraph??n?.row?.paragraph??{};n?.style&&(r={...r,style:n.style.id});const o={children:await Promise.all((t.content||[]).map(i=>C(i,{options:r}))),...n?.cell?.run};if(t.attrs?.colspan&&t.attrs.colspan>1&&(o.columnSpan=t.attrs.colspan),t.attrs?.rowspan&&t.attrs.rowspan>1&&(o.rowSpan=t.attrs.rowspan),t.attrs?.colwidth!==null&&t.attrs?.colwidth!==void 0){const i=Array.isArray(t.attrs.colwidth)?t.attrs.colwidth[0]:t.attrs.colwidth;if(i&&i>0){const c=Math.round(i*15);o.width={size:c,type:"dxa"}}}if(t.attrs?.backgroundColor){const i=t.attrs.backgroundColor.replace("#","");o.shading={fill:i}}if(t.attrs?.verticalAlign){const i=t.attrs.verticalAlign==="middle"?"center":t.attrs.verticalAlign;o.verticalAlign=i}const a={top:M(t.attrs?.borderTop),bottom:M(t.attrs?.borderBottom),left:M(t.attrs?.borderLeft),right:M(t.attrs?.borderRight)};return(a.top||a.bottom||a.left||a.right)&&(o.borders=a),new X(o)}function S(t){if(!t)return;const e={solid:"single",dashed:"dashed",dotted:"dotted",double:"double",none:"none"},n=t.style&&e[t.style]||"single",r=t.color?.replace("#","")||"auto",o=t.width?t.width*6:4;return{color:r,size:o,style:n}}async function gt(t,e){const{options:n}=e;let r=n?.header?.paragraph??n?.cell?.paragraph??n?.row?.paragraph??{};n?.style&&(r={...r,style:n.style.id});const o={children:await Promise.all((t.content||[]).map(i=>C(i,{options:r}))),...n?.header?.run};if(t.attrs?.colspan&&t.attrs.colspan>1&&(o.columnSpan=t.attrs.colspan),t.attrs?.rowspan&&t.attrs.rowspan>1&&(o.rowSpan=t.attrs.rowspan),t.attrs?.colwidth!==null&&t.attrs?.colwidth!==void 0){const i=Array.isArray(t.attrs.colwidth)?t.attrs.colwidth[0]:t.attrs.colwidth;if(i&&i>0){const c=Math.round(i*15);o.width={size:c,type:"dxa"}}}if(t.attrs?.backgroundColor){const i=t.attrs.backgroundColor.replace("#","");o.shading={fill:i}}if(t.attrs?.verticalAlign){const i=t.attrs.verticalAlign==="middle"?"center":t.attrs.verticalAlign;o.verticalAlign=i}const a={top:S(t.attrs?.borderTop),bottom:S(t.attrs?.borderBottom),left:S(t.attrs?.borderLeft),right:S(t.attrs?.borderRight)};return(a.top||a.bottom||a.left||a.right)&&(o.borders=a),new X(o)}async function mt(t,e){const{options:n}=e,r=n?.row,o={children:(await Promise.all((t.content||[]).map(async a=>a.type==="tableCell"?await ft(a,e):a.type==="tableHeader"?await gt(a,e):null))).filter(a=>a!==void 0),...r};if(t.attrs?.rowHeight){const a=g(t.attrs.rowHeight),i=f(a);i>0&&(o.height={rule:"atLeast",value:i})}return new St(o)}const Vt=(t,e)=>{const n={top:e.attrs?.marginTop??void 0,bottom:e.attrs?.marginBottom??void 0,left:e.attrs?.marginLeft??void 0,right:e.attrs?.marginRight??void 0};return n.top||n.bottom||n.left||n.right?{...t,margins:n}:t};async function yt(t,e){const{options:n}=e;let r={rows:await Promise.all((t.content||[]).map(o=>mt(o,e))),...n?.run};return r=Vt(r,t),new J(r)}function wt(t){const e=t.content?.map(n=>n.text||"").join("")||"";return new d({children:[new v({text:e,font:"Consolas"})]})}async function O(t,e){if(!t.content||t.content.length===0)return new d({});const n=t.content[0];return n.type==="paragraph"?await C(n,{options:e.options}):new d({})}function vt(){return{numbering:{reference:"bullet-list",level:0}}}function bt(t){const e=t.attrs?.start||1;return{numbering:{reference:"ordered-list",level:0},start:e}}async function j(t,e){const{listType:n}=e;if(!t.content)return[];const r=[],o=n==="bullet"?vt():bt(t);let a=o.numbering.reference;n==="ordered"&&o.start&&o.start!==1&&(a=`ordered-list-start-${o.start}`);for(const i of t.content)if(i.type==="listItem"){const c=await O(i,{options:{numbering:{reference:a,level:0}}});r.push(c)}return r}function _(t){if(!t.content||t.content.length===0)return new d({});const e=t.content[0];if(e.type==="paragraph"){const n=t.attrs?.checked?"\u2611 ":"\u2610 ",r=e.content?.flatMap(a=>a.type==="text"?x(a):a.type==="hardBreak"?k(a.marks):[])||[],o=new v({text:n});return new d({children:[o,...r]})}return new d({})}function kt(t){return!t.content||t.content.length===0?[]:t.content.filter(e=>e.type==="taskItem").map(e=>_(e))}function Ft(t,e){const n={children:[new Ht]};return new d({...n,...e.options?.paragraph})}function At(t,e){const n=z(t.content||[]).filter(r=>r!==void 0);return new d({children:n,...e.options?.summary?.paragraph})}async function Yt(t,e){const{title:n,subject:r,creator:o,keywords:a,description:i,lastModifiedBy:c,revision:p,styles:l,tableOfContents:u,sections:m,fonts:h,hyphenation:F,compatibility:y,customProperties:w,evenAndOddHeaderAndFooters:s,defaultTabStop:A,outputType:Ct}=e,H=await Tt(t,{options:e}),B=u?new Pt(u.title,{...u.run}):null,Bt=te(t),L=[];e.image?.style&&L.push(e.image.style),e.table?.style&&L.push(e.table.style);const Lt=l?{...l,...L.length>0&&{paragraphStyles:[...l.paragraphStyles||[],...L]}}:{},Dt={sections:m?m.map((It,U)=>{const D=[];return U===0&&B&&D.push(B),U===0&&D.push(...H),{...It,...D.length>0?{children:D}:{}}}):[{children:B?[B,...H]:H}],title:n||"Document",subject:r||"",creator:o||"",keywords:a||"",description:i||"",lastModifiedBy:c||"",revision:p||1,styles:Lt,numbering:Bt,...h&&h.length>0&&{fonts:h},...F&&{hyphenation:F},...y&&{compatibility:y},...w&&w.length>0&&{customProperties:w},...s!==void 0&&{evenAndOddHeaderAndFooters:s},...A!==void 0&&{defaultTabStop:A}},Et=new Nt(Dt);return Rt.pack(Et,Ct||"arraybuffer")}async function Tt(t,e){const n=[];if(!t||!Array.isArray(t.content))return n;const r=ot(e.options);for(const o of t.content){const a=await G(o,e.options,r);Array.isArray(a)?n.push(...a):a&&(n.push(a),o.type==="table"&&n.length>=2&&n[n.length-2]instanceof J&&n.push(new d({})))}return n}async function G(t,e,n){if(!t||!t.type)return null;switch(t.type){case"paragraph":return await C(t,{image:{maxWidth:n}});case"heading":return pt(t);case"blockquote":return ht(t);case"codeBlock":return wt(t);case"image":const r=await W(t,{maxWidth:n}),o=e.image?.style?{children:[r],style:e.image.style.id}:{children:[r]};return new d(o);case"table":return await yt(t,{options:e.table});case"bulletList":return await j(t,{listType:"bullet"});case"orderedList":return await j(t,{listType:"ordered"});case"taskList":return kt(t);case"listItem":return O(t,{options:void 0});case"taskItem":return _(t);case"hardBreak":return new d({children:[k()]});case"horizontalRule":return Ft(t,{options:e.horizontalRule});case"details":const a=[];if(t.content)for(const i of t.content){const c=await G(i,e,n);Array.isArray(c)?a.push(...c):c&&a.push(c)}return a;case"detailsSummary":return At(t,{options:e.details});default:return new d({children:[new v({text:`[Unsupported: ${t.type}]`})]})}}const Zt=t=>({level:0,format:Q.DECIMAL,text:"%1.",alignment:K.START,start:t??1,style:{paragraph:{indent:{left:E(.5),hanging:E(.25)}}}}),xt=t=>({reference:t&&t!==1?`ordered-list-start-${t}`:"ordered-list",levels:[Zt(t)]});function te(t){const e=new Set;function n(o){if(o.type==="orderedList"&&o.attrs?.start&&e.add(o.attrs.start),o.content)for(const a of o.content)n(a)}n(t);const r=[{reference:"bullet-list",levels:[{level:0,format:Q.BULLET,text:"\u2022",alignment:K.START,style:{paragraph:{indent:{left:E(.5),hanging:E(.25)}}}}]},xt(1)];return e.forEach(o=>{o!==1&&r.push(xt(o))}),{config:r}}export{tt as COLOR_NAME_TO_HEX,Y as DOCX_DPI,$ as applyParagraphStyleAttributes,ot as calculateEffectiveContentWidth,ht as convertBlockquote,vt as convertBulletList,wt as convertCodeBlock,N as convertColorToHex,g as convertCssLengthToPixels,At as convertDetailsSummary,Tt as convertDocument,Ot as convertEmuStringToPixels,Z as convertEmuToPixels,k as convertHardBreak,pt as convertHeading,Ft as convertHorizontalRule,W as convertImage,j as convertList,O as convertListItem,I as convertMeasureToInches,P as convertMeasureToPixels,G as convertNode,bt as convertOrderedList,C as convertParagraph,Wt as convertPixelsToEmu,f as convertPixelsToTwip,yt as convertTable,ft as convertTableCell,gt as convertTableHeader,mt as convertTableRow,_ as convertTaskItem,kt as convertTaskList,x as convertText,z as convertTextNodes,st as convertToDocxImageType,zt as convertTwipToCssString,T as convertTwipToPixels,Qt as createFloatingOptions,Xt as findChild,et as findDeepChild,nt as findDeepChildren,Yt as generateDOCX,ut as getImageDataAndMeta,dt as getImageHeight,R as getImageTypeFromSrc,lt as getImageWidth,qt as normalizeHexColor,Jt as parseTwipAttr};
1
+ import { AlignmentType, Document, ExternalHyperlink, HeadingLevel, ImageRun, LevelFormat, Packer, PageBreak, Paragraph, Table, TableCell, TableOfContents, TableRow, TextRun, convertInchesToTwip } from "docx";
2
+ import { imageMeta } from "image-meta";
3
+ import { ofetch } from "ofetch";
4
+ //#region ../utils/dist/index.mjs
5
+ /**
6
+ * Shared constants for DOCX processing
7
+ * Used across @docen/export-docx and @docen/import-docx packages
8
+ */
9
+ /**
10
+ * DOCX DPI (dots per inch) for pixel conversions
11
+ * Word uses 96 DPI internally
12
+ */
13
+ const DOCX_DPI = 96;
14
+ /**
15
+ * TWIP (Twentieth of a Point) conversion constants
16
+ * 1 inch = 1440 TWIPs
17
+ */
18
+ const TWIPS_PER_INCH = 1440;
19
+ /**
20
+ * EMU (English Metric Unit) conversion constants
21
+ * 1 inch = 914400 EMUs
22
+ */
23
+ const EMUS_PER_INCH = 914400;
24
+ /**
25
+ * Font size conversion factors
26
+ * DOCX uses half-points, TipTap uses pixels
27
+ * 1px ≈ 0.75pt, 1pt = 2 half-points
28
+ * So: px * 0.75 * 2 = px * 1.5
29
+ */
30
+ const HALF_POINTS_PER_PIXEL = 1.5;
31
+ const PIXELS_PER_HALF_POINT = 1 / 1.5;
32
+ /**
33
+ * Default code font family
34
+ */
35
+ const DEFAULT_CODE_FONT = "Consolas";
36
+ /**
37
+ * Checkbox symbols for task lists
38
+ */
39
+ const CHECKBOX_SYMBOLS = {
40
+ checked: "☑",
41
+ unchecked: "☐"
42
+ };
43
+ /**
44
+ * DOCX style names
45
+ */
46
+ const DOCX_STYLE_NAMES = {
47
+ CODE_BLOCK: "CodeBlock",
48
+ CODE_PREFIX: "Code"
49
+ };
50
+ /**
51
+ * Text alignment mappings
52
+ */
53
+ const TEXT_ALIGN_MAP = {
54
+ tiptapToDocx: {
55
+ left: "left",
56
+ right: "right",
57
+ center: "center",
58
+ justify: "both"
59
+ },
60
+ docxToTipTap: {
61
+ left: "left",
62
+ right: "right",
63
+ center: "center",
64
+ both: "justify"
65
+ }
66
+ };
67
+ /**
68
+ * Common page dimensions in TWIPs
69
+ */
70
+ const PAGE_DIMENSIONS = {
71
+ A4_WIDTH_TWIP: 11906,
72
+ DEFAULT_MARGIN_TWIP: 1440
73
+ };
74
+ /**
75
+ * Unit conversion utilities for DOCX processing
76
+ * Handles conversions between TWIPs, EMUs, pixels, and other units
77
+ */
78
+ const PIXELS_PER_INCH = 96;
79
+ /**
80
+ * Convert TWIPs to CSS pixels (returns number)
81
+ * @param twip - Value in TWIPs (1 inch = 1440 TWIPs)
82
+ * @returns Number value in pixels
83
+ *
84
+ * @example
85
+ * convertTwipToPixels(1440) // returns 96
86
+ */
87
+ function convertTwipToPixels(twip) {
88
+ return Math.round(twip * PIXELS_PER_INCH / TWIPS_PER_INCH);
89
+ }
90
+ /**
91
+ * Convert TWIPs to CSS string (returns "px" string)
92
+ * @param twip - Value in TWIPs
93
+ * @returns CSS value string in pixels (e.g., "20px")
94
+ *
95
+ * @example
96
+ * convertTwipToCssString(1440) // returns "96px"
97
+ */
98
+ function convertTwipToCssString(twip) {
99
+ return `${convertTwipToPixels(twip)}px`;
100
+ }
101
+ /**
102
+ * Convert pixels to TWIPs
103
+ * @param px - Value in pixels
104
+ * @returns Value in TWIPs
105
+ *
106
+ * @example
107
+ * convertPixelsToTwip(96) // returns 1440
108
+ */
109
+ function convertPixelsToTwip(px) {
110
+ return Math.round(px * (TWIPS_PER_INCH / PIXELS_PER_INCH));
111
+ }
112
+ /**
113
+ * Convert EMUs to pixels
114
+ * EMU = English Metric Unit (1 inch = 914400 EMUs)
115
+ * @param emu - Value in EMUs
116
+ * @returns Value in pixels
117
+ *
118
+ * @example
119
+ * convertEmuToPixels(914400) // returns 96
120
+ */
121
+ function convertEmuToPixels(emu) {
122
+ return Math.round(emu / (EMUS_PER_INCH / PIXELS_PER_INCH));
123
+ }
124
+ /**
125
+ * Convert pixels to EMUs
126
+ * @param px - Value in pixels
127
+ * @returns Value in EMUs
128
+ *
129
+ * @example
130
+ * convertPixelsToEmu(96) // returns 914400
131
+ */
132
+ function convertPixelsToEmu(px) {
133
+ return Math.round(px * (EMUS_PER_INCH / PIXELS_PER_INCH));
134
+ }
135
+ /**
136
+ * Convert EMU string to pixels
137
+ * @param emuStr - EMU value as string
138
+ * @returns Pixel value or undefined if invalid
139
+ *
140
+ * @example
141
+ * convertEmuStringToPixels("914400") // returns 96
142
+ * convertEmuStringToPixels("invalid") // returns undefined
143
+ */
144
+ function convertEmuStringToPixels(emuStr) {
145
+ const emu = parseInt(emuStr, 10);
146
+ if (isNaN(emu)) return void 0;
147
+ return convertEmuToPixels(emu);
148
+ }
149
+ /**
150
+ * Regular expression for matching CSS length values
151
+ * Supports: px, pt, em, rem, %, and unitless values (including negative values)
152
+ */
153
+ const CSS_LENGTH_REGEX = /^(-?[\d.]+)(px|pt|em|rem|%|)?$/;
154
+ /**
155
+ * Conversion factors from various units to pixels
156
+ */
157
+ const UNIT_TO_PIXELS = {
158
+ px: 1,
159
+ pt: 1.333,
160
+ em: 16,
161
+ rem: 16,
162
+ "%": .16
163
+ };
164
+ /**
165
+ * Parse CSS length value to pixels
166
+ * Supports: px, pt, em, rem, %, and unitless values
167
+ * @param value - CSS length value (e.g., "20px", "1.5em", "100%")
168
+ * @returns Value in pixels
169
+ *
170
+ * @example
171
+ * convertCssLengthToPixels("20px") // returns 20
172
+ * convertCssLengthToPixels("1.5em") // returns 24
173
+ * convertCssLengthToPixels("100%") // returns 16
174
+ * convertCssLengthToPixels("20") // returns 20 (unitless treated as px)
175
+ */
176
+ function convertCssLengthToPixels(value) {
177
+ if (!value) return 0;
178
+ value = value.trim();
179
+ const match = value.match(CSS_LENGTH_REGEX);
180
+ if (!match) return 0;
181
+ const num = parseFloat(match[1]);
182
+ if (isNaN(num)) return 0;
183
+ const factor = UNIT_TO_PIXELS[match[2] || "px"] ?? 1;
184
+ return Math.round(num * factor);
185
+ }
186
+ /**
187
+ * Regular expression for matching universal measure values
188
+ * Used in DOCX for specifying dimensions in various units
189
+ */
190
+ const MEASURE_REGEX = /^([\d.]+)(in|mm|cm|pt|pc|pi)$/;
191
+ /**
192
+ * Conversion factors from various units to inches
193
+ */
194
+ const UNIT_TO_INCHES = {
195
+ in: 1,
196
+ mm: 1 / 25.4,
197
+ cm: 1 / 2.54,
198
+ pt: 1 / 72,
199
+ pc: 1 / 6,
200
+ pi: 1 / 6
201
+ };
202
+ /**
203
+ * Convert universal measure to inches
204
+ * Compatible with docx.js UniversalMeasure type
205
+ * @param value - Value in various units (number or string)
206
+ * @returns Value in inches
207
+ *
208
+ * @example
209
+ * convertMeasureToInches(6.5) // returns 6.5
210
+ * convertMeasureToInches("1in") // returns 1
211
+ * convertMeasureToInches("25.4mm") // returns ~1
212
+ */
213
+ function convertMeasureToInches(value) {
214
+ if (typeof value === "number") return value;
215
+ const match = value.match(MEASURE_REGEX);
216
+ if (match) {
217
+ const numValue = parseFloat(match[1]);
218
+ const factor = UNIT_TO_INCHES[match[2]];
219
+ return factor !== void 0 ? numValue * factor : numValue;
220
+ }
221
+ const num = parseFloat(value);
222
+ return isNaN(num) ? 6.5 : num;
223
+ }
224
+ /**
225
+ * Convert universal measure to pixels
226
+ * Compatible with docx.js UniversalMeasure type
227
+ * @param value - Value in various units (number or string)
228
+ * @returns Value in pixels
229
+ *
230
+ * @example
231
+ * convertMeasureToPixels("1in") // returns 96
232
+ * convertMeasureToPixels(6.5) // returns 624
233
+ */
234
+ function convertMeasureToPixels(value) {
235
+ if (typeof value === "number") return value;
236
+ const inches = convertMeasureToInches(value);
237
+ return Math.round(inches * 96);
238
+ }
239
+ /**
240
+ * Color conversion utilities for DOCX processing
241
+ * Handles conversion between color names and hex values
242
+ */
243
+ /**
244
+ * Color name to hex value mapping
245
+ * Includes common HTML/CSS color names
246
+ */
247
+ const COLOR_NAME_TO_HEX = {
248
+ black: "#000000",
249
+ white: "#FFFFFF",
250
+ red: "#FF0000",
251
+ green: "#008000",
252
+ blue: "#0000FF",
253
+ yellow: "#FFFF00",
254
+ orange: "#FFA500",
255
+ purple: "#800080",
256
+ pink: "#FFC0CB",
257
+ brown: "#A52A2A",
258
+ gray: "#808080",
259
+ grey: "#808080",
260
+ cyan: "#00FFFF",
261
+ magenta: "#FF00FF",
262
+ lime: "#00FF00",
263
+ navy: "#000080",
264
+ teal: "#008080",
265
+ maroon: "#800000",
266
+ olive: "#808000",
267
+ silver: "#C0C0C0",
268
+ gold: "#FFD700",
269
+ indigo: "#4B0082",
270
+ violet: "#EE82EE",
271
+ aqua: "#00FFFF",
272
+ fuchsia: "#FF00FF",
273
+ darkblue: "#00008B",
274
+ darkcyan: "#008B8B",
275
+ darkgrey: "#A9A9A9",
276
+ darkgreen: "#006400",
277
+ darkkhaki: "#BDB76B",
278
+ darkmagenta: "#8B008B",
279
+ darkolivegreen: "#556B2F",
280
+ darkorange: "#FF8C00",
281
+ darkorchid: "#9932CC",
282
+ darkred: "#8B0000",
283
+ darksalmon: "#E9967A",
284
+ darkviolet: "#9400D3",
285
+ lightblue: "#ADD8E6",
286
+ lightcyan: "#E0FFFF",
287
+ lightgreen: "#90EE90",
288
+ lightgrey: "#D3D3D3",
289
+ lightpink: "#FFB6C1",
290
+ lightyellow: "#FFFFE0"
291
+ };
292
+ /**
293
+ * Convert color name or hex to normalized hex value
294
+ * @param color - Color as name (e.g., "red") or hex (e.g., "#FF0000" or "FF0000")
295
+ * @returns Normalized hex color string (e.g., "#FF0000") or undefined if invalid
296
+ *
297
+ * @example
298
+ * convertColorToHex("red") // returns "#FF0000"
299
+ * convertColorToHex("#FF0000") // returns "#FF0000"
300
+ * convertColorToHex("FF0000") // returns "#FF0000"
301
+ * convertColorToHex("invalid") // returns undefined
302
+ */
303
+ function convertColorToHex(color) {
304
+ if (!color) return void 0;
305
+ if (color.startsWith("#")) return color;
306
+ return COLOR_NAME_TO_HEX[color.toLowerCase()] || color;
307
+ }
308
+ /**
309
+ * Normalize hex color to ensure it has a # prefix
310
+ * @param color - Color value with or without # prefix
311
+ * @returns Hex color with # prefix
312
+ *
313
+ * @example
314
+ * normalizeHexColor("FF0000") // returns "#FF0000"
315
+ * normalizeHexColor("#FF0000") // returns "#FF0000"
316
+ */
317
+ function normalizeHexColor(color) {
318
+ if (color.startsWith("#")) return color;
319
+ return `#${color}`;
320
+ }
321
+ /**
322
+ * Find direct child element with specified name
323
+ * @param node - Parent XML element or root node
324
+ * @param name - Child element name to find (can include namespace prefix, e.g., "w:p")
325
+ * @returns Child element if found, null otherwise
326
+ *
327
+ * @example
328
+ * const paragraph = findChild(document, "w:p");
329
+ */
330
+ function findChild(node, name) {
331
+ if (!node.children) return null;
332
+ for (const child of node.children) if (child.type === "element" && child.name === name) return child;
333
+ return null;
334
+ }
335
+ /**
336
+ * Find deep descendant element with specified name (recursive)
337
+ * Searches through all descendants, not just direct children
338
+ * @param node - Root XML element
339
+ * @param name - Descendant element name to find
340
+ * @returns Descendant element if found, null otherwise
341
+ *
342
+ * @example
343
+ * const textElement = findDeepChild(run, "w:t");
344
+ */
345
+ function findDeepChild(node, name) {
346
+ if (!node.children) return null;
347
+ for (const child of node.children) if (child.type === "element") {
348
+ if (child.name === name) return child;
349
+ const found = findDeepChild(child, name);
350
+ if (found) return found;
351
+ }
352
+ return null;
353
+ }
354
+ /**
355
+ * Find all deep descendant elements with specified name (recursive)
356
+ * @param node - Root XML element
357
+ * @param name - Descendant element name to find
358
+ * @returns Array of matching descendant elements
359
+ *
360
+ * @example
361
+ * const allTextRuns = findDeepChildren(paragraph, "w:r");
362
+ */
363
+ function findDeepChildren(node, name) {
364
+ const results = [];
365
+ if (!node.children) return results;
366
+ for (const child of node.children) if (child.type === "element") {
367
+ if (child.name === name) results.push(child);
368
+ results.push(...findDeepChildren(child, name));
369
+ }
370
+ return results;
371
+ }
372
+ /**
373
+ * Parse TWIP attribute value from element attributes
374
+ * TWIP = Twentieth of a Point (1 inch = 1440 TWIPs)
375
+ * @param attributes - Element attributes object
376
+ * @param name - Attribute name to parse
377
+ * @returns TWIP value as string, or undefined if not found
378
+ *
379
+ * @example
380
+ * const leftIndent = parseTwipAttr(pPr.attributes, "w:left");
381
+ */
382
+ function parseTwipAttr(attributes, name) {
383
+ const value = attributes[name];
384
+ if (!value) return void 0;
385
+ const num = parseInt(value, 10);
386
+ if (isNaN(num)) return void 0;
387
+ return value;
388
+ }
389
+ /**
390
+ * Type guard utilities for DOCX processing
391
+ */
392
+ /**
393
+ * Type guard factory function
394
+ * Creates a type guard function that checks if a value is one of the valid values
395
+ *
396
+ * @param validValues - Readonly array of valid string values
397
+ * @returns Type guard function
398
+ *
399
+ * @example
400
+ * const isValidAlign = createStringValidator(["left", "right", "center"] as const);
401
+ * if (isValidAlign(value)) {
402
+ * // value is typed as "left" | "right" | "center"
403
+ * }
404
+ */
405
+ function createStringValidator(validValues) {
406
+ return (value) => {
407
+ return validValues.includes(value);
408
+ };
409
+ }
410
+ //#endregion
411
+ //#region src/utils/conversion.ts
412
+ /**
413
+ * Normalize margin value to TWIPs
414
+ * Converts number (already TWIPs) or PositiveUniversalMeasure to TWIPs
415
+ */
416
+ const normalizeMarginToTwip = (margin, fallback) => {
417
+ if (!margin) return fallback;
418
+ return typeof margin === "number" ? margin : Math.round(convertMeasureToInches(margin) * TWIPS_PER_INCH);
419
+ };
420
+ /**
421
+ * Calculate effective content width from document options
422
+ */
423
+ function calculateEffectiveContentWidth(options) {
424
+ const DEFAULT_PAGE_WIDTH_TWIP = PAGE_DIMENSIONS.A4_WIDTH_TWIP;
425
+ const DEFAULT_MARGIN_TWIP = PAGE_DIMENSIONS.DEFAULT_MARGIN_TWIP;
426
+ if (!options?.sections?.length) return convertTwipToPixels(DEFAULT_PAGE_WIDTH_TWIP - DEFAULT_MARGIN_TWIP * 2);
427
+ const firstSection = options.sections[0];
428
+ if (!firstSection.properties?.page) return convertTwipToPixels(DEFAULT_PAGE_WIDTH_TWIP - DEFAULT_MARGIN_TWIP * 2);
429
+ const pageSettings = firstSection.properties.page;
430
+ let pageWidth = DEFAULT_PAGE_WIDTH_TWIP;
431
+ if (pageSettings.size?.width) {
432
+ const widthValue = pageSettings.size.width;
433
+ pageWidth = typeof widthValue === "number" ? widthValue : Math.round(convertMeasureToInches(widthValue) * TWIPS_PER_INCH);
434
+ }
435
+ const marginSettings = pageSettings.margin;
436
+ const marginLeft = normalizeMarginToTwip(marginSettings?.left, DEFAULT_MARGIN_TWIP);
437
+ const marginRight = normalizeMarginToTwip(marginSettings?.right, DEFAULT_MARGIN_TWIP);
438
+ const effectiveWidth = pageWidth - marginLeft - marginRight;
439
+ return Math.max(convertTwipToPixels(effectiveWidth), 96);
440
+ }
441
+ //#endregion
442
+ //#region src/utils/image.ts
443
+ const DEFAULT_MAX_IMAGE_WIDTH_PIXELS = 6.5 * 96;
444
+ /**
445
+ * Unified image type mapping
446
+ * Maps file extensions and MIME types to standardized type names and DOCX types
447
+ */
448
+ const IMAGE_TYPE_MAPPING = {
449
+ mimeToInternal: {
450
+ jpg: "jpeg",
451
+ jpeg: "jpeg",
452
+ png: "png",
453
+ gif: "gif",
454
+ bmp: "bmp",
455
+ tiff: "tiff"
456
+ },
457
+ internalToDocx: {
458
+ jpeg: "jpg",
459
+ png: "png",
460
+ gif: "gif",
461
+ bmp: "bmp",
462
+ tiff: "bmp"
463
+ }
464
+ };
465
+ /**
466
+ * Convert image MIME type to DOCX type
467
+ */
468
+ function convertToDocxImageType(mimeType) {
469
+ if (!mimeType) return "png";
470
+ const typeKey = mimeType.toLowerCase();
471
+ const internalType = IMAGE_TYPE_MAPPING.mimeToInternal[typeKey] || "png";
472
+ return IMAGE_TYPE_MAPPING.internalToDocx[internalType] || "png";
473
+ }
474
+ /**
475
+ * Extract image type from URL or base64 data
476
+ */
477
+ function getImageTypeFromSrc(src) {
478
+ if (src.startsWith("data:")) {
479
+ const match = src.match(/data:image\/(\w+);/);
480
+ if (match) {
481
+ const type = match[1].toLowerCase();
482
+ return IMAGE_TYPE_MAPPING.mimeToInternal[type] || "png";
483
+ }
484
+ } else {
485
+ const extension = src.split(".").pop()?.toLowerCase();
486
+ if (extension) return IMAGE_TYPE_MAPPING.mimeToInternal[extension] || "png";
487
+ }
488
+ return "png";
489
+ }
490
+ /**
491
+ * Calculate appropriate display size for image (mimicking Word's behavior)
492
+ */
493
+ const calculateDisplaySize = (imageMeta, maxWidthPixels = DEFAULT_MAX_IMAGE_WIDTH_PIXELS) => {
494
+ if (!imageMeta.width || !imageMeta.height) return {
495
+ width: maxWidthPixels,
496
+ height: Math.round(maxWidthPixels * .75)
497
+ };
498
+ if (imageMeta.width <= maxWidthPixels) return {
499
+ width: imageMeta.width,
500
+ height: imageMeta.height
501
+ };
502
+ const scaleFactor = maxWidthPixels / imageMeta.width;
503
+ return {
504
+ width: maxWidthPixels,
505
+ height: Math.round(imageMeta.height * scaleFactor)
506
+ };
507
+ };
508
+ /**
509
+ * Create floating options for full-width images
510
+ */
511
+ function createFloatingOptions() {
512
+ return {
513
+ horizontalPosition: {
514
+ relative: "page",
515
+ align: "center"
516
+ },
517
+ verticalPosition: {
518
+ relative: "page",
519
+ align: "top"
520
+ },
521
+ lockAnchor: true,
522
+ behindDocument: false,
523
+ inFrontOfText: false
524
+ };
525
+ }
526
+ /**
527
+ * Get image width with priority: node attrs > image meta > calculated > default
528
+ */
529
+ function getImageWidth(node, imageMeta, maxWidth) {
530
+ if (node.attrs?.width !== void 0 && node.attrs?.width !== null) return node.attrs.width;
531
+ const maxWidthPixels = maxWidth !== void 0 ? convertMeasureToPixels(maxWidth) : void 0;
532
+ if (imageMeta?.width && imageMeta?.height) return calculateDisplaySize(imageMeta, maxWidthPixels).width;
533
+ return maxWidthPixels || DEFAULT_MAX_IMAGE_WIDTH_PIXELS;
534
+ }
535
+ /**
536
+ * Get image height with priority: node attrs > image meta > calculated > default
537
+ */
538
+ function getImageHeight(node, width, imageMeta, maxWidth) {
539
+ if (node.attrs?.height !== void 0 && node.attrs?.height !== null) return node.attrs.height;
540
+ const maxWidthPixels = maxWidth !== void 0 ? convertMeasureToPixels(maxWidth) : void 0;
541
+ if (imageMeta?.width && imageMeta?.height) return calculateDisplaySize(imageMeta, maxWidthPixels).height;
542
+ return Math.round(width * .75);
543
+ }
544
+ /**
545
+ * Fetch image data and metadata from HTTP/HTTPS URL
546
+ * (Only for use without custom handler)
547
+ */
548
+ async function getImageDataAndMeta(url) {
549
+ try {
550
+ const data = await (await ofetch(url, { responseType: "blob" })).bytes();
551
+ let meta;
552
+ try {
553
+ meta = imageMeta(data);
554
+ } catch (error) {
555
+ console.warn(`Failed to extract image metadata:`, error);
556
+ meta = {
557
+ width: void 0,
558
+ height: void 0,
559
+ type: getImageTypeFromSrc(url) || "png",
560
+ orientation: void 0
561
+ };
562
+ }
563
+ return {
564
+ data,
565
+ meta
566
+ };
567
+ } catch (error) {
568
+ console.warn(`Failed to fetch image from ${url}:`, error);
569
+ throw error;
570
+ }
571
+ }
572
+ //#endregion
573
+ //#region src/utils/paragraph.ts
574
+ /**
575
+ * Apply paragraph style attributes to options
576
+ */
577
+ const applyParagraphStyleAttributes = (options, attrs) => {
578
+ if (!attrs) return options;
579
+ let result = { ...options };
580
+ if (attrs.indentLeft || attrs.indentRight || attrs.indentFirstLine) result = {
581
+ ...result,
582
+ indent: {
583
+ ...attrs.indentLeft && { left: convertPixelsToTwip(convertCssLengthToPixels(attrs.indentLeft)) },
584
+ ...attrs.indentRight && { right: convertPixelsToTwip(convertCssLengthToPixels(attrs.indentRight)) },
585
+ ...attrs.indentFirstLine && { firstLine: convertPixelsToTwip(convertCssLengthToPixels(attrs.indentFirstLine)) }
586
+ }
587
+ };
588
+ if (attrs.spacingBefore || attrs.spacingAfter) result = {
589
+ ...result,
590
+ spacing: {
591
+ ...attrs.spacingBefore && { before: convertPixelsToTwip(convertCssLengthToPixels(attrs.spacingBefore)) },
592
+ ...attrs.spacingAfter && { after: convertPixelsToTwip(convertCssLengthToPixels(attrs.spacingAfter)) }
593
+ }
594
+ };
595
+ if (attrs.textAlign) result = {
596
+ ...result,
597
+ alignment: TEXT_ALIGN_MAP.tiptapToDocx[attrs.textAlign]
598
+ };
599
+ return result;
600
+ };
601
+ //#endregion
602
+ //#region src/utils/table.ts
603
+ /**
604
+ * Convert TipTap border to DOCX border format
605
+ *
606
+ * @param border - TipTap table cell border definition
607
+ * @returns DOCX border options or undefined if no border
608
+ */
609
+ function convertBorder(border) {
610
+ if (!border) return void 0;
611
+ const docxStyle = border.style ? {
612
+ solid: "single",
613
+ dashed: "dashed",
614
+ dotted: "dotted",
615
+ double: "double",
616
+ none: "none"
617
+ }[border.style] || "single" : "single";
618
+ return {
619
+ color: border.color?.replace("#", "") || "auto",
620
+ size: border.width ? border.width * 6 : 4,
621
+ style: docxStyle
622
+ };
623
+ }
624
+ //#endregion
625
+ //#region src/converters/text.ts
626
+ /**
627
+ * Convert TipTap text node to DOCX TextRun or ExternalHyperlink
628
+ */
629
+ function convertText(node) {
630
+ const isBold = node.marks?.some((m) => m.type === "bold");
631
+ const isItalic = node.marks?.some((m) => m.type === "italic");
632
+ const isUnderline = node.marks?.some((m) => m.type === "underline");
633
+ const isStrike = node.marks?.some((m) => m.type === "strike");
634
+ const isCode = node.marks?.some((m) => m.type === "code");
635
+ const isSubscript = node.marks?.some((m) => m.type === "subscript");
636
+ const isSuperscript = node.marks?.some((m) => m.type === "superscript");
637
+ const linkMark = node.marks?.find((m) => m.type === "link");
638
+ const textStyleMark = node.marks?.find((m) => m.type === "textStyle");
639
+ const hasHighlight = node.marks?.some((m) => m.type === "highlight");
640
+ const textColor = convertColorToHex(textStyleMark?.attrs?.color);
641
+ const backgroundColor = convertColorToHex(textStyleMark?.attrs?.backgroundColor);
642
+ let fontSize;
643
+ if (textStyleMark?.attrs?.fontSize) {
644
+ const fontSizeStr = textStyleMark.attrs.fontSize;
645
+ if (fontSizeStr.endsWith("px")) {
646
+ const px = parseFloat(fontSizeStr);
647
+ if (!isNaN(px)) fontSize = Math.round(px * HALF_POINTS_PER_PIXEL);
648
+ }
649
+ }
650
+ let fontFamily;
651
+ if (isCode) fontFamily = DEFAULT_CODE_FONT;
652
+ else if (textStyleMark?.attrs?.fontFamily) fontFamily = textStyleMark.attrs.fontFamily;
653
+ const baseOptions = {
654
+ text: node.text || "",
655
+ bold: isBold || void 0,
656
+ italics: isItalic || void 0,
657
+ underline: isUnderline ? {} : void 0,
658
+ strike: isStrike || void 0,
659
+ font: fontFamily,
660
+ size: fontSize,
661
+ subScript: isSubscript || void 0,
662
+ superScript: isSuperscript || void 0,
663
+ color: textColor,
664
+ shading: backgroundColor ? { fill: backgroundColor } : void 0,
665
+ highlight: hasHighlight ? "yellow" : void 0
666
+ };
667
+ if (linkMark?.attrs?.href) return new ExternalHyperlink({
668
+ children: [new TextRun({
669
+ ...baseOptions,
670
+ style: "Hyperlink"
671
+ })],
672
+ link: linkMark.attrs.href
673
+ });
674
+ return new TextRun(baseOptions);
675
+ }
676
+ /**
677
+ * Convert TipTap hardBreak node to DOCX TextRun with break
678
+ */
679
+ function convertHardBreak(marks) {
680
+ const options = {
681
+ text: "",
682
+ break: 1
683
+ };
684
+ if (marks) for (const mark of marks) switch (mark.type) {
685
+ case "bold":
686
+ options.bold = true;
687
+ break;
688
+ case "italic":
689
+ options.italics = true;
690
+ break;
691
+ case "underline":
692
+ options.underline = {};
693
+ break;
694
+ case "strike":
695
+ options.strike = true;
696
+ break;
697
+ case "textStyle":
698
+ if (mark.attrs?.color) options.color = mark.attrs.color;
699
+ break;
700
+ }
701
+ return new TextRun(options);
702
+ }
703
+ /**
704
+ * Convert array of text nodes (text, hardBreak) to DOCX elements
705
+ * Returns flattened array of TextRun or ExternalHyperlink
706
+ */
707
+ function convertTextNodes(nodes = []) {
708
+ return nodes.flatMap((contentNode) => {
709
+ if (contentNode.type === "text") return [convertText(contentNode)];
710
+ else if (contentNode.type === "hardBreak") return [convertHardBreak(contentNode.marks)];
711
+ return [];
712
+ });
713
+ }
714
+ //#endregion
715
+ //#region src/converters/image.ts
716
+ /**
717
+ * Convert TipTap image node to DOCX ImageRun
718
+ *
719
+ * @param node - TipTap image node
720
+ * @param params - Conversion parameters
721
+ * @returns Promise<DOCX ImageRun>
722
+ */
723
+ async function convertImage(node, params) {
724
+ const getImageType = (metaType) => {
725
+ return convertToDocxImageType(metaType || getImageTypeFromSrc(node.attrs?.src || ""));
726
+ };
727
+ let imageData;
728
+ let imageMeta$1;
729
+ try {
730
+ const src = node.attrs?.src || "";
731
+ if (params?.handler) {
732
+ imageData = await params.handler(src);
733
+ try {
734
+ imageMeta$1 = imageMeta(imageData);
735
+ } catch {
736
+ imageMeta$1 = {
737
+ type: getImageTypeFromSrc(src),
738
+ width: void 0,
739
+ height: void 0,
740
+ orientation: void 0
741
+ };
742
+ }
743
+ } else if (src.startsWith("http")) {
744
+ const result = await getImageDataAndMeta(src);
745
+ imageData = result.data;
746
+ imageMeta$1 = result.meta;
747
+ } else if (src.startsWith("data:")) {
748
+ const base64Data = src.split(",")[1];
749
+ if (!base64Data) throw new Error("Invalid data URL: missing base64 data");
750
+ const binaryString = atob(base64Data);
751
+ imageData = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
752
+ try {
753
+ imageMeta$1 = imageMeta(imageData);
754
+ } catch {
755
+ imageMeta$1 = {
756
+ type: "png",
757
+ width: void 0,
758
+ height: void 0,
759
+ orientation: void 0
760
+ };
761
+ }
762
+ } else throw new Error(`Unsupported image source format: ${src.substring(0, 20)}...`);
763
+ } catch (error) {
764
+ console.warn(`Failed to process image:`, error);
765
+ return new ImageRun({
766
+ type: "png",
767
+ data: new Uint8Array(0),
768
+ transformation: {
769
+ width: 100,
770
+ height: 100
771
+ },
772
+ altText: { name: node.attrs?.alt || "Failed to load image" }
773
+ });
774
+ }
775
+ const finalWidth = getImageWidth(node, imageMeta$1, params?.maxWidth);
776
+ const transformation = {
777
+ width: finalWidth,
778
+ height: getImageHeight(node, finalWidth, imageMeta$1, params?.maxWidth)
779
+ };
780
+ if (node.attrs?.rotation !== void 0) transformation.rotation = node.attrs.rotation;
781
+ return new ImageRun({
782
+ ...params?.options,
783
+ type: getImageType(imageMeta$1.type),
784
+ data: imageData,
785
+ transformation,
786
+ altText: {
787
+ name: node.attrs?.alt || "",
788
+ description: void 0,
789
+ title: node.attrs?.title || void 0
790
+ },
791
+ ...node.attrs?.floating && { floating: node.attrs.floating },
792
+ ...node.attrs?.outline && { outline: node.attrs.outline }
793
+ });
794
+ }
795
+ //#endregion
796
+ //#region src/converters/paragraph.ts
797
+ /**
798
+ * Convert TipTap paragraph node to DOCX paragraph options
799
+ *
800
+ * This converter only handles data transformation from node.attrs to DOCX format properties.
801
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
802
+ *
803
+ * @param node - TipTap paragraph node
804
+ * @param params - Conversion parameters
805
+ * @returns Promise<DOCX paragraph options (pure data object)>
806
+ */
807
+ async function convertParagraph(node, params) {
808
+ const { options, image } = params || {};
809
+ const children = [];
810
+ for (const contentNode of node.content || []) if (contentNode.type === "text") children.push(convertText(contentNode));
811
+ else if (contentNode.type === "hardBreak") children.push(convertHardBreak(contentNode.marks));
812
+ else if (contentNode.type === "image") {
813
+ const imageRun = await convertImage(contentNode, {
814
+ maxWidth: image?.maxWidth,
815
+ options: image?.options,
816
+ handler: image?.handler
817
+ });
818
+ children.push(imageRun);
819
+ }
820
+ let paragraphOptions = { children };
821
+ if (options) paragraphOptions = {
822
+ ...paragraphOptions,
823
+ ...options
824
+ };
825
+ if (node.attrs) paragraphOptions = applyParagraphStyleAttributes(paragraphOptions, node.attrs);
826
+ return paragraphOptions;
827
+ }
828
+ //#endregion
829
+ //#region src/converters/heading.ts
830
+ /**
831
+ * Convert TipTap heading node to DOCX paragraph options
832
+ *
833
+ * This converter only handles data transformation from node.attrs to DOCX format properties.
834
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
835
+ *
836
+ * @param node - TipTap heading node
837
+ * @returns DOCX paragraph options (pure data object)
838
+ */
839
+ function convertHeading(node) {
840
+ const level = node?.attrs?.level;
841
+ let paragraphOptions = {
842
+ children: convertTextNodes(node.content).filter((item) => item !== void 0),
843
+ heading: {
844
+ 1: HeadingLevel.HEADING_1,
845
+ 2: HeadingLevel.HEADING_2,
846
+ 3: HeadingLevel.HEADING_3,
847
+ 4: HeadingLevel.HEADING_4,
848
+ 5: HeadingLevel.HEADING_5,
849
+ 6: HeadingLevel.HEADING_6
850
+ }[level]
851
+ };
852
+ if (node.attrs) paragraphOptions = applyParagraphStyleAttributes(paragraphOptions, node.attrs);
853
+ return paragraphOptions;
854
+ }
855
+ //#endregion
856
+ //#region src/converters/blockquote.ts
857
+ /**
858
+ * Convert TipTap blockquote node to array of paragraph options
859
+ *
860
+ * This converter only handles data transformation from node content to DOCX format properties.
861
+ * It returns pure data objects (IParagraphOptions[]), not DOCX instances.
862
+ *
863
+ * @param node - TipTap blockquote node
864
+ * @returns Array of paragraph options (pure data objects)
865
+ */
866
+ function convertBlockquote(node) {
867
+ if (!node.content) return [];
868
+ return node.content.map((contentNode) => {
869
+ if (contentNode.type === "paragraph") return {
870
+ children: contentNode.content?.flatMap((node) => {
871
+ if (node.type === "text") return convertText(node);
872
+ else if (node.type === "hardBreak") return convertHardBreak(node.marks);
873
+ return [];
874
+ }) || [],
875
+ indent: { left: 720 },
876
+ border: { left: { style: "single" } }
877
+ };
878
+ return {};
879
+ });
880
+ }
881
+ //#endregion
882
+ //#region src/converters/table-cell.ts
883
+ /**
884
+ * Convert TipTap table cell node to DOCX TableCell
885
+ *
886
+ * @param node - TipTap table cell node
887
+ * @param params - Conversion parameters
888
+ * @returns Promise<DOCX TableCell object>
889
+ */
890
+ async function convertTableCell(node, params) {
891
+ const { options } = params;
892
+ let cellParagraphOptions = options?.cell?.paragraph ?? options?.row?.paragraph ?? {};
893
+ if (options?.style) cellParagraphOptions = {
894
+ ...cellParagraphOptions,
895
+ style: options.style.id
896
+ };
897
+ const cellOptions = {
898
+ children: (await Promise.all((node.content || []).map((p) => convertParagraph(p, { options: cellParagraphOptions })))).map((options) => new Paragraph(options)),
899
+ ...options?.cell?.run
900
+ };
901
+ if (node.attrs?.colspan && node.attrs.colspan > 1) cellOptions.columnSpan = node.attrs.colspan;
902
+ if (node.attrs?.rowspan && node.attrs.rowspan > 1) cellOptions.rowSpan = node.attrs.rowspan;
903
+ if (node.attrs?.colwidth !== null && node.attrs?.colwidth !== void 0) {
904
+ const widthInPixels = Array.isArray(node.attrs.colwidth) ? node.attrs.colwidth[0] : node.attrs.colwidth;
905
+ if (widthInPixels && widthInPixels > 0) cellOptions.width = {
906
+ size: Math.round(widthInPixels * 15),
907
+ type: "dxa"
908
+ };
909
+ }
910
+ if (node.attrs?.backgroundColor) cellOptions.shading = { fill: node.attrs.backgroundColor.replace("#", "") };
911
+ if (node.attrs?.verticalAlign) cellOptions.verticalAlign = node.attrs.verticalAlign === "middle" ? "center" : node.attrs.verticalAlign;
912
+ const borders = {
913
+ top: convertBorder(node.attrs?.borderTop),
914
+ bottom: convertBorder(node.attrs?.borderBottom),
915
+ left: convertBorder(node.attrs?.borderLeft),
916
+ right: convertBorder(node.attrs?.borderRight)
917
+ };
918
+ if (borders.top || borders.bottom || borders.left || borders.right) cellOptions.borders = borders;
919
+ return new TableCell(cellOptions);
920
+ }
921
+ //#endregion
922
+ //#region src/converters/table-header.ts
923
+ /**
924
+ * Convert TipTap table header node to DOCX TableCell
925
+ *
926
+ * @param node - TipTap table header node
927
+ * @param params - Conversion parameters
928
+ * @returns Promise<DOCX TableCell object for header>
929
+ */
930
+ async function convertTableHeader(node, params) {
931
+ const { options } = params;
932
+ let headerParagraphOptions = options?.header?.paragraph ?? options?.cell?.paragraph ?? options?.row?.paragraph ?? {};
933
+ if (options?.style) headerParagraphOptions = {
934
+ ...headerParagraphOptions,
935
+ style: options.style.id
936
+ };
937
+ const headerCellOptions = {
938
+ children: (await Promise.all((node.content || []).map((p) => convertParagraph(p, { options: headerParagraphOptions })))).map((options) => new Paragraph(options)),
939
+ ...options?.header?.run
940
+ };
941
+ if (node.attrs?.colspan && node.attrs.colspan > 1) headerCellOptions.columnSpan = node.attrs.colspan;
942
+ if (node.attrs?.rowspan && node.attrs.rowspan > 1) headerCellOptions.rowSpan = node.attrs.rowspan;
943
+ if (node.attrs?.colwidth !== null && node.attrs?.colwidth !== void 0) {
944
+ const widthInPixels = Array.isArray(node.attrs.colwidth) ? node.attrs.colwidth[0] : node.attrs.colwidth;
945
+ if (widthInPixels && widthInPixels > 0) headerCellOptions.width = {
946
+ size: Math.round(widthInPixels * 15),
947
+ type: "dxa"
948
+ };
949
+ }
950
+ if (node.attrs?.backgroundColor) headerCellOptions.shading = { fill: node.attrs.backgroundColor.replace("#", "") };
951
+ if (node.attrs?.verticalAlign) headerCellOptions.verticalAlign = node.attrs.verticalAlign === "middle" ? "center" : node.attrs.verticalAlign;
952
+ const borders = {
953
+ top: convertBorder(node.attrs?.borderTop),
954
+ bottom: convertBorder(node.attrs?.borderBottom),
955
+ left: convertBorder(node.attrs?.borderLeft),
956
+ right: convertBorder(node.attrs?.borderRight)
957
+ };
958
+ if (borders.top || borders.bottom || borders.left || borders.right) headerCellOptions.borders = borders;
959
+ return new TableCell(headerCellOptions);
960
+ }
961
+ //#endregion
962
+ //#region src/converters/table-row.ts
963
+ /**
964
+ * Convert TipTap table row node to DOCX TableRow
965
+ *
966
+ * @param node - TipTap table row node
967
+ * @param params - Conversion parameters
968
+ * @returns Promise<DOCX TableRow object>
969
+ */
970
+ async function convertTableRow(node, params) {
971
+ const { options } = params;
972
+ const rowOptions = options?.row;
973
+ const tableRowOptions = {
974
+ children: (await Promise.all((node.content || []).map(async (cellNode) => {
975
+ if (cellNode.type === "tableCell") return await convertTableCell(cellNode, params);
976
+ else if (cellNode.type === "tableHeader") return await convertTableHeader(cellNode, params);
977
+ return null;
978
+ }))).filter((cell) => cell !== void 0),
979
+ ...rowOptions
980
+ };
981
+ if (node.attrs?.rowHeight) {
982
+ const twips = convertPixelsToTwip(convertCssLengthToPixels(node.attrs.rowHeight));
983
+ if (twips > 0) tableRowOptions.height = {
984
+ rule: "atLeast",
985
+ value: twips
986
+ };
987
+ }
988
+ return new TableRow(tableRowOptions);
989
+ }
990
+ //#endregion
991
+ //#region src/converters/table.ts
992
+ /**
993
+ * Apply table margins to table options
994
+ */
995
+ const applyTableMargins = (options, node) => {
996
+ const margins = {
997
+ top: node.attrs?.marginTop ?? void 0,
998
+ bottom: node.attrs?.marginBottom ?? void 0,
999
+ left: node.attrs?.marginLeft ?? void 0,
1000
+ right: node.attrs?.marginRight ?? void 0
1001
+ };
1002
+ if (margins.top || margins.bottom || margins.left || margins.right) return {
1003
+ ...options,
1004
+ margins
1005
+ };
1006
+ return options;
1007
+ };
1008
+ /**
1009
+ * Convert TipTap table node to DOCX Table
1010
+ *
1011
+ * @param node - TipTap table node
1012
+ * @param params - Conversion parameters
1013
+ * @returns Promise<Table>
1014
+ */
1015
+ async function convertTable(node, params) {
1016
+ const { options } = params;
1017
+ let tableOptions = {
1018
+ rows: await Promise.all((node.content || []).map((row) => convertTableRow(row, params))),
1019
+ ...options?.run
1020
+ };
1021
+ tableOptions = applyTableMargins(tableOptions, node);
1022
+ return new Table(tableOptions);
1023
+ }
1024
+ //#endregion
1025
+ //#region src/converters/code-block.ts
1026
+ /**
1027
+ * Convert TipTap codeBlock node to DOCX paragraph options
1028
+ *
1029
+ * This converter only handles data transformation from node.attrs to DOCX format properties.
1030
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
1031
+ *
1032
+ * @param node - TipTap codeBlock node
1033
+ * @returns DOCX paragraph options (pure data object)
1034
+ */
1035
+ function convertCodeBlock(node) {
1036
+ if (!node.content || node.content.length === 0) return { children: [new TextRun({
1037
+ text: "",
1038
+ font: DEFAULT_CODE_FONT
1039
+ })] };
1040
+ const textRuns = node.content.flatMap((contentNode) => {
1041
+ if (contentNode.type === "text") return convertText(contentNode);
1042
+ return [];
1043
+ });
1044
+ return { children: textRuns.length > 0 ? textRuns : [new TextRun({
1045
+ text: "",
1046
+ font: DEFAULT_CODE_FONT
1047
+ })] };
1048
+ }
1049
+ //#endregion
1050
+ //#region src/converters/list-item.ts
1051
+ /**
1052
+ * Convert TipTap list item node to paragraph options
1053
+ *
1054
+ * This converter only handles data transformation from node content to DOCX format properties.
1055
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
1056
+ *
1057
+ * Note: The numbering reference (including start value) is typically
1058
+ * handled by the parent list converter. This function focuses on
1059
+ * converting the paragraph content of the list item.
1060
+ *
1061
+ * @param node - TipTap list item node
1062
+ * @param params - Conversion parameters
1063
+ * @returns Promise<Paragraph options (pure data object)>
1064
+ */
1065
+ async function convertListItem(node, params) {
1066
+ if (!node.content || node.content.length === 0) return {};
1067
+ const firstParagraph = node.content[0];
1068
+ if (firstParagraph.type === "paragraph") return await convertParagraph(firstParagraph, { options: params.options });
1069
+ return {};
1070
+ }
1071
+ //#endregion
1072
+ //#region src/converters/list.ts
1073
+ function convertBulletList() {
1074
+ return { numbering: {
1075
+ reference: "bullet-list",
1076
+ level: 0
1077
+ } };
1078
+ }
1079
+ function convertOrderedList(node) {
1080
+ return {
1081
+ numbering: {
1082
+ reference: "ordered-list",
1083
+ level: 0
1084
+ },
1085
+ start: node.attrs?.start || 1
1086
+ };
1087
+ }
1088
+ /**
1089
+ * Convert list nodes (bullet or ordered) with proper numbering
1090
+ *
1091
+ * This converter only handles data transformation from node content to DOCX format properties.
1092
+ * It returns pure data objects (IParagraphOptions[]), not DOCX instances.
1093
+ */
1094
+ async function convertList(node, params) {
1095
+ const { listType } = params;
1096
+ if (!node.content) return [];
1097
+ const elements = [];
1098
+ const listOptions = listType === "bullet" ? convertBulletList() : convertOrderedList(node);
1099
+ let numberingReference = listOptions.numbering.reference;
1100
+ if (listType === "ordered" && listOptions.start && listOptions.start !== 1) numberingReference = `ordered-list-start-${listOptions.start}`;
1101
+ for (const item of node.content) if (item.type === "listItem") {
1102
+ const paragraphOptions = await convertListItem(item, { options: { numbering: {
1103
+ reference: numberingReference,
1104
+ level: 0
1105
+ } } });
1106
+ elements.push(paragraphOptions);
1107
+ }
1108
+ return elements;
1109
+ }
1110
+ //#endregion
1111
+ //#region src/converters/task-item.ts
1112
+ /**
1113
+ * Convert TipTap task item node to paragraph options with checkbox
1114
+ *
1115
+ * This converter only handles data transformation from node content to DOCX format properties.
1116
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
1117
+ *
1118
+ * @param node - TipTap task item node
1119
+ * @returns Paragraph options (pure data object) with checkbox
1120
+ */
1121
+ function convertTaskItem(node) {
1122
+ if (!node.content || node.content.length === 0) return {};
1123
+ const firstParagraph = node.content[0];
1124
+ if (firstParagraph.type === "paragraph") {
1125
+ const checkboxText = node.attrs?.checked || false ? CHECKBOX_SYMBOLS.checked + " " : CHECKBOX_SYMBOLS.unchecked + " ";
1126
+ const children = firstParagraph.content?.flatMap((contentNode) => {
1127
+ if (contentNode.type === "text") return convertText(contentNode);
1128
+ else if (contentNode.type === "hardBreak") return convertHardBreak(contentNode.marks);
1129
+ return [];
1130
+ }) || [];
1131
+ return { children: [new TextRun({ text: checkboxText }), ...children] };
1132
+ }
1133
+ return {};
1134
+ }
1135
+ //#endregion
1136
+ //#region src/converters/task-list.ts
1137
+ /**
1138
+ * Convert TipTap task list node to array of paragraph options
1139
+ *
1140
+ * This converter only handles data transformation from node content to DOCX format properties.
1141
+ * It returns pure data objects (IParagraphOptions[]), not DOCX instances.
1142
+ *
1143
+ * @param node - TipTap task list node
1144
+ * @returns Array of paragraph options (pure data objects) with checkboxes
1145
+ */
1146
+ function convertTaskList(node) {
1147
+ if (!node.content || node.content.length === 0) return [];
1148
+ return node.content.filter((item) => item.type === "taskItem").map((item) => convertTaskItem(item));
1149
+ }
1150
+ //#endregion
1151
+ //#region src/converters/horizontal-rule.ts
1152
+ /**
1153
+ * Convert TipTap horizontalRule node to paragraph options
1154
+ *
1155
+ * This converter only handles data transformation from node to DOCX format properties.
1156
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
1157
+ *
1158
+ * Uses page break by default (consistent with import-docx behavior)
1159
+ *
1160
+ * @param node - TipTap horizontalRule node
1161
+ * @param params - Conversion parameters
1162
+ * @returns Paragraph options (pure data object) with page break or custom styling
1163
+ */
1164
+ function convertHorizontalRule(node, params) {
1165
+ return {
1166
+ children: [new PageBreak()],
1167
+ ...params.options?.paragraph
1168
+ };
1169
+ }
1170
+ //#endregion
1171
+ //#region src/converters/details.ts
1172
+ /**
1173
+ * Convert TipTap detailsSummary node to paragraph options
1174
+ *
1175
+ * This converter only handles data transformation from node content to DOCX format properties.
1176
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
1177
+ *
1178
+ * @param node - TipTap detailsSummary node
1179
+ * @param params - Conversion parameters
1180
+ * @returns Paragraph options (pure data object) with summary styling
1181
+ */
1182
+ function convertDetailsSummary(node, params) {
1183
+ return {
1184
+ children: convertTextNodes(node.content || []).filter((item) => item !== void 0),
1185
+ ...params.options?.summary?.paragraph
1186
+ };
1187
+ }
1188
+ //#endregion
1189
+ //#region src/generator.ts
1190
+ /**
1191
+ * Convert TipTap JSONContent to DOCX format
1192
+ *
1193
+ * @param docJson - TipTap document JSON
1194
+ * @param options - Export options
1195
+ * @returns Promise with DOCX in specified format
1196
+ */
1197
+ async function generateDOCX(docJson, options) {
1198
+ const { title, subject, creator, keywords, description, lastModifiedBy, revision, styles, tableOfContents, sections, fonts, hyphenation, compatibility, customProperties, evenAndOddHeaderAndFooters, defaultTabStop, outputType } = options;
1199
+ const children = await convertDocument(docJson, { options });
1200
+ const tocElement = tableOfContents ? new TableOfContents(tableOfContents.title, { ...tableOfContents.run }) : null;
1201
+ const numberingOptions = createNumberingOptions(docJson);
1202
+ const additionalParagraphStyles = [];
1203
+ if (options.image?.style) additionalParagraphStyles.push(options.image.style);
1204
+ if (options.table?.style) additionalParagraphStyles.push(options.table.style);
1205
+ if (options.code?.style) additionalParagraphStyles.push(options.code.style);
1206
+ const mergedStyles = styles ? {
1207
+ ...styles,
1208
+ ...additionalParagraphStyles.length > 0 && { paragraphStyles: [...styles.paragraphStyles || [], ...additionalParagraphStyles] }
1209
+ } : {};
1210
+ const doc = new Document({
1211
+ sections: sections ? sections.map((section, index) => {
1212
+ const sectionChildren = [];
1213
+ if (index === 0 && tocElement) sectionChildren.push(tocElement);
1214
+ if (index === 0) sectionChildren.push(...children);
1215
+ return {
1216
+ ...section,
1217
+ ...sectionChildren.length > 0 ? { children: sectionChildren } : {}
1218
+ };
1219
+ }) : [{ children: tocElement ? [tocElement, ...children] : children }],
1220
+ title: title || "Document",
1221
+ subject: subject || "",
1222
+ creator: creator || "",
1223
+ keywords: keywords || "",
1224
+ description: description || "",
1225
+ lastModifiedBy: lastModifiedBy || "",
1226
+ revision: revision || 1,
1227
+ styles: mergedStyles,
1228
+ numbering: numberingOptions,
1229
+ ...fonts && fonts.length > 0 && { fonts },
1230
+ ...hyphenation && { hyphenation },
1231
+ ...compatibility && { compatibility },
1232
+ ...customProperties && customProperties.length > 0 && { customProperties },
1233
+ ...evenAndOddHeaderAndFooters !== void 0 && { evenAndOddHeaderAndFooters },
1234
+ ...defaultTabStop !== void 0 && { defaultTabStop }
1235
+ });
1236
+ return Packer.pack(doc, outputType || "arraybuffer");
1237
+ }
1238
+ /**
1239
+ * Convert document content to DOCX elements
1240
+ */
1241
+ async function convertDocument(node, params) {
1242
+ const elements = [];
1243
+ if (!node || !Array.isArray(node.content)) return elements;
1244
+ const effectiveContentWidth = calculateEffectiveContentWidth(params.options);
1245
+ for (const childNode of node.content) {
1246
+ const element = await convertNode(childNode, params.options, effectiveContentWidth);
1247
+ if (Array.isArray(element)) elements.push(...element);
1248
+ else if (element) {
1249
+ elements.push(element);
1250
+ if (childNode.type === "table" && elements.length >= 2 && elements[elements.length - 2] instanceof Table) elements.push(new Paragraph({}));
1251
+ }
1252
+ }
1253
+ return elements;
1254
+ }
1255
+ /**
1256
+ * Convert a single node to DOCX element(s)
1257
+ *
1258
+ * This function implements a three-layer architecture:
1259
+ * 1. Data Transformation: Convert node.attrs → IParagraphOptions (pure data)
1260
+ * 2. Style Application: Apply styleId references (if configured)
1261
+ * 3. Object Creation: Create actual DOCX instances (Paragraph, Table, etc.)
1262
+ */
1263
+ async function convertNode(node, options, effectiveContentWidth) {
1264
+ if (!node || !node.type) return null;
1265
+ const dataResult = await convertNodeData(node, options, effectiveContentWidth);
1266
+ if (dataResult instanceof Table) return dataResult;
1267
+ if (Array.isArray(dataResult)) {
1268
+ const styleId = getStyleIdByNodeType(node.type, options);
1269
+ return dataResult.map((paragraphOptions) => {
1270
+ return new Paragraph(applyStyleReference(paragraphOptions, styleId));
1271
+ });
1272
+ }
1273
+ return createDOCXObject(applyStyleReference(dataResult, getStyleIdByNodeType(node.type, options)));
1274
+ }
1275
+ /**
1276
+ * Layer 1: Data Transformation
1277
+ *
1278
+ * Convert node data to DOCX format properties.
1279
+ * Returns pure data objects (IParagraphOptions) or arrays, not DOCX instances.
1280
+ * This layer does NOT handle styleId references.
1281
+ */
1282
+ async function convertNodeData(node, options, effectiveContentWidth) {
1283
+ switch (node.type) {
1284
+ case "paragraph": return await convertParagraph(node, { image: {
1285
+ maxWidth: effectiveContentWidth,
1286
+ options: options.image?.run,
1287
+ handler: options.image?.handler
1288
+ } });
1289
+ case "heading": return convertHeading(node);
1290
+ case "blockquote": return convertBlockquote(node);
1291
+ case "codeBlock": return convertCodeBlock(node);
1292
+ case "image": return { children: [await convertImage(node, {
1293
+ maxWidth: effectiveContentWidth,
1294
+ options: options.image?.run,
1295
+ handler: options.image?.handler
1296
+ })] };
1297
+ case "table": return await convertTable(node, { options: options.table });
1298
+ case "bulletList": return await convertList(node, { listType: "bullet" });
1299
+ case "orderedList": return await convertList(node, { listType: "ordered" });
1300
+ case "taskList": return convertTaskList(node);
1301
+ case "taskItem": return convertTaskItem(node);
1302
+ case "hardBreak": return { children: [convertHardBreak()] };
1303
+ case "horizontalRule": return convertHorizontalRule(node, { options: options.horizontalRule });
1304
+ case "details": return await convertDetails(node, options, effectiveContentWidth);
1305
+ case "detailsSummary": return convertDetailsSummary(node, { options: options.details });
1306
+ default: return { children: [new TextRun({ text: `[Unsupported: ${node.type}]` })] };
1307
+ }
1308
+ }
1309
+ /**
1310
+ * Helper to convert details node (needs to recursively call convertNode)
1311
+ */
1312
+ async function convertDetails(node, options, effectiveContentWidth) {
1313
+ const elements = [];
1314
+ if (node.content) for (const child of node.content) {
1315
+ const element = await convertNode(child, options, effectiveContentWidth);
1316
+ if (Array.isArray(element)) elements.push(...element);
1317
+ else if (element) elements.push(element);
1318
+ }
1319
+ return elements;
1320
+ }
1321
+ /**
1322
+ * Create a single ordered list level configuration
1323
+ */
1324
+ const createOrderedListLevel = (start) => ({
1325
+ level: 0,
1326
+ format: LevelFormat.DECIMAL,
1327
+ text: "%1.",
1328
+ alignment: AlignmentType.START,
1329
+ start: start ?? 1,
1330
+ style: { paragraph: { indent: {
1331
+ left: convertInchesToTwip(.5),
1332
+ hanging: convertInchesToTwip(.25)
1333
+ } } }
1334
+ });
1335
+ /**
1336
+ * Create a numbering reference configuration
1337
+ */
1338
+ const createNumberingReference = (start) => ({
1339
+ reference: start && start !== 1 ? `ordered-list-start-${start}` : "ordered-list",
1340
+ levels: [createOrderedListLevel(start)]
1341
+ });
1342
+ /**
1343
+ * Create numbering options for the document
1344
+ */
1345
+ function createNumberingOptions(docJson) {
1346
+ const orderedListStarts = /* @__PURE__ */ new Set();
1347
+ function collectListStarts(node) {
1348
+ if (node.type === "orderedList" && node.attrs?.start) orderedListStarts.add(node.attrs.start);
1349
+ if (node.content) for (const child of node.content) collectListStarts(child);
1350
+ }
1351
+ collectListStarts(docJson);
1352
+ const numberingOptions = [{
1353
+ reference: "bullet-list",
1354
+ levels: [{
1355
+ level: 0,
1356
+ format: LevelFormat.BULLET,
1357
+ text: "•",
1358
+ alignment: AlignmentType.START,
1359
+ style: { paragraph: { indent: {
1360
+ left: convertInchesToTwip(.5),
1361
+ hanging: convertInchesToTwip(.25)
1362
+ } } }
1363
+ }]
1364
+ }, createNumberingReference(1)];
1365
+ orderedListStarts.forEach((start) => {
1366
+ if (start !== 1) numberingOptions.push(createNumberingReference(start));
1367
+ });
1368
+ return { config: numberingOptions };
1369
+ }
1370
+ /**
1371
+ * Get style ID for a specific node type from export options
1372
+ *
1373
+ * This is a centralized mapping of node types to their configured style IDs.
1374
+ * Style references are applied separately from data transformation.
1375
+ *
1376
+ * @param nodeType - The type of TipTap node
1377
+ * @param options - Export options containing style configurations
1378
+ * @returns Style ID string if configured, undefined otherwise
1379
+ */
1380
+ function getStyleIdByNodeType(nodeType, options) {
1381
+ return {
1382
+ codeBlock: options.code?.style?.id,
1383
+ image: options.image?.style?.id
1384
+ }[nodeType];
1385
+ }
1386
+ /**
1387
+ * Apply style reference to paragraph options
1388
+ *
1389
+ * This function handles the final step of adding a style ID reference to
1390
+ * paragraph options. It's called after data transformation is complete.
1391
+ *
1392
+ * @param paragraphOptions - Paragraph options from converter
1393
+ * @param styleId - Style ID to apply (optional)
1394
+ * @returns Paragraph options with style ID applied if provided
1395
+ */
1396
+ function applyStyleReference(paragraphOptions, styleId) {
1397
+ if (!styleId) return paragraphOptions;
1398
+ return {
1399
+ ...paragraphOptions,
1400
+ style: styleId
1401
+ };
1402
+ }
1403
+ /**
1404
+ * Create a DOCX object from paragraph options
1405
+ *
1406
+ * This is the final step that creates actual DOCX instances from
1407
+ * pure data objects.
1408
+ *
1409
+ * @param options - Paragraph options or table
1410
+ * @returns DOCX Paragraph or Table instance
1411
+ */
1412
+ function createDOCXObject(options) {
1413
+ if (options instanceof Table) return options;
1414
+ return new Paragraph(options);
1415
+ }
1416
+ //#endregion
1417
+ export { CHECKBOX_SYMBOLS, COLOR_NAME_TO_HEX, DEFAULT_CODE_FONT, DOCX_DPI, DOCX_STYLE_NAMES, EMUS_PER_INCH, HALF_POINTS_PER_PIXEL, PAGE_DIMENSIONS, PIXELS_PER_HALF_POINT, TEXT_ALIGN_MAP, TWIPS_PER_INCH, applyParagraphStyleAttributes, applyTableMargins, calculateEffectiveContentWidth, convertBlockquote, convertBorder, convertBulletList, convertCodeBlock, convertColorToHex, convertCssLengthToPixels, convertDetailsSummary, convertDocument, convertEmuStringToPixels, convertEmuToPixels, convertHardBreak, convertHeading, convertHorizontalRule, convertImage, convertList, convertListItem, convertMeasureToInches, convertMeasureToPixels, convertNode, convertOrderedList, convertParagraph, convertPixelsToEmu, convertPixelsToTwip, convertTable, convertTableCell, convertTableHeader, convertTableRow, convertTaskItem, convertTaskList, convertText, convertTextNodes, convertToDocxImageType, convertTwipToCssString, convertTwipToPixels, createFloatingOptions, createStringValidator, findChild, findDeepChild, findDeepChildren, generateDOCX, getImageDataAndMeta, getImageHeight, getImageTypeFromSrc, getImageWidth, normalizeHexColor, parseTwipAttr };