jspdf-md-renderer 3.4.1 → 3.5.1

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.umd.js CHANGED
@@ -1,6 +1,1707 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require(`marked`),require(`jspdf-autotable`)):typeof define==`function`&&define.amd?define([`exports`,`marked`,`jspdf-autotable`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.JspdfMdRenderer={},e.marked,e.jspdfAutoTable))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};n=((e,t,n)=>(n=e==null?{}:r(s(e)),l(t||!e||!e.__esModule?i(n,`default`,{value:e,enumerable:!0}):n,e)))(n);var u=function(e){return e.Heading=`heading`,e.Paragraph=`paragraph`,e.List=`list`,e.ListItem=`list_item`,e.Blockquote=`blockquote`,e.Code=`code`,e.CodeSpan=`codespan`,e.Table=`table`,e.Html=`html`,e.Hr=`hr`,e.Image=`image`,e.Link=`link`,e.Strong=`strong`,e.Em=`em`,e.TableHeader=`table_header`,e.TableCell=`table_cell`,e.Raw=`raw`,e.Text=`text`,e.Br=`br`,e}({}),d=`__jmr_`,f=/(!\[[^\]]*\]\()([^)]+)(\))\s*\{([^}]+)\}/g,p=/(\w+)\s*=\s*(\w+)/g,m=[`left`,`center`,`right`],h=e=>{let t=[];return e.width!==void 0&&t.push(`w=${e.width}`),e.height!==void 0&&t.push(`h=${e.height}`),e.align&&t.push(`a=${e.align}`),t.length>0?`#${d}${t.join(`&`)}`:``},g=e=>{let t={},n;for(;(n=p.exec(e))!==null;){let e=n[1].toLowerCase(),r=n[2];switch(e){case`width`:case`w`:{let e=parseInt(r,10);!isNaN(e)&&e>0&&(t.width=e);break}case`height`:case`h`:{let e=parseInt(r,10);!isNaN(e)&&e>0&&(t.height=e);break}case`align`:{let e=r.toLowerCase();m.includes(e)&&(t.align=e);break}}}return t},_=e=>e.replace(f,(e,t,n,r,i)=>`${t}${n}${h(g(i))}${r}`),v=e=>{let t=e.indexOf(`#${d}`);if(t===-1)return{cleanHref:e,attrs:{}};let n=e.substring(0,t),r=e.substring(t+1+6),i={},a=r.split(`&`);for(let e of a){let[t,n]=e.split(`=`);switch(t){case`w`:{let e=parseInt(n,10);!isNaN(e)&&e>0&&(i.width=e);break}case`h`:{let e=parseInt(n,10);!isNaN(e)&&e>0&&(i.height=e);break}case`a`:m.includes(n)&&(i.align=n);break}}return{cleanHref:n,attrs:i}},y=async e=>{let n=_(e);return b(await t.marked.lexer(n,{async:!0,gfm:!0}))},b=e=>{let t=[];return e.forEach(e=>{try{let n=x[e.type];n?t.push(n(e)):t.push({type:u.Raw,content:e.raw})}catch(t){console.error(`Failed to handle token ==>`,e,t)}}),t},x={[u.Heading]:e=>({type:u.Heading,depth:e.depth,content:e.text,items:e.tokens?b(e.tokens):[]}),[u.Paragraph]:e=>({type:u.Paragraph,content:e.text,items:e.tokens?b(e.tokens):[]}),[u.List]:e=>({type:u.List,ordered:e.ordered,start:e.start,items:e.items?b(e.items):[]}),[u.ListItem]:e=>({type:u.ListItem,content:e.text,items:e.tokens?b(e.tokens):[]}),[u.Code]:e=>({type:u.Code,lang:e.lang,code:e.text}),[u.Table]:e=>({type:u.Table,header:e.header.map(e=>({type:u.TableHeader,content:e.text})),rows:e.rows.map(e=>e.map(e=>({type:u.TableCell,content:e.text})))}),[u.Image]:e=>{let{cleanHref:t,attrs:n}=v(e.href);return{type:u.Image,src:t,alt:e.text,width:n.width,height:n.height,align:n.align}},[u.Link]:e=>({type:u.Link,href:e.href,text:e.text,items:e.tokens?b(e.tokens):[]}),[u.Strong]:e=>({type:u.Strong,content:e.text,items:e.tokens?b(e.tokens):[]}),[u.Em]:e=>({type:u.Em,content:e.text,items:e.tokens?b(e.tokens):[]}),[u.Text]:e=>({type:u.Text,content:e.text,items:e.tokens?b(e.tokens):[]}),[u.Hr]:e=>({type:u.Hr,content:e.raw,items:e.tokens?b(e.tokens):[]}),[u.CodeSpan]:e=>({type:u.CodeSpan,content:e.text,items:e.tokens?b(e.tokens):[]}),[u.Blockquote]:e=>({type:u.Blockquote,content:e.text,items:e.tokens?b(e.tokens):[]}),[u.Html]:e=>{let t=String(e.raw??e.text??``).trim();return/^<br\s*\/?>$/i.test(t)?{type:u.Br,content:`
2
- `}:{type:u.Raw,content:e.raw??e.text??``}},[u.Br]:()=>({type:u.Br,content:`
3
- `})},S=e=>e.getTextDimensions(`H`).h,C=e=>e.getTextDimensions(`H`).w,w=(e,t,n,r,i)=>{let a=6-(t?.depth??0)>0?6-(t?.depth??0):1;if(e.setFontSize(r.options.page.defaultFontSize+a),t?.items&&t?.items.length>0)for(let e of t?.items??[])i(e,n,r,!1);else{let i=S(e);e.text(t?.content??``,r.X+n,r.Y,{align:`left`,maxWidth:r.options.page.maxContentWidth-n,baseline:`top`}),r.recordContentY(r.Y+i),r.updateY(S(e),`add`)}e.setFontSize(r.options.page.defaultFontSize),r.updateX(r.options.page.xpading,`set`)},T=(e,t)=>{typeof t.options.pageBreakHandler==`function`?t.options.pageBreakHandler(e):e.addPage(t.options.page?.format,t.options.page?.orientation),t.updateY(t.options.page.topmargin),t.updateX(t.options.page.xpading)},E=96,D=(e,t=`mm`)=>{switch(t){case`pt`:return e*72/E;case`in`:return e/E;case`px`:return e;default:return e*25.4/E}},O=e=>{try{let t=``;if(e.includes(`base64,`)){let n=e.split(`base64,`)[1];t=typeof window<`u`&&typeof window.atob==`function`?decodeURIComponent(escape(window.atob(n))):typeof Buffer<`u`?Buffer.from(n,`base64`).toString(`utf-8`):decodeURIComponent(escape(atob(n)))}else t=decodeURIComponent(e.split(`,`)[1]||``);let n=t.match(/<svg[^>]*\swidth=(?:'|")([0-9.]+)[a-zA-Z]*(?:'|")/i),r=t.match(/<svg[^>]*\sheight=(?:'|")([0-9.]+)[a-zA-Z]*(?:'|")/i),i=t.match(/<svg[^>]*\sviewBox=(?:'|")[^'"]*(?:'|")/i),a=n?parseFloat(n[1]):0,o=r?parseFloat(r[1]):0;if((!a||!o)&&i){let e=i[0].match(/viewBox=(?:'|")([^'"]+)(?:'|")/i);if(e){let t=e[1].split(/[ ,]+/).filter(Boolean).map(parseFloat);t.length>=4&&(a||=t[2],o||=t[3])}}if(a>0&&o>0)return{width:a,height:o}}catch(e){console.warn(`Failed to extract SVG dimensions:`,e)}return null},k=(e,t,n,r,i=`mm`)=>{if(!t.data)return{finalWidth:0,finalHeight:0};let a=t.naturalWidth||0,o=t.naturalHeight||0;if(!a||!o)if(t.data.startsWith(`data:image/svg`)){let e=O(t.data);e&&(a=e.width,o=e.height)}else try{let n=e.getImageProperties(t.data);a=n.width,o=n.height}catch(e){console.warn(`Failed to get image properties for intrinsic sizing:`,e)}let s=o>0?a/o:1,c,l;if(t.width&&t.height?(c=D(t.width,i),l=D(t.height,i)):t.width?(c=D(t.width,i),l=c/s):t.height?(l=D(t.height,i),c=l*s):(c=D(a,i),l=D(o,i)),c>n){let e=n/c;c=n,l*=e}if(l>r){let e=r/l;l=r,c*=e}return{finalWidth:c,finalHeight:l}},A=async e=>{for(let t of e){if(t.type===u.Image&&t.src)try{if(t.src.startsWith(`data:`))t.data=t.src;else{let e=await fetch(t.src);if(!e.ok)throw Error(`Failed to fetch image: ${e.statusText}`);let n=await e.blob();t.data=await new Promise((e,t)=>{let r=new FileReader;r.onloadend=()=>{typeof r.result==`string`?e(r.result):t(Error(`Failed to convert image to base64 string`))},r.onerror=t,r.readAsDataURL(n)})}t.data&&t.data.startsWith(`data:image/svg`)&&typeof window<`u`&&typeof document<`u`&&(t.data=await new Promise(e=>{let n=new Image;n.onload=()=>{let r=document.createElement(`canvas`),i=O(t.data),a=i?i.width:n.width||300,o=i?i.height:n.height||150;t.naturalWidth=a,t.naturalHeight=o,r.width=a*4,r.height=o*4;let s=r.getContext(`2d`);s?(s.scale(4,4),s.drawImage(n,0,0,a,o),e(r.toDataURL(`image/png`))):e(t.data)},n.onerror=()=>e(t.data),n.src=t.data}))}catch(e){console.warn(`[jspdf-md-renderer] Warning: Failed to load image at ${t.src}. It will be skipped.`,e)}t.items&&t.items.length>0&&await A(t.items)}},j=class{static getCodespanOptions(e){let t=e.options.codespan??{};return{backgroundColor:t.backgroundColor??`#EEEEEE`,padding:t.padding??.5,showBackground:t.showBackground!==!1,fontSizeScale:t.fontSizeScale??.9}}static applyStyle(e,t,n){let r=e.getFont().fontName,i=e.getFontSize(),a=()=>{let e=n.options.font.bold?.name;return e&&e!==``?e:r},o=()=>{let e=n.options.font.regular?.name;return e&&e!==``?e:r};switch(t){case`bold`:e.setFont(a(),n.options.font.bold?.style||`bold`);break;case`italic`:e.setFont(o(),`italic`);break;case`bolditalic`:e.setFont(a(),`bolditalic`);break;case`codespan`:e.setFont(`courier`,`normal`),e.setFontSize(i*this.getCodespanOptions(n).fontSizeScale);break;default:e.setFont(o(),e.getFont().fontStyle);break}}static measureWordWidth(e,t,n,r){let i=e.getFont(),a=e.getFontSize();this.applyStyle(e,n,r);let o=e.getTextWidth(t),s=e.getCharSpace?.()??0,c=o+t.length*s;return e.setFont(i.fontName,i.fontStyle),e.setFontSize(a),c}static getStyleFromType(e,t){switch(e){case`strong`:return t===`italic`?`bolditalic`:`bold`;case`em`:return t===`bold`?`bolditalic`:`italic`;case`codespan`:return`codespan`;default:return t||`normal`}}static flattenToWords(e,t,n,r=`normal`,i=!1,a){let o=[];for(let s of t){let t=this.getStyleFromType(s.type,r),c=s.type===`link`||i,l=s.href||a;if(s.items&&s.items.length>0){let r=this.flattenToWords(e,s.items,n,t,c,l);o.push(...r)}else if(s.type===`image`){let r=n.options.page.maxContentHeight-n.options.page.topmargin,{finalWidth:i,finalHeight:a}=k(e,s,n.options.page.maxContentWidth-n.options.page.indent*0,r,n.options.page.unit||`mm`);o.push({text:``,width:i,style:t,isLink:c,href:l,linkColor:c?n.options.link?.linkColor||[0,0,255]:void 0,isImage:!0,imageElement:s,imageHeight:a})}else if(s.type===`br`)o.push({text:``,width:0,style:t,isBr:!0});else{let r=s.content||s.text||``;if(!r)continue;if(/^\s/.test(r)&&o.length>0&&(o[o.length-1].hasTrailingSpace=!0),t===`codespan`){let i=r.trim();i&&o.push({text:i,width:this.measureWordWidth(e,i,t,n),style:t,isLink:c,href:l,linkColor:c?n.options.link?.linkColor||[0,0,255]:void 0,hasTrailingSpace:/\s$/.test(r)});continue}let i=r.trim().split(/\s+/).filter(e=>e.length>0);for(let a=0;a<i.length;a++){let s=a!==i.length-1||/\s$/.test(r);o.push({text:i[a],width:this.measureWordWidth(e,i[a],t,n),style:t,isLink:c,href:l,linkColor:c?n.options.link?.linkColor||[0,0,255]:void 0,hasTrailingSpace:s})}}}return o}static breakIntoLines(e,t,n,r){let i=[],a=[],o=0,s=0,c=S(e)*r.options.page.defaultLineHeightFactor,l=e.getTextWidth(` `);for(let u=0;u<t.length;u++){let d=t[u],f=a[a.length-1]?.hasTrailingSpace?l+d.width:d.width,p=d.isImage&&d.imageHeight?d.imageHeight:S(e)*r.options.page.defaultLineHeightFactor;if(d.isBr){i.push({words:a,totalTextWidth:o,isLastLine:!0,lineHeight:c}),a=[],o=0,s=0,c=S(e)*r.options.page.defaultLineHeightFactor;continue}s+f>n&&a.length>0?(i.push({words:a,totalTextWidth:o,isLastLine:!1,lineHeight:c}),a=[d],o=d.width,s=d.width,c=p):(a.push(d),o+=d.width,s+=f,c=Math.max(c,p))}return a.length>0&&i.push({words:a,totalTextWidth:o,isLastLine:!0,lineHeight:c}),i}static renderWord(e,t,n,r,i){let a=e.getFont(),o=e.getFontSize(),s=e.getTextColor();if(this.applyStyle(e,t.style,i),t.isLink&&t.linkColor&&e.setTextColor(...t.linkColor),t.isImage&&t.imageElement&&t.imageElement.data)try{let i=`JPEG`;if(t.imageElement.data.startsWith(`data:image/png`))i=`PNG`;else if(t.imageElement.data.startsWith(`data:image/webp`))i=`WEBP`;else if(t.imageElement.data.startsWith(`data:image/gif`))i=`GIF`;else if(t.imageElement.src){let e=t.imageElement.src.split(`?`)[0].split(`#`)[0].split(`.`).pop()?.toUpperCase();e&&[`PNG`,`JPEG`,`JPG`,`WEBP`,`GIF`].includes(e)&&(i=e===`JPG`?`JPEG`:e)}if(t.width>0&&(t.imageHeight||0)>0){let a=t.imageHeight||0,o=r;e.addImage(t.imageElement.data,i,n,o,t.width,a)}}catch(e){console.warn(`Failed to render inline image`,e)}else{if(t.style===`codespan`){let a=this.getCodespanOptions(i);if(a.showBackground){let i=S(e),o=a.padding;e.setFillColor(a.backgroundColor),e.rect(n-o,r-o,t.width+o*2,i+o*2,`F`),e.setFillColor(`#000000`)}}e.text(t.text,n,r,{baseline:`top`})}if(t.isLink&&t.href){let i=t.isImage&&t.imageHeight?t.imageHeight:S(e);e.link(n,r,t.width,i,{url:t.href})}e.setFont(a.fontName,a.fontStyle),e.setFontSize(o),e.setTextColor(s)}static renderAlignedLine(e,t,n,r,i,a,o=`left`){let{words:s,totalTextWidth:c,isLastLine:l}=t;if(s.length===0)return;let u=e.getTextWidth(` `),d=n,f=u,p=c,m=0;for(let e=0;e<s.length-1;e++)s[e].hasTrailingSpace&&(p+=u,m++);switch(o){case`right`:d=n+i-p;break;case`center`:d=n+(i-p)/2;break;case`justify`:!l&&m>0&&(f=(i-c)/m);break;default:break}let h=d,g=S(e)*a.options.page.defaultLineHeightFactor;for(let n=0;n<s.length;n++){let i=s[n],o=r,c=i.isImage&&i.imageHeight?i.imageHeight:g;i.isImage?o=r:c<t.lineHeight&&(o=r+(t.lineHeight-c)),this.renderWord(e,i,h,o,a),h+=i.width,n<s.length-1&&i.hasTrailingSpace&&(h+=f)}}static renderStyledParagraph(e,t,n,r,i,a,o){let s=o??a.options.content?.textAlignment??`left`,c=this.flattenToWords(e,t,a);if(c.length===0)return;let l=this.breakIntoLines(e,c,i,a),u=r;for(let t of l)u+t.lineHeight>a.options.page.maxContentHeight&&(T(e,a),u=a.Y),this.renderAlignedLine(e,t,n,u,i,a,s),a.recordContentY(u+t.lineHeight),u+=t.lineHeight,a.updateY(t.lineHeight,`add`);let d=l[l.length-1];if(d){let t=0;for(let e=0;e<d.words.length-1;e++)d.words[e].hasTrailingSpace&&t++;let r=d.totalTextWidth+t*e.getTextWidth(` `);a.updateX(n+r,`set`)}}static renderJustifiedParagraph(e,t,n,r,i,a){this.renderStyledParagraph(e,t,n,r,i,a)}},M=class{static renderText(e,t,n,r=n.X,i=n.Y,a,o=!1){let s=e.splitTextToSize(t,a),c=S(e),l=c*n.options.page.defaultLineHeightFactor,u=i;for(let t=0;t<s.length;t++){let i=s[t];u+l>n.options.page.maxContentHeight&&(T(e,n),u=n.Y),o?t===s.length-1?e.text(i,r,u,{baseline:`top`}):e.text(i,r,u,{maxWidth:a,align:`justify`,baseline:`top`}):e.text(i,r,u,{baseline:`top`}),n.recordContentY(u+c),u+=l,n.updateY(l,`add`)}return u}},N=(e,t,n,r,i)=>{r.activateInlineLock(),e.setFontSize(r.options.page.defaultFontSize);let a=r.options.page.maxContentWidth-n;if(t?.items&&t?.items.length>0){if(t.items.length===1&&t.items[0].type===`image`){i(t.items[0],n,r,!1),r.updateX(r.options.page.xpading),r.deactivateInlineLock();return}let o=[u.Strong,u.Em,u.Text,u.CodeSpan,u.Link,u.Image,u.Br];if(t.items.some(e=>!o.includes(e.type))){let s=[],c=()=>{s.length>0&&(j.renderStyledParagraph(e,s,r.X+n,r.Y,a,r),s.length=0)};for(let e of t.items)o.includes(e.type)?s.push(e):(c(),i(e,n,r,!1));c()}else j.renderStyledParagraph(e,t.items,r.X+n,r.Y,a,r)}else{let i=t.content??``,o=r.options.content?.textAlignment??`left`;i.trim()&&M.renderText(e,i,r,r.X+n,r.Y,a,o===`justify`)}r.updateX(r.options.page.xpading),r.deactivateInlineLock()},P=(e,t,n,r,i)=>{e.setFontSize(r.options.page.defaultFontSize);for(let[e,a]of t?.items?.entries()??[]){let o=t.ordered?(t.start??0)+e:t.start;i(a,n+1,r,!0,o,t.ordered)}},F=(e,t,n,r,i,a,o)=>{r.Y+S(e)>=r.options.page.maxContentHeight&&T(e,r);let s=r.options,c=n*s.page.indent,l=o?`${a}. `:`• `,d=s.page.xpading;r.updateX(d,`set`),e.setFont(s.font.regular.name,s.font.regular.style),e.text(l,d+c,r.Y,{baseline:`top`});let f=e.getTextWidth(l),p=d+c+f,m=s.page.maxContentWidth-c-f;if(t.items&&t.items.length>0){let s=[],c=()=>{s.length>0&&(j.renderStyledParagraph(e,s,p,r.Y,m,r),s.length=0,r.updateX(d,`set`))};for(let e of t.items)e.type===u.List?(c(),i(e,n,r,!0,a,e.ordered??!1)):e.type===u.ListItem?(c(),i(e,n,r,!0,a,o)):s.push(e);c()}else if(t.content){let n=s.content?.textAlignment??`left`;M.renderText(e,t.content,r,p,r.Y,m,n===`justify`)}},I=(e,t,n,r,i,a,o,s,c=!0)=>{if(t?.items&&t?.items.length>0)for(let e of t?.items??[])a(e,n,r,i,o,s,c);else{let a=r.options,l=n*a.page.indent,u=i?s?`${o}. `:`• `:``,d=t.content||``,f=a.page.xpading;if(!d&&!u)return;if(!d.trim()&&!u){let t=(d.match(/\n/g)||[]).length;if(t>1){let n=(t-1)*(e.getTextDimensions(`A`).h*a.page.defaultLineHeightFactor);r.Y+n>a.page.maxContentHeight?T(e,r):(r.updateY(n,`add`),r.recordContentY(r.Y))}return}if(r.updateX(f,`set`),i&&u){let t=e.getTextWidth(u),n=a.page.maxContentWidth-l-t;e.setFont(a.font.regular.name,a.font.regular.style),e.text(u,f+l,r.Y,{baseline:`top`}),M.renderText(e,d,r,f+l+t,r.Y,n,c)}else{let t=a.page.maxContentWidth-l;M.renderText(e,d,r,f+l,r.Y,t,c)}r.updateX(f,`set`)}},L=(e,t)=>{let n=e.internal.pageSize.getWidth();e.setLineDashPattern([1,1],0),e.setLineWidth(.1),e.line(t.options.page.xpading,t.Y,n-t.options.page.xpading,t.Y),e.setLineWidth(.1),e.setLineDashPattern([],0),t.updateY(S(e),`add`)},R=(e,t,n,r)=>{let i=e.getFont(),a=e.getFontSize();e.setFont(`courier`,`normal`);let o=r.options.page.defaultFontSize*.9;e.setFontSize(o);let s=n*r.options.page.indent,c=r.options.page.maxContentWidth-s-8,l=e.getLineHeightFactor(),u=o/e.internal.scaleFactor*l,d=(t.code??``).replace(/[\r\n\s]+$/,``);if(!d){e.setFont(i.fontName,i.fontStyle),e.setFontSize(a);return}let f=e.splitTextToSize(d,c);for(;f.length>0&&f[f.length-1].trim()===``;)f.pop();if(f.length===0){e.setFont(i.fontName,i.fontStyle),e.setFontSize(a);return}let p=0;for(;p<f.length;){let n=r.options.page.maxContentHeight-r.Y,i=f.length-p,a=n-8,o=Math.floor(a/u);if(o<=0){T(e,r);continue}o>i&&(o=i);let s=f.slice(p,p+o),c=p===0,l=p+o>=f.length,d=o*u;if(c&&r.updateY(4,`add`),e.setFillColor(`#EEEEEE`),e.setDrawColor(`#DDDDDD`),e.roundedRect(r.X,r.Y-4,r.options.page.maxContentWidth,d+(c?4:0)+(l?4:0),2,2,`FD`),c&&t.lang){let n=e.getFontSize();e.setFontSize(10),e.setTextColor(`#666666`),e.text(t.lang,r.X+r.options.page.maxContentWidth-e.getTextWidth(t.lang)-4,r.Y,{baseline:`top`}),e.setFontSize(n),e.setTextColor(`#000000`)}let m=r.Y;for(let t of s)e.text(t,r.X+4,m,{baseline:`top`}),m+=u;r.updateY(d,`add`),r.recordContentY(r.Y+(l?4:0)),l&&r.updateY(4,`add`),p+=o,p<f.length&&T(e,r)}e.setFont(i.fontName,i.fontStyle),e.setFontSize(a)},z=(e,t,n,r)=>{let i=e.getFont().fontName,a=e.getFont().fontStyle,o=e.getFontSize(),s=e=>{switch(e){case`normal`:return 0;case`bold`:return 1;case`italic`:return 1.5;case`bolditalic`:return 1.5;case`codespan`:return .5;default:return 0}},c=(t,c)=>{c===`bold`?e.setFont(r.options.font.bold.name&&r.options.font.bold.name!==``?r.options.font.bold.name:i,r.options.font.bold.style||`bold`):c===`italic`?e.setFont(r.options.font.regular.name,`italic`):c===`bolditalic`?e.setFont(r.options.font.bold.name&&r.options.font.bold.name!==``?r.options.font.bold.name:i,`bolditalic`):c===`codespan`?(e.setFont(`courier`,`normal`),e.setFontSize(o*.9)):e.setFont(r.options.font.regular.name,a);let l=r.options.page.maxContentWidth-n-r.X,u=e.splitTextToSize(t,l),d=c===`codespan`,f=`#EEEEEE`;if(r.isInlineLockActive)for(let t=0;t<u.length;t++){if(d){let i=e.getTextWidth(u[t])+C(e),a=S(e);e.setFillColor(f),e.roundedRect(r.X+n-1,r.Y-1,i+2,a+2,2,2,`F`),e.setFillColor(`#000000`)}e.text(u[t],r.X+n,r.Y,{baseline:`top`,maxWidth:l}),r.updateX(e.getTextDimensions(u[t]).w+(d?2:1),`add`),t<u.length-1&&(r.updateY(S(e),`add`),r.updateX(r.options.page.xpading,`set`))}else if(u.length>1){let t=u[0],i=u?.slice(1)?.join(` `);if(d){let i=e.getTextWidth(t)+C(e),a=S(e);e.setFillColor(f),e.roundedRect(r.X+(n>=2?n+2:0)-1,r.Y-1,i+2,a+2,2,2,`F`),e.setFillColor(`#000000`)}e.text(t,r.X+(n>=2?n+2*s(c):0),r.Y,{baseline:`top`,maxWidth:l}),r.updateX(r.options.page.xpading+n),r.updateY(S(e),`add`);let a=r.options.page.maxContentWidth-n-r.options.page.xpading;e.splitTextToSize(i,a).forEach(t=>{if(d){let n=e.getTextWidth(t)+C(e),i=S(e);e.setFillColor(f),e.roundedRect(r.X+C(e)-1,r.Y-1,n+2,i+2,2,2,`F`),e.setFillColor(`#000000`)}e.text(t,r.X+C(e),r.Y,{baseline:`top`,maxWidth:a})})}else{if(d){let i=e.getTextWidth(t)+C(e),a=S(e);e.setFillColor(f),e.roundedRect(r.X+n-1,r.Y-1,i+2,a+2,2,2,`F`),e.setFillColor(`#000000`)}e.text(t,r.X+n,r.Y,{baseline:`top`,maxWidth:l}),r.updateX(e.getTextDimensions(t).w+(n>=2?t.split(` `).length+2:2)*s(c)*.5+(d?2:0),`add`)}};if(t.type===`text`&&t.items&&t.items.length>0)for(let e of t.items)if(e.type===`codespan`)c(e.content||``,`codespan`);else if(e.type===`em`||e.type===`strong`){let t=e.type===`em`?`italic`:`bold`;if(e.items&&e.items.length>0)for(let n of e.items)n.type===`strong`&&t===`italic`||n.type===`em`&&t===`bold`?c(n.content||``,`bolditalic`):c(n.content||``,t);else c(e.content||``,t)}else c(e.content||``,`normal`);else t.type===`em`?c(t.content||``,`italic`):t.type===`strong`?c(t.content||``,`bold`):t.type===`codespan`?c(t.content||``,`codespan`):c(t.content||``,`normal`);e.setFont(i,a),e.setFontSize(o)},B=(e,t,n,r)=>{let i=e.getFont().fontName,a=e.getFont().fontStyle,o=e.getFontSize(),s=e.getTextColor(),c=r.options.link?.linkColor||[0,0,255];e.setTextColor(...c);let l=r.options.page.maxContentWidth-n-r.X,u=t.text||t.content||``,d=t.href||``,f=e.splitTextToSize(u,l);if(r.isInlineLockActive)for(let t=0;t<f.length;t++){let i=e.getTextDimensions(f[t]).w,a=S(e)/2;e.link(r.X+n,r.Y,i,a,{url:d}),e.text(f[t],r.X+n,r.Y,{baseline:`top`,maxWidth:l}),r.updateX(i+1,`add`),r.X+i>r.options.page.maxContentWidth-n&&(r.updateY(a,`add`),r.updateX(r.options.page.xpading+n,`set`)),t<f.length-1&&(r.updateY(a,`add`),r.updateX(r.options.page.xpading+n,`set`))}else if(f.length>1){let t=f[0],i=f?.slice(1)?.join(` `),a=e.getTextDimensions(t).w,o=S(e)/2;e.link(r.X+n,r.Y,a,o,{url:d}),e.text(t,r.X+n,r.Y,{baseline:`top`,maxWidth:l}),r.updateX(r.options.page.xpading+n),r.updateY(o,`add`);let s=r.options.page.maxContentWidth-n-r.options.page.xpading;e.splitTextToSize(i,s).forEach(t=>{let n=e.getTextDimensions(t).w;e.link(r.X+C(e),r.Y,n,o,{url:d}),e.text(t,r.X+C(e),r.Y,{baseline:`top`,maxWidth:s})})}else{let t=e.getTextDimensions(u).w,i=S(e)/2;e.link(r.X+n,r.Y,t,i,{url:d}),e.text(u,r.X+n,r.Y,{baseline:`top`,maxWidth:l}),r.updateX(t+2,`add`)}e.setFont(i,a),e.setFontSize(o),e.setTextColor(s)},V=(e,t,n,r,i)=>{let a=r.options,o=n+1,s=r.X+n*a.page.indent,c=r.Y,l=s+a.page.indent/2,u=c,d=e.internal.getCurrentPageInfo().pageNumber;t.items&&t.items.length>0&&t.items.forEach(e=>{i(e,o,r)});let f=r.Y,p=e.internal.getCurrentPageInfo().pageNumber;e.setDrawColor(100),e.setLineWidth(1);for(let t=d;t<=p;t++){e.setPage(t);let n=t===d,r=t===p,i=n?u:a.page.topmargin,o=r?f:a.page.maxContentHeight;e.line(l,i,l,o)}r.recordContentY(),e.setPage(p)},H=e=>{if(e.data){if(e.data.startsWith(`data:image/png`))return`PNG`;if(e.data.startsWith(`data:image/jpeg`)||e.data.startsWith(`data:image/jpg`))return`JPEG`;if(e.data.startsWith(`data:image/webp`)||e.data.startsWith(`data:image/webp`))return`WEBP`;if(e.data.startsWith(`data:image/gif`))return`GIF`}if(e.src){let t=e.src.split(`?`)[0].split(`#`)[0].split(`.`).pop()?.toUpperCase();if(t&&[`PNG`,`JPEG`,`JPG`,`WEBP`,`GIF`].includes(t))return t===`JPG`?`JPEG`:t}return`JPEG`},U=(e,t,n,r)=>{if(!t.data)return;let i=r.options,a=i.page.unit||`mm`,o=n*i.page.indent,s=i.page.maxContentWidth-o,c=r.X+o,l=r.Y;try{let{finalWidth:n,finalHeight:o}=k(e,t,s,i.page.maxContentHeight-i.page.topmargin,a);l+o>i.page.maxContentHeight&&(T(e,r),l=r.Y);let u=t.align||i.image?.defaultAlign||`left`,d;switch(u){case`right`:d=c+s-n;break;case`center`:d=c+(s-n)/2;break;default:d=c;break}let f=H(t);n>0&&o>0&&e.addImage(t.data,f,d,l,n,o),r.updateY(o,`add`),r.recordContentY()}catch(e){console.warn(`Failed to render image`,e)}},W=()=>{let e=n.default;if(typeof n.default==`function`)return n.default;if(typeof e.default==`function`)return e.default;if(typeof e.autoTable==`function`)return e.autoTable;throw Error(`Could not resolve jspdf-autotable export. Expected a callable export.`)},G=(e,t,n,r)=>{if(!t.header||!t.rows)return;let i=r.options,a=i.page.xmargin+n*i.page.indent,o=[t.header.map(e=>e.content||``)],s=t.rows.map(e=>e.map(e=>e.content||``)),c=i.table||{};W()(e,{head:o,body:s,startY:r.Y,margin:{left:a,right:i.page.xmargin},...c,didDrawPage:e=>{c.didDrawPage&&c.didDrawPage(e)},didDrawCell:e=>{c.didDrawCell&&c.didDrawCell(e)}});let l=e.lastAutoTable?.finalY;typeof l==`number`&&(r.updateY(l+i.page.lineSpace,`set`),r.updateX(i.page.xpading,`set`),r.recordContentY())},K=class{constructor(e){this.cursor={x:0,y:0},this.lastContentY_=0,this.inlineLock=!1,this.options_=e,this.cursor={x:e.cursor.x,y:e.cursor.y},this.lastContentY_=e.cursor.y}getCursor(){return this.cursor}setCursor(e){this.cursor=e}get options(){return this.options_}get isInlineLockActive(){return this.inlineLock}activateInlineLock(){this.inlineLock=!0}deactivateInlineLock(){this.inlineLock=!1}updateX(e,t=`set`){t===`set`?this.cursor.x=e:t===`add`&&(this.cursor.x+=e)}updateY(e,t=`set`){t===`set`?this.cursor.y=e:t===`add`&&(this.cursor.y+=e)}recordContentY(e){this.lastContentY_=e===void 0?this.cursor.y:e}get lastContentY(){return this.lastContentY_}get X(){return this.cursor.x}get Y(){return this.cursor.y}},q={page:{indent:10,maxContentWidth:190,maxContentHeight:277,lineSpace:1.5,defaultLineHeightFactor:1.2,defaultFontSize:12,defaultTitleFontSize:14,topmargin:10,xpading:10,xmargin:10,format:`a4`,orientation:`p`},font:{bold:{name:`helvetica`,style:`bold`},regular:{name:`helvetica`,style:`normal`},light:{name:`helvetica`,style:`light`}},image:{defaultAlign:`left`}},J=e=>{if(!e)throw Error(`RenderOption is required`);let t={...q.page,...e.page},n={...q.font,...e.font},r={...q.image,...e.image};return t.maxContentWidth||=190,t.maxContentHeight||=277,{...e,page:t,font:n,image:r}};e.MdTextParser=y,e.MdTextRender=async(e,t,n)=>{let r=J(n),i=new K(r),a=await y(t);await A(a);let o=(t,n=0,i,a=!1,s=0,c=!1)=>{let l=n*r.page.indent;switch(t.type){case u.Heading:w(e,t,l,i,o);break;case u.Paragraph:N(e,t,l,i,o);break;case u.List:P(e,t,n,i,o);break;case u.ListItem:F(e,t,n,i,o,s,c);break;case u.Hr:L(e,i);break;case u.Code:R(e,t,n,i);break;case u.Strong:case u.Em:case u.CodeSpan:z(e,t,l,i);break;case u.Link:B(e,t,l,i);break;case u.Blockquote:V(e,t,n,i,o);break;case u.Image:U(e,t,n,i);break;case u.Br:{i.updateX(r.page.xpading,`set`);let t=S(e)*r.page.defaultLineHeightFactor;i.Y+t>r.page.maxContentHeight?T(e,i):i.updateY(t,`add`),i.recordContentY();break}case u.Table:G(e,t,n,i);break;case u.Raw:case u.Text:I(e,t,n,i,a,o,s,c,r.content?.textAlignment===`justify`);break;default:console.warn(`Warning: Unsupported element type encountered: ${t.type}.
1
+ /*!
2
+ * jspdf-md-renderer
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Jeel Gajera
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the "Software"), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ */
27
+ (function(global, factory) {
28
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports, require("marked"), require("jspdf-autotable")) : typeof define === "function" && define.amd ? define([
29
+ "exports",
30
+ "marked",
31
+ "jspdf-autotable"
32
+ ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.JspdfMdRenderer = {}, global.marked, global.jspdfAutoTable));
33
+ })(this, function(exports, marked, jspdf_autotable) {
34
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
35
+ //#region \0rolldown/runtime.js
36
+ var __create = Object.create;
37
+ var __defProp = Object.defineProperty;
38
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
39
+ var __getOwnPropNames = Object.getOwnPropertyNames;
40
+ var __getProtoOf = Object.getPrototypeOf;
41
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
42
+ var __copyProps = (to, from, except, desc) => {
43
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
44
+ key = keys[i];
45
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
46
+ get: ((k) => from[k]).bind(null, key),
47
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
48
+ });
49
+ }
50
+ return to;
51
+ };
52
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
53
+ value: mod,
54
+ enumerable: true
55
+ }) : target, mod));
56
+ //#endregion
57
+ jspdf_autotable = __toESM(jspdf_autotable);
58
+ //#region src/parser/imageExtension.ts
59
+ /**
60
+ * Internal hash prefix used to encode image attributes in the URL fragment.
61
+ * This is stripped during token conversion and never reaches the image fetcher.
62
+ */
63
+ const ATTR_HASH_PREFIX = "__jmr_";
64
+ /**
65
+ * Regex to match an image tag followed by an attribute block.
66
+ * Captures:
67
+ * Group 1: Everything before the closing `)` (i.e., `![alt](url`)
68
+ * Group 2: The image URL inside the parentheses
69
+ * Group 3: The attribute block content (e.g., `width=200 height=150 align=center`)
70
+ *
71
+ * Pattern: ![...](url){key=value ...}
72
+ */
73
+ const IMAGE_WITH_ATTRS_REGEX = /(!\[[^\]]*\]\()([^)]+)(\))\s*\{([^}]+)\}/g;
74
+ /**
75
+ * Regex to extract individual key=value pairs from the attribute block.
76
+ */
77
+ const ATTR_PAIR_REGEX = /(\w+)\s*=\s*(\w+)/g;
78
+ /** Valid alignment values */
79
+ const VALID_ALIGNMENTS = [
80
+ "left",
81
+ "center",
82
+ "right"
83
+ ];
84
+ /**
85
+ * Encodes image attributes into a URL hash fragment.
86
+ * Example: {width: 200, height: 100, align: 'center'} → '#__jmr_w=200&h=100&a=center'
87
+ */
88
+ const encodeAttrsToFragment = (attrs) => {
89
+ const parts = [];
90
+ if (attrs.width !== void 0) parts.push(`w=${attrs.width}`);
91
+ if (attrs.height !== void 0) parts.push(`h=${attrs.height}`);
92
+ if (attrs.align) parts.push(`a=${attrs.align}`);
93
+ return parts.length > 0 ? `#${ATTR_HASH_PREFIX}${parts.join("&")}` : "";
94
+ };
95
+ /**
96
+ * Parses an attribute string like "width=200 height=150 align=center"
97
+ * into a structured object.
98
+ */
99
+ const parseRawAttributes = (attrString) => {
100
+ const attrs = {};
101
+ let match;
102
+ while ((match = ATTR_PAIR_REGEX.exec(attrString)) !== null) {
103
+ const key = match[1].toLowerCase();
104
+ const value = match[2];
105
+ switch (key) {
106
+ case "width":
107
+ case "w": {
108
+ const num = parseInt(value, 10);
109
+ if (!isNaN(num) && num > 0) attrs.width = num;
110
+ break;
111
+ }
112
+ case "height":
113
+ case "h": {
114
+ const num = parseInt(value, 10);
115
+ if (!isNaN(num) && num > 0) attrs.height = num;
116
+ break;
117
+ }
118
+ case "align": {
119
+ const alignVal = value.toLowerCase();
120
+ if (VALID_ALIGNMENTS.includes(alignVal)) attrs.align = alignVal;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+ return attrs;
126
+ };
127
+ /**
128
+ * Pre-processes markdown text to embed image attributes into URL fragments.
129
+ *
130
+ * Transforms `![alt](url){width=200 align=center}` into
131
+ * `![alt](url#__jmr_w=200&a=center)` so that each image token
132
+ * carries its own attributes — no shared state needed.
133
+ *
134
+ * Supported attributes:
135
+ * - `width` or `w`: Image width in pixels (number)
136
+ * - `height` or `h`: Image height in pixels (number)
137
+ * - `align`: Image alignment - 'left', 'center', or 'right'
138
+ *
139
+ * @param text - The raw markdown text
140
+ * @returns The cleaned markdown text with attributes encoded in URLs
141
+ */
142
+ const preprocessImageAttributes = (text) => {
143
+ return text.replace(IMAGE_WITH_ATTRS_REGEX, (_fullMatch, before, url, closeParen, attrsContent) => {
144
+ return `${before}${url}${encodeAttrsToFragment(parseRawAttributes(attrsContent))}${closeParen}`;
145
+ });
146
+ };
147
+ /**
148
+ * Extracts image attributes from a URL that may contain an encoded fragment.
149
+ * Returns the clean URL (without the attribute fragment) and parsed attributes.
150
+ *
151
+ * @param href - The image URL, possibly containing `#__jmr_...` fragment
152
+ * @returns Object with cleanHref and parsed attrs
153
+ */
154
+ const parseImageAttrsFromHref = (href) => {
155
+ const fragmentIdx = href.indexOf(`#${ATTR_HASH_PREFIX}`);
156
+ if (fragmentIdx === -1) return {
157
+ cleanHref: href,
158
+ attrs: {}
159
+ };
160
+ const cleanHref = href.substring(0, fragmentIdx);
161
+ const fragment = href.substring(fragmentIdx + 1 + 6);
162
+ const attrs = {};
163
+ const pairs = fragment.split("&");
164
+ for (const pair of pairs) {
165
+ const [key, value] = pair.split("=");
166
+ switch (key) {
167
+ case "w": {
168
+ const num = parseInt(value, 10);
169
+ if (!isNaN(num) && num > 0) attrs.width = num;
170
+ break;
171
+ }
172
+ case "h": {
173
+ const num = parseInt(value, 10);
174
+ if (!isNaN(num) && num > 0) attrs.height = num;
175
+ break;
176
+ }
177
+ case "a":
178
+ if (VALID_ALIGNMENTS.includes(value)) attrs.align = value;
179
+ break;
180
+ }
181
+ }
182
+ return {
183
+ cleanHref,
184
+ attrs
185
+ };
186
+ };
187
+ //#endregion
188
+ //#region src/parser/MdTextParser.ts
189
+ /**
190
+ * Parses markdown into tokens and converts to a custom parsed structure.
191
+ *
192
+ * @param text - The markdown content to parse.
193
+ * @returns Parsed markdown elements.
194
+ */
195
+ const MdTextParser = async (text) => {
196
+ const processedText = preprocessImageAttributes(text);
197
+ return convertTokens(await marked.marked.lexer(processedText, {
198
+ async: true,
199
+ gfm: true
200
+ }));
201
+ };
202
+ /**
203
+ * Convert the markdown tokens to ParsedElements.
204
+ *
205
+ * @param tokens - The list of markdown tokens.
206
+ * @returns Parsed elements in a custom structure.
207
+ */
208
+ const convertTokens = (tokens) => {
209
+ const parsedElements = [];
210
+ tokens.forEach((token) => {
211
+ try {
212
+ const handler = tokenHandlers[token.type];
213
+ if (handler) parsedElements.push(handler(token));
214
+ else parsedElements.push({
215
+ type: "raw",
216
+ content: token.raw
217
+ });
218
+ } catch (error) {
219
+ console.error("Failed to handle token ==>", token, error);
220
+ }
221
+ });
222
+ return parsedElements;
223
+ };
224
+ /**
225
+ * Map each token type to its handler function.
226
+ */
227
+ const tokenHandlers = {
228
+ ["heading"]: (token) => ({
229
+ type: "heading",
230
+ depth: token.depth,
231
+ content: token.text,
232
+ items: token.tokens ? convertTokens(token.tokens) : []
233
+ }),
234
+ ["paragraph"]: (token) => ({
235
+ type: "paragraph",
236
+ content: token.text,
237
+ items: token.tokens ? convertTokens(token.tokens) : []
238
+ }),
239
+ ["list"]: (token) => ({
240
+ type: "list",
241
+ ordered: token.ordered,
242
+ start: token.start,
243
+ items: token.items ? convertTokens(token.items) : []
244
+ }),
245
+ ["list_item"]: (token) => ({
246
+ type: "list_item",
247
+ content: token.text,
248
+ items: token.tokens ? convertTokens(token.tokens) : []
249
+ }),
250
+ ["code"]: (token) => ({
251
+ type: "code",
252
+ lang: token.lang,
253
+ code: token.text
254
+ }),
255
+ ["table"]: (token) => ({
256
+ type: "table",
257
+ header: token.header.map((header) => ({
258
+ type: "table_header",
259
+ content: header.text
260
+ })),
261
+ rows: token.rows.map((row) => row.map((cell) => ({
262
+ type: "table_cell",
263
+ content: cell.text
264
+ })))
265
+ }),
266
+ ["image"]: (token) => {
267
+ const { cleanHref, attrs } = parseImageAttrsFromHref(token.href);
268
+ return {
269
+ type: "image",
270
+ src: cleanHref,
271
+ alt: token.text,
272
+ width: attrs.width,
273
+ height: attrs.height,
274
+ align: attrs.align
275
+ };
276
+ },
277
+ ["link"]: (token) => ({
278
+ type: "link",
279
+ href: token.href,
280
+ text: token.text,
281
+ items: token.tokens ? convertTokens(token.tokens) : []
282
+ }),
283
+ ["strong"]: (token) => ({
284
+ type: "strong",
285
+ content: token.text,
286
+ items: token.tokens ? convertTokens(token.tokens) : []
287
+ }),
288
+ ["em"]: (token) => ({
289
+ type: "em",
290
+ content: token.text,
291
+ items: token.tokens ? convertTokens(token.tokens) : []
292
+ }),
293
+ ["text"]: (token) => ({
294
+ type: "text",
295
+ content: token.text,
296
+ items: token.tokens ? convertTokens(token.tokens) : []
297
+ }),
298
+ ["hr"]: (token) => ({
299
+ type: "hr",
300
+ content: token.raw,
301
+ items: token.tokens ? convertTokens(token.tokens) : []
302
+ }),
303
+ ["codespan"]: (token) => ({
304
+ type: "codespan",
305
+ content: token.text,
306
+ items: token.tokens ? convertTokens(token.tokens) : []
307
+ }),
308
+ ["blockquote"]: (token) => ({
309
+ type: "blockquote",
310
+ content: token.text,
311
+ items: token.tokens ? convertTokens(token.tokens) : []
312
+ }),
313
+ ["html"]: (token) => {
314
+ const rawHtml = String(token.raw ?? token.text ?? "").trim();
315
+ if (/^<br\s*\/?>$/i.test(rawHtml)) return {
316
+ type: "br",
317
+ content: "\n"
318
+ };
319
+ return {
320
+ type: "raw",
321
+ content: token.raw ?? token.text ?? ""
322
+ };
323
+ },
324
+ ["br"]: () => ({
325
+ type: "br",
326
+ content: "\n"
327
+ })
328
+ };
329
+ //#endregion
330
+ //#region src/utils/doc-helpers.ts
331
+ const getCharHight = (doc) => {
332
+ return doc.getTextDimensions("H").h;
333
+ };
334
+ const getCharWidth = (doc) => {
335
+ return doc.getTextDimensions("H").w;
336
+ };
337
+ //#endregion
338
+ //#region src/utils/handlePageBreak.ts
339
+ const HandlePageBreaks = (doc, store) => {
340
+ if (typeof store.options.pageBreakHandler === "function") store.options.pageBreakHandler(doc);
341
+ else doc.addPage(store.options.page?.format, store.options.page?.orientation);
342
+ store.updateY(store.options.page.topmargin);
343
+ store.updateX(store.options.page.xpading);
344
+ };
345
+ //#endregion
346
+ //#region src/utils/image-utils.ts
347
+ /**
348
+ * Standard DPI for web/screen pixels.
349
+ */
350
+ const DEFAULT_DPI = 96;
351
+ /**
352
+ * Converts pixel values to the document's unit system.
353
+ * Uses 96 DPI as the standard web pixel density.
354
+ *
355
+ * @param px - Value in pixels
356
+ * @param unit - The document unit ('mm' | 'pt' | 'in' | 'px')
357
+ * @returns Value in document units
358
+ */
359
+ const pxToDocUnit = (px, unit = "mm") => {
360
+ switch (unit) {
361
+ case "pt": return px * 72 / DEFAULT_DPI;
362
+ case "in": return px / DEFAULT_DPI;
363
+ case "px": return px;
364
+ default: return px * 25.4 / DEFAULT_DPI;
365
+ }
366
+ };
367
+ /**
368
+ * Extracts width and height from an SVG data URI if possible.
369
+ */
370
+ const extractSvgDimensions = (dataUri) => {
371
+ try {
372
+ let svgString = "";
373
+ if (dataUri.includes("base64,")) {
374
+ const base64 = dataUri.split("base64,")[1];
375
+ if (typeof window !== "undefined" && typeof window.atob === "function") svgString = decodeURIComponent(escape(window.atob(base64)));
376
+ else if (typeof Buffer !== "undefined") svgString = Buffer.from(base64, "base64").toString("utf-8");
377
+ else svgString = decodeURIComponent(escape(atob(base64)));
378
+ } else svgString = decodeURIComponent(dataUri.split(",")[1] || "");
379
+ const widthMatch = svgString.match(/<svg[^>]*\swidth=(?:'|")([0-9.]+)[a-zA-Z]*(?:'|")/i);
380
+ const heightMatch = svgString.match(/<svg[^>]*\sheight=(?:'|")([0-9.]+)[a-zA-Z]*(?:'|")/i);
381
+ const viewBoxMatch = svgString.match(/<svg[^>]*\sviewBox=(?:'|")[^'"]*(?:'|")/i);
382
+ let w = widthMatch ? parseFloat(widthMatch[1]) : 0;
383
+ let h = heightMatch ? parseFloat(heightMatch[1]) : 0;
384
+ if ((!w || !h) && viewBoxMatch) {
385
+ const viewBoxStr = viewBoxMatch[0].match(/viewBox=(?:'|")([^'"]+)(?:'|")/i);
386
+ if (viewBoxStr) {
387
+ const parts = viewBoxStr[1].split(/[ ,]+/).filter(Boolean).map(parseFloat);
388
+ if (parts.length >= 4) {
389
+ w = w || parts[2];
390
+ h = h || parts[3];
391
+ }
392
+ }
393
+ }
394
+ if (w > 0 && h > 0) return {
395
+ width: w,
396
+ height: h
397
+ };
398
+ } catch (e) {
399
+ console.warn("Failed to extract SVG dimensions:", e);
400
+ }
401
+ return null;
402
+ };
403
+ /**
404
+ * Calculates final dimensions for an image, respecting intrinsic size,
405
+ * user-specified attributes, and page bounds.
406
+ */
407
+ const calculateImageDimensions = (doc, element, maxWidth, maxHeight, docUnit = "mm") => {
408
+ if (!element.data) return {
409
+ finalWidth: 0,
410
+ finalHeight: 0
411
+ };
412
+ let intrinsicPxW = element.naturalWidth || 0;
413
+ let intrinsicPxH = element.naturalHeight || 0;
414
+ if (!intrinsicPxW || !intrinsicPxH) if (!element.data.startsWith("data:image/svg")) try {
415
+ const props = doc.getImageProperties(element.data);
416
+ intrinsicPxW = props.width;
417
+ intrinsicPxH = props.height;
418
+ } catch (e) {
419
+ console.warn("Failed to get image properties for intrinsic sizing:", e);
420
+ }
421
+ else {
422
+ const svgDims = extractSvgDimensions(element.data);
423
+ if (svgDims) {
424
+ intrinsicPxW = svgDims.width;
425
+ intrinsicPxH = svgDims.height;
426
+ }
427
+ }
428
+ const aspectRatio = intrinsicPxH > 0 ? intrinsicPxW / intrinsicPxH : 1;
429
+ let finalWidth;
430
+ let finalHeight;
431
+ if (element.width && element.height) {
432
+ finalWidth = pxToDocUnit(element.width, docUnit);
433
+ finalHeight = pxToDocUnit(element.height, docUnit);
434
+ } else if (element.width) {
435
+ finalWidth = pxToDocUnit(element.width, docUnit);
436
+ finalHeight = finalWidth / aspectRatio;
437
+ } else if (element.height) {
438
+ finalHeight = pxToDocUnit(element.height, docUnit);
439
+ finalWidth = finalHeight * aspectRatio;
440
+ } else {
441
+ finalWidth = pxToDocUnit(intrinsicPxW, docUnit);
442
+ finalHeight = pxToDocUnit(intrinsicPxH, docUnit);
443
+ }
444
+ if (finalWidth > maxWidth) {
445
+ const scale = maxWidth / finalWidth;
446
+ finalWidth = maxWidth;
447
+ finalHeight = finalHeight * scale;
448
+ }
449
+ if (finalHeight > maxHeight) {
450
+ const scale = maxHeight / finalHeight;
451
+ finalHeight = maxHeight;
452
+ finalWidth = finalWidth * scale;
453
+ }
454
+ return {
455
+ finalWidth,
456
+ finalHeight
457
+ };
458
+ };
459
+ /**
460
+ * Recursively traverses parsed elements and loads image data for Image tokens.
461
+ * @param elements - The parsed elements to process.
462
+ */
463
+ const prefetchImages = async (elements) => {
464
+ for (const element of elements) {
465
+ if (element.type === "image" && element.src) try {
466
+ if (element.src.startsWith("data:")) element.data = element.src;
467
+ else {
468
+ const response = await fetch(element.src);
469
+ if (!response.ok) throw new Error(`Failed to fetch image: ${response.statusText}`);
470
+ const blob = await response.blob();
471
+ element.data = await new Promise((resolve, reject) => {
472
+ const reader = new FileReader();
473
+ reader.onloadend = () => {
474
+ if (typeof reader.result === "string") resolve(reader.result);
475
+ else reject(/* @__PURE__ */ new Error("Failed to convert image to base64 string"));
476
+ };
477
+ reader.onerror = reject;
478
+ reader.readAsDataURL(blob);
479
+ });
480
+ }
481
+ if (element.data && element.data.startsWith("data:image/svg")) {
482
+ if (typeof window !== "undefined" && typeof document !== "undefined") element.data = await new Promise((resolve) => {
483
+ const img = new Image();
484
+ img.onload = () => {
485
+ const canvas = document.createElement("canvas");
486
+ const dims = extractSvgDimensions(element.data);
487
+ const w = dims ? dims.width : img.width || 300;
488
+ const h = dims ? dims.height : img.height || 150;
489
+ element.naturalWidth = w;
490
+ element.naturalHeight = h;
491
+ const scale = 4;
492
+ canvas.width = w * scale;
493
+ canvas.height = h * scale;
494
+ const ctx = canvas.getContext("2d");
495
+ if (ctx) {
496
+ ctx.scale(scale, scale);
497
+ ctx.drawImage(img, 0, 0, w, h);
498
+ resolve(canvas.toDataURL("image/png"));
499
+ } else resolve(element.data);
500
+ };
501
+ img.onerror = () => resolve(element.data);
502
+ img.src = element.data;
503
+ });
504
+ }
505
+ } catch (error) {
506
+ console.warn(`[jspdf-md-renderer] Warning: Failed to load image at ${element.src}. It will be skipped.`, error);
507
+ }
508
+ if (element.items && element.items.length > 0) await prefetchImages(element.items);
509
+ }
510
+ };
511
+ //#endregion
512
+ //#region src/utils/justifiedTextRenderer.ts
513
+ /**
514
+ * JustifiedTextRenderer - Renders mixed inline elements with proper alignment.
515
+ *
516
+ * Features:
517
+ * - Handles bold, italic, codespan, links mixed in paragraph
518
+ * - Proper word spacing distribution for justified alignment
519
+ * - Supports left, right, center, and justify alignments
520
+ * - Page break handling
521
+ * - Preserves link clickability
522
+ * - Codespan background rendering
523
+ */
524
+ var JustifiedTextRenderer = class {
525
+ static getCodespanOptions(store) {
526
+ const opts = store.options.codespan ?? {};
527
+ return {
528
+ backgroundColor: opts.backgroundColor ?? "#EEEEEE",
529
+ padding: opts.padding ?? .5,
530
+ showBackground: opts.showBackground !== false,
531
+ fontSizeScale: opts.fontSizeScale ?? .9
532
+ };
533
+ }
534
+ /**
535
+ * Apply font style to the jsPDF document.
536
+ */
537
+ static applyStyle(doc, style, store) {
538
+ const currentFont = doc.getFont().fontName;
539
+ const currentFontSize = doc.getFontSize();
540
+ const getBoldFont = () => {
541
+ const boldName = store.options.font.bold?.name;
542
+ return boldName && boldName !== "" ? boldName : currentFont;
543
+ };
544
+ const getRegularFont = () => {
545
+ const regularName = store.options.font.regular?.name;
546
+ return regularName && regularName !== "" ? regularName : currentFont;
547
+ };
548
+ switch (style) {
549
+ case "bold":
550
+ doc.setFont(getBoldFont(), store.options.font.bold?.style || "bold");
551
+ break;
552
+ case "italic":
553
+ doc.setFont(getRegularFont(), "italic");
554
+ break;
555
+ case "bolditalic":
556
+ doc.setFont(getBoldFont(), "bolditalic");
557
+ break;
558
+ case "codespan":
559
+ const codeFont = store.options.font.code || {
560
+ name: "courier",
561
+ style: "normal"
562
+ };
563
+ doc.setFont(codeFont.name, codeFont.style);
564
+ doc.setFontSize(currentFontSize * this.getCodespanOptions(store).fontSizeScale);
565
+ break;
566
+ default:
567
+ doc.setFont(getRegularFont(), doc.getFont().fontStyle);
568
+ break;
569
+ }
570
+ }
571
+ /**
572
+ * Measure word width with a specific style applied.
573
+ * NOTE: jsPDF's getTextWidth() does NOT include charSpace in its calculation,
574
+ * so we must manually add it: effectiveWidth = getTextWidth(text) + (text.length * charSpace)
575
+ */
576
+ static measureWordWidth(doc, text, style, store) {
577
+ const savedFont = doc.getFont();
578
+ const savedSize = doc.getFontSize();
579
+ this.applyStyle(doc, style, store);
580
+ const baseWidth = doc.getTextWidth(text);
581
+ const charSpace = doc.getCharSpace?.() ?? 0;
582
+ const effectiveWidth = baseWidth + text.length * charSpace;
583
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
584
+ doc.setFontSize(savedSize);
585
+ return effectiveWidth;
586
+ }
587
+ /**
588
+ * Extract style from element type string.
589
+ */
590
+ static getStyleFromType(type, parentStyle) {
591
+ switch (type) {
592
+ case "strong":
593
+ if (parentStyle === "italic") return "bolditalic";
594
+ return "bold";
595
+ case "em":
596
+ if (parentStyle === "bold") return "bolditalic";
597
+ return "italic";
598
+ case "codespan": return "codespan";
599
+ default: return parentStyle || "normal";
600
+ }
601
+ }
602
+ /**
603
+ * Flatten ParsedElement tree into an array of StyledWordInfo.
604
+ * Handles nested inline elements.
605
+ */
606
+ static flattenToWords(doc, elements, store, parentStyle = "normal", isLink = false, href) {
607
+ const result = [];
608
+ for (const el of elements) {
609
+ const style = this.getStyleFromType(el.type, parentStyle);
610
+ const elIsLink = el.type === "link" || isLink;
611
+ const elHref = el.href || href;
612
+ if (el.items && el.items.length > 0) {
613
+ const nested = this.flattenToWords(doc, el.items, store, style, elIsLink, elHref);
614
+ result.push(...nested);
615
+ } else if (el.type === "image") {
616
+ const maxH = store.options.page.maxContentHeight - store.options.page.topmargin;
617
+ const { finalWidth, finalHeight } = calculateImageDimensions(doc, el, store.options.page.maxContentWidth - store.options.page.indent * 0, maxH, store.options.page.unit || "mm");
618
+ result.push({
619
+ text: "",
620
+ width: finalWidth,
621
+ style,
622
+ isLink: elIsLink,
623
+ href: elHref,
624
+ linkColor: elIsLink ? store.options.link?.linkColor || [
625
+ 0,
626
+ 0,
627
+ 255
628
+ ] : void 0,
629
+ isImage: true,
630
+ imageElement: el,
631
+ imageHeight: finalHeight
632
+ });
633
+ } else if (el.type === "br") result.push({
634
+ text: "",
635
+ width: 0,
636
+ style,
637
+ isBr: true
638
+ });
639
+ else {
640
+ const text = el.content || el.text || "";
641
+ if (!text) continue;
642
+ if (/^\s/.test(text) && result.length > 0) result[result.length - 1].hasTrailingSpace = true;
643
+ if (style === "codespan") {
644
+ const trimmedText = text.trim();
645
+ if (trimmedText) result.push({
646
+ text: trimmedText,
647
+ width: this.measureWordWidth(doc, trimmedText, style, store),
648
+ style,
649
+ isLink: elIsLink,
650
+ href: elHref,
651
+ linkColor: elIsLink ? store.options.link?.linkColor || [
652
+ 0,
653
+ 0,
654
+ 255
655
+ ] : void 0,
656
+ hasTrailingSpace: /\s$/.test(text)
657
+ });
658
+ continue;
659
+ }
660
+ const lines = text.split("\n");
661
+ for (let partIndex = 0; partIndex < lines.length; partIndex++) {
662
+ const lineStr = lines[partIndex];
663
+ const words = lineStr.trim().split(/[ \t\r\v\f]+/).filter((w) => w.length > 0);
664
+ for (let i = 0; i < words.length; i++) {
665
+ const hasTrailingSpace = !(i === words.length - 1) || /[ \t\r\v\f]$/.test(lineStr);
666
+ result.push({
667
+ text: words[i],
668
+ width: this.measureWordWidth(doc, words[i], style, store),
669
+ style,
670
+ isLink: elIsLink,
671
+ href: elHref,
672
+ linkColor: elIsLink ? store.options.link?.linkColor || [
673
+ 0,
674
+ 0,
675
+ 255
676
+ ] : void 0,
677
+ hasTrailingSpace
678
+ });
679
+ }
680
+ if (partIndex < lines.length - 1) result.push({
681
+ text: "",
682
+ width: 0,
683
+ style,
684
+ isBr: true
685
+ });
686
+ }
687
+ }
688
+ }
689
+ return result;
690
+ }
691
+ /**
692
+ * Break a flat list of words into lines that fit within maxWidth.
693
+ * Correctly tracks totalTextWidth (sum of word widths only) for justification.
694
+ */
695
+ static breakIntoLines(doc, words, maxWidth, store) {
696
+ const lines = [];
697
+ let currentLine = [];
698
+ let currentTextWidth = 0;
699
+ let currentLineWidth = 0;
700
+ let currentLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
701
+ const spaceWidth = doc.getTextWidth(" ");
702
+ for (let i = 0; i < words.length; i++) {
703
+ const word = words[i];
704
+ const neededWidthWithSpace = currentLine[currentLine.length - 1]?.hasTrailingSpace ? spaceWidth + word.width : word.width;
705
+ const itemHeight = word.isImage && word.imageHeight ? word.imageHeight : getCharHight(doc) * store.options.page.defaultLineHeightFactor;
706
+ if (word.isBr) {
707
+ lines.push({
708
+ words: currentLine,
709
+ totalTextWidth: currentTextWidth,
710
+ isLastLine: true,
711
+ lineHeight: currentLineHeight
712
+ });
713
+ currentLine = [];
714
+ currentTextWidth = 0;
715
+ currentLineWidth = 0;
716
+ currentLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
717
+ continue;
718
+ }
719
+ if (currentLineWidth + neededWidthWithSpace > maxWidth && currentLine.length > 0) {
720
+ lines.push({
721
+ words: currentLine,
722
+ totalTextWidth: currentTextWidth,
723
+ isLastLine: false,
724
+ lineHeight: currentLineHeight
725
+ });
726
+ currentLine = [word];
727
+ currentTextWidth = word.width;
728
+ currentLineWidth = word.width;
729
+ currentLineHeight = itemHeight;
730
+ } else {
731
+ currentLine.push(word);
732
+ currentTextWidth += word.width;
733
+ currentLineWidth += neededWidthWithSpace;
734
+ currentLineHeight = Math.max(currentLineHeight, itemHeight);
735
+ }
736
+ }
737
+ if (currentLine.length > 0) lines.push({
738
+ words: currentLine,
739
+ totalTextWidth: currentTextWidth,
740
+ isLastLine: true,
741
+ lineHeight: currentLineHeight
742
+ });
743
+ return lines;
744
+ }
745
+ /**
746
+ * Render a single word with its style applied.
747
+ */
748
+ static renderWord(doc, word, x, y, store) {
749
+ const savedFont = doc.getFont();
750
+ const savedSize = doc.getFontSize();
751
+ const savedColor = doc.getTextColor();
752
+ this.applyStyle(doc, word.style, store);
753
+ if (word.isLink && word.linkColor) doc.setTextColor(...word.linkColor);
754
+ if (word.isImage && word.imageElement && word.imageElement.data) try {
755
+ let imgFormat = "JPEG";
756
+ if (word.imageElement.data.startsWith("data:image/png")) imgFormat = "PNG";
757
+ else if (word.imageElement.data.startsWith("data:image/webp")) imgFormat = "WEBP";
758
+ else if (word.imageElement.data.startsWith("data:image/gif")) imgFormat = "GIF";
759
+ else if (word.imageElement.src) {
760
+ const ext = word.imageElement.src.split("?")[0].split("#")[0].split(".").pop()?.toUpperCase();
761
+ if (ext && [
762
+ "PNG",
763
+ "JPEG",
764
+ "JPG",
765
+ "WEBP",
766
+ "GIF"
767
+ ].includes(ext)) imgFormat = ext === "JPG" ? "JPEG" : ext;
768
+ }
769
+ if (word.width > 0 && (word.imageHeight || 0) > 0) {
770
+ const imgH = word.imageHeight || 0;
771
+ const imgY = y;
772
+ doc.addImage(word.imageElement.data, imgFormat, x, imgY, word.width, imgH);
773
+ }
774
+ } catch (e) {
775
+ console.warn("Failed to render inline image", e);
776
+ }
777
+ else {
778
+ if (word.style === "codespan") {
779
+ const codespanOpts = this.getCodespanOptions(store);
780
+ if (codespanOpts.showBackground) {
781
+ const h = getCharHight(doc);
782
+ const pad = codespanOpts.padding;
783
+ doc.setFillColor(codespanOpts.backgroundColor);
784
+ doc.rect(x - pad, y - pad, word.width + pad * 2, h + pad * 2, "F");
785
+ doc.setFillColor("#000000");
786
+ }
787
+ }
788
+ doc.text(word.text, x, y, { baseline: "top" });
789
+ }
790
+ if (word.isLink && word.href) {
791
+ const h = word.isImage && word.imageHeight ? word.imageHeight : getCharHight(doc);
792
+ doc.link(x, y, word.width, h, { url: word.href });
793
+ }
794
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
795
+ doc.setFontSize(savedSize);
796
+ doc.setTextColor(savedColor);
797
+ }
798
+ /**
799
+ * Render a single line with specified alignment.
800
+ */
801
+ static renderAlignedLine(doc, line, x, y, maxWidth, store, alignment = "left") {
802
+ const { words, totalTextWidth, isLastLine } = line;
803
+ if (words.length === 0) return;
804
+ const normalSpaceWidth = doc.getTextWidth(" ");
805
+ let startX = x;
806
+ let wordSpacing = normalSpaceWidth;
807
+ let lineWidthWithNormalSpaces = totalTextWidth;
808
+ let expandableSpacesCount = 0;
809
+ for (let i = 0; i < words.length - 1; i++) if (words[i].hasTrailingSpace) {
810
+ lineWidthWithNormalSpaces += normalSpaceWidth;
811
+ expandableSpacesCount++;
812
+ }
813
+ switch (alignment) {
814
+ case "right":
815
+ startX = x + maxWidth - lineWidthWithNormalSpaces;
816
+ break;
817
+ case "center":
818
+ startX = x + (maxWidth - lineWidthWithNormalSpaces) / 2;
819
+ break;
820
+ case "justify":
821
+ if (!isLastLine && expandableSpacesCount > 0) wordSpacing = (maxWidth - totalTextWidth) / expandableSpacesCount;
822
+ break;
823
+ default: break;
824
+ }
825
+ let currentX = startX;
826
+ const textHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
827
+ for (let i = 0; i < words.length; i++) {
828
+ const word = words[i];
829
+ let drawY = y;
830
+ const elementHeight = word.isImage && word.imageHeight ? word.imageHeight : textHeight;
831
+ if (word.isImage) drawY = y;
832
+ else if (elementHeight < line.lineHeight) drawY = y + (line.lineHeight - elementHeight);
833
+ this.renderWord(doc, word, currentX, drawY, store);
834
+ currentX += word.width;
835
+ if (i < words.length - 1 && word.hasTrailingSpace) currentX += wordSpacing;
836
+ }
837
+ }
838
+ /**
839
+ * Main entry point: Render a paragraph with mixed inline elements.
840
+ * Respects user's textAlignment option from store.
841
+ *
842
+ * @param doc jsPDF instance
843
+ * @param elements Array of ParsedElement (inline items in a paragraph)
844
+ * @param x Starting X coordinate
845
+ * @param y Starting Y coordinate
846
+ * @param maxWidth Maximum width for text wrapping
847
+ * @param store RenderStore instance to use
848
+ * @param alignment Optional alignment override (defaults to store option)
849
+ */
850
+ static renderStyledParagraph(doc, elements, x, y, maxWidth, store, alignment) {
851
+ const textAlignment = alignment ?? store.options.content?.textAlignment ?? "left";
852
+ const words = this.flattenToWords(doc, elements, store);
853
+ if (words.length === 0) return;
854
+ const lines = this.breakIntoLines(doc, words, maxWidth, store);
855
+ let currentY = y;
856
+ for (const line of lines) {
857
+ if (currentY + line.lineHeight > store.options.page.maxContentHeight) {
858
+ HandlePageBreaks(doc, store);
859
+ currentY = store.Y;
860
+ }
861
+ this.renderAlignedLine(doc, line, x, currentY, maxWidth, store, textAlignment);
862
+ store.recordContentY(currentY + line.lineHeight);
863
+ currentY += line.lineHeight;
864
+ store.updateY(line.lineHeight, "add");
865
+ }
866
+ const lastLine = lines[lines.length - 1];
867
+ if (lastLine) {
868
+ let actualSpacesCount = 0;
869
+ for (let i = 0; i < lastLine.words.length - 1; i++) if (lastLine.words[i].hasTrailingSpace) actualSpacesCount++;
870
+ const lastLineWidth = lastLine.totalTextWidth + actualSpacesCount * doc.getTextWidth(" ");
871
+ store.updateX(x + lastLineWidth, "set");
872
+ }
873
+ }
874
+ /**
875
+ * @deprecated Use renderStyledParagraph instead
876
+ */
877
+ static renderJustifiedParagraph(doc, elements, x, y, maxWidth, store) {
878
+ this.renderStyledParagraph(doc, elements, x, y, maxWidth, store);
879
+ }
880
+ };
881
+ //#endregion
882
+ //#region src/renderer/components/heading.ts
883
+ /**
884
+ * Renders heading elements.
885
+ */
886
+ const renderHeading = (doc, element, indent, store, parentElementRenderer) => {
887
+ const size = 6 - (element?.depth ?? 0) > 0 ? 6 - (element?.depth ?? 0) : 1;
888
+ doc.setFontSize(store.options.page.defaultFontSize + size);
889
+ if (element?.items && element?.items.length > 0) {
890
+ const originalLineHeightFactor = store.options.page.defaultLineHeightFactor;
891
+ store.options.page.defaultLineHeightFactor = 1;
892
+ JustifiedTextRenderer.renderStyledParagraph(doc, element.items, store.X + indent, store.Y, store.options.page.maxContentWidth - indent, store, "left");
893
+ store.options.page.defaultLineHeightFactor = originalLineHeightFactor;
894
+ } else {
895
+ const charHeight = getCharHight(doc);
896
+ doc.text(element?.content ?? "", store.X + indent, store.Y, {
897
+ align: "left",
898
+ maxWidth: store.options.page.maxContentWidth - indent,
899
+ baseline: "top"
900
+ });
901
+ store.recordContentY(store.Y + charHeight);
902
+ store.updateY(getCharHight(doc), "add");
903
+ }
904
+ doc.setFontSize(store.options.page.defaultFontSize);
905
+ store.updateX(store.options.page.xpading, "set");
906
+ };
907
+ //#endregion
908
+ //#region src/utils/text-renderer.ts
909
+ var TextRenderer = class {
910
+ /**
911
+ * Renders text with automatic line wrapping and page breaking.
912
+ * @param doc jsPDF instance
913
+ * @param text Text to render
914
+ * @param store RenderStore instance to use
915
+ * @param x X coordinate (if not provided, uses store.X)
916
+ * @param y Y coordinate (if not provided, uses store.Y)
917
+ * @param maxWidth Max width for text wrapping
918
+ * @param justify Whether to justify the text
919
+ */
920
+ static renderText(doc, text, store, x = store.X, y = store.Y, maxWidth, justify = false) {
921
+ const lines = doc.splitTextToSize(text, maxWidth);
922
+ const charHeight = getCharHight(doc);
923
+ const lineHeight = charHeight * store.options.page.defaultLineHeightFactor;
924
+ let currentY = y;
925
+ for (let i = 0; i < lines.length; i++) {
926
+ const line = lines[i];
927
+ if (currentY + lineHeight > store.options.page.maxContentHeight) {
928
+ HandlePageBreaks(doc, store);
929
+ currentY = store.Y;
930
+ }
931
+ if (justify) if (i === lines.length - 1) doc.text(line, x, currentY, { baseline: "top" });
932
+ else doc.text(line, x, currentY, {
933
+ maxWidth,
934
+ align: "justify",
935
+ baseline: "top"
936
+ });
937
+ else doc.text(line, x, currentY, { baseline: "top" });
938
+ store.recordContentY(currentY + charHeight);
939
+ currentY += lineHeight;
940
+ store.updateY(lineHeight, "add");
941
+ }
942
+ return currentY;
943
+ }
944
+ };
945
+ //#endregion
946
+ //#region src/renderer/components/paragraph.ts
947
+ /**
948
+ * Renders paragraph elements with proper text alignment.
949
+ * Handles mixed inline styles (bold, italic, codespan) and links.
950
+ * Respects user's textAlignment option from RenderStore.
951
+ */
952
+ const renderParagraph = (doc, element, indent, store, parentElementRenderer) => {
953
+ store.activateInlineLock();
954
+ doc.setFontSize(store.options.page.defaultFontSize);
955
+ const maxWidth = store.options.page.maxContentWidth - indent;
956
+ if (element?.items && element?.items.length > 0) {
957
+ if (element.items.length === 1 && element.items[0].type === "image") {
958
+ parentElementRenderer(element.items[0], indent, store, false);
959
+ store.updateX(store.options.page.xpading);
960
+ store.deactivateInlineLock();
961
+ return;
962
+ }
963
+ const inlineTypes = [
964
+ "strong",
965
+ "em",
966
+ "text",
967
+ "codespan",
968
+ "link",
969
+ "image",
970
+ "br"
971
+ ];
972
+ if (element.items.some((item) => !inlineTypes.includes(item.type))) {
973
+ const inlineBuffer = [];
974
+ const flushInlineBuffer = () => {
975
+ if (inlineBuffer.length > 0) {
976
+ JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, store.X + indent, store.Y, maxWidth, store);
977
+ inlineBuffer.length = 0;
978
+ }
979
+ };
980
+ for (const item of element.items) if (inlineTypes.includes(item.type)) inlineBuffer.push(item);
981
+ else {
982
+ flushInlineBuffer();
983
+ parentElementRenderer(item, indent, store, false);
984
+ }
985
+ flushInlineBuffer();
986
+ } else JustifiedTextRenderer.renderStyledParagraph(doc, element.items, store.X + indent, store.Y, maxWidth, store);
987
+ } else {
988
+ const content = element.content ?? "";
989
+ const textAlignment = store.options.content?.textAlignment ?? "left";
990
+ if (content.trim()) TextRenderer.renderText(doc, content, store, store.X + indent, store.Y, maxWidth, textAlignment === "justify");
991
+ }
992
+ store.updateX(store.options.page.xpading);
993
+ store.deactivateInlineLock();
994
+ };
995
+ //#endregion
996
+ //#region src/renderer/components/list.ts
997
+ const renderList = (doc, element, indentLevel, store, parentElementRenderer) => {
998
+ doc.setFontSize(store.options.page.defaultFontSize);
999
+ for (const [i, point] of element?.items?.entries() ?? []) {
1000
+ const _start = element.ordered ? (element.start ?? 0) + i : element.start;
1001
+ parentElementRenderer(point, indentLevel + 1, store, true, _start, element.ordered);
1002
+ }
1003
+ };
1004
+ //#endregion
1005
+ //#region src/renderer/components/listItem.ts
1006
+ /**
1007
+ * Render a single list item, including bullets/numbering, inline text, and any nested lists.
1008
+ */
1009
+ const renderListItem = (doc, element, indentLevel, store, parentElementRenderer, start, ordered) => {
1010
+ if (store.Y + getCharHight(doc) >= store.options.page.maxContentHeight) HandlePageBreaks(doc, store);
1011
+ const options = store.options;
1012
+ const baseIndent = indentLevel * options.page.indent;
1013
+ const bullet = ordered ? `${start}. ` : "• ";
1014
+ const xLeft = options.page.xpading;
1015
+ store.updateX(xLeft, "set");
1016
+ doc.setFont(options.font.regular.name, options.font.regular.style);
1017
+ doc.text(bullet, xLeft + baseIndent, store.Y, { baseline: "top" });
1018
+ const bulletWidth = doc.getTextWidth(bullet);
1019
+ const contentX = xLeft + baseIndent + bulletWidth;
1020
+ const textMaxWidth = options.page.maxContentWidth - baseIndent - bulletWidth;
1021
+ if (element.items && element.items.length > 0) {
1022
+ const inlineBuffer = [];
1023
+ const flushInlineBuffer = () => {
1024
+ if (inlineBuffer.length > 0) {
1025
+ JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, contentX, store.Y, textMaxWidth, store);
1026
+ inlineBuffer.length = 0;
1027
+ store.updateX(xLeft, "set");
1028
+ }
1029
+ };
1030
+ for (const subItem of element.items) if (subItem.type === "list") {
1031
+ flushInlineBuffer();
1032
+ parentElementRenderer(subItem, indentLevel, store, true, start, subItem.ordered ?? false);
1033
+ } else if (subItem.type === "list_item") {
1034
+ flushInlineBuffer();
1035
+ parentElementRenderer(subItem, indentLevel, store, true, start, ordered);
1036
+ } else inlineBuffer.push(subItem);
1037
+ flushInlineBuffer();
1038
+ } else if (element.content) {
1039
+ const textAlignment = options.content?.textAlignment ?? "left";
1040
+ TextRenderer.renderText(doc, element.content, store, contentX, store.Y, textMaxWidth, textAlignment === "justify");
1041
+ }
1042
+ };
1043
+ //#endregion
1044
+ //#region src/renderer/components/rawItem.ts
1045
+ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentElementRenderer, start, ordered, justify = true) => {
1046
+ if (element?.items && element?.items.length > 0) for (const item of element?.items ?? []) parentElementRenderer(item, indentLevel, store, hasRawBullet, start, ordered, justify);
1047
+ else {
1048
+ const options = store.options;
1049
+ const indent = indentLevel * options.page.indent;
1050
+ const bullet = hasRawBullet ? ordered ? `${start}. ` : "• " : "";
1051
+ const content = element.content || "";
1052
+ const xLeft = options.page.xpading;
1053
+ if (!content && !bullet) return;
1054
+ if (!content.trim() && !bullet) {
1055
+ const newlines = (content.match(/\n/g) || []).length;
1056
+ if (newlines > 1) {
1057
+ const addedHeight = (newlines - 1) * (doc.getTextDimensions("A").h * options.page.defaultLineHeightFactor);
1058
+ if (store.Y + addedHeight > options.page.maxContentHeight) HandlePageBreaks(doc, store);
1059
+ else {
1060
+ store.updateY(addedHeight, "add");
1061
+ store.recordContentY(store.Y);
1062
+ }
1063
+ }
1064
+ return;
1065
+ }
1066
+ store.updateX(xLeft, "set");
1067
+ if (hasRawBullet && bullet) {
1068
+ const bulletWidth = doc.getTextWidth(bullet);
1069
+ const textMaxWidth = options.page.maxContentWidth - indent - bulletWidth;
1070
+ doc.setFont(options.font.regular.name, options.font.regular.style);
1071
+ doc.text(bullet, xLeft + indent, store.Y, { baseline: "top" });
1072
+ TextRenderer.renderText(doc, content, store, xLeft + indent + bulletWidth, store.Y, textMaxWidth, justify);
1073
+ } else {
1074
+ const textMaxWidth = options.page.maxContentWidth - indent;
1075
+ TextRenderer.renderText(doc, content, store, xLeft + indent, store.Y, textMaxWidth, justify);
1076
+ }
1077
+ store.updateX(xLeft, "set");
1078
+ }
1079
+ };
1080
+ //#endregion
1081
+ //#region src/renderer/components/hr.ts
1082
+ const renderHR = (doc, store) => {
1083
+ const pageWidth = doc.internal.pageSize.getWidth();
1084
+ doc.setLineDashPattern([1, 1], 0);
1085
+ doc.setLineWidth(.1);
1086
+ doc.line(store.options.page.xpading, store.Y, pageWidth - store.options.page.xpading, store.Y);
1087
+ doc.setLineWidth(.1);
1088
+ doc.setLineDashPattern([], 0);
1089
+ store.updateY(getCharHight(doc), "add");
1090
+ };
1091
+ //#endregion
1092
+ //#region src/renderer/components/code.ts
1093
+ const renderCodeBlock = (doc, element, indentLevel, store) => {
1094
+ const savedFont = doc.getFont();
1095
+ const savedFontSize = doc.getFontSize();
1096
+ const codeFont = store.options.font.code || {
1097
+ name: "courier",
1098
+ style: "normal"
1099
+ };
1100
+ doc.setFont(codeFont.name, codeFont.style);
1101
+ const codeFontSize = store.options.page.defaultFontSize * .9;
1102
+ doc.setFontSize(codeFontSize);
1103
+ const indent = indentLevel * store.options.page.indent;
1104
+ const maxWidth = store.options.page.maxContentWidth - indent - 8;
1105
+ const lineHeightFactor = doc.getLineHeightFactor();
1106
+ const lineHeight = codeFontSize / doc.internal.scaleFactor * lineHeightFactor;
1107
+ const content = (element.code ?? "").replace(/[\r\n\s]+$/, "");
1108
+ if (!content) {
1109
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
1110
+ doc.setFontSize(savedFontSize);
1111
+ return;
1112
+ }
1113
+ const lines = doc.splitTextToSize(content, maxWidth);
1114
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
1115
+ if (lines.length === 0) {
1116
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
1117
+ doc.setFontSize(savedFontSize);
1118
+ return;
1119
+ }
1120
+ const padding = 4;
1121
+ const bgColor = "#EEEEEE";
1122
+ const drawColor = "#DDDDDD";
1123
+ let currentLineIndex = 0;
1124
+ while (currentLineIndex < lines.length) {
1125
+ const availableHeight = store.options.page.maxContentHeight - store.Y;
1126
+ const remainingLines = lines.length - currentLineIndex;
1127
+ const effectiveAvailable = availableHeight - padding * 2;
1128
+ let linesToRenderCount = Math.floor(effectiveAvailable / lineHeight);
1129
+ if (linesToRenderCount <= 0) {
1130
+ HandlePageBreaks(doc, store);
1131
+ continue;
1132
+ }
1133
+ if (linesToRenderCount > remainingLines) linesToRenderCount = remainingLines;
1134
+ const linesToRender = lines.slice(currentLineIndex, currentLineIndex + linesToRenderCount);
1135
+ const isFirstChunk = currentLineIndex === 0;
1136
+ const isLastChunk = currentLineIndex + linesToRenderCount >= lines.length;
1137
+ const textBlockHeight = linesToRenderCount * lineHeight;
1138
+ if (isFirstChunk) store.updateY(padding, "add");
1139
+ doc.setFillColor(bgColor);
1140
+ doc.setDrawColor(drawColor);
1141
+ doc.roundedRect(store.X, store.Y - padding, store.options.page.maxContentWidth, textBlockHeight + (isFirstChunk ? padding : 0) + (isLastChunk ? padding : 0), 2, 2, "FD");
1142
+ if (isFirstChunk && element.lang) {
1143
+ const savedCodeFontSize = doc.getFontSize();
1144
+ doc.setFontSize(10);
1145
+ doc.setTextColor("#666666");
1146
+ doc.text(element.lang, store.X + store.options.page.maxContentWidth - doc.getTextWidth(element.lang) - 4, store.Y, { baseline: "top" });
1147
+ doc.setFontSize(savedCodeFontSize);
1148
+ doc.setTextColor("#000000");
1149
+ }
1150
+ let yPos = store.Y;
1151
+ for (const line of linesToRender) {
1152
+ doc.text(line, store.X + 4, yPos, { baseline: "top" });
1153
+ yPos += lineHeight;
1154
+ }
1155
+ store.updateY(textBlockHeight, "add");
1156
+ store.recordContentY(store.Y + (isLastChunk ? padding : 0));
1157
+ if (isLastChunk) store.updateY(padding, "add");
1158
+ currentLineIndex += linesToRenderCount;
1159
+ if (currentLineIndex < lines.length) HandlePageBreaks(doc, store);
1160
+ }
1161
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
1162
+ doc.setFontSize(savedFontSize);
1163
+ };
1164
+ //#endregion
1165
+ //#region src/renderer/components/inlineText.ts
1166
+ /**
1167
+ * Renders inline text elements (Strong, Em, and Text) with proper inline styling.
1168
+ */
1169
+ const renderInlineText = (doc, element, indent, store) => {
1170
+ const currentFont = doc.getFont().fontName;
1171
+ const currentFontStyle = doc.getFont().fontStyle;
1172
+ const currentFontSize = doc.getFontSize();
1173
+ const spaceMultiplier = (style) => {
1174
+ switch (style) {
1175
+ case "normal": return 0;
1176
+ case "bold": return 1;
1177
+ case "italic": return 1.5;
1178
+ case "bolditalic": return 1.5;
1179
+ case "codespan": return .5;
1180
+ default: return 0;
1181
+ }
1182
+ };
1183
+ const renderTextWithStyle = (text, style) => {
1184
+ if (style === "bold") doc.setFont(store.options.font.bold.name && store.options.font.bold.name !== "" ? store.options.font.bold.name : currentFont, store.options.font.bold.style || "bold");
1185
+ else if (style === "italic") doc.setFont(store.options.font.regular.name, "italic");
1186
+ else if (style === "bolditalic") doc.setFont(store.options.font.bold.name && store.options.font.bold.name !== "" ? store.options.font.bold.name : currentFont, "bolditalic");
1187
+ else if (style === "codespan") {
1188
+ const codeFont = store.options.font.code || {
1189
+ name: "courier",
1190
+ style: "normal"
1191
+ };
1192
+ doc.setFont(codeFont.name, codeFont.style);
1193
+ doc.setFontSize(currentFontSize * .9);
1194
+ } else doc.setFont(store.options.font.regular.name, currentFontStyle);
1195
+ const availableWidth = store.options.page.maxContentWidth - indent - store.X;
1196
+ const textLines = doc.splitTextToSize(text, availableWidth);
1197
+ const isCodeSpan = style === "codespan";
1198
+ const codePadding = 1;
1199
+ const codeBgColor = "#EEEEEE";
1200
+ if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
1201
+ if (isCodeSpan) {
1202
+ const lineWidth = doc.getTextWidth(textLines[i]) + getCharWidth(doc);
1203
+ const lineHeight = getCharHight(doc);
1204
+ doc.setFillColor(codeBgColor);
1205
+ doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, lineWidth + codePadding * 2, lineHeight + codePadding * 2, 2, 2, "F");
1206
+ doc.setFillColor("#000000");
1207
+ }
1208
+ doc.text(textLines[i], store.X + indent, store.Y, {
1209
+ baseline: "top",
1210
+ maxWidth: availableWidth
1211
+ });
1212
+ store.updateX(doc.getTextDimensions(textLines[i]).w + (isCodeSpan ? codePadding * 2 : 1), "add");
1213
+ if (i < textLines.length - 1) {
1214
+ store.updateY(getCharHight(doc), "add");
1215
+ store.updateX(store.options.page.xpading, "set");
1216
+ }
1217
+ }
1218
+ else if (textLines.length > 1) {
1219
+ const firstLine = textLines[0];
1220
+ const restContent = textLines?.slice(1)?.join(" ");
1221
+ if (isCodeSpan) {
1222
+ const w = doc.getTextWidth(firstLine) + getCharWidth(doc);
1223
+ const h = getCharHight(doc);
1224
+ doc.setFillColor(codeBgColor);
1225
+ doc.roundedRect(store.X + (indent >= 2 ? indent + 2 : 0) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
1226
+ doc.setFillColor("#000000");
1227
+ }
1228
+ doc.text(firstLine, store.X + (indent >= 2 ? indent + 2 * spaceMultiplier(style) : 0), store.Y, {
1229
+ baseline: "top",
1230
+ maxWidth: availableWidth
1231
+ });
1232
+ store.updateX(store.options.page.xpading + indent);
1233
+ store.updateY(getCharHight(doc), "add");
1234
+ const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
1235
+ doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
1236
+ if (isCodeSpan) {
1237
+ const w = doc.getTextWidth(line) + getCharWidth(doc);
1238
+ const h = getCharHight(doc);
1239
+ doc.setFillColor(codeBgColor);
1240
+ doc.roundedRect(store.X + getCharWidth(doc) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
1241
+ doc.setFillColor("#000000");
1242
+ }
1243
+ doc.text(line, store.X + getCharWidth(doc), store.Y, {
1244
+ baseline: "top",
1245
+ maxWidth: maxWidthForRest
1246
+ });
1247
+ });
1248
+ } else {
1249
+ if (isCodeSpan) {
1250
+ const w = doc.getTextWidth(text) + getCharWidth(doc);
1251
+ const h = getCharHight(doc);
1252
+ doc.setFillColor(codeBgColor);
1253
+ doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
1254
+ doc.setFillColor("#000000");
1255
+ }
1256
+ doc.text(text, store.X + indent, store.Y, {
1257
+ baseline: "top",
1258
+ maxWidth: availableWidth
1259
+ });
1260
+ store.updateX(doc.getTextDimensions(text).w + (indent >= 2 ? text.split(" ").length + 2 : 2) * spaceMultiplier(style) * .5 + (isCodeSpan ? codePadding * 2 : 0), "add");
1261
+ }
1262
+ };
1263
+ if (element.type === "text" && element.items && element.items.length > 0) for (const item of element.items) if (item.type === "codespan") renderTextWithStyle(item.content || "", "codespan");
1264
+ else if (item.type === "em" || item.type === "strong") {
1265
+ const baseStyle = item.type === "em" ? "italic" : "bold";
1266
+ if (item.items && item.items.length > 0) for (const subItem of item.items) if (subItem.type === "strong" && baseStyle === "italic") renderTextWithStyle(subItem.content || "", "bolditalic");
1267
+ else if (subItem.type === "em" && baseStyle === "bold") renderTextWithStyle(subItem.content || "", "bolditalic");
1268
+ else renderTextWithStyle(subItem.content || "", baseStyle);
1269
+ else renderTextWithStyle(item.content || "", baseStyle);
1270
+ } else renderTextWithStyle(item.content || "", "normal");
1271
+ else if (element.type === "em") renderTextWithStyle(element.content || "", "italic");
1272
+ else if (element.type === "strong") renderTextWithStyle(element.content || "", "bold");
1273
+ else if (element.type === "codespan") renderTextWithStyle(element.content || "", "codespan");
1274
+ else renderTextWithStyle(element.content || "", "normal");
1275
+ doc.setFont(currentFont, currentFontStyle);
1276
+ doc.setFontSize(currentFontSize);
1277
+ };
1278
+ //#endregion
1279
+ //#region src/renderer/components/link.ts
1280
+ /**
1281
+ * Renders link elements with proper styling and URL handling.
1282
+ * Links are rendered in blue color and underlined to distinguish them from regular text.
1283
+ */
1284
+ const renderLink = (doc, element, indent, store) => {
1285
+ const currentFont = doc.getFont().fontName;
1286
+ const currentFontStyle = doc.getFont().fontStyle;
1287
+ const currentFontSize = doc.getFontSize();
1288
+ const currentTextColor = doc.getTextColor();
1289
+ const linkColor = store.options.link?.linkColor || [
1290
+ 0,
1291
+ 0,
1292
+ 255
1293
+ ];
1294
+ doc.setTextColor(...linkColor);
1295
+ const availableWidth = store.options.page.maxContentWidth - indent - store.X;
1296
+ const linkText = element.text || element.content || "";
1297
+ const linkUrl = element.href || "";
1298
+ const textLines = doc.splitTextToSize(linkText, availableWidth);
1299
+ if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
1300
+ const textWidth = doc.getTextDimensions(textLines[i]).w;
1301
+ const textHeight = getCharHight(doc) / 2;
1302
+ doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
1303
+ doc.text(textLines[i], store.X + indent, store.Y, {
1304
+ baseline: "top",
1305
+ maxWidth: availableWidth
1306
+ });
1307
+ store.updateX(textWidth + 1, "add");
1308
+ if (store.X + textWidth > store.options.page.maxContentWidth - indent) {
1309
+ store.updateY(textHeight, "add");
1310
+ store.updateX(store.options.page.xpading + indent, "set");
1311
+ }
1312
+ if (i < textLines.length - 1) {
1313
+ store.updateY(textHeight, "add");
1314
+ store.updateX(store.options.page.xpading + indent, "set");
1315
+ }
1316
+ }
1317
+ else if (textLines.length > 1) {
1318
+ const firstLine = textLines[0];
1319
+ const restContent = textLines?.slice(1)?.join(" ");
1320
+ const firstLineWidth = doc.getTextDimensions(firstLine).w;
1321
+ const textHeight = getCharHight(doc) / 2;
1322
+ doc.link(store.X + indent, store.Y, firstLineWidth, textHeight, { url: linkUrl });
1323
+ doc.text(firstLine, store.X + indent, store.Y, {
1324
+ baseline: "top",
1325
+ maxWidth: availableWidth
1326
+ });
1327
+ store.updateX(store.options.page.xpading + indent);
1328
+ store.updateY(textHeight, "add");
1329
+ const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
1330
+ doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
1331
+ const lineWidth = doc.getTextDimensions(line).w;
1332
+ doc.link(store.X + getCharWidth(doc), store.Y, lineWidth, textHeight, { url: linkUrl });
1333
+ doc.text(line, store.X + getCharWidth(doc), store.Y, {
1334
+ baseline: "top",
1335
+ maxWidth: maxWidthForRest
1336
+ });
1337
+ });
1338
+ } else {
1339
+ const textWidth = doc.getTextDimensions(linkText).w;
1340
+ const textHeight = getCharHight(doc) / 2;
1341
+ doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
1342
+ doc.text(linkText, store.X + indent, store.Y, {
1343
+ baseline: "top",
1344
+ maxWidth: availableWidth
1345
+ });
1346
+ store.updateX(textWidth + 2, "add");
1347
+ }
1348
+ doc.setFont(currentFont, currentFontStyle);
1349
+ doc.setFontSize(currentFontSize);
1350
+ doc.setTextColor(currentTextColor);
1351
+ };
1352
+ //#endregion
1353
+ //#region src/renderer/components/blockquote.ts
1354
+ const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
1355
+ const options = store.options;
1356
+ const blockquoteIndent = indentLevel + 1;
1357
+ const currentX = store.X + indentLevel * options.page.indent;
1358
+ const currentY = store.Y;
1359
+ const barX = currentX + options.page.indent / 2;
1360
+ const startY = currentY;
1361
+ const startPage = doc.internal.getCurrentPageInfo().pageNumber;
1362
+ if (element.items && element.items.length > 0) element.items.forEach((item) => {
1363
+ renderElement(item, blockquoteIndent, store);
1364
+ });
1365
+ const endY = store.Y;
1366
+ const endPage = doc.internal.getCurrentPageInfo().pageNumber;
1367
+ doc.setDrawColor(100);
1368
+ doc.setLineWidth(1);
1369
+ for (let p = startPage; p <= endPage; p++) {
1370
+ doc.setPage(p);
1371
+ const isStart = p === startPage;
1372
+ const isEnd = p === endPage;
1373
+ const lineTop = isStart ? startY : options.page.topmargin;
1374
+ const lineBottom = isEnd ? endY : options.page.maxContentHeight;
1375
+ doc.line(barX, lineTop, barX, lineBottom);
1376
+ }
1377
+ store.recordContentY();
1378
+ doc.setPage(endPage);
1379
+ };
1380
+ //#endregion
1381
+ //#region src/renderer/components/image.ts
1382
+ /**
1383
+ * Detects the image format from element data and source.
1384
+ */
1385
+ const detectImageFormat = (element) => {
1386
+ if (element.data) {
1387
+ if (element.data.startsWith("data:image/png")) return "PNG";
1388
+ if (element.data.startsWith("data:image/jpeg") || element.data.startsWith("data:image/jpg")) return "JPEG";
1389
+ if (element.data.startsWith("data:image/webp")) return "WEBP";
1390
+ if (element.data.startsWith("data:image/webp")) return "WEBP";
1391
+ if (element.data.startsWith("data:image/gif")) return "GIF";
1392
+ }
1393
+ if (element.src) {
1394
+ const ext = element.src.split("?")[0].split("#")[0].split(".").pop()?.toUpperCase();
1395
+ if (ext && [
1396
+ "PNG",
1397
+ "JPEG",
1398
+ "JPG",
1399
+ "WEBP",
1400
+ "GIF"
1401
+ ].includes(ext)) return ext === "JPG" ? "JPEG" : ext;
1402
+ }
1403
+ return "JPEG";
1404
+ };
1405
+ /**
1406
+ * Renders an image element into the jsPDF document with smart sizing and alignment.
1407
+ *
1408
+ * Sizing logic (in order of priority):
1409
+ * 1. If both width & height are specified by user → convert from px, use as-is
1410
+ * 2. If only width is specified → convert from px, calculate height from aspect ratio
1411
+ * 3. If only height is specified → convert from px, calculate width from aspect ratio
1412
+ * 4. If nothing specified → use intrinsic dimensions (converted from px to doc units)
1413
+ * 5. Always clamp to page bounds (scale down proportionally if needed)
1414
+ *
1415
+ * Alignment: 'left' (default) | 'center' | 'right'
1416
+ * Can be set per-image via markdown attributes or globally via RenderOption.image.defaultAlign
1417
+ */
1418
+ const renderImage = (doc, element, indentLevel, store) => {
1419
+ if (!element.data) return;
1420
+ const options = store.options;
1421
+ const docUnit = options.page.unit || "mm";
1422
+ const indent = indentLevel * options.page.indent;
1423
+ const maxWidth = options.page.maxContentWidth - indent;
1424
+ const pageLeftX = store.X + indent;
1425
+ let currentY = store.Y;
1426
+ try {
1427
+ const { finalWidth, finalHeight } = calculateImageDimensions(doc, element, maxWidth, options.page.maxContentHeight - options.page.topmargin, docUnit);
1428
+ if (currentY + finalHeight > options.page.maxContentHeight) {
1429
+ HandlePageBreaks(doc, store);
1430
+ currentY = store.Y;
1431
+ }
1432
+ const align = element.align || options.image?.defaultAlign || "left";
1433
+ let drawX;
1434
+ switch (align) {
1435
+ case "right":
1436
+ drawX = pageLeftX + maxWidth - finalWidth;
1437
+ break;
1438
+ case "center":
1439
+ drawX = pageLeftX + (maxWidth - finalWidth) / 2;
1440
+ break;
1441
+ default:
1442
+ drawX = pageLeftX;
1443
+ break;
1444
+ }
1445
+ const imgFormat = detectImageFormat(element);
1446
+ if (finalWidth > 0 && finalHeight > 0) doc.addImage(element.data, imgFormat, drawX, currentY, finalWidth, finalHeight);
1447
+ store.updateY(finalHeight, "add");
1448
+ store.recordContentY();
1449
+ } catch (e) {
1450
+ console.warn("Failed to render image", e);
1451
+ }
1452
+ };
1453
+ //#endregion
1454
+ //#region src/renderer/components/table.ts
1455
+ const resolveAutoTable = () => {
1456
+ const autoTableCandidate = jspdf_autotable.default;
1457
+ if (typeof jspdf_autotable.default === "function") return jspdf_autotable.default;
1458
+ if (typeof autoTableCandidate.default === "function") return autoTableCandidate.default;
1459
+ if (typeof autoTableCandidate.autoTable === "function") return autoTableCandidate.autoTable;
1460
+ throw new Error("Could not resolve jspdf-autotable export. Expected a callable export.");
1461
+ };
1462
+ const renderTable = (doc, element, indentLevel, store) => {
1463
+ if (!element.header || !element.rows) return;
1464
+ const options = store.options;
1465
+ const marginLeft = options.page.xmargin + indentLevel * options.page.indent;
1466
+ const head = [element.header.map((h) => h.content || "")];
1467
+ const body = element.rows.map((row) => row.map((cell) => cell.content || ""));
1468
+ const userTableOptions = options.table || {};
1469
+ resolveAutoTable()(doc, {
1470
+ head,
1471
+ body,
1472
+ startY: store.Y,
1473
+ margin: {
1474
+ left: marginLeft,
1475
+ right: options.page.xmargin
1476
+ },
1477
+ ...userTableOptions,
1478
+ didDrawPage: (data) => {
1479
+ if (userTableOptions.didDrawPage) userTableOptions.didDrawPage(data);
1480
+ },
1481
+ didDrawCell: (data) => {
1482
+ if (userTableOptions.didDrawCell) userTableOptions.didDrawCell(data);
1483
+ }
1484
+ });
1485
+ const finalY = doc.lastAutoTable?.finalY;
1486
+ if (typeof finalY === "number") {
1487
+ store.updateY(finalY + options.page.lineSpace, "set");
1488
+ store.updateX(options.page.xpading, "set");
1489
+ store.recordContentY();
1490
+ }
1491
+ };
1492
+ //#endregion
1493
+ //#region src/store/renderStore.ts
1494
+ var RenderStore = class {
1495
+ constructor(options) {
1496
+ this.cursor = {
1497
+ x: 0,
1498
+ y: 0
1499
+ };
1500
+ this.lastContentY_ = 0;
1501
+ this.inlineLock = false;
1502
+ this.options_ = options;
1503
+ this.cursor = {
1504
+ x: options.cursor.x,
1505
+ y: options.cursor.y
1506
+ };
1507
+ this.lastContentY_ = options.cursor.y;
1508
+ }
1509
+ getCursor() {
1510
+ return this.cursor;
1511
+ }
1512
+ setCursor(newCursor) {
1513
+ this.cursor = newCursor;
1514
+ }
1515
+ get options() {
1516
+ return this.options_;
1517
+ }
1518
+ get isInlineLockActive() {
1519
+ return this.inlineLock;
1520
+ }
1521
+ activateInlineLock() {
1522
+ this.inlineLock = true;
1523
+ }
1524
+ deactivateInlineLock() {
1525
+ this.inlineLock = false;
1526
+ }
1527
+ /**
1528
+ * Updates the x pointer of the cursor.
1529
+ * @param value The value to set or add.
1530
+ * @param operation 'set' to assign a new value, 'add' to increment the current value.
1531
+ * @default operation = 'set'
1532
+ */
1533
+ updateX(value, operation = "set") {
1534
+ if (operation === "set") this.cursor.x = value;
1535
+ else if (operation === "add") this.cursor.x += value;
1536
+ }
1537
+ /**
1538
+ * Updates the y pointer of the cursor.
1539
+ * @param value The value to set or add.
1540
+ * @param operation 'set' to assign a new value, 'add' to increment the current value.
1541
+ * @default operation = 'set'
1542
+ */
1543
+ updateY(value, operation = "set") {
1544
+ if (operation === "set") this.cursor.y = value;
1545
+ else if (operation === "add") this.cursor.y += value;
1546
+ }
1547
+ /**
1548
+ * Records a Y position as the bottom of rendered content.
1549
+ * This is useful for container components (like blockquotes) to know
1550
+ * where their actual text content ends, ignoring any trailing margins.
1551
+ * @param specificY Optional Y value to record. Defaults to current cursor Y.
1552
+ */
1553
+ recordContentY(specificY) {
1554
+ this.lastContentY_ = specificY !== void 0 ? specificY : this.cursor.y;
1555
+ }
1556
+ /**
1557
+ * Gets the last Y position recorded as content bottom.
1558
+ */
1559
+ get lastContentY() {
1560
+ return this.lastContentY_;
1561
+ }
1562
+ get X() {
1563
+ return this.cursor.x;
1564
+ }
1565
+ get Y() {
1566
+ return this.cursor.y;
1567
+ }
1568
+ };
1569
+ //#endregion
1570
+ //#region src/utils/options-validation.ts
1571
+ const defaultOptions = {
1572
+ page: {
1573
+ indent: 10,
1574
+ maxContentWidth: 190,
1575
+ maxContentHeight: 277,
1576
+ lineSpace: 1.5,
1577
+ defaultLineHeightFactor: 1.2,
1578
+ defaultFontSize: 12,
1579
+ defaultTitleFontSize: 14,
1580
+ topmargin: 10,
1581
+ xpading: 10,
1582
+ xmargin: 10,
1583
+ format: "a4",
1584
+ orientation: "p"
1585
+ },
1586
+ font: {
1587
+ bold: {
1588
+ name: "helvetica",
1589
+ style: "bold"
1590
+ },
1591
+ regular: {
1592
+ name: "helvetica",
1593
+ style: "normal"
1594
+ },
1595
+ light: {
1596
+ name: "helvetica",
1597
+ style: "light"
1598
+ },
1599
+ code: {
1600
+ name: "courier",
1601
+ style: "normal"
1602
+ }
1603
+ },
1604
+ image: { defaultAlign: "left" }
1605
+ };
1606
+ const validateOptions = (options) => {
1607
+ if (!options) throw new Error("RenderOption is required");
1608
+ const mergedPage = {
1609
+ ...defaultOptions.page,
1610
+ ...options.page
1611
+ };
1612
+ const mergedFont = {
1613
+ ...defaultOptions.font,
1614
+ ...options.font
1615
+ };
1616
+ const mergedImage = {
1617
+ ...defaultOptions.image,
1618
+ ...options.image
1619
+ };
1620
+ if (!mergedPage.maxContentWidth) mergedPage.maxContentWidth = 190;
1621
+ if (!mergedPage.maxContentHeight) mergedPage.maxContentHeight = 277;
1622
+ return {
1623
+ ...options,
1624
+ page: mergedPage,
1625
+ font: mergedFont,
1626
+ image: mergedImage
1627
+ };
1628
+ };
1629
+ //#endregion
1630
+ //#region src/renderer/MdTextRender.ts
1631
+ /**
1632
+ * Renders parsed markdown text into jsPDF document.
1633
+ *
1634
+ * @param doc - The jsPDF document.
1635
+ * @param text - The markdown content to render.
1636
+ * @param options - The render options (fonts, page margins, etc.).
1637
+ */
1638
+ const MdTextRender = async (doc, text, options) => {
1639
+ const validOptions = validateOptions(options);
1640
+ const store = new RenderStore(validOptions);
1641
+ const parsedElements = await MdTextParser(text);
1642
+ await prefetchImages(parsedElements);
1643
+ const renderElement = (element, indentLevel = 0, store, hasRawBullet = false, start = 0, ordered = false) => {
1644
+ const indent = indentLevel * validOptions.page.indent;
1645
+ switch (element.type) {
1646
+ case "heading":
1647
+ renderHeading(doc, element, indent, store, renderElement);
1648
+ break;
1649
+ case "paragraph":
1650
+ renderParagraph(doc, element, indent, store, renderElement);
1651
+ break;
1652
+ case "list":
1653
+ renderList(doc, element, indentLevel, store, renderElement);
1654
+ break;
1655
+ case "list_item":
1656
+ renderListItem(doc, element, indentLevel, store, renderElement, start, ordered);
1657
+ break;
1658
+ case "hr":
1659
+ renderHR(doc, store);
1660
+ break;
1661
+ case "code":
1662
+ renderCodeBlock(doc, element, indentLevel, store);
1663
+ break;
1664
+ case "strong":
1665
+ case "em":
1666
+ case "codespan":
1667
+ renderInlineText(doc, element, indent, store);
1668
+ break;
1669
+ case "link":
1670
+ renderLink(doc, element, indent, store);
1671
+ break;
1672
+ case "blockquote":
1673
+ renderBlockquote(doc, element, indentLevel, store, renderElement);
1674
+ break;
1675
+ case "image":
1676
+ renderImage(doc, element, indentLevel, store);
1677
+ break;
1678
+ case "br": {
1679
+ store.updateX(validOptions.page.xpading, "set");
1680
+ const brHeight = getCharHight(doc) * validOptions.page.defaultLineHeightFactor;
1681
+ if (store.Y + brHeight > validOptions.page.maxContentHeight) HandlePageBreaks(doc, store);
1682
+ else store.updateY(brHeight, "add");
1683
+ store.recordContentY();
1684
+ break;
1685
+ }
1686
+ case "table":
1687
+ renderTable(doc, element, indentLevel, store);
1688
+ break;
1689
+ case "raw":
1690
+ case "text":
1691
+ renderRawItem(doc, element, indentLevel, store, hasRawBullet, renderElement, start, ordered, validOptions.content?.textAlignment === "justify");
1692
+ break;
1693
+ default:
1694
+ console.warn(`Warning: Unsupported element type encountered: ${element.type}.
4
1695
  If you believe this element type should be supported, please create an issue at:
5
1696
  https://github.com/JeelGajera/jspdf-md-renderer/issues
6
- with details of the element and expected behavior. Thanks for helping to improve this library!`);break}};for(let e of a)o(e,0,i);r.endCursorYHandler(i.Y)}});
1697
+ with details of the element and expected behavior. Thanks for helping to improve this library!`);
1698
+ break;
1699
+ }
1700
+ };
1701
+ for (const item of parsedElements) renderElement(item, 0, store);
1702
+ validOptions.endCursorYHandler(store.Y);
1703
+ };
1704
+ //#endregion
1705
+ exports.MdTextParser = MdTextParser;
1706
+ exports.MdTextRender = MdTextRender;
1707
+ });