@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/README.md +271 -271
- package/dist/docx.d.mts +1 -1
- package/dist/docx.mjs +2 -1
- package/dist/index.d.mts +6738 -13347
- package/dist/index.mjs +1417 -1
- package/package.json +13 -13
- package/dist/docx.cjs +0 -1
- package/dist/docx.d.cts +0 -1
- package/dist/docx.d.ts +0 -1
- package/dist/index.cjs +0 -1
- package/dist/index.d.cts +0 -14428
- package/dist/index.d.ts +0 -14428
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 };
|