@docen/export-docx 0.0.10 → 0.0.12

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,1446 @@
1
- import{TextRun as f,ExternalHyperlink as $t,ImageRun as Z,HeadingLevel as b,Paragraph as I,TableCell as tt,TableRow as zt,Table as B,PageBreak as jt,TableOfContents as Gt,Document as Ut,Packer as Xt,AlignmentType as et,LevelFormat as nt,convertInchesToTwip as _}from"docx";import{imageMeta as O}from"image-meta";import{ofetch as qt}from"ofetch";const W=96,$=1440,Kt=914400,rt=1.5,Yt=.6666666666666666,P="Consolas",z={checked:"\u2611",unchecked:"\u2610"},Vt={CODE_BLOCK:"CodeBlock",CODE_PREFIX:"Code"},ot={tiptapToDocx:{left:"left",right:"right",center:"center",justify:"both"},docxToTipTap:{left:"left",right:"right",center:"center",both:"justify"}},j={A4_WIDTH_TWIP:11906,DEFAULT_MARGIN_TWIP:1440},M=96;function E(t){return Math.round(t*M/1440)}function Jt(t){return`${E(t)}px`}function g(t){return Math.round(t*(1440/M))}function at(t){return Math.round(t/(914400/M))}function Qt(t){return Math.round(t*(914400/M))}function Zt(t){const e=parseInt(t,10);if(!isNaN(e))return at(e)}const te=/^(-?[\d.]+)(px|pt|em|rem|%|)?$/,ee={px:1,pt:1.333,em:16,rem:16,"%":.16};function m(t){if(!t)return 0;t=t.trim();const e=t.match(te);if(!e)return 0;const n=parseFloat(e[1]);if(isNaN(n))return 0;const o=e[2]||"px",r=ee[o]??1;return Math.round(n*r)}const ne=/^([\d.]+)(in|mm|cm|pt|pc|pi)$/,re={in:1,mm:1/25.4,cm:1/2.54,pt:1/72,pc:1/6,pi:1/6};function S(t){if(typeof t=="number")return t;const e=t.match(ne);if(e){const o=parseFloat(e[1]),r=e[2],a=re[r];return a!==void 0?o*a:o}const n=parseFloat(t);return isNaN(n)?6.5:n}function G(t){if(typeof t=="number")return t;const e=S(t);return Math.round(e*96)}const it={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 U(t){if(t)return t.startsWith("#")?t:it[t.toLowerCase()]||t}function oe(t){return t.startsWith("#")?t:`#${t}`}function ae(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 st(t,e){if(!t.children)return null;for(const n of t.children)if(n.type==="element"){if(n.name===e)return n;const o=st(n,e);if(o)return o}return null}function ct(t,e){const n=[];if(!t.children)return n;for(const o of t.children)o.type==="element"&&(o.name===e&&n.push(o),n.push(...ct(o,e)));return n}function ie(t,e){const n=t[e];if(!n)return;const o=parseInt(n,10);if(!isNaN(o))return n}function se(t){return e=>t.includes(e)}const lt=(t,e)=>t?typeof t=="number"?t:Math.round(S(t)*$):e;function dt(t){const e=j.A4_WIDTH_TWIP,n=j.DEFAULT_MARGIN_TWIP;if(!t?.sections?.length)return E(e-n*2);const o=t.sections[0];if(!o.properties?.page)return E(e-n*2);const r=o.properties.page;let a=e;if(r.size?.width){const d=r.size.width;a=typeof d=="number"?d:Math.round(S(d)*$)}const i=r.margin,l=lt(i?.left,n),u=lt(i?.right,n),c=a-l-u;return Math.max(E(c),W)}const ut=6.5*W,N={mimeToInternal:{jpg:"jpeg",jpeg:"jpeg",png:"png",gif:"gif",bmp:"bmp",tiff:"tiff"},internalToDocx:{jpeg:"jpg",png:"png",gif:"gif",bmp:"bmp",tiff:"bmp"}};function pt(t){if(!t)return"png";const e=t.toLowerCase(),n=N.mimeToInternal[e]||"png";return N.internalToDocx[n]||"png"}function H(t){if(t.startsWith("data:")){const e=t.match(/data:image\/(\w+);/);if(e){const n=e[1].toLowerCase();return N.mimeToInternal[n]||"png"}}else{const e=t.split(".").pop()?.toLowerCase();if(e)return N.mimeToInternal[e]||"png"}return"png"}const ht=(t,e=ut)=>{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 ce(){return{horizontalPosition:{relative:"page",align:"center"},verticalPosition:{relative:"page",align:"top"},lockAnchor:!0,behindDocument:!1,inFrontOfText:!1}}function ft(t,e,n){if(t.attrs?.width!==void 0&&t.attrs?.width!==null)return t.attrs.width;const o=n!==void 0?G(n):void 0;return e?.width&&e?.height?ht(e,o).width:o||ut}function gt(t,e,n,o){if(t.attrs?.height!==void 0&&t.attrs?.height!==null)return t.attrs.height;const r=o!==void 0?G(o):void 0;return n?.width&&n?.height?ht(n,r).height:Math.round(e*.75)}async function mt(t){try{const e=await(await qt(t,{responseType:"blob"})).bytes();let n;try{n=O(e)}catch(o){console.warn("Failed to extract image metadata:",o),n={width:void 0,height:void 0,type:H(t)||"png",orientation:void 0}}return{data:e,meta:n}}catch(e){throw console.warn(`Failed to fetch image from ${t}:`,e),e}}const X=(t,e)=>{if(!e)return t;let n={...t};return(e.indentLeft||e.indentRight||e.indentFirstLine)&&(n={...n,indent:{...e.indentLeft&&{left:g(m(e.indentLeft))},...e.indentRight&&{right:g(m(e.indentRight))},...e.indentFirstLine&&{firstLine:g(m(e.indentFirstLine))}}}),(e.spacingBefore||e.spacingAfter)&&(n={...n,spacing:{...e.spacingBefore&&{before:g(m(e.spacingBefore))},...e.spacingAfter&&{after:g(m(e.spacingAfter))}}}),e.textAlign&&(n={...n,alignment:ot.tiptapToDocx[e.textAlign]}),n};function p(t){if(!t)return;const e={solid:"single",dashed:"dashed",dotted:"dotted",double:"double",none:"none"},n=t.style&&e[t.style]||"single",o=t.color?.replace("#","")||"auto",r=t.width?t.width*6:4;return{color:o,size:r,style:n}}function T(t){const e=t.marks?.some(s=>s.type==="bold"),n=t.marks?.some(s=>s.type==="italic"),o=t.marks?.some(s=>s.type==="underline"),r=t.marks?.some(s=>s.type==="strike"),a=t.marks?.some(s=>s.type==="code"),i=t.marks?.some(s=>s.type==="subscript"),l=t.marks?.some(s=>s.type==="superscript"),u=t.marks?.find(s=>s.type==="link"),c=t.marks?.find(s=>s.type==="textStyle"),d=t.marks?.some(s=>s.type==="highlight"),y=U(c?.attrs?.color),h=U(c?.attrs?.backgroundColor);let F;if(c?.attrs?.fontSize){const s=c.attrs.fontSize;if(s.endsWith("px")){const A=parseFloat(s);isNaN(A)||(F=Math.round(A*rt))}}let w;a?w=P:c?.attrs?.fontFamily&&(w=c.attrs.fontFamily);const v={text:t.text||"",bold:e||void 0,italics:n||void 0,underline:o?{}:void 0,strike:r||void 0,font:w,size:F,subScript:i||void 0,superScript:l||void 0,color:y,shading:h?{fill:h}:void 0,highlight:d?"yellow":void 0};return u?.attrs?.href?new $t({children:[new f({...v,style:"Hyperlink"})],link:u.attrs.href}):new f(v)}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 f(e)}function q(t=[]){return t.flatMap(e=>e.type==="text"?[T(e)]:e.type==="hardBreak"?[k(e.marks)]:[])}async function K(t,e){const n=c=>{const d=c||H(t.attrs?.src||"");return pt(d)};let o,r;try{const c=t.attrs?.src||"";if(e?.handler){o=await e.handler(c);try{r=O(o)}catch{r={type:H(c),width:void 0,height:void 0,orientation:void 0}}}else if(c.startsWith("http")){const d=await mt(c);o=d.data,r=d.meta}else if(c.startsWith("data:")){const d=c.split(",")[1];if(!d)throw new Error("Invalid data URL: missing base64 data");const y=atob(d);o=Uint8Array.from(y,h=>h.charCodeAt(0));try{r=O(o)}catch{r={type:"png",width:void 0,height:void 0,orientation:void 0}}}else throw new Error(`Unsupported image source format: ${c.substring(0,20)}...`)}catch(c){return console.warn("Failed to process image:",c),new Z({type:"png",data:new Uint8Array(0),transformation:{width:100,height:100},altText:{name:t.attrs?.alt||"Failed to load image"}})}const a=ft(t,r,e?.maxWidth),i=gt(t,a,r,e?.maxWidth),l={width:a,height:i};t.attrs?.rotation!==void 0&&(l.rotation=t.attrs.rotation);const u={...e?.options,type:n(r.type),data:o,transformation:l,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 Z(u)}async function C(t,e){const{options:n,image:o}=e||{},r=[];for(const i of t.content||[])if(i.type==="text")r.push(T(i));else if(i.type==="hardBreak")r.push(k(i.marks));else if(i.type==="image"){const l=await K(i,{maxWidth:o?.maxWidth,options:o?.options,handler:o?.handler});r.push(l)}let a={children:r};return n&&(a={...a,...n}),t.attrs&&(a=X(a,t.attrs)),a}function yt(t){const e=t?.attrs?.level,n=q(t.content).filter(a=>a!==void 0),o={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 r={children:n,heading:o[e]};return t.attrs&&(r=X(r,t.attrs)),r}function wt(t){return t.content?t.content.map(e=>e.type==="paragraph"?{children:e.content?.flatMap(n=>n.type==="text"?T(n):n.type==="hardBreak"?k(n.marks):[])||[],indent:{left:720},border:{left:{style:"single"}}}:{}):[]}async function vt(t,e){const{options:n}=e;let o=n?.cell?.paragraph??n?.row?.paragraph??{};n?.style&&(o={...o,style:n.style.id});const r={children:(await Promise.all((t.content||[]).map(i=>C(i,{options:o})))).map(i=>new I(i)),...n?.cell?.run};if(t.attrs?.colspan&&t.attrs.colspan>1&&(r.columnSpan=t.attrs.colspan),t.attrs?.rowspan&&t.attrs.rowspan>1&&(r.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 l=Math.round(i*15);r.width={size:l,type:"dxa"}}}if(t.attrs?.backgroundColor){const i=t.attrs.backgroundColor.replace("#","");r.shading={fill:i}}if(t.attrs?.verticalAlign){const i=t.attrs.verticalAlign==="middle"?"center":t.attrs.verticalAlign;r.verticalAlign=i}const a={top:p(t.attrs?.borderTop),bottom:p(t.attrs?.borderBottom),left:p(t.attrs?.borderLeft),right:p(t.attrs?.borderRight)};return(a.top||a.bottom||a.left||a.right)&&(r.borders=a),new tt(r)}async function bt(t,e){const{options:n}=e;let o=n?.header?.paragraph??n?.cell?.paragraph??n?.row?.paragraph??{};n?.style&&(o={...o,style:n.style.id});const r={children:(await Promise.all((t.content||[]).map(i=>C(i,{options:o})))).map(i=>new I(i)),...n?.header?.run};if(t.attrs?.colspan&&t.attrs.colspan>1&&(r.columnSpan=t.attrs.colspan),t.attrs?.rowspan&&t.attrs.rowspan>1&&(r.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 l=Math.round(i*15);r.width={size:l,type:"dxa"}}}if(t.attrs?.backgroundColor){const i=t.attrs.backgroundColor.replace("#","");r.shading={fill:i}}if(t.attrs?.verticalAlign){const i=t.attrs.verticalAlign==="middle"?"center":t.attrs.verticalAlign;r.verticalAlign=i}const a={top:p(t.attrs?.borderTop),bottom:p(t.attrs?.borderBottom),left:p(t.attrs?.borderLeft),right:p(t.attrs?.borderRight)};return(a.top||a.bottom||a.left||a.right)&&(r.borders=a),new tt(r)}async function Tt(t,e){const{options:n}=e,o=n?.row,r={children:(await Promise.all((t.content||[]).map(async a=>a.type==="tableCell"?await vt(a,e):a.type==="tableHeader"?await bt(a,e):null))).filter(a=>a!==void 0),...o};if(t.attrs?.rowHeight){const a=m(t.attrs.rowHeight),i=g(a);i>0&&(r.height={rule:"atLeast",value:i})}return new zt(r)}const kt=(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 Ft(t,e){const{options:n}=e;let o={rows:await Promise.all((t.content||[]).map(r=>Tt(r,e))),...n?.run};return o=kt(o,t),new B(o)}function At(t){if(!t.content||t.content.length===0)return{children:[new f({text:"",font:P})]};const e=t.content.flatMap(n=>n.type==="text"?T(n):[]);return{children:e.length>0?e:[new f({text:"",font:P})]}}async function xt(t,e){if(!t.content||t.content.length===0)return{};const n=t.content[0];return n.type==="paragraph"?await C(n,{options:e.options}):{}}function It(){return{numbering:{reference:"bullet-list",level:0}}}function Et(t){const e=t.attrs?.start||1;return{numbering:{reference:"ordered-list",level:0},start:e}}async function Y(t,e){const{listType:n}=e;if(!t.content)return[];const o=[],r=n==="bullet"?It():Et(t);let a=r.numbering.reference;n==="ordered"&&r.start&&r.start!==1&&(a=`ordered-list-start-${r.start}`);for(const i of t.content)if(i.type==="listItem"){const l=await xt(i,{options:{numbering:{reference:a,level:0}}});o.push(l)}return o}function V(t){if(!t.content||t.content.length===0)return{};const e=t.content[0];if(e.type==="paragraph"){const n=t.attrs?.checked?z.checked+" ":z.unchecked+" ",o=e.content?.flatMap(r=>r.type==="text"?T(r):r.type==="hardBreak"?k(r.marks):[])||[];return{children:[new f({text:n}),...o]}}return{}}function Ct(t){return!t.content||t.content.length===0?[]:t.content.filter(e=>e.type==="taskItem").map(e=>V(e))}function Lt(t,e){return{children:[new jt],...e.options?.paragraph}}function Dt(t,e){return{children:q(t.content||[]).filter(n=>n!==void 0),...e.options?.summary?.paragraph}}async function le(t,e){const{title:n,subject:o,creator:r,keywords:a,description:i,lastModifiedBy:l,revision:u,styles:c,tableOfContents:d,sections:y,fonts:h,hyphenation:F,compatibility:w,customProperties:v,evenAndOddHeaderAndFooters:s,defaultTabStop:A,outputType:St}=e,R=await Bt(t,{options:e}),L=d?new Gt(d.title,{...d.run}):null,Nt=he(t),x=[];e.image?.style&&x.push(e.image.style),e.table?.style&&x.push(e.table.style),e.code?.style&&x.push(e.code.style);const Ht=c?{...c,...x.length>0&&{paragraphStyles:[...c.paragraphStyles||[],...x]}}:{},Rt={sections:y?y.map((Wt,Q)=>{const D=[];return Q===0&&L&&D.push(L),Q===0&&D.push(...R),{...Wt,...D.length>0?{children:D}:{}}}):[{children:L?[L,...R]:R}],title:n||"Document",subject:o||"",creator:r||"",keywords:a||"",description:i||"",lastModifiedBy:l||"",revision:u||1,styles:Ht,numbering:Nt,...h&&h.length>0&&{fonts:h},...F&&{hyphenation:F},...w&&{compatibility:w},...v&&v.length>0&&{customProperties:v},...s!==void 0&&{evenAndOddHeaderAndFooters:s},...A!==void 0&&{defaultTabStop:A}},Ot=new Ut(Rt);return Xt.pack(Ot,St||"arraybuffer")}async function Bt(t,e){const n=[];if(!t||!Array.isArray(t.content))return n;const o=dt(e.options);for(const r of t.content){const a=await J(r,e.options,o);Array.isArray(a)?n.push(...a):a&&(n.push(a),r.type==="table"&&n.length>=2&&n[n.length-2]instanceof B&&n.push(new I({})))}return n}async function J(t,e,n){if(!t||!t.type)return null;const o=await de(t,e,n);if(o instanceof B)return o;if(Array.isArray(o)){const i=Pt(t.type,e);return o.map(l=>{const u=Mt(l,i);return new I(u)})}const r=Pt(t.type,e),a=Mt(o,r);return fe(a)}async function de(t,e,n){switch(t.type){case"paragraph":return await C(t,{image:{maxWidth:n,options:e.image?.run,handler:e.image?.handler}});case"heading":return yt(t);case"blockquote":return wt(t);case"codeBlock":return At(t);case"image":return{children:[await K(t,{maxWidth:n,options:e.image?.run,handler:e.image?.handler})]};case"table":return await Ft(t,{options:e.table});case"bulletList":return await Y(t,{listType:"bullet"});case"orderedList":return await Y(t,{listType:"ordered"});case"taskList":return Ct(t);case"taskItem":return V(t);case"hardBreak":return{children:[k()]};case"horizontalRule":return Lt(t,{options:e.horizontalRule});case"details":return await ue(t,e,n);case"detailsSummary":return Dt(t,{options:e.details});default:return{children:[new f({text:`[Unsupported: ${t.type}]`})]}}}async function ue(t,e,n){const o=[];if(t.content)for(const r of t.content){const a=await J(r,e,n);Array.isArray(a)?o.push(...a):a&&o.push(a)}return o}const pe=t=>({level:0,format:nt.DECIMAL,text:"%1.",alignment:et.START,start:t??1,style:{paragraph:{indent:{left:_(.5),hanging:_(.25)}}}}),_t=t=>({reference:t&&t!==1?`ordered-list-start-${t}`:"ordered-list",levels:[pe(t)]});function he(t){const e=new Set;function n(r){if(r.type==="orderedList"&&r.attrs?.start&&e.add(r.attrs.start),r.content)for(const a of r.content)n(a)}n(t);const o=[{reference:"bullet-list",levels:[{level:0,format:nt.BULLET,text:"\u2022",alignment:et.START,style:{paragraph:{indent:{left:_(.5),hanging:_(.25)}}}}]},_t(1)];return e.forEach(r=>{r!==1&&o.push(_t(r))}),{config:o}}function Pt(t,e){return{codeBlock:e.code?.style?.id,image:e.image?.style?.id}[t]}function Mt(t,e){return e?{...t,style:e}:t}function fe(t){return t instanceof B?t:new I(t)}export{z as CHECKBOX_SYMBOLS,it as COLOR_NAME_TO_HEX,P as DEFAULT_CODE_FONT,W as DOCX_DPI,Vt as DOCX_STYLE_NAMES,Kt as EMUS_PER_INCH,rt as HALF_POINTS_PER_PIXEL,j as PAGE_DIMENSIONS,Yt as PIXELS_PER_HALF_POINT,ot as TEXT_ALIGN_MAP,$ as TWIPS_PER_INCH,X as applyParagraphStyleAttributes,kt as applyTableMargins,dt as calculateEffectiveContentWidth,wt as convertBlockquote,p as convertBorder,It as convertBulletList,At as convertCodeBlock,U as convertColorToHex,m as convertCssLengthToPixels,Dt as convertDetailsSummary,Bt as convertDocument,Zt as convertEmuStringToPixels,at as convertEmuToPixels,k as convertHardBreak,yt as convertHeading,Lt as convertHorizontalRule,K as convertImage,Y as convertList,xt as convertListItem,S as convertMeasureToInches,G as convertMeasureToPixels,J as convertNode,Et as convertOrderedList,C as convertParagraph,Qt as convertPixelsToEmu,g as convertPixelsToTwip,Ft as convertTable,vt as convertTableCell,bt as convertTableHeader,Tt as convertTableRow,V as convertTaskItem,Ct as convertTaskList,T as convertText,q as convertTextNodes,pt as convertToDocxImageType,Jt as convertTwipToCssString,E as convertTwipToPixels,ce as createFloatingOptions,se as createStringValidator,ae as findChild,st as findDeepChild,ct as findDeepChildren,le as generateDOCX,mt as getImageDataAndMeta,gt as getImageHeight,H as getImageTypeFromSrc,ft as getImageWidth,oe as normalizeHexColor,ie 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
+ * Note: maxWidth constraint only applies to inline (non-floating) images.
530
+ * Floating images maintain their original dimensions.
531
+ */
532
+ function getImageWidth(node, imageMeta, maxWidth) {
533
+ if (node.attrs?.width !== void 0 && node.attrs?.width !== null) {
534
+ const requestedWidth = node.attrs.width;
535
+ if (!node.attrs.floating && maxWidth) {
536
+ const maxWidthPixels = maxWidth !== void 0 ? convertMeasureToPixels(maxWidth) : void 0;
537
+ if (maxWidthPixels && requestedWidth > maxWidthPixels) return maxWidthPixels;
538
+ }
539
+ return requestedWidth;
540
+ }
541
+ const maxWidthPixels = maxWidth !== void 0 ? convertMeasureToPixels(maxWidth) : void 0;
542
+ if (imageMeta?.width && imageMeta?.height) return calculateDisplaySize(imageMeta, maxWidthPixels).width;
543
+ return maxWidthPixels || DEFAULT_MAX_IMAGE_WIDTH_PIXELS;
544
+ }
545
+ /**
546
+ * Get image height with priority: node attrs > image meta > calculated > default
547
+ *
548
+ * Note: maxWidth constraint only applies to inline (non-floating) images.
549
+ * Floating images maintain their original dimensions and aspect ratio.
550
+ */
551
+ function getImageHeight(node, width, imageMeta, maxWidth) {
552
+ if (node.attrs?.height !== void 0 && node.attrs?.height !== null) {
553
+ const requestedHeight = node.attrs.height;
554
+ if (!node.attrs.floating && maxWidth && node.attrs?.width) {
555
+ const maxWidthPixels = maxWidth !== void 0 ? convertMeasureToPixels(maxWidth) : void 0;
556
+ const requestedWidth = node.attrs.width;
557
+ if (maxWidthPixels && requestedWidth > maxWidthPixels) {
558
+ const scaleFactor = maxWidthPixels / requestedWidth;
559
+ return Math.round(requestedHeight * scaleFactor);
560
+ }
561
+ }
562
+ return requestedHeight;
563
+ }
564
+ const maxWidthPixels = maxWidth !== void 0 ? convertMeasureToPixels(maxWidth) : void 0;
565
+ if (imageMeta?.width && imageMeta?.height) return calculateDisplaySize(imageMeta, maxWidthPixels).height;
566
+ return Math.round(width * .75);
567
+ }
568
+ /**
569
+ * Fetch image data and metadata from HTTP/HTTPS URL
570
+ * (Only for use without custom handler)
571
+ */
572
+ async function getImageDataAndMeta(url) {
573
+ try {
574
+ const data = await (await ofetch(url, { responseType: "blob" })).bytes();
575
+ let meta;
576
+ try {
577
+ meta = imageMeta(data);
578
+ } catch (error) {
579
+ console.warn(`Failed to extract image metadata:`, error);
580
+ meta = {
581
+ width: void 0,
582
+ height: void 0,
583
+ type: getImageTypeFromSrc(url) || "png",
584
+ orientation: void 0
585
+ };
586
+ }
587
+ return {
588
+ data,
589
+ meta
590
+ };
591
+ } catch (error) {
592
+ console.warn(`Failed to fetch image from ${url}:`, error);
593
+ throw error;
594
+ }
595
+ }
596
+ //#endregion
597
+ //#region src/utils/paragraph.ts
598
+ /**
599
+ * Apply paragraph style attributes to options
600
+ */
601
+ const applyParagraphStyleAttributes = (options, attrs) => {
602
+ if (!attrs) return options;
603
+ let result = { ...options };
604
+ if (attrs.indentLeft || attrs.indentRight || attrs.indentFirstLine) result = {
605
+ ...result,
606
+ indent: {
607
+ ...attrs.indentLeft && { left: convertPixelsToTwip(convertCssLengthToPixels(attrs.indentLeft)) },
608
+ ...attrs.indentRight && { right: convertPixelsToTwip(convertCssLengthToPixels(attrs.indentRight)) },
609
+ ...attrs.indentFirstLine && { firstLine: convertPixelsToTwip(convertCssLengthToPixels(attrs.indentFirstLine)) }
610
+ }
611
+ };
612
+ if (attrs.spacingBefore || attrs.spacingAfter) result = {
613
+ ...result,
614
+ spacing: {
615
+ ...attrs.spacingBefore && { before: convertPixelsToTwip(convertCssLengthToPixels(attrs.spacingBefore)) },
616
+ ...attrs.spacingAfter && { after: convertPixelsToTwip(convertCssLengthToPixels(attrs.spacingAfter)) }
617
+ }
618
+ };
619
+ if (attrs.textAlign) result = {
620
+ ...result,
621
+ alignment: TEXT_ALIGN_MAP.tiptapToDocx[attrs.textAlign]
622
+ };
623
+ return result;
624
+ };
625
+ //#endregion
626
+ //#region src/utils/table.ts
627
+ /**
628
+ * Convert TipTap border to DOCX border format
629
+ *
630
+ * @param border - TipTap table cell border definition
631
+ * @returns DOCX border options or undefined if no border
632
+ */
633
+ function convertBorder(border) {
634
+ if (!border) return void 0;
635
+ const docxStyle = border.style ? {
636
+ solid: "single",
637
+ dashed: "dashed",
638
+ dotted: "dotted",
639
+ double: "double",
640
+ none: "none"
641
+ }[border.style] || "single" : "single";
642
+ return {
643
+ color: border.color?.replace("#", "") || "auto",
644
+ size: border.width ? border.width * 6 : 4,
645
+ style: docxStyle
646
+ };
647
+ }
648
+ //#endregion
649
+ //#region src/converters/text.ts
650
+ /**
651
+ * Convert TipTap text node to DOCX TextRun or ExternalHyperlink
652
+ */
653
+ function convertText(node) {
654
+ const isBold = node.marks?.some((m) => m.type === "bold");
655
+ const isItalic = node.marks?.some((m) => m.type === "italic");
656
+ const isUnderline = node.marks?.some((m) => m.type === "underline");
657
+ const isStrike = node.marks?.some((m) => m.type === "strike");
658
+ const isCode = node.marks?.some((m) => m.type === "code");
659
+ const isSubscript = node.marks?.some((m) => m.type === "subscript");
660
+ const isSuperscript = node.marks?.some((m) => m.type === "superscript");
661
+ const linkMark = node.marks?.find((m) => m.type === "link");
662
+ const textStyleMark = node.marks?.find((m) => m.type === "textStyle");
663
+ const hasHighlight = node.marks?.some((m) => m.type === "highlight");
664
+ const textColor = convertColorToHex(textStyleMark?.attrs?.color);
665
+ const backgroundColor = convertColorToHex(textStyleMark?.attrs?.backgroundColor);
666
+ let fontSize;
667
+ if (textStyleMark?.attrs?.fontSize) {
668
+ const fontSizeStr = textStyleMark.attrs.fontSize;
669
+ if (fontSizeStr.endsWith("px")) {
670
+ const px = parseFloat(fontSizeStr);
671
+ if (!isNaN(px)) fontSize = Math.round(px * HALF_POINTS_PER_PIXEL);
672
+ }
673
+ }
674
+ let fontFamily;
675
+ if (isCode) fontFamily = DEFAULT_CODE_FONT;
676
+ else if (textStyleMark?.attrs?.fontFamily) fontFamily = textStyleMark.attrs.fontFamily;
677
+ const baseOptions = {
678
+ text: node.text || "",
679
+ bold: isBold || void 0,
680
+ italics: isItalic || void 0,
681
+ underline: isUnderline ? {} : void 0,
682
+ strike: isStrike || void 0,
683
+ font: fontFamily,
684
+ size: fontSize,
685
+ subScript: isSubscript || void 0,
686
+ superScript: isSuperscript || void 0,
687
+ color: textColor,
688
+ shading: backgroundColor ? { fill: backgroundColor } : void 0,
689
+ highlight: hasHighlight ? "yellow" : void 0
690
+ };
691
+ if (linkMark?.attrs?.href) return new ExternalHyperlink({
692
+ children: [new TextRun({
693
+ ...baseOptions,
694
+ style: "Hyperlink"
695
+ })],
696
+ link: linkMark.attrs.href
697
+ });
698
+ return new TextRun(baseOptions);
699
+ }
700
+ /**
701
+ * Convert TipTap hardBreak node to DOCX TextRun with break
702
+ */
703
+ function convertHardBreak(marks) {
704
+ const options = {
705
+ text: "",
706
+ break: 1
707
+ };
708
+ if (marks) for (const mark of marks) switch (mark.type) {
709
+ case "bold":
710
+ options.bold = true;
711
+ break;
712
+ case "italic":
713
+ options.italics = true;
714
+ break;
715
+ case "underline":
716
+ options.underline = {};
717
+ break;
718
+ case "strike":
719
+ options.strike = true;
720
+ break;
721
+ case "textStyle":
722
+ if (mark.attrs?.color) options.color = mark.attrs.color;
723
+ break;
724
+ }
725
+ return new TextRun(options);
726
+ }
727
+ /**
728
+ * Convert array of text nodes (text, hardBreak) to DOCX elements
729
+ * Returns flattened array of TextRun or ExternalHyperlink
730
+ */
731
+ function convertTextNodes(nodes = []) {
732
+ return nodes.flatMap((contentNode) => {
733
+ if (contentNode.type === "text") return [convertText(contentNode)];
734
+ else if (contentNode.type === "hardBreak") return [convertHardBreak(contentNode.marks)];
735
+ return [];
736
+ });
737
+ }
738
+ //#endregion
739
+ //#region src/converters/image.ts
740
+ /**
741
+ * Convert TipTap image node to DOCX ImageRun
742
+ *
743
+ * @param node - TipTap image node
744
+ * @param params - Conversion parameters
745
+ * @returns Promise<DOCX ImageRun>
746
+ */
747
+ async function convertImage(node, params) {
748
+ const getImageType = (metaType) => {
749
+ return convertToDocxImageType(metaType || getImageTypeFromSrc(node.attrs?.src || ""));
750
+ };
751
+ let imageData;
752
+ let imageMeta$1;
753
+ try {
754
+ const src = node.attrs?.src || "";
755
+ if (params?.handler) {
756
+ imageData = await params.handler(src);
757
+ try {
758
+ imageMeta$1 = imageMeta(imageData);
759
+ } catch {
760
+ imageMeta$1 = {
761
+ type: getImageTypeFromSrc(src),
762
+ width: void 0,
763
+ height: void 0,
764
+ orientation: void 0
765
+ };
766
+ }
767
+ } else if (src.startsWith("http")) {
768
+ const result = await getImageDataAndMeta(src);
769
+ imageData = result.data;
770
+ imageMeta$1 = result.meta;
771
+ } else if (src.startsWith("data:")) {
772
+ const base64Data = src.split(",")[1];
773
+ if (!base64Data) throw new Error("Invalid data URL: missing base64 data");
774
+ const binaryString = atob(base64Data);
775
+ imageData = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
776
+ try {
777
+ imageMeta$1 = imageMeta(imageData);
778
+ } catch {
779
+ imageMeta$1 = {
780
+ type: "png",
781
+ width: void 0,
782
+ height: void 0,
783
+ orientation: void 0
784
+ };
785
+ }
786
+ } else throw new Error(`Unsupported image source format: ${src.substring(0, 20)}...`);
787
+ } catch (error) {
788
+ console.warn(`Failed to process image:`, error);
789
+ return new ImageRun({
790
+ type: "png",
791
+ data: new Uint8Array(0),
792
+ transformation: {
793
+ width: 100,
794
+ height: 100
795
+ },
796
+ altText: { name: node.attrs?.alt || "Failed to load image" }
797
+ });
798
+ }
799
+ const finalWidth = getImageWidth(node, imageMeta$1, params?.maxWidth);
800
+ const transformation = {
801
+ width: finalWidth,
802
+ height: getImageHeight(node, finalWidth, imageMeta$1, params?.maxWidth)
803
+ };
804
+ if (node.attrs?.rotation !== void 0) transformation.rotation = node.attrs.rotation;
805
+ return new ImageRun({
806
+ ...params?.options,
807
+ type: getImageType(imageMeta$1.type),
808
+ data: imageData,
809
+ transformation,
810
+ altText: {
811
+ name: node.attrs?.alt || "",
812
+ description: void 0,
813
+ title: node.attrs?.title || void 0
814
+ },
815
+ ...node.attrs?.floating && { floating: node.attrs.floating },
816
+ ...node.attrs?.outline && { outline: node.attrs.outline }
817
+ });
818
+ }
819
+ //#endregion
820
+ //#region src/converters/paragraph.ts
821
+ /**
822
+ * Convert TipTap paragraph node to DOCX paragraph options
823
+ *
824
+ * This converter only handles data transformation from node.attrs to DOCX format properties.
825
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
826
+ *
827
+ * @param node - TipTap paragraph node
828
+ * @param params - Conversion parameters
829
+ * @returns Promise<DOCX paragraph options (pure data object)>
830
+ */
831
+ async function convertParagraph(node, params) {
832
+ const { options, image } = params || {};
833
+ const children = [];
834
+ for (const contentNode of node.content || []) if (contentNode.type === "text") children.push(convertText(contentNode));
835
+ else if (contentNode.type === "hardBreak") children.push(convertHardBreak(contentNode.marks));
836
+ else if (contentNode.type === "image") {
837
+ const imageRun = await convertImage(contentNode, {
838
+ maxWidth: image?.maxWidth,
839
+ options: image?.options,
840
+ handler: image?.handler
841
+ });
842
+ children.push(imageRun);
843
+ }
844
+ let paragraphOptions = { children };
845
+ if (options) paragraphOptions = {
846
+ ...paragraphOptions,
847
+ ...options
848
+ };
849
+ if (node.attrs) paragraphOptions = applyParagraphStyleAttributes(paragraphOptions, node.attrs);
850
+ return paragraphOptions;
851
+ }
852
+ //#endregion
853
+ //#region src/converters/heading.ts
854
+ /**
855
+ * Convert TipTap heading node to DOCX paragraph options
856
+ *
857
+ * This converter only handles data transformation from node.attrs to DOCX format properties.
858
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
859
+ *
860
+ * @param node - TipTap heading node
861
+ * @returns DOCX paragraph options (pure data object)
862
+ */
863
+ function convertHeading(node) {
864
+ const level = node?.attrs?.level;
865
+ let paragraphOptions = {
866
+ children: convertTextNodes(node.content).filter((item) => item !== void 0),
867
+ heading: {
868
+ 1: HeadingLevel.HEADING_1,
869
+ 2: HeadingLevel.HEADING_2,
870
+ 3: HeadingLevel.HEADING_3,
871
+ 4: HeadingLevel.HEADING_4,
872
+ 5: HeadingLevel.HEADING_5,
873
+ 6: HeadingLevel.HEADING_6
874
+ }[level]
875
+ };
876
+ if (node.attrs) paragraphOptions = applyParagraphStyleAttributes(paragraphOptions, node.attrs);
877
+ return paragraphOptions;
878
+ }
879
+ //#endregion
880
+ //#region src/converters/blockquote.ts
881
+ /**
882
+ * Convert TipTap blockquote node to array of paragraph options
883
+ *
884
+ * This converter only handles data transformation from node content to DOCX format properties.
885
+ * It returns pure data objects (IParagraphOptions[]), not DOCX instances.
886
+ *
887
+ * @param node - TipTap blockquote node
888
+ * @returns Array of paragraph options (pure data objects)
889
+ */
890
+ function convertBlockquote(node) {
891
+ if (!node.content) return [];
892
+ return node.content.map((contentNode) => {
893
+ if (contentNode.type === "paragraph") return {
894
+ children: contentNode.content?.flatMap((node) => {
895
+ if (node.type === "text") return convertText(node);
896
+ else if (node.type === "hardBreak") return convertHardBreak(node.marks);
897
+ return [];
898
+ }) || [],
899
+ indent: { left: 720 },
900
+ border: { left: { style: "single" } }
901
+ };
902
+ return {};
903
+ });
904
+ }
905
+ //#endregion
906
+ //#region src/converters/table-cell.ts
907
+ /**
908
+ * Convert TipTap table cell node to DOCX TableCell
909
+ *
910
+ * @param node - TipTap table cell node
911
+ * @param params - Conversion parameters
912
+ * @returns Promise<DOCX TableCell object>
913
+ */
914
+ async function convertTableCell(node, params) {
915
+ const { options } = params;
916
+ let cellParagraphOptions = options?.cell?.paragraph ?? options?.row?.paragraph ?? {};
917
+ if (options?.style) cellParagraphOptions = {
918
+ ...cellParagraphOptions,
919
+ style: options.style.id
920
+ };
921
+ const cellOptions = {
922
+ children: (await Promise.all((node.content || []).map((p) => convertParagraph(p, { options: cellParagraphOptions })))).map((options) => new Paragraph(options)),
923
+ ...options?.cell?.run
924
+ };
925
+ if (node.attrs?.colspan && node.attrs.colspan > 1) cellOptions.columnSpan = node.attrs.colspan;
926
+ if (node.attrs?.rowspan && node.attrs.rowspan > 1) cellOptions.rowSpan = node.attrs.rowspan;
927
+ if (node.attrs?.colwidth !== null && node.attrs?.colwidth !== void 0) {
928
+ const widthInPixels = Array.isArray(node.attrs.colwidth) ? node.attrs.colwidth[0] : node.attrs.colwidth;
929
+ if (widthInPixels && widthInPixels > 0) cellOptions.width = {
930
+ size: Math.round(widthInPixels * 15),
931
+ type: "dxa"
932
+ };
933
+ }
934
+ if (node.attrs?.backgroundColor) cellOptions.shading = { fill: node.attrs.backgroundColor.replace("#", "") };
935
+ if (node.attrs?.verticalAlign) cellOptions.verticalAlign = node.attrs.verticalAlign === "middle" ? "center" : node.attrs.verticalAlign;
936
+ const borders = {
937
+ top: convertBorder(node.attrs?.borderTop),
938
+ bottom: convertBorder(node.attrs?.borderBottom),
939
+ left: convertBorder(node.attrs?.borderLeft),
940
+ right: convertBorder(node.attrs?.borderRight)
941
+ };
942
+ if (borders.top || borders.bottom || borders.left || borders.right) cellOptions.borders = borders;
943
+ return new TableCell(cellOptions);
944
+ }
945
+ //#endregion
946
+ //#region src/converters/table-header.ts
947
+ /**
948
+ * Convert TipTap table header node to DOCX TableCell
949
+ *
950
+ * @param node - TipTap table header node
951
+ * @param params - Conversion parameters
952
+ * @returns Promise<DOCX TableCell object for header>
953
+ */
954
+ async function convertTableHeader(node, params) {
955
+ const { options } = params;
956
+ let headerParagraphOptions = options?.header?.paragraph ?? options?.cell?.paragraph ?? options?.row?.paragraph ?? {};
957
+ if (options?.style) headerParagraphOptions = {
958
+ ...headerParagraphOptions,
959
+ style: options.style.id
960
+ };
961
+ const headerCellOptions = {
962
+ children: (await Promise.all((node.content || []).map((p) => convertParagraph(p, { options: headerParagraphOptions })))).map((options) => new Paragraph(options)),
963
+ ...options?.header?.run
964
+ };
965
+ if (node.attrs?.colspan && node.attrs.colspan > 1) headerCellOptions.columnSpan = node.attrs.colspan;
966
+ if (node.attrs?.rowspan && node.attrs.rowspan > 1) headerCellOptions.rowSpan = node.attrs.rowspan;
967
+ if (node.attrs?.colwidth !== null && node.attrs?.colwidth !== void 0) {
968
+ const widthInPixels = Array.isArray(node.attrs.colwidth) ? node.attrs.colwidth[0] : node.attrs.colwidth;
969
+ if (widthInPixels && widthInPixels > 0) headerCellOptions.width = {
970
+ size: Math.round(widthInPixels * 15),
971
+ type: "dxa"
972
+ };
973
+ }
974
+ if (node.attrs?.backgroundColor) headerCellOptions.shading = { fill: node.attrs.backgroundColor.replace("#", "") };
975
+ if (node.attrs?.verticalAlign) headerCellOptions.verticalAlign = node.attrs.verticalAlign === "middle" ? "center" : node.attrs.verticalAlign;
976
+ const borders = {
977
+ top: convertBorder(node.attrs?.borderTop),
978
+ bottom: convertBorder(node.attrs?.borderBottom),
979
+ left: convertBorder(node.attrs?.borderLeft),
980
+ right: convertBorder(node.attrs?.borderRight)
981
+ };
982
+ if (borders.top || borders.bottom || borders.left || borders.right) headerCellOptions.borders = borders;
983
+ return new TableCell(headerCellOptions);
984
+ }
985
+ //#endregion
986
+ //#region src/converters/table-row.ts
987
+ /**
988
+ * Convert TipTap table row node to DOCX TableRow
989
+ *
990
+ * @param node - TipTap table row node
991
+ * @param params - Conversion parameters
992
+ * @returns Promise<DOCX TableRow object>
993
+ */
994
+ async function convertTableRow(node, params) {
995
+ const { options } = params;
996
+ const rowOptions = options?.row;
997
+ const tableRowOptions = {
998
+ children: (await Promise.all((node.content || []).map(async (cellNode) => {
999
+ if (cellNode.type === "tableCell") return await convertTableCell(cellNode, params);
1000
+ else if (cellNode.type === "tableHeader") return await convertTableHeader(cellNode, params);
1001
+ return null;
1002
+ }))).filter((cell) => cell !== void 0),
1003
+ ...rowOptions
1004
+ };
1005
+ if (node.attrs?.rowHeight) {
1006
+ const twips = convertPixelsToTwip(convertCssLengthToPixels(node.attrs.rowHeight));
1007
+ if (twips > 0) tableRowOptions.height = {
1008
+ rule: "atLeast",
1009
+ value: twips
1010
+ };
1011
+ }
1012
+ return new TableRow(tableRowOptions);
1013
+ }
1014
+ //#endregion
1015
+ //#region src/converters/table.ts
1016
+ /**
1017
+ * Apply table margins to table options
1018
+ */
1019
+ const applyTableMargins = (options, node) => {
1020
+ const margins = {
1021
+ top: node.attrs?.marginTop ?? void 0,
1022
+ bottom: node.attrs?.marginBottom ?? void 0,
1023
+ left: node.attrs?.marginLeft ?? void 0,
1024
+ right: node.attrs?.marginRight ?? void 0
1025
+ };
1026
+ if (margins.top || margins.bottom || margins.left || margins.right) return {
1027
+ ...options,
1028
+ margins
1029
+ };
1030
+ return options;
1031
+ };
1032
+ /**
1033
+ * Convert TipTap table node to DOCX Table
1034
+ *
1035
+ * @param node - TipTap table node
1036
+ * @param params - Conversion parameters
1037
+ * @returns Promise<Table>
1038
+ */
1039
+ async function convertTable(node, params) {
1040
+ const { options } = params;
1041
+ let tableOptions = {
1042
+ rows: await Promise.all((node.content || []).map((row) => convertTableRow(row, params))),
1043
+ ...options?.style?.id && { style: options.style.id },
1044
+ ...options?.run
1045
+ };
1046
+ tableOptions = applyTableMargins(tableOptions, node);
1047
+ return new Table(tableOptions);
1048
+ }
1049
+ //#endregion
1050
+ //#region src/converters/code-block.ts
1051
+ /**
1052
+ * Convert TipTap codeBlock node to DOCX paragraph options
1053
+ *
1054
+ * This converter only handles data transformation from node.attrs to DOCX format properties.
1055
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
1056
+ *
1057
+ * @param node - TipTap codeBlock node
1058
+ * @returns DOCX paragraph options (pure data object)
1059
+ */
1060
+ function convertCodeBlock(node) {
1061
+ if (!node.content || node.content.length === 0) return { children: [new TextRun({
1062
+ text: "",
1063
+ font: DEFAULT_CODE_FONT
1064
+ })] };
1065
+ const textRuns = node.content.flatMap((contentNode) => {
1066
+ if (contentNode.type === "text") return convertText(contentNode);
1067
+ return [];
1068
+ });
1069
+ return { children: textRuns.length > 0 ? textRuns : [new TextRun({
1070
+ text: "",
1071
+ font: DEFAULT_CODE_FONT
1072
+ })] };
1073
+ }
1074
+ //#endregion
1075
+ //#region src/converters/list-item.ts
1076
+ /**
1077
+ * Convert TipTap list item node to paragraph options
1078
+ *
1079
+ * This converter only handles data transformation from node content to DOCX format properties.
1080
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
1081
+ *
1082
+ * Note: The numbering reference (including start value) is typically
1083
+ * handled by the parent list converter. This function focuses on
1084
+ * converting the paragraph content of the list item.
1085
+ *
1086
+ * @param node - TipTap list item node
1087
+ * @param params - Conversion parameters
1088
+ * @returns Promise<Paragraph options (pure data object)>
1089
+ */
1090
+ async function convertListItem(node, params) {
1091
+ if (!node.content || node.content.length === 0) return {};
1092
+ const firstParagraph = node.content[0];
1093
+ if (firstParagraph.type === "paragraph") return await convertParagraph(firstParagraph, { options: params.options });
1094
+ return {};
1095
+ }
1096
+ //#endregion
1097
+ //#region src/converters/list.ts
1098
+ function convertBulletList() {
1099
+ return { numbering: {
1100
+ reference: "bullet-list",
1101
+ level: 0
1102
+ } };
1103
+ }
1104
+ function convertOrderedList(node) {
1105
+ return {
1106
+ numbering: {
1107
+ reference: "ordered-list",
1108
+ level: 0
1109
+ },
1110
+ start: node.attrs?.start || 1
1111
+ };
1112
+ }
1113
+ /**
1114
+ * Convert list nodes (bullet or ordered) with proper numbering
1115
+ *
1116
+ * This converter only handles data transformation from node content to DOCX format properties.
1117
+ * It returns pure data objects (IParagraphOptions[]), not DOCX instances.
1118
+ */
1119
+ async function convertList(node, params) {
1120
+ const { listType } = params;
1121
+ if (!node.content) return [];
1122
+ const elements = [];
1123
+ const listOptions = listType === "bullet" ? convertBulletList() : convertOrderedList(node);
1124
+ let numberingReference = listOptions.numbering.reference;
1125
+ if (listType === "ordered" && listOptions.start && listOptions.start !== 1) numberingReference = `ordered-list-start-${listOptions.start}`;
1126
+ for (const item of node.content) if (item.type === "listItem") {
1127
+ const paragraphOptions = await convertListItem(item, { options: { numbering: {
1128
+ reference: numberingReference,
1129
+ level: 0
1130
+ } } });
1131
+ elements.push(paragraphOptions);
1132
+ }
1133
+ return elements;
1134
+ }
1135
+ //#endregion
1136
+ //#region src/converters/task-item.ts
1137
+ /**
1138
+ * Convert TipTap task item node to paragraph options with checkbox
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 item node
1144
+ * @returns Paragraph options (pure data object) with checkbox
1145
+ */
1146
+ function convertTaskItem(node) {
1147
+ if (!node.content || node.content.length === 0) return {};
1148
+ const firstParagraph = node.content[0];
1149
+ if (firstParagraph.type === "paragraph") {
1150
+ const checkboxText = node.attrs?.checked || false ? CHECKBOX_SYMBOLS.checked + " " : CHECKBOX_SYMBOLS.unchecked + " ";
1151
+ const children = firstParagraph.content?.flatMap((contentNode) => {
1152
+ if (contentNode.type === "text") return convertText(contentNode);
1153
+ else if (contentNode.type === "hardBreak") return convertHardBreak(contentNode.marks);
1154
+ return [];
1155
+ }) || [];
1156
+ return { children: [new TextRun({ text: checkboxText }), ...children] };
1157
+ }
1158
+ return {};
1159
+ }
1160
+ //#endregion
1161
+ //#region src/converters/task-list.ts
1162
+ /**
1163
+ * Convert TipTap task list node to array of paragraph options
1164
+ *
1165
+ * This converter only handles data transformation from node content to DOCX format properties.
1166
+ * It returns pure data objects (IParagraphOptions[]), not DOCX instances.
1167
+ *
1168
+ * @param node - TipTap task list node
1169
+ * @returns Array of paragraph options (pure data objects) with checkboxes
1170
+ */
1171
+ function convertTaskList(node) {
1172
+ if (!node.content || node.content.length === 0) return [];
1173
+ return node.content.filter((item) => item.type === "taskItem").map((item) => convertTaskItem(item));
1174
+ }
1175
+ //#endregion
1176
+ //#region src/converters/horizontal-rule.ts
1177
+ /**
1178
+ * Convert TipTap horizontalRule node to paragraph options
1179
+ *
1180
+ * This converter only handles data transformation from node to DOCX format properties.
1181
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
1182
+ *
1183
+ * Uses page break by default (consistent with import-docx behavior)
1184
+ *
1185
+ * @param node - TipTap horizontalRule node
1186
+ * @param params - Conversion parameters
1187
+ * @returns Paragraph options (pure data object) with page break or custom styling
1188
+ */
1189
+ function convertHorizontalRule(node, params) {
1190
+ return {
1191
+ children: [new PageBreak()],
1192
+ ...params.options?.paragraph
1193
+ };
1194
+ }
1195
+ //#endregion
1196
+ //#region src/converters/details.ts
1197
+ /**
1198
+ * Convert TipTap detailsSummary node to paragraph options
1199
+ *
1200
+ * This converter only handles data transformation from node content to DOCX format properties.
1201
+ * It returns pure data objects (IParagraphOptions), not DOCX instances.
1202
+ *
1203
+ * @param node - TipTap detailsSummary node
1204
+ * @param params - Conversion parameters
1205
+ * @returns Paragraph options (pure data object) with summary styling
1206
+ */
1207
+ function convertDetailsSummary(node, params) {
1208
+ return {
1209
+ children: convertTextNodes(node.content || []).filter((item) => item !== void 0),
1210
+ ...params.options?.summary?.paragraph
1211
+ };
1212
+ }
1213
+ //#endregion
1214
+ //#region src/generator.ts
1215
+ /**
1216
+ * Convert TipTap JSONContent to DOCX format
1217
+ *
1218
+ * @param docJson - TipTap document JSON
1219
+ * @param options - Export options
1220
+ * @returns Promise with DOCX in specified format
1221
+ */
1222
+ async function generateDOCX(docJson, options) {
1223
+ const { title, subject, creator, keywords, description, lastModifiedBy, revision, styles, tableOfContents, sections, fonts, hyphenation, compatibility, customProperties, evenAndOddHeaderAndFooters, defaultTabStop, outputType } = options;
1224
+ const children = await convertDocument(docJson, { options });
1225
+ const tocElement = tableOfContents ? new TableOfContents(tableOfContents.title, { ...tableOfContents.run }) : null;
1226
+ const numberingOptions = createNumberingOptions(docJson);
1227
+ const additionalParagraphStyles = [];
1228
+ if (options.image?.style) additionalParagraphStyles.push(options.image.style);
1229
+ if (options.table?.style) additionalParagraphStyles.push(options.table.style);
1230
+ if (options.code?.style) additionalParagraphStyles.push(options.code.style);
1231
+ const mergedStyles = styles ? {
1232
+ ...styles,
1233
+ ...additionalParagraphStyles.length > 0 && { paragraphStyles: [...styles.paragraphStyles || [], ...additionalParagraphStyles] }
1234
+ } : {};
1235
+ const doc = new Document({
1236
+ sections: sections ? sections.map((section, index) => {
1237
+ const sectionChildren = [];
1238
+ if (index === 0 && tocElement) sectionChildren.push(tocElement);
1239
+ if (index === 0) sectionChildren.push(...children);
1240
+ return {
1241
+ ...section,
1242
+ ...sectionChildren.length > 0 ? { children: sectionChildren } : {}
1243
+ };
1244
+ }) : [{ children: tocElement ? [tocElement, ...children] : children }],
1245
+ title: title || "Document",
1246
+ subject: subject || "",
1247
+ creator: creator || "",
1248
+ keywords: keywords || "",
1249
+ description: description || "",
1250
+ lastModifiedBy: lastModifiedBy || "",
1251
+ revision: revision || 1,
1252
+ styles: mergedStyles,
1253
+ numbering: numberingOptions,
1254
+ ...fonts && fonts.length > 0 && { fonts },
1255
+ ...hyphenation && { hyphenation },
1256
+ ...compatibility && { compatibility },
1257
+ ...customProperties && customProperties.length > 0 && { customProperties },
1258
+ ...evenAndOddHeaderAndFooters !== void 0 && { evenAndOddHeaderAndFooters },
1259
+ ...defaultTabStop !== void 0 && { defaultTabStop }
1260
+ });
1261
+ return Packer.pack(doc, outputType || "arraybuffer");
1262
+ }
1263
+ /**
1264
+ * Convert document content to DOCX elements
1265
+ */
1266
+ async function convertDocument(node, params) {
1267
+ const elements = [];
1268
+ if (!node || !Array.isArray(node.content)) return elements;
1269
+ const effectiveContentWidth = calculateEffectiveContentWidth(params.options);
1270
+ for (const childNode of node.content) {
1271
+ const element = await convertNode(childNode, params.options, effectiveContentWidth);
1272
+ if (Array.isArray(element)) elements.push(...element);
1273
+ else if (element) {
1274
+ elements.push(element);
1275
+ if (childNode.type === "table" && elements.length >= 2 && elements[elements.length - 2] instanceof Table) elements.push(new Paragraph({}));
1276
+ }
1277
+ }
1278
+ return elements;
1279
+ }
1280
+ /**
1281
+ * Convert a single node to DOCX element(s)
1282
+ *
1283
+ * This function implements a three-layer architecture:
1284
+ * 1. Data Transformation: Convert node.attrs → IParagraphOptions (pure data)
1285
+ * 2. Style Application: Apply styleId references (if configured)
1286
+ * 3. Object Creation: Create actual DOCX instances (Paragraph, Table, etc.)
1287
+ */
1288
+ async function convertNode(node, options, effectiveContentWidth) {
1289
+ if (!node || !node.type) return null;
1290
+ const dataResult = await convertNodeData(node, options, effectiveContentWidth);
1291
+ if (dataResult instanceof Table) return dataResult;
1292
+ if (Array.isArray(dataResult)) {
1293
+ const styleId = getStyleIdByNodeType(node.type, options);
1294
+ return dataResult.map((paragraphOptions) => {
1295
+ return new Paragraph(applyStyleReference(paragraphOptions, styleId));
1296
+ });
1297
+ }
1298
+ let styleId = getStyleIdByNodeType(node.type, options);
1299
+ if (!styleId && node.type === "paragraph" && node.content) {
1300
+ if (node.content.length > 0 && node.content.every((child) => child.type === "image")) styleId = options.image?.style?.id;
1301
+ }
1302
+ return createDOCXObject(applyStyleReference(dataResult, styleId));
1303
+ }
1304
+ /**
1305
+ * Layer 1: Data Transformation
1306
+ *
1307
+ * Convert node data to DOCX format properties.
1308
+ * Returns pure data objects (IParagraphOptions) or arrays, not DOCX instances.
1309
+ * This layer does NOT handle styleId references.
1310
+ */
1311
+ async function convertNodeData(node, options, effectiveContentWidth) {
1312
+ switch (node.type) {
1313
+ case "paragraph": return await convertParagraph(node, { image: {
1314
+ maxWidth: effectiveContentWidth,
1315
+ options: options.image?.run,
1316
+ handler: options.image?.handler
1317
+ } });
1318
+ case "heading": return convertHeading(node);
1319
+ case "blockquote": return convertBlockquote(node);
1320
+ case "codeBlock": return convertCodeBlock(node);
1321
+ case "image": return applyStyleReference({ children: [await convertImage(node, {
1322
+ maxWidth: effectiveContentWidth,
1323
+ options: options.image?.run,
1324
+ handler: options.image?.handler
1325
+ })] }, getStyleIdByNodeType("image", options));
1326
+ case "table": return await convertTable(node, { options: options.table });
1327
+ case "bulletList": return await convertList(node, { listType: "bullet" });
1328
+ case "orderedList": return await convertList(node, { listType: "ordered" });
1329
+ case "taskList": return convertTaskList(node);
1330
+ case "taskItem": return convertTaskItem(node);
1331
+ case "hardBreak": return { children: [convertHardBreak()] };
1332
+ case "horizontalRule": return convertHorizontalRule(node, { options: options.horizontalRule });
1333
+ case "details": return await convertDetails(node, options, effectiveContentWidth);
1334
+ case "detailsSummary": return convertDetailsSummary(node, { options: options.details });
1335
+ default: return { children: [new TextRun({ text: `[Unsupported: ${node.type}]` })] };
1336
+ }
1337
+ }
1338
+ /**
1339
+ * Helper to convert details node (needs to recursively call convertNode)
1340
+ */
1341
+ async function convertDetails(node, options, effectiveContentWidth) {
1342
+ const elements = [];
1343
+ if (node.content) for (const child of node.content) {
1344
+ const element = await convertNode(child, options, effectiveContentWidth);
1345
+ if (Array.isArray(element)) elements.push(...element);
1346
+ else if (element) elements.push(element);
1347
+ }
1348
+ return elements;
1349
+ }
1350
+ /**
1351
+ * Create a single ordered list level configuration
1352
+ */
1353
+ const createOrderedListLevel = (start) => ({
1354
+ level: 0,
1355
+ format: LevelFormat.DECIMAL,
1356
+ text: "%1.",
1357
+ alignment: AlignmentType.START,
1358
+ start: start ?? 1,
1359
+ style: { paragraph: { indent: {
1360
+ left: convertInchesToTwip(.5),
1361
+ hanging: convertInchesToTwip(.25)
1362
+ } } }
1363
+ });
1364
+ /**
1365
+ * Create a numbering reference configuration
1366
+ */
1367
+ const createNumberingReference = (start) => ({
1368
+ reference: start && start !== 1 ? `ordered-list-start-${start}` : "ordered-list",
1369
+ levels: [createOrderedListLevel(start)]
1370
+ });
1371
+ /**
1372
+ * Create numbering options for the document
1373
+ */
1374
+ function createNumberingOptions(docJson) {
1375
+ const orderedListStarts = /* @__PURE__ */ new Set();
1376
+ function collectListStarts(node) {
1377
+ if (node.type === "orderedList" && node.attrs?.start) orderedListStarts.add(node.attrs.start);
1378
+ if (node.content) for (const child of node.content) collectListStarts(child);
1379
+ }
1380
+ collectListStarts(docJson);
1381
+ const numberingOptions = [{
1382
+ reference: "bullet-list",
1383
+ levels: [{
1384
+ level: 0,
1385
+ format: LevelFormat.BULLET,
1386
+ text: "•",
1387
+ alignment: AlignmentType.START,
1388
+ style: { paragraph: { indent: {
1389
+ left: convertInchesToTwip(.5),
1390
+ hanging: convertInchesToTwip(.25)
1391
+ } } }
1392
+ }]
1393
+ }, createNumberingReference(1)];
1394
+ orderedListStarts.forEach((start) => {
1395
+ if (start !== 1) numberingOptions.push(createNumberingReference(start));
1396
+ });
1397
+ return { config: numberingOptions };
1398
+ }
1399
+ /**
1400
+ * Get style ID for a specific node type from export options
1401
+ *
1402
+ * This is a centralized mapping of node types to their configured style IDs.
1403
+ * Style references are applied separately from data transformation.
1404
+ *
1405
+ * @param nodeType - The type of TipTap node
1406
+ * @param options - Export options containing style configurations
1407
+ * @returns Style ID string if configured, undefined otherwise
1408
+ */
1409
+ function getStyleIdByNodeType(nodeType, options) {
1410
+ return {
1411
+ codeBlock: options.code?.style?.id,
1412
+ image: options.image?.style?.id
1413
+ }[nodeType];
1414
+ }
1415
+ /**
1416
+ * Apply style reference to paragraph options
1417
+ *
1418
+ * This function handles the final step of adding a style ID reference to
1419
+ * paragraph options. It's called after data transformation is complete.
1420
+ *
1421
+ * @param paragraphOptions - Paragraph options from converter
1422
+ * @param styleId - Style ID to apply (optional)
1423
+ * @returns Paragraph options with style ID applied if provided
1424
+ */
1425
+ function applyStyleReference(paragraphOptions, styleId) {
1426
+ if (!styleId) return paragraphOptions;
1427
+ return {
1428
+ ...paragraphOptions,
1429
+ style: styleId
1430
+ };
1431
+ }
1432
+ /**
1433
+ * Create a DOCX object from paragraph options
1434
+ *
1435
+ * This is the final step that creates actual DOCX instances from
1436
+ * pure data objects.
1437
+ *
1438
+ * @param options - Paragraph options or table
1439
+ * @returns DOCX Paragraph or Table instance
1440
+ */
1441
+ function createDOCXObject(options) {
1442
+ if (options instanceof Table) return options;
1443
+ return new Paragraph(options);
1444
+ }
1445
+ //#endregion
1446
+ 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 };