jspdf-md-renderer 3.5.1 → 4.0.0

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
@@ -55,6 +55,30 @@
55
55
  }) : target, mod));
56
56
  //#endregion
57
57
  jspdf_autotable = __toESM(jspdf_autotable);
58
+ //#region src/enums/mdTokenType.ts
59
+ let MdTokenType = /* @__PURE__ */ function(MdTokenType) {
60
+ MdTokenType["Heading"] = "heading";
61
+ MdTokenType["Paragraph"] = "paragraph";
62
+ MdTokenType["List"] = "list";
63
+ MdTokenType["ListItem"] = "list_item";
64
+ MdTokenType["Blockquote"] = "blockquote";
65
+ MdTokenType["Code"] = "code";
66
+ MdTokenType["CodeSpan"] = "codespan";
67
+ MdTokenType["Table"] = "table";
68
+ MdTokenType["Html"] = "html";
69
+ MdTokenType["Hr"] = "hr";
70
+ MdTokenType["Image"] = "image";
71
+ MdTokenType["Link"] = "link";
72
+ MdTokenType["Strong"] = "strong";
73
+ MdTokenType["Em"] = "em";
74
+ MdTokenType["TableHeader"] = "table_header";
75
+ MdTokenType["TableCell"] = "table_cell";
76
+ MdTokenType["Raw"] = "raw";
77
+ MdTokenType["Text"] = "text";
78
+ MdTokenType["Br"] = "br";
79
+ return MdTokenType;
80
+ }({});
81
+ //#endregion
58
82
  //#region src/parser/imageExtension.ts
59
83
  /**
60
84
  * Internal hash prefix used to encode image attributes in the URL fragment.
@@ -73,8 +97,12 @@
73
97
  const IMAGE_WITH_ATTRS_REGEX = /(!\[[^\]]*\]\()([^)]+)(\))\s*\{([^}]+)\}/g;
74
98
  /**
75
99
  * Regex to extract individual key=value pairs from the attribute block.
100
+ * Supports:
101
+ * - Bare values: width=200
102
+ * - Quoted values: width="200.5" or width='200'
103
+ * - Decimal values: width=200.5
76
104
  */
77
- const ATTR_PAIR_REGEX = /(\w+)\s*=\s*(\w+)/g;
105
+ const ATTR_PAIR_REGEX = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/g;
78
106
  /** Valid alignment values */
79
107
  const VALID_ALIGNMENTS = [
80
108
  "left",
@@ -101,17 +129,17 @@
101
129
  let match;
102
130
  while ((match = ATTR_PAIR_REGEX.exec(attrString)) !== null) {
103
131
  const key = match[1].toLowerCase();
104
- const value = match[2];
132
+ const value = (match[2] ?? match[3] ?? match[4] ?? "").trim();
105
133
  switch (key) {
106
134
  case "width":
107
135
  case "w": {
108
- const num = parseInt(value, 10);
136
+ const num = parseFloat(value);
109
137
  if (!isNaN(num) && num > 0) attrs.width = num;
110
138
  break;
111
139
  }
112
140
  case "height":
113
141
  case "h": {
114
- const num = parseInt(value, 10);
142
+ const num = parseFloat(value);
115
143
  if (!isNaN(num) && num > 0) attrs.height = num;
116
144
  break;
117
145
  }
@@ -165,12 +193,12 @@
165
193
  const [key, value] = pair.split("=");
166
194
  switch (key) {
167
195
  case "w": {
168
- const num = parseInt(value, 10);
196
+ const num = parseFloat(value);
169
197
  if (!isNaN(num) && num > 0) attrs.width = num;
170
198
  break;
171
199
  }
172
200
  case "h": {
173
- const num = parseInt(value, 10);
201
+ const num = parseFloat(value);
174
202
  if (!isNaN(num) && num > 0) attrs.height = num;
175
203
  break;
176
204
  }
@@ -245,6 +273,8 @@
245
273
  ["list_item"]: (token) => ({
246
274
  type: "list_item",
247
275
  content: token.text,
276
+ task: token.task ?? false,
277
+ checked: token.checked ?? false,
248
278
  items: token.tokens ? convertTokens(token.tokens) : []
249
279
  }),
250
280
  ["code"]: (token) => ({
@@ -311,14 +341,39 @@
311
341
  items: token.tokens ? convertTokens(token.tokens) : []
312
342
  }),
313
343
  ["html"]: (token) => {
314
- const rawHtml = String(token.raw ?? token.text ?? "").trim();
315
- if (/^<br\s*\/?>$/i.test(rawHtml)) return {
344
+ const raw = String(token.raw ?? token.text ?? "").trim();
345
+ if (/^<br\s*\/?>$/i.test(raw)) return {
316
346
  type: "br",
317
347
  content: "\n"
318
348
  };
349
+ const inlineTagMap = {
350
+ strong: "strong",
351
+ b: "strong",
352
+ em: "em",
353
+ i: "em"
354
+ };
355
+ const inlineMatch = raw.match(/^<(\w+)[^>]*>(.*?)<\/\1>$/is);
356
+ if (inlineMatch) {
357
+ const tag = inlineMatch[1].toLowerCase();
358
+ const innerText = inlineMatch[2];
359
+ const mappedType = inlineTagMap[tag];
360
+ if (mappedType) return {
361
+ type: mappedType,
362
+ content: innerText
363
+ };
364
+ }
365
+ if (/^<(s|del)[^>]*>(.*?)<\/(s|del)>$/is.test(raw)) return {
366
+ type: "raw",
367
+ content: raw.match(/>([^<]+)</)?.[1] ?? raw
368
+ };
369
+ const strippedText = raw.replace(/<[^>]+>/g, "").trim();
370
+ if (strippedText) return {
371
+ type: "raw",
372
+ content: strippedText
373
+ };
319
374
  return {
320
375
  type: "raw",
321
- content: token.raw ?? token.text ?? ""
376
+ content: ""
322
377
  };
323
378
  },
324
379
  ["br"]: () => ({
@@ -327,14 +382,6 @@
327
382
  })
328
383
  };
329
384
  //#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
385
  //#region src/utils/handlePageBreak.ts
339
386
  const HandlePageBreaks = (doc, store) => {
340
387
  if (typeof store.options.pageBreakHandler === "function") store.options.pageBreakHandler(doc);
@@ -342,6 +389,35 @@
342
389
  store.updateY(store.options.page.topmargin);
343
390
  store.updateX(store.options.page.xpading);
344
391
  };
392
+ /**
393
+ * Returns true if adding `height` to the current Y would exceed the page.
394
+ */
395
+ const willOverflow = (store, height) => {
396
+ return store.Y + height > store.options.page.maxContentHeight;
397
+ };
398
+ /**
399
+ * Checks if we will overflow, and if so, breaks the page first.
400
+ * Returns true if a page break was performed.
401
+ */
402
+ const breakIfOverflow = (doc, store, height) => {
403
+ if (willOverflow(store, height)) {
404
+ HandlePageBreaks(doc, store);
405
+ return true;
406
+ }
407
+ return false;
408
+ };
409
+ /**
410
+ * Ensures there is at least `minHeight` space remaining on the page.
411
+ * If not, breaks to next page.
412
+ */
413
+ const ensureSpace = (doc, store, minHeight) => {
414
+ if (store.options.page.maxContentHeight - store.Y < minHeight) HandlePageBreaks(doc, store);
415
+ };
416
+ //#endregion
417
+ //#region src/utils/doc-helpers.ts
418
+ const getCharHight = (doc) => {
419
+ return doc.getFontSize() / doc.internal.scaleFactor;
420
+ };
345
421
  //#endregion
346
422
  //#region src/utils/image-utils.ts
347
423
  /**
@@ -509,115 +585,127 @@
509
585
  }
510
586
  };
511
587
  //#endregion
512
- //#region src/utils/justifiedTextRenderer.ts
588
+ //#region src/layout/wordSplitter.ts
513
589
  /**
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
590
+ * Maps a ParsedElement type string to a TextStyle.
523
591
  */
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
- }
592
+ const resolveStyle = (type, parentStyle) => {
593
+ switch (type) {
594
+ case "strong": return parentStyle === "italic" ? "bolditalic" : "bold";
595
+ case "em": return parentStyle === "bold" ? "bolditalic" : "italic";
596
+ case "codespan": return "codespan";
597
+ default: return parentStyle ?? "normal";
570
598
  }
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;
599
+ };
600
+ /**
601
+ * Measures the width of `text` rendered with `style`, including jsPDF charSpace.
602
+ */
603
+ const measureStyledWidth = (doc, text, style, store) => {
604
+ const savedFont = doc.getFont();
605
+ const savedSize = doc.getFontSize();
606
+ applyStyleToDoc(doc, style, store);
607
+ const charSpace = doc.getCharSpace?.() ?? 0;
608
+ const width = doc.getTextWidth(text) + text.length * charSpace;
609
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
610
+ doc.setFontSize(savedSize);
611
+ return width;
612
+ };
613
+ /**
614
+ * Applies a TextStyle to the jsPDF document.
615
+ */
616
+ const applyStyleToDoc = (doc, style, store) => {
617
+ const curFont = doc.getFont().fontName;
618
+ const curSize = doc.getFontSize();
619
+ const boldFont = store.options.font.bold?.name || curFont;
620
+ const regularFont = store.options.font.regular?.name || curFont;
621
+ const codeFont = store.options.font.code || {
622
+ name: "courier",
623
+ style: "normal"
624
+ };
625
+ switch (style) {
626
+ case "bold":
627
+ doc.setFont(boldFont, store.options.font.bold?.style || "bold");
628
+ break;
629
+ case "italic":
630
+ doc.setFont(regularFont, "italic");
631
+ break;
632
+ case "bolditalic":
633
+ doc.setFont(boldFont, "bolditalic");
634
+ break;
635
+ case "codespan":
636
+ doc.setFont(codeFont.name, codeFont.style);
637
+ doc.setFontSize(curSize * (store.options.codespan?.fontSizeScale ?? .88));
638
+ break;
639
+ default:
640
+ doc.setFont(regularFont, "normal");
641
+ break;
586
642
  }
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";
643
+ };
644
+ /**
645
+ * Flattens a ParsedElement tree into a flat array of StyledWordInfo.
646
+ * This is the single source of truth for converting AST → layout words.
647
+ */
648
+ const flattenToWords = (doc, elements, store, parentStyle = "normal", isLink = false, href) => {
649
+ const result = [];
650
+ for (const el of elements) {
651
+ const style = resolveStyle(el.type, parentStyle);
652
+ const elIsLink = el.type === "link" || isLink;
653
+ const elHref = el.href || href;
654
+ if (el.type === "br") {
655
+ result.push({
656
+ text: "",
657
+ width: 0,
658
+ style,
659
+ isBr: true
660
+ });
661
+ continue;
600
662
  }
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");
663
+ if (el.type === "image") {
664
+ const { finalWidth, finalHeight } = calculateImageDimensions(doc, el, store.options.page.maxContentWidth, store.options.page.maxContentHeight - store.options.page.topmargin, store.options.page.unit || "mm");
665
+ result.push({
666
+ text: "",
667
+ width: finalWidth,
668
+ style,
669
+ isLink: elIsLink,
670
+ href: elHref,
671
+ isImage: true,
672
+ imageElement: el,
673
+ imageHeight: finalHeight
674
+ });
675
+ continue;
676
+ }
677
+ if (el.items && el.items.length > 0) {
678
+ result.push(...flattenToWords(doc, el.items, store, style, elIsLink, elHref));
679
+ continue;
680
+ }
681
+ const text = el.content || el.text || "";
682
+ if (!text) continue;
683
+ if (/^\s/.test(text) && result.length > 0) result[result.length - 1].hasTrailingSpace = true;
684
+ if (style === "codespan") {
685
+ const trimmed = text.trim();
686
+ if (trimmed) result.push({
687
+ text: trimmed,
688
+ width: measureStyledWidth(doc, trimmed, style, store),
689
+ style,
690
+ isLink: elIsLink,
691
+ href: elHref,
692
+ linkColor: elIsLink ? store.options.link?.linkColor || [
693
+ 0,
694
+ 0,
695
+ 255
696
+ ] : void 0,
697
+ hasTrailingSpace: /\s$/.test(text)
698
+ });
699
+ continue;
700
+ }
701
+ const lines = text.split("\n");
702
+ for (let li = 0; li < lines.length; li++) {
703
+ const words = lines[li].trim().split(/[ \t\r\v\f]+/).filter(Boolean);
704
+ for (let wi = 0; wi < words.length; wi++) {
705
+ const isLastInLine = wi === words.length - 1;
618
706
  result.push({
619
- text: "",
620
- width: finalWidth,
707
+ text: words[wi],
708
+ width: measureStyledWidth(doc, words[wi], style, store),
621
709
  style,
622
710
  isLink: elIsLink,
623
711
  href: elHref,
@@ -626,338 +714,253 @@
626
714
  0,
627
715
  255
628
716
  ] : void 0,
629
- isImage: true,
630
- imageElement: el,
631
- imageHeight: finalHeight
717
+ hasTrailingSpace: !isLastInLine || /[ \t]$/.test(lines[li])
632
718
  });
633
- } else if (el.type === "br") result.push({
719
+ }
720
+ if (li < lines.length - 1) result.push({
634
721
  text: "",
635
722
  width: 0,
636
723
  style,
637
724
  isBr: true
638
725
  });
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
726
  }
689
- return result;
690
727
  }
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
- }
728
+ return result;
729
+ };
730
+ //#endregion
731
+ //#region src/layout/lineBreaker.ts
732
+ const splitOversizedWord = (doc, word, maxWidth, store) => {
733
+ if (!word.text || word.width <= maxWidth || word.isImage) return [word];
734
+ const chunks = [];
735
+ let remaining = word.text;
736
+ while (remaining.length > 0) {
737
+ let chunk = "";
738
+ for (const ch of remaining) {
739
+ const candidate = chunk + ch;
740
+ const candidateWidth = measureStyledWidth(doc, candidate, word.style, store);
741
+ if (candidateWidth > maxWidth && chunk.length > 0) break;
742
+ chunk = candidate;
743
+ if (candidateWidth >= maxWidth) break;
736
744
  }
745
+ if (!chunk) chunk = remaining.charAt(0);
746
+ chunks.push({
747
+ ...word,
748
+ text: chunk,
749
+ width: measureStyledWidth(doc, chunk, word.style, store),
750
+ hasTrailingSpace: false
751
+ });
752
+ remaining = remaining.slice(chunk.length);
753
+ }
754
+ if (chunks.length > 0) chunks[chunks.length - 1].hasTrailingSpace = word.hasTrailingSpace;
755
+ return chunks;
756
+ };
757
+ const breakIntoLines = (doc, words, maxWidth, store) => {
758
+ const normalizedWords = [];
759
+ for (const word of words) normalizedWords.push(...splitOversizedWord(doc, word, maxWidth, store));
760
+ const lines = [];
761
+ let currentLine = [];
762
+ let currentTextWidth = 0;
763
+ let currentLineWidth = 0;
764
+ const baseLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
765
+ let currentLineHeight = baseLineHeight;
766
+ const spaceWidth = doc.getTextWidth(" ");
767
+ const pushLine = (isLast) => {
737
768
  if (currentLine.length > 0) lines.push({
738
769
  words: currentLine,
739
770
  totalTextWidth: currentTextWidth,
740
- isLastLine: true,
771
+ isLastLine: isLast,
741
772
  lineHeight: currentLineHeight
742
773
  });
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" });
774
+ };
775
+ for (const word of normalizedWords) {
776
+ if (word.isBr) {
777
+ pushLine(true);
778
+ currentLine = [];
779
+ currentTextWidth = 0;
780
+ currentLineWidth = 0;
781
+ currentLineHeight = baseLineHeight;
782
+ continue;
789
783
  }
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 });
784
+ const neededWidth = (currentLine[currentLine.length - 1]?.hasTrailingSpace ? spaceWidth : 0) + word.width;
785
+ const itemHeight = word.isImage && word.imageHeight ? word.imageHeight : baseLineHeight;
786
+ if (currentLineWidth + neededWidth > maxWidth && currentLine.length > 0) {
787
+ pushLine(false);
788
+ currentLine = [word];
789
+ currentTextWidth = word.width;
790
+ currentLineWidth = word.width;
791
+ currentLineHeight = itemHeight;
792
+ } else {
793
+ currentLine.push(word);
794
+ currentTextWidth += word.width;
795
+ currentLineWidth += neededWidth;
796
+ currentLineHeight = Math.max(currentLineHeight, itemHeight);
793
797
  }
794
- doc.setFont(savedFont.fontName, savedFont.fontStyle);
795
- doc.setFontSize(savedSize);
796
- doc.setTextColor(savedColor);
797
798
  }
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
- }
799
+ pushLine(true);
800
+ return lines;
801
+ };
802
+ //#endregion
803
+ //#region src/layout/lineRenderer.ts
804
+ const renderLine = (doc, line, x, y, maxWidth, store, alignment = "left") => {
805
+ const { words, totalTextWidth, isLastLine } = line;
806
+ if (words.length === 0) return;
807
+ const normalSpaceWidth = doc.getTextWidth(" ");
808
+ let lineWidthWithSpaces = totalTextWidth;
809
+ let expandableSpaces = 0;
810
+ for (let i = 0; i < words.length - 1; i++) if (words[i].hasTrailingSpace) {
811
+ lineWidthWithSpaces += normalSpaceWidth;
812
+ expandableSpaces++;
837
813
  }
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
- }
814
+ let startX = x;
815
+ let wordSpacing = normalSpaceWidth;
816
+ switch (alignment) {
817
+ case "right":
818
+ startX = x + maxWidth - lineWidthWithSpaces;
819
+ break;
820
+ case "center":
821
+ startX = x + (maxWidth - lineWidthWithSpaces) / 2;
822
+ break;
823
+ case "justify":
824
+ if (!isLastLine && expandableSpaces > 0) wordSpacing = (maxWidth - totalTextWidth) / expandableSpaces;
825
+ break;
873
826
  }
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);
827
+ const textLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
828
+ let currentX = startX;
829
+ for (let i = 0; i < words.length; i++) {
830
+ const word = words[i];
831
+ const elementHeight = word.isImage && word.imageHeight ? word.imageHeight : textLineHeight;
832
+ const drawY = word.isImage ? y : y + (line.lineHeight - elementHeight);
833
+ renderSingleWord(doc, word, currentX, drawY, store);
834
+ currentX += word.width;
835
+ if (i < words.length - 1 && word.hasTrailingSpace) currentX += wordSpacing;
879
836
  }
880
837
  };
838
+ const renderSingleWord = (doc, word, x, y, store) => {
839
+ const savedFont = doc.getFont();
840
+ const savedSize = doc.getFontSize();
841
+ const savedColor = doc.getTextColor();
842
+ applyStyleToDoc(doc, word.style, store);
843
+ if (word.isLink && word.linkColor) doc.setTextColor(...word.linkColor);
844
+ if (word.isImage && word.imageElement?.data) renderInlineImage(doc, word, x, y);
845
+ else {
846
+ if (word.style === "codespan") renderCodespanBackground(doc, word, x, y, store);
847
+ doc.text(word.text, x, y, { baseline: "top" });
848
+ }
849
+ if (word.isLink && word.href) {
850
+ const h = word.isImage && word.imageHeight ? word.imageHeight : doc.getTextDimensions("H").h;
851
+ doc.link(x, y, word.width, h, { url: word.href });
852
+ }
853
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
854
+ doc.setFontSize(savedSize);
855
+ doc.setTextColor(savedColor);
856
+ };
857
+ const renderCodespanBackground = (doc, word, x, y, store) => {
858
+ const opts = store.options.codespan ?? {};
859
+ if (opts.showBackground === false) return;
860
+ const bg = opts.backgroundColor ?? "#EEEEEE";
861
+ const pad = opts.padding ?? .8;
862
+ const h = doc.getTextDimensions("H").h;
863
+ doc.setFillColor(bg);
864
+ doc.rect(x - pad, y - pad, word.width + pad * 2, h + pad * 2, "F");
865
+ doc.setFillColor("#000000");
866
+ };
867
+ const renderInlineImage = (doc, word, x, y) => {
868
+ const el = word.imageElement;
869
+ let fmt = "JPEG";
870
+ if (el.data.startsWith("data:image/png")) fmt = "PNG";
871
+ else if (el.data.startsWith("data:image/webp")) fmt = "WEBP";
872
+ else if (el.data.startsWith("data:image/gif")) fmt = "GIF";
873
+ if (word.width > 0 && (word.imageHeight || 0) > 0) doc.addImage(el.data, fmt, x, y, word.width, word.imageHeight);
874
+ };
881
875
  //#endregion
882
- //#region src/renderer/components/heading.ts
876
+ //#region src/layout/layoutEngine.ts
883
877
  /**
884
- * Renders heading elements.
878
+ * THE single entry point for rendering any mixed inline content.
879
+ * All paragraph, heading, list item, and blockquote text must go through here.
880
+ *
881
+ * Handles: word splitting, line breaking, page breaks, styled rendering.
882
+ * Returns the final Y position after rendering.
885
883
  */
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");
884
+ const renderInlineContent = (doc, elements, x, y, maxWidth, store, opts = {}) => {
885
+ const alignment = opts.alignment ?? store.options.content?.textAlignment ?? "left";
886
+ const trimLastLine = opts.trimLastLine ?? false;
887
+ const words = flattenToWords(doc, elements, store);
888
+ if (words.length === 0) return y;
889
+ const lines = breakIntoLines(doc, words, maxWidth, store);
890
+ let currentY = y;
891
+ for (let i = 0; i < lines.length; i++) {
892
+ const line = lines[i];
893
+ if (currentY + line.lineHeight > store.options.page.maxContentHeight) {
894
+ HandlePageBreaks(doc, store);
895
+ currentY = store.Y;
896
+ }
897
+ renderLine(doc, line, x, currentY, maxWidth, store, alignment);
898
+ const yAdvance = i === lines.length - 1 && trimLastLine && !line.words.some((w) => w.isImage && w.imageHeight) ? getCharHight(doc) : line.lineHeight;
899
+ store.recordContentY(currentY + yAdvance);
900
+ currentY += yAdvance;
901
+ store.updateY(yAdvance, "add");
903
902
  }
904
- doc.setFontSize(store.options.page.defaultFontSize);
905
- store.updateX(store.options.page.xpading, "set");
903
+ const lastLine = lines[lines.length - 1];
904
+ if (lastLine) {
905
+ let spaceCount = 0;
906
+ for (let i = 0; i < lastLine.words.length - 1; i++) if (lastLine.words[i].hasTrailingSpace) spaceCount++;
907
+ const lastLineW = lastLine.totalTextWidth + spaceCount * doc.getTextWidth(" ");
908
+ store.updateX(x + lastLineW, "set");
909
+ }
910
+ return currentY;
911
+ };
912
+ /**
913
+ * Render a single string of plain (unstyled) text with wrapping and page breaks.
914
+ * Use this for simple text in list items and raw items where there are no inline styles.
915
+ */
916
+ const renderPlainText = (doc, text, x, y, maxWidth, store, opts = {}) => {
917
+ return renderInlineContent(doc, [{
918
+ type: "text",
919
+ content: text
920
+ }], x, y, maxWidth, store, opts);
906
921
  };
907
922
  //#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
- }
923
+ //#region src/renderer/components/heading.ts
924
+ const renderHeading = (doc, element, indent, store) => {
925
+ const savedColor = doc.getTextColor();
926
+ const headingKey = `h${element?.depth ?? 1}`;
927
+ const fontSize = store.options.heading?.[headingKey] ?? store.options.page.defaultFontSize;
928
+ const headingColor = store.options.heading?.[`${headingKey}Color`] ?? store.options.heading?.color ?? "#000000";
929
+ const savedSize = doc.getFontSize();
930
+ doc.setFontSize(fontSize);
931
+ doc.setFont(store.options.font.bold.name, store.options.font.bold.style || "bold");
932
+ doc.setTextColor(headingColor);
933
+ breakIfOverflow(doc, store, getCharHight(doc) * 1.8);
934
+ const maxWidth = store.options.page.maxContentWidth - indent;
935
+ if (element.items && element.items.length > 0) renderInlineContent(doc, element.items, store.X + indent, store.Y, maxWidth, store, {
936
+ alignment: "left",
937
+ trimLastLine: true
938
+ });
939
+ else renderPlainText(doc, element?.content ?? "", store.X + indent, store.Y, maxWidth, store, {
940
+ alignment: "left",
941
+ trimLastLine: true
942
+ });
943
+ const bottomSpacing = store.options.heading?.bottomSpacing ?? store.options.spacing?.afterHeading ?? 2;
944
+ store.updateY(bottomSpacing, "add");
945
+ doc.setFontSize(savedSize);
946
+ doc.setFont(store.options.font.regular.name, store.options.font.regular.style);
947
+ doc.setTextColor(savedColor);
948
+ store.updateX(store.options.page.xpading, "set");
944
949
  };
945
950
  //#endregion
946
951
  //#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
952
  const renderParagraph = (doc, element, indent, store, parentElementRenderer) => {
953
- store.activateInlineLock();
953
+ const indentLevel = indent / store.options.page.indent;
954
+ const savedColor = doc.getTextColor();
954
955
  doc.setFontSize(store.options.page.defaultFontSize);
956
+ doc.setFont(store.options.font.regular.name, store.options.font.regular.style);
957
+ doc.setTextColor(store.options.paragraph?.color ?? "#000000");
955
958
  const maxWidth = store.options.page.maxContentWidth - indent;
956
- if (element?.items && element?.items.length > 0) {
959
+ if (element.items && element.items.length > 0) {
957
960
  if (element.items.length === 1 && element.items[0].type === "image") {
958
- parentElementRenderer(element.items[0], indent, store, false);
961
+ parentElementRenderer(element.items[0], indentLevel, store, false);
959
962
  store.updateX(store.options.page.xpading);
960
- store.deactivateInlineLock();
963
+ doc.setTextColor(savedColor);
961
964
  return;
962
965
  }
963
966
  const inlineTypes = [
@@ -971,35 +974,38 @@
971
974
  ];
972
975
  if (element.items.some((item) => !inlineTypes.includes(item.type))) {
973
976
  const inlineBuffer = [];
974
- const flushInlineBuffer = () => {
977
+ const flush = () => {
975
978
  if (inlineBuffer.length > 0) {
976
- JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, store.X + indent, store.Y, maxWidth, store);
979
+ renderInlineContent(doc, inlineBuffer, store.X + indent, store.Y, maxWidth, store, { trimLastLine: true });
977
980
  inlineBuffer.length = 0;
978
981
  }
979
982
  };
980
983
  for (const item of element.items) if (inlineTypes.includes(item.type)) inlineBuffer.push(item);
981
984
  else {
982
- flushInlineBuffer();
983
- parentElementRenderer(item, indent, store, false);
985
+ flush();
986
+ parentElementRenderer(item, indentLevel, store, false);
984
987
  }
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
- }
988
+ flush();
989
+ } else renderInlineContent(doc, element.items, store.X + indent, store.Y, maxWidth, store, { trimLastLine: true });
990
+ } else if (element.content?.trim()) renderInlineContent(doc, [{
991
+ type: "text",
992
+ content: element.content
993
+ }], store.X + indent, store.Y, maxWidth, store, { trimLastLine: true });
994
+ const bottomSpacing = store.options.paragraph?.bottomSpacing ?? store.options.spacing?.afterParagraph ?? store.options.page.lineSpace;
995
+ store.updateY(bottomSpacing, "add");
992
996
  store.updateX(store.options.page.xpading);
993
- store.deactivateInlineLock();
997
+ doc.setTextColor(savedColor);
994
998
  };
995
999
  //#endregion
996
1000
  //#region src/renderer/components/list.ts
997
1001
  const renderList = (doc, element, indentLevel, store, parentElementRenderer) => {
998
1002
  doc.setFontSize(store.options.page.defaultFontSize);
999
1003
  for (const [i, point] of element?.items?.entries() ?? []) {
1000
- const _start = element.ordered ? (element.start ?? 0) + i : element.start;
1004
+ const _start = element.ordered ? (element.start ?? 1) + i : void 0;
1001
1005
  parentElementRenderer(point, indentLevel + 1, store, true, _start, element.ordered);
1006
+ if (i < (element.items?.length ?? 0) - 1) store.updateY(store.options.spacing?.betweenListItems ?? 0, "add");
1002
1007
  }
1008
+ store.updateY(store.options.spacing?.afterList ?? 3, "add");
1003
1009
  };
1004
1010
  //#endregion
1005
1011
  //#region src/renderer/components/listItem.ts
@@ -1007,22 +1013,46 @@
1007
1013
  * Render a single list item, including bullets/numbering, inline text, and any nested lists.
1008
1014
  */
1009
1015
  const renderListItem = (doc, element, indentLevel, store, parentElementRenderer, start, ordered) => {
1010
- if (store.Y + getCharHight(doc) >= store.options.page.maxContentHeight) HandlePageBreaks(doc, store);
1016
+ breakIfOverflow(doc, store, getCharHight(doc));
1011
1017
  const options = store.options;
1012
- const baseIndent = indentLevel * options.page.indent;
1013
- const bullet = ordered ? `${start}. ` : "• ";
1018
+ const listOpts = store.options.list ?? {};
1019
+ const baseIndent = indentLevel * (listOpts.indentSize ?? options.page.indent);
1020
+ const bullet = ordered ? `${start}. ` : listOpts.bulletChar ?? "• ";
1014
1021
  const xLeft = options.page.xpading;
1015
1022
  store.updateX(xLeft, "set");
1016
1023
  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;
1024
+ let contentX = xLeft + baseIndent;
1025
+ let bulletWidth;
1026
+ if (element.task) {
1027
+ const cbSize = doc.getFontSize() * .5;
1028
+ const cbX = xLeft + baseIndent;
1029
+ const cbY = store.Y + (getCharHight(doc) - cbSize) / 2;
1030
+ doc.setDrawColor("#555555");
1031
+ doc.setLineWidth(.4);
1032
+ doc.rect(cbX, cbY, cbSize, cbSize);
1033
+ if (element.checked) {
1034
+ doc.setDrawColor("#2B6CB0");
1035
+ doc.setLineWidth(.5);
1036
+ const pad = cbSize * .2;
1037
+ doc.line(cbX + pad, cbY + cbSize * .55, cbX + cbSize * .4, cbY + cbSize - pad);
1038
+ doc.line(cbX + cbSize * .4, cbY + cbSize - pad, cbX + cbSize - pad, cbY + pad);
1039
+ doc.setDrawColor("#000000");
1040
+ }
1041
+ doc.setLineWidth(.1);
1042
+ bulletWidth = cbSize + 2;
1043
+ } else {
1044
+ doc.text(bullet, xLeft + baseIndent, store.Y, { baseline: "top" });
1045
+ bulletWidth = doc.getTextWidth(bullet);
1046
+ }
1047
+ contentX += bulletWidth;
1020
1048
  const textMaxWidth = options.page.maxContentWidth - baseIndent - bulletWidth;
1049
+ const originalTextColor = doc.getTextColor();
1050
+ if (element.checked) doc.setTextColor(150, 150, 150);
1021
1051
  if (element.items && element.items.length > 0) {
1022
1052
  const inlineBuffer = [];
1023
1053
  const flushInlineBuffer = () => {
1024
1054
  if (inlineBuffer.length > 0) {
1025
- JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, contentX, store.Y, textMaxWidth, store);
1055
+ renderInlineContent(doc, inlineBuffer, contentX, store.Y, textMaxWidth, store);
1026
1056
  inlineBuffer.length = 0;
1027
1057
  store.updateX(xLeft, "set");
1028
1058
  }
@@ -1035,10 +1065,8 @@
1035
1065
  parentElementRenderer(subItem, indentLevel, store, true, start, ordered);
1036
1066
  } else inlineBuffer.push(subItem);
1037
1067
  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
- }
1068
+ } else if (element.content) renderPlainText(doc, element.content, contentX, store.Y, textMaxWidth, store);
1069
+ doc.setTextColor(originalTextColor);
1042
1070
  };
1043
1071
  //#endregion
1044
1072
  //#region src/renderer/components/rawItem.ts
@@ -1047,19 +1075,18 @@
1047
1075
  else {
1048
1076
  const options = store.options;
1049
1077
  const indent = indentLevel * options.page.indent;
1050
- const bullet = hasRawBullet ? ordered ? `${start}. ` : "• " : "";
1078
+ const listOpts = store.options.list ?? {};
1079
+ const bullet = hasRawBullet ? ordered ? `${start}. ` : listOpts.bulletChar ?? "• " : "";
1051
1080
  const content = element.content || "";
1052
1081
  const xLeft = options.page.xpading;
1053
1082
  if (!content && !bullet) return;
1054
1083
  if (!content.trim() && !bullet) {
1055
1084
  const newlines = (content.match(/\n/g) || []).length;
1056
1085
  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
- }
1086
+ const addedHeight = (newlines - 1) * (getCharHight(doc) * options.page.defaultLineHeightFactor);
1087
+ breakIfOverflow(doc, store, addedHeight);
1088
+ store.updateY(addedHeight, "add");
1089
+ store.recordContentY(store.Y);
1063
1090
  }
1064
1091
  return;
1065
1092
  }
@@ -1069,10 +1096,10 @@
1069
1096
  const textMaxWidth = options.page.maxContentWidth - indent - bulletWidth;
1070
1097
  doc.setFont(options.font.regular.name, options.font.regular.style);
1071
1098
  doc.text(bullet, xLeft + indent, store.Y, { baseline: "top" });
1072
- TextRenderer.renderText(doc, content, store, xLeft + indent + bulletWidth, store.Y, textMaxWidth, justify);
1099
+ renderPlainText(doc, content, xLeft + indent + bulletWidth, store.Y, textMaxWidth, store, { alignment: justify ? "justify" : "left" });
1073
1100
  } else {
1074
1101
  const textMaxWidth = options.page.maxContentWidth - indent;
1075
- TextRenderer.renderText(doc, content, store, xLeft + indent, store.Y, textMaxWidth, justify);
1102
+ renderPlainText(doc, content, xLeft + indent, store.Y, textMaxWidth, store, { alignment: justify ? "justify" : "left" });
1076
1103
  }
1077
1104
  store.updateX(xLeft, "set");
1078
1105
  }
@@ -1080,6 +1107,9 @@
1080
1107
  //#endregion
1081
1108
  //#region src/renderer/components/hr.ts
1082
1109
  const renderHR = (doc, store) => {
1110
+ const savedDrawColor = doc.getDrawColor();
1111
+ const savedLineWidth = doc.getLineWidth();
1112
+ breakIfOverflow(doc, store, getCharHight(doc));
1083
1113
  const pageWidth = doc.internal.pageSize.getWidth();
1084
1114
  doc.setLineDashPattern([1, 1], 0);
1085
1115
  doc.setLineWidth(.1);
@@ -1087,21 +1117,30 @@
1087
1117
  doc.setLineWidth(.1);
1088
1118
  doc.setLineDashPattern([], 0);
1089
1119
  store.updateY(getCharHight(doc), "add");
1120
+ store.updateY(store.options.spacing?.afterHR ?? 2, "add");
1121
+ doc.setDrawColor(savedDrawColor);
1122
+ doc.setLineWidth(savedLineWidth);
1090
1123
  };
1091
1124
  //#endregion
1092
1125
  //#region src/renderer/components/code.ts
1093
1126
  const renderCodeBlock = (doc, element, indentLevel, store) => {
1094
1127
  const savedFont = doc.getFont();
1095
1128
  const savedFontSize = doc.getFontSize();
1129
+ const savedTextColor = doc.getTextColor();
1130
+ const savedDrawColor = doc.getDrawColor();
1131
+ const savedFillColor = doc.getFillColor();
1096
1132
  const codeFont = store.options.font.code || {
1097
1133
  name: "courier",
1098
1134
  style: "normal"
1099
1135
  };
1100
1136
  doc.setFont(codeFont.name, codeFont.style);
1101
- const codeFontSize = store.options.page.defaultFontSize * .9;
1137
+ const codeOpts = store.options.codeBlock ?? {};
1138
+ const codeFontSizeScale = codeOpts.fontSizeScale ?? .9;
1139
+ const codeFontSize = store.options.page.defaultFontSize * codeFontSizeScale;
1102
1140
  doc.setFontSize(codeFontSize);
1103
1141
  const indent = indentLevel * store.options.page.indent;
1104
- const maxWidth = store.options.page.maxContentWidth - indent - 8;
1142
+ const padding = codeOpts.padding ?? 4;
1143
+ const maxWidth = store.options.page.maxContentWidth - indent - padding * 2;
1105
1144
  const lineHeightFactor = doc.getLineHeightFactor();
1106
1145
  const lineHeight = codeFontSize / doc.internal.scaleFactor * lineHeightFactor;
1107
1146
  const content = (element.code ?? "").replace(/[\r\n\s]+$/, "");
@@ -1117,9 +1156,9 @@
1117
1156
  doc.setFontSize(savedFontSize);
1118
1157
  return;
1119
1158
  }
1120
- const padding = 4;
1121
- const bgColor = "#EEEEEE";
1122
- const drawColor = "#DDDDDD";
1159
+ const bgColor = codeOpts.backgroundColor ?? "#F6F8FA";
1160
+ const drawColor = codeOpts.borderColor ?? "#E1E4E8";
1161
+ const radius = codeOpts.borderRadius ?? 2;
1123
1162
  let currentLineIndex = 0;
1124
1163
  while (currentLineIndex < lines.length) {
1125
1164
  const availableHeight = store.options.page.maxContentHeight - store.Y;
@@ -1138,20 +1177,22 @@
1138
1177
  if (isFirstChunk) store.updateY(padding, "add");
1139
1178
  doc.setFillColor(bgColor);
1140
1179
  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) {
1180
+ doc.roundedRect(store.X, store.Y - padding, store.options.page.maxContentWidth, textBlockHeight + (isFirstChunk ? padding : 0) + (isLastChunk ? padding : 0), radius, radius, "FD");
1181
+ if (isFirstChunk && element.lang && codeOpts.showLanguageLabel !== false) {
1143
1182
  const savedCodeFontSize = doc.getFontSize();
1144
1183
  doc.setFontSize(10);
1145
- doc.setTextColor("#666666");
1184
+ doc.setTextColor(codeOpts.labelColor ?? "#666666");
1146
1185
  doc.text(element.lang, store.X + store.options.page.maxContentWidth - doc.getTextWidth(element.lang) - 4, store.Y, { baseline: "top" });
1147
1186
  doc.setFontSize(savedCodeFontSize);
1148
- doc.setTextColor("#000000");
1187
+ doc.setTextColor(savedTextColor);
1149
1188
  }
1150
1189
  let yPos = store.Y;
1190
+ doc.setTextColor(codeOpts.textColor ?? "#000000");
1151
1191
  for (const line of linesToRender) {
1152
1192
  doc.text(line, store.X + 4, yPos, { baseline: "top" });
1153
1193
  yPos += lineHeight;
1154
1194
  }
1195
+ doc.setTextColor(savedTextColor);
1155
1196
  store.updateY(textBlockHeight, "add");
1156
1197
  store.recordContentY(store.Y + (isLastChunk ? padding : 0));
1157
1198
  if (isLastChunk) store.updateY(padding, "add");
@@ -1160,199 +1201,17 @@
1160
1201
  }
1161
1202
  doc.setFont(savedFont.fontName, savedFont.fontStyle);
1162
1203
  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);
1204
+ doc.setTextColor(savedTextColor);
1205
+ doc.setDrawColor(savedDrawColor);
1206
+ doc.setFillColor(savedFillColor);
1207
+ store.updateY(store.options.spacing?.afterCodeBlock ?? 3, "add");
1351
1208
  };
1352
1209
  //#endregion
1353
1210
  //#region src/renderer/components/blockquote.ts
1354
1211
  const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
1355
1212
  const options = store.options;
1213
+ const savedDrawColor = doc.getDrawColor();
1214
+ const savedLineWidth = doc.getLineWidth();
1356
1215
  const blockquoteIndent = indentLevel + 1;
1357
1216
  const currentX = store.X + indentLevel * options.page.indent;
1358
1217
  const currentY = store.Y;
@@ -1362,10 +1221,13 @@
1362
1221
  if (element.items && element.items.length > 0) element.items.forEach((item) => {
1363
1222
  renderElement(item, blockquoteIndent, store);
1364
1223
  });
1365
- const endY = store.Y;
1224
+ const endY = store.lastContentY || store.Y;
1366
1225
  const endPage = doc.internal.getCurrentPageInfo().pageNumber;
1367
- doc.setDrawColor(100);
1368
- doc.setLineWidth(1);
1226
+ const bqOpts = store.options.blockquote ?? {};
1227
+ const barColor = bqOpts.barColor ?? "#AAAAAA";
1228
+ const barWidth = bqOpts.barWidth ?? 1;
1229
+ doc.setDrawColor(barColor);
1230
+ doc.setLineWidth(barWidth);
1369
1231
  for (let p = startPage; p <= endPage; p++) {
1370
1232
  doc.setPage(p);
1371
1233
  const isStart = p === startPage;
@@ -1376,6 +1238,10 @@
1376
1238
  }
1377
1239
  store.recordContentY();
1378
1240
  doc.setPage(endPage);
1241
+ const bqBottomSpacing = bqOpts.bottomSpacing ?? options.spacing?.afterBlockquote ?? options.page.lineSpace;
1242
+ store.updateY(bqBottomSpacing, "add");
1243
+ doc.setDrawColor(savedDrawColor);
1244
+ doc.setLineWidth(savedLineWidth);
1379
1245
  };
1380
1246
  //#endregion
1381
1247
  //#region src/renderer/components/image.ts
@@ -1445,9 +1311,10 @@
1445
1311
  const imgFormat = detectImageFormat(element);
1446
1312
  if (finalWidth > 0 && finalHeight > 0) doc.addImage(element.data, imgFormat, drawX, currentY, finalWidth, finalHeight);
1447
1313
  store.updateY(finalHeight, "add");
1314
+ store.updateY(store.options.spacing?.afterImage ?? 2, "add");
1448
1315
  store.recordContentY();
1449
1316
  } catch (e) {
1450
- console.warn("Failed to render image", e);
1317
+ console.warn("[jspdf-md-renderer] Failed to render image", e);
1451
1318
  }
1452
1319
  };
1453
1320
  //#endregion
@@ -1460,34 +1327,56 @@
1460
1327
  throw new Error("Could not resolve jspdf-autotable export. Expected a callable export.");
1461
1328
  };
1462
1329
  const renderTable = (doc, element, indentLevel, store) => {
1463
- if (!element.header || !element.rows) return;
1330
+ if (!element.header || element.header.length === 0) {
1331
+ console.warn("[jspdf-md-renderer] Table skipped: no header row");
1332
+ return;
1333
+ }
1464
1334
  const options = store.options;
1465
1335
  const marginLeft = options.page.xmargin + indentLevel * options.page.indent;
1336
+ ensureSpace(doc, store, 20);
1337
+ const columnCount = element.header.length;
1338
+ const rows = (element.rows ?? []).map((row) => {
1339
+ const normalized = [...row];
1340
+ while (normalized.length < columnCount) normalized.push({
1341
+ type: "table_cell",
1342
+ content: ""
1343
+ });
1344
+ return normalized.slice(0, columnCount).map((cell) => cell.content || "");
1345
+ });
1466
1346
  const head = [element.header.map((h) => h.content || "")];
1467
- const body = element.rows.map((row) => row.map((cell) => cell.content || ""));
1468
1347
  const userTableOptions = options.table || {};
1348
+ const safeDidDrawPage = (data) => {
1349
+ try {
1350
+ if (userTableOptions.didDrawPage) userTableOptions.didDrawPage(data);
1351
+ } catch (e) {
1352
+ console.warn("[jspdf-md-renderer] table.didDrawPage callback threw:", e);
1353
+ }
1354
+ };
1355
+ const safeDidDrawCell = (data) => {
1356
+ try {
1357
+ if (userTableOptions.didDrawCell) userTableOptions.didDrawCell(data);
1358
+ } catch (e) {
1359
+ console.warn("[jspdf-md-renderer] table.didDrawCell callback threw:", e);
1360
+ }
1361
+ };
1469
1362
  resolveAutoTable()(doc, {
1470
1363
  head,
1471
- body,
1364
+ body: rows,
1472
1365
  startY: store.Y,
1473
1366
  margin: {
1474
1367
  left: marginLeft,
1475
1368
  right: options.page.xmargin
1476
1369
  },
1477
1370
  ...userTableOptions,
1478
- didDrawPage: (data) => {
1479
- if (userTableOptions.didDrawPage) userTableOptions.didDrawPage(data);
1480
- },
1481
- didDrawCell: (data) => {
1482
- if (userTableOptions.didDrawCell) userTableOptions.didDrawCell(data);
1483
- }
1371
+ didDrawPage: safeDidDrawPage,
1372
+ didDrawCell: safeDidDrawCell
1484
1373
  });
1485
1374
  const finalY = doc.lastAutoTable?.finalY;
1486
1375
  if (typeof finalY === "number") {
1487
- store.updateY(finalY + options.page.lineSpace, "set");
1376
+ store.updateY(finalY + (options.spacing?.afterTable ?? 3), "set");
1488
1377
  store.updateX(options.page.xpading, "set");
1489
1378
  store.recordContentY();
1490
- }
1379
+ } else console.warn("[jspdf-md-renderer] autoTable did not return a finalY. Y position may be incorrect.");
1491
1380
  };
1492
1381
  //#endregion
1493
1382
  //#region src/store/renderStore.ts
@@ -1568,65 +1457,220 @@
1568
1457
  };
1569
1458
  //#endregion
1570
1459
  //#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"
1460
+ const DEFAULT_HEADING_SIZES = {
1461
+ h1: 24,
1462
+ h2: 20,
1463
+ h3: 17,
1464
+ h4: 15,
1465
+ h5: 13,
1466
+ h6: 12,
1467
+ bottomSpacing: 2
1468
+ };
1469
+ const DEFAULT_FONT = {
1470
+ bold: {
1471
+ name: "helvetica",
1472
+ style: "bold"
1585
1473
  },
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
- }
1474
+ regular: {
1475
+ name: "helvetica",
1476
+ style: "normal"
1603
1477
  },
1604
- image: { defaultAlign: "left" }
1478
+ light: {
1479
+ name: "helvetica",
1480
+ style: "light"
1481
+ },
1482
+ code: {
1483
+ name: "courier",
1484
+ style: "normal"
1485
+ }
1486
+ };
1487
+ const DEFAULT_PAGE = {
1488
+ format: "a4",
1489
+ unit: "mm",
1490
+ orientation: "portrait",
1491
+ maxContentWidth: 190,
1492
+ maxContentHeight: 277,
1493
+ lineSpace: 3,
1494
+ defaultLineHeightFactor: 1.4,
1495
+ defaultFontSize: 11,
1496
+ defaultTitleFontSize: 14,
1497
+ topmargin: 10,
1498
+ xpading: 10,
1499
+ xmargin: 10,
1500
+ indent: 8
1605
1501
  };
1606
1502
  const validateOptions = (options) => {
1607
- if (!options) throw new Error("RenderOption is required");
1608
- const mergedPage = {
1609
- ...defaultOptions.page,
1503
+ if (!options) throw new Error("[jspdf-md-renderer] RenderOption is required");
1504
+ const page = {
1505
+ ...DEFAULT_PAGE,
1610
1506
  ...options.page
1611
1507
  };
1612
- const mergedFont = {
1613
- ...defaultOptions.font,
1508
+ if (page.maxContentWidth <= 0) throw new Error("[jspdf-md-renderer] page.maxContentWidth must be > 0");
1509
+ if (page.maxContentHeight <= 0) throw new Error("[jspdf-md-renderer] page.maxContentHeight must be > 0");
1510
+ if (page.indent < 0) throw new Error("[jspdf-md-renderer] page.indent must be >= 0");
1511
+ if (page.defaultFontSize < 1) throw new Error("[jspdf-md-renderer] page.defaultFontSize must be >= 1");
1512
+ if (page.defaultLineHeightFactor < 1) page.defaultLineHeightFactor = 1.4;
1513
+ if (!options.font?.regular?.name) throw new Error("[jspdf-md-renderer] font.regular.name is required");
1514
+ const font = {
1515
+ ...DEFAULT_FONT,
1614
1516
  ...options.font
1615
1517
  };
1616
- const mergedImage = {
1617
- ...defaultOptions.image,
1618
- ...options.image
1518
+ if (!font.bold?.name) font.bold = DEFAULT_FONT.bold;
1519
+ if (!font.code?.name) font.code = DEFAULT_FONT.code;
1520
+ const heading = {
1521
+ ...DEFAULT_HEADING_SIZES,
1522
+ ...options.heading ?? {}
1523
+ };
1524
+ [
1525
+ "h1",
1526
+ "h2",
1527
+ "h3",
1528
+ "h4",
1529
+ "h5",
1530
+ "h6"
1531
+ ].forEach((k) => {
1532
+ if (heading[k] < 6 || heading[k] > 72) heading[k] = DEFAULT_HEADING_SIZES[k];
1533
+ });
1534
+ const codespan = {
1535
+ backgroundColor: "#EEEEEE",
1536
+ padding: .8,
1537
+ showBackground: true,
1538
+ fontSizeScale: .88,
1539
+ ...options.codespan ?? {}
1540
+ };
1541
+ const blockquote = {
1542
+ barColor: "#AAAAAA",
1543
+ barWidth: 1,
1544
+ paddingLeft: 4,
1545
+ ...options.blockquote ?? {}
1546
+ };
1547
+ const list = {
1548
+ bulletChar: "• ",
1549
+ indentSize: page.indent,
1550
+ itemSpacing: 0,
1551
+ ...options.list ?? {}
1552
+ };
1553
+ const paragraph = {
1554
+ bottomSpacing: page.lineSpace,
1555
+ ...options.paragraph ?? {}
1556
+ };
1557
+ const codeBlock = {
1558
+ backgroundColor: "#F6F8FA",
1559
+ borderColor: "#E1E4E8",
1560
+ borderRadius: 2,
1561
+ padding: 4,
1562
+ fontSizeScale: .9,
1563
+ showLanguageLabel: true,
1564
+ ...options.codeBlock ?? {}
1565
+ };
1566
+ const spacing = {
1567
+ afterHeading: 2,
1568
+ afterParagraph: 3,
1569
+ afterCodeBlock: 3,
1570
+ afterBlockquote: 3,
1571
+ afterImage: 2,
1572
+ afterHR: 2,
1573
+ betweenListItems: 0,
1574
+ afterList: 3,
1575
+ afterTable: 3,
1576
+ ...options.spacing ?? {}
1619
1577
  };
1620
- if (!mergedPage.maxContentWidth) mergedPage.maxContentWidth = 190;
1621
- if (!mergedPage.maxContentHeight) mergedPage.maxContentHeight = 277;
1578
+ [
1579
+ "afterHeading",
1580
+ "afterParagraph",
1581
+ "afterCodeBlock",
1582
+ "afterBlockquote",
1583
+ "afterImage",
1584
+ "afterHR",
1585
+ "betweenListItems",
1586
+ "afterList",
1587
+ "afterTable"
1588
+ ].forEach((key) => {
1589
+ if ((spacing[key] ?? 0) < 0) spacing[key] = 0;
1590
+ });
1591
+ if ((heading.bottomSpacing ?? 0) < 0) heading.bottomSpacing = 0;
1592
+ if ((paragraph.bottomSpacing ?? 0) < 0) paragraph.bottomSpacing = 0;
1593
+ if ((blockquote.bottomSpacing ?? 0) < 0) blockquote.bottomSpacing = 0;
1594
+ const image = {
1595
+ defaultAlign: "left",
1596
+ ...options.image ?? {}
1597
+ };
1598
+ const endCursorYHandler = options.endCursorYHandler ?? (() => {});
1622
1599
  return {
1623
1600
  ...options,
1624
- page: mergedPage,
1625
- font: mergedFont,
1626
- image: mergedImage
1601
+ page,
1602
+ font,
1603
+ heading,
1604
+ codespan,
1605
+ blockquote,
1606
+ list,
1607
+ paragraph,
1608
+ codeBlock,
1609
+ spacing,
1610
+ image,
1611
+ endCursorYHandler
1627
1612
  };
1628
1613
  };
1629
1614
  //#endregion
1615
+ //#region src/utils/pageDecorations.ts
1616
+ const applyPageDecorations = (doc, options) => {
1617
+ const totalPages = doc.internal.getNumberOfPages();
1618
+ for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
1619
+ doc.setPage(pageNum);
1620
+ applyHeader(doc, options, pageNum, totalPages);
1621
+ applyFooter(doc, options, pageNum, totalPages);
1622
+ }
1623
+ };
1624
+ const applyHeader = (doc, options, pageNum, totalPages) => {
1625
+ const hOpts = options.header;
1626
+ if (!hOpts) return;
1627
+ const text = typeof hOpts.text === "function" ? hOpts.text(pageNum, totalPages) : hOpts.text ?? "";
1628
+ if (!text.trim()) return;
1629
+ const savedFont = doc.getFont();
1630
+ const savedSize = doc.getFontSize();
1631
+ const savedColor = doc.getTextColor();
1632
+ doc.setFontSize(hOpts.fontSize ?? 9);
1633
+ doc.setTextColor(hOpts.color ?? "#666666");
1634
+ const y = hOpts.y ?? 5;
1635
+ const align = hOpts.align ?? "center";
1636
+ const pageWidth = doc.internal.pageSize.getWidth();
1637
+ let x = pageWidth / 2;
1638
+ if (align === "left") x = options.page.xmargin;
1639
+ if (align === "right") x = pageWidth - options.page.xmargin;
1640
+ doc.text(text, x, y, {
1641
+ align,
1642
+ baseline: "top"
1643
+ });
1644
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
1645
+ doc.setFontSize(savedSize);
1646
+ doc.setTextColor(savedColor);
1647
+ };
1648
+ const applyFooter = (doc, options, pageNum, totalPages) => {
1649
+ const fOpts = options.footer;
1650
+ if (!fOpts) return;
1651
+ const text = fOpts.showPageNumbers ? `Page ${pageNum} of ${totalPages}` : typeof fOpts.text === "function" ? fOpts.text(pageNum, totalPages) : fOpts.text ?? "";
1652
+ if (!text.trim()) return;
1653
+ const savedFont = doc.getFont();
1654
+ const savedSize = doc.getFontSize();
1655
+ const savedColor = doc.getTextColor();
1656
+ doc.setFontSize(fOpts.fontSize ?? 9);
1657
+ doc.setTextColor(fOpts.color ?? "#666666");
1658
+ const pageHeight = doc.internal.pageSize.getHeight();
1659
+ const y = fOpts.y ?? pageHeight - 5;
1660
+ const align = fOpts.align ?? "right";
1661
+ const pageWidth = doc.internal.pageSize.getWidth();
1662
+ let x = pageWidth / 2;
1663
+ if (align === "left") x = options.page.xmargin;
1664
+ if (align === "right") x = pageWidth - options.page.xmargin;
1665
+ doc.text(text, x, y, {
1666
+ align,
1667
+ baseline: "bottom"
1668
+ });
1669
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
1670
+ doc.setFontSize(savedSize);
1671
+ doc.setTextColor(savedColor);
1672
+ };
1673
+ //#endregion
1630
1674
  //#region src/renderer/MdTextRender.ts
1631
1675
  /**
1632
1676
  * Renders parsed markdown text into jsPDF document.
@@ -1644,7 +1688,7 @@
1644
1688
  const indent = indentLevel * validOptions.page.indent;
1645
1689
  switch (element.type) {
1646
1690
  case "heading":
1647
- renderHeading(doc, element, indent, store, renderElement);
1691
+ renderHeading(doc, element, indent, store);
1648
1692
  break;
1649
1693
  case "paragraph":
1650
1694
  renderParagraph(doc, element, indent, store, renderElement);
@@ -1664,10 +1708,8 @@
1664
1708
  case "strong":
1665
1709
  case "em":
1666
1710
  case "codespan":
1667
- renderInlineText(doc, element, indent, store);
1668
- break;
1669
1711
  case "link":
1670
- renderLink(doc, element, indent, store);
1712
+ renderInlineContent(doc, [element], store.X + indent, store.Y, validOptions.page.maxContentWidth - indent, store);
1671
1713
  break;
1672
1714
  case "blockquote":
1673
1715
  renderBlockquote(doc, element, indentLevel, store, renderElement);
@@ -1699,9 +1741,14 @@
1699
1741
  }
1700
1742
  };
1701
1743
  for (const item of parsedElements) renderElement(item, 0, store);
1744
+ applyPageDecorations(doc, validOptions);
1702
1745
  validOptions.endCursorYHandler(store.Y);
1703
1746
  };
1704
1747
  //#endregion
1705
1748
  exports.MdTextParser = MdTextParser;
1706
1749
  exports.MdTextRender = MdTextRender;
1750
+ exports.MdTokenType = MdTokenType;
1751
+ exports.renderInlineContent = renderInlineContent;
1752
+ exports.renderPlainText = renderPlainText;
1753
+ exports.validateOptions = validateOptions;
1707
1754
  });