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.mjs CHANGED
@@ -26,6 +26,30 @@
26
26
  */
27
27
  import { marked } from "marked";
28
28
  import autoTable from "jspdf-autotable";
29
+ //#region src/enums/mdTokenType.ts
30
+ let MdTokenType = /* @__PURE__ */ function(MdTokenType) {
31
+ MdTokenType["Heading"] = "heading";
32
+ MdTokenType["Paragraph"] = "paragraph";
33
+ MdTokenType["List"] = "list";
34
+ MdTokenType["ListItem"] = "list_item";
35
+ MdTokenType["Blockquote"] = "blockquote";
36
+ MdTokenType["Code"] = "code";
37
+ MdTokenType["CodeSpan"] = "codespan";
38
+ MdTokenType["Table"] = "table";
39
+ MdTokenType["Html"] = "html";
40
+ MdTokenType["Hr"] = "hr";
41
+ MdTokenType["Image"] = "image";
42
+ MdTokenType["Link"] = "link";
43
+ MdTokenType["Strong"] = "strong";
44
+ MdTokenType["Em"] = "em";
45
+ MdTokenType["TableHeader"] = "table_header";
46
+ MdTokenType["TableCell"] = "table_cell";
47
+ MdTokenType["Raw"] = "raw";
48
+ MdTokenType["Text"] = "text";
49
+ MdTokenType["Br"] = "br";
50
+ return MdTokenType;
51
+ }({});
52
+ //#endregion
29
53
  //#region src/parser/imageExtension.ts
30
54
  /**
31
55
  * Internal hash prefix used to encode image attributes in the URL fragment.
@@ -44,8 +68,12 @@ const ATTR_HASH_PREFIX = "__jmr_";
44
68
  const IMAGE_WITH_ATTRS_REGEX = /(!\[[^\]]*\]\()([^)]+)(\))\s*\{([^}]+)\}/g;
45
69
  /**
46
70
  * Regex to extract individual key=value pairs from the attribute block.
71
+ * Supports:
72
+ * - Bare values: width=200
73
+ * - Quoted values: width="200.5" or width='200'
74
+ * - Decimal values: width=200.5
47
75
  */
48
- const ATTR_PAIR_REGEX = /(\w+)\s*=\s*(\w+)/g;
76
+ const ATTR_PAIR_REGEX = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/g;
49
77
  /** Valid alignment values */
50
78
  const VALID_ALIGNMENTS = [
51
79
  "left",
@@ -72,17 +100,17 @@ const parseRawAttributes = (attrString) => {
72
100
  let match;
73
101
  while ((match = ATTR_PAIR_REGEX.exec(attrString)) !== null) {
74
102
  const key = match[1].toLowerCase();
75
- const value = match[2];
103
+ const value = (match[2] ?? match[3] ?? match[4] ?? "").trim();
76
104
  switch (key) {
77
105
  case "width":
78
106
  case "w": {
79
- const num = parseInt(value, 10);
107
+ const num = parseFloat(value);
80
108
  if (!isNaN(num) && num > 0) attrs.width = num;
81
109
  break;
82
110
  }
83
111
  case "height":
84
112
  case "h": {
85
- const num = parseInt(value, 10);
113
+ const num = parseFloat(value);
86
114
  if (!isNaN(num) && num > 0) attrs.height = num;
87
115
  break;
88
116
  }
@@ -136,12 +164,12 @@ const parseImageAttrsFromHref = (href) => {
136
164
  const [key, value] = pair.split("=");
137
165
  switch (key) {
138
166
  case "w": {
139
- const num = parseInt(value, 10);
167
+ const num = parseFloat(value);
140
168
  if (!isNaN(num) && num > 0) attrs.width = num;
141
169
  break;
142
170
  }
143
171
  case "h": {
144
- const num = parseInt(value, 10);
172
+ const num = parseFloat(value);
145
173
  if (!isNaN(num) && num > 0) attrs.height = num;
146
174
  break;
147
175
  }
@@ -216,6 +244,8 @@ const tokenHandlers = {
216
244
  ["list_item"]: (token) => ({
217
245
  type: "list_item",
218
246
  content: token.text,
247
+ task: token.task ?? false,
248
+ checked: token.checked ?? false,
219
249
  items: token.tokens ? convertTokens(token.tokens) : []
220
250
  }),
221
251
  ["code"]: (token) => ({
@@ -282,14 +312,39 @@ const tokenHandlers = {
282
312
  items: token.tokens ? convertTokens(token.tokens) : []
283
313
  }),
284
314
  ["html"]: (token) => {
285
- const rawHtml = String(token.raw ?? token.text ?? "").trim();
286
- if (/^<br\s*\/?>$/i.test(rawHtml)) return {
315
+ const raw = String(token.raw ?? token.text ?? "").trim();
316
+ if (/^<br\s*\/?>$/i.test(raw)) return {
287
317
  type: "br",
288
318
  content: "\n"
289
319
  };
320
+ const inlineTagMap = {
321
+ strong: "strong",
322
+ b: "strong",
323
+ em: "em",
324
+ i: "em"
325
+ };
326
+ const inlineMatch = raw.match(/^<(\w+)[^>]*>(.*?)<\/\1>$/is);
327
+ if (inlineMatch) {
328
+ const tag = inlineMatch[1].toLowerCase();
329
+ const innerText = inlineMatch[2];
330
+ const mappedType = inlineTagMap[tag];
331
+ if (mappedType) return {
332
+ type: mappedType,
333
+ content: innerText
334
+ };
335
+ }
336
+ if (/^<(s|del)[^>]*>(.*?)<\/(s|del)>$/is.test(raw)) return {
337
+ type: "raw",
338
+ content: raw.match(/>([^<]+)</)?.[1] ?? raw
339
+ };
340
+ const strippedText = raw.replace(/<[^>]+>/g, "").trim();
341
+ if (strippedText) return {
342
+ type: "raw",
343
+ content: strippedText
344
+ };
290
345
  return {
291
346
  type: "raw",
292
- content: token.raw ?? token.text ?? ""
347
+ content: ""
293
348
  };
294
349
  },
295
350
  ["br"]: () => ({
@@ -298,14 +353,6 @@ const tokenHandlers = {
298
353
  })
299
354
  };
300
355
  //#endregion
301
- //#region src/utils/doc-helpers.ts
302
- const getCharHight = (doc) => {
303
- return doc.getTextDimensions("H").h;
304
- };
305
- const getCharWidth = (doc) => {
306
- return doc.getTextDimensions("H").w;
307
- };
308
- //#endregion
309
356
  //#region src/utils/handlePageBreak.ts
310
357
  const HandlePageBreaks = (doc, store) => {
311
358
  if (typeof store.options.pageBreakHandler === "function") store.options.pageBreakHandler(doc);
@@ -313,6 +360,35 @@ const HandlePageBreaks = (doc, store) => {
313
360
  store.updateY(store.options.page.topmargin);
314
361
  store.updateX(store.options.page.xpading);
315
362
  };
363
+ /**
364
+ * Returns true if adding `height` to the current Y would exceed the page.
365
+ */
366
+ const willOverflow = (store, height) => {
367
+ return store.Y + height > store.options.page.maxContentHeight;
368
+ };
369
+ /**
370
+ * Checks if we will overflow, and if so, breaks the page first.
371
+ * Returns true if a page break was performed.
372
+ */
373
+ const breakIfOverflow = (doc, store, height) => {
374
+ if (willOverflow(store, height)) {
375
+ HandlePageBreaks(doc, store);
376
+ return true;
377
+ }
378
+ return false;
379
+ };
380
+ /**
381
+ * Ensures there is at least `minHeight` space remaining on the page.
382
+ * If not, breaks to next page.
383
+ */
384
+ const ensureSpace = (doc, store, minHeight) => {
385
+ if (store.options.page.maxContentHeight - store.Y < minHeight) HandlePageBreaks(doc, store);
386
+ };
387
+ //#endregion
388
+ //#region src/utils/doc-helpers.ts
389
+ const getCharHight = (doc) => {
390
+ return doc.getFontSize() / doc.internal.scaleFactor;
391
+ };
316
392
  //#endregion
317
393
  //#region src/utils/image-utils.ts
318
394
  /**
@@ -480,115 +556,127 @@ const prefetchImages = async (elements) => {
480
556
  }
481
557
  };
482
558
  //#endregion
483
- //#region src/utils/justifiedTextRenderer.ts
559
+ //#region src/layout/wordSplitter.ts
484
560
  /**
485
- * JustifiedTextRenderer - Renders mixed inline elements with proper alignment.
486
- *
487
- * Features:
488
- * - Handles bold, italic, codespan, links mixed in paragraph
489
- * - Proper word spacing distribution for justified alignment
490
- * - Supports left, right, center, and justify alignments
491
- * - Page break handling
492
- * - Preserves link clickability
493
- * - Codespan background rendering
561
+ * Maps a ParsedElement type string to a TextStyle.
494
562
  */
495
- var JustifiedTextRenderer = class {
496
- static getCodespanOptions(store) {
497
- const opts = store.options.codespan ?? {};
498
- return {
499
- backgroundColor: opts.backgroundColor ?? "#EEEEEE",
500
- padding: opts.padding ?? .5,
501
- showBackground: opts.showBackground !== false,
502
- fontSizeScale: opts.fontSizeScale ?? .9
503
- };
504
- }
505
- /**
506
- * Apply font style to the jsPDF document.
507
- */
508
- static applyStyle(doc, style, store) {
509
- const currentFont = doc.getFont().fontName;
510
- const currentFontSize = doc.getFontSize();
511
- const getBoldFont = () => {
512
- const boldName = store.options.font.bold?.name;
513
- return boldName && boldName !== "" ? boldName : currentFont;
514
- };
515
- const getRegularFont = () => {
516
- const regularName = store.options.font.regular?.name;
517
- return regularName && regularName !== "" ? regularName : currentFont;
518
- };
519
- switch (style) {
520
- case "bold":
521
- doc.setFont(getBoldFont(), store.options.font.bold?.style || "bold");
522
- break;
523
- case "italic":
524
- doc.setFont(getRegularFont(), "italic");
525
- break;
526
- case "bolditalic":
527
- doc.setFont(getBoldFont(), "bolditalic");
528
- break;
529
- case "codespan":
530
- const codeFont = store.options.font.code || {
531
- name: "courier",
532
- style: "normal"
533
- };
534
- doc.setFont(codeFont.name, codeFont.style);
535
- doc.setFontSize(currentFontSize * this.getCodespanOptions(store).fontSizeScale);
536
- break;
537
- default:
538
- doc.setFont(getRegularFont(), doc.getFont().fontStyle);
539
- break;
540
- }
563
+ const resolveStyle = (type, parentStyle) => {
564
+ switch (type) {
565
+ case "strong": return parentStyle === "italic" ? "bolditalic" : "bold";
566
+ case "em": return parentStyle === "bold" ? "bolditalic" : "italic";
567
+ case "codespan": return "codespan";
568
+ default: return parentStyle ?? "normal";
541
569
  }
542
- /**
543
- * Measure word width with a specific style applied.
544
- * NOTE: jsPDF's getTextWidth() does NOT include charSpace in its calculation,
545
- * so we must manually add it: effectiveWidth = getTextWidth(text) + (text.length * charSpace)
546
- */
547
- static measureWordWidth(doc, text, style, store) {
548
- const savedFont = doc.getFont();
549
- const savedSize = doc.getFontSize();
550
- this.applyStyle(doc, style, store);
551
- const baseWidth = doc.getTextWidth(text);
552
- const charSpace = doc.getCharSpace?.() ?? 0;
553
- const effectiveWidth = baseWidth + text.length * charSpace;
554
- doc.setFont(savedFont.fontName, savedFont.fontStyle);
555
- doc.setFontSize(savedSize);
556
- return effectiveWidth;
570
+ };
571
+ /**
572
+ * Measures the width of `text` rendered with `style`, including jsPDF charSpace.
573
+ */
574
+ const measureStyledWidth = (doc, text, style, store) => {
575
+ const savedFont = doc.getFont();
576
+ const savedSize = doc.getFontSize();
577
+ applyStyleToDoc(doc, style, store);
578
+ const charSpace = doc.getCharSpace?.() ?? 0;
579
+ const width = doc.getTextWidth(text) + text.length * charSpace;
580
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
581
+ doc.setFontSize(savedSize);
582
+ return width;
583
+ };
584
+ /**
585
+ * Applies a TextStyle to the jsPDF document.
586
+ */
587
+ const applyStyleToDoc = (doc, style, store) => {
588
+ const curFont = doc.getFont().fontName;
589
+ const curSize = doc.getFontSize();
590
+ const boldFont = store.options.font.bold?.name || curFont;
591
+ const regularFont = store.options.font.regular?.name || curFont;
592
+ const codeFont = store.options.font.code || {
593
+ name: "courier",
594
+ style: "normal"
595
+ };
596
+ switch (style) {
597
+ case "bold":
598
+ doc.setFont(boldFont, store.options.font.bold?.style || "bold");
599
+ break;
600
+ case "italic":
601
+ doc.setFont(regularFont, "italic");
602
+ break;
603
+ case "bolditalic":
604
+ doc.setFont(boldFont, "bolditalic");
605
+ break;
606
+ case "codespan":
607
+ doc.setFont(codeFont.name, codeFont.style);
608
+ doc.setFontSize(curSize * (store.options.codespan?.fontSizeScale ?? .88));
609
+ break;
610
+ default:
611
+ doc.setFont(regularFont, "normal");
612
+ break;
557
613
  }
558
- /**
559
- * Extract style from element type string.
560
- */
561
- static getStyleFromType(type, parentStyle) {
562
- switch (type) {
563
- case "strong":
564
- if (parentStyle === "italic") return "bolditalic";
565
- return "bold";
566
- case "em":
567
- if (parentStyle === "bold") return "bolditalic";
568
- return "italic";
569
- case "codespan": return "codespan";
570
- default: return parentStyle || "normal";
614
+ };
615
+ /**
616
+ * Flattens a ParsedElement tree into a flat array of StyledWordInfo.
617
+ * This is the single source of truth for converting AST → layout words.
618
+ */
619
+ const flattenToWords = (doc, elements, store, parentStyle = "normal", isLink = false, href) => {
620
+ const result = [];
621
+ for (const el of elements) {
622
+ const style = resolveStyle(el.type, parentStyle);
623
+ const elIsLink = el.type === "link" || isLink;
624
+ const elHref = el.href || href;
625
+ if (el.type === "br") {
626
+ result.push({
627
+ text: "",
628
+ width: 0,
629
+ style,
630
+ isBr: true
631
+ });
632
+ continue;
571
633
  }
572
- }
573
- /**
574
- * Flatten ParsedElement tree into an array of StyledWordInfo.
575
- * Handles nested inline elements.
576
- */
577
- static flattenToWords(doc, elements, store, parentStyle = "normal", isLink = false, href) {
578
- const result = [];
579
- for (const el of elements) {
580
- const style = this.getStyleFromType(el.type, parentStyle);
581
- const elIsLink = el.type === "link" || isLink;
582
- const elHref = el.href || href;
583
- if (el.items && el.items.length > 0) {
584
- const nested = this.flattenToWords(doc, el.items, store, style, elIsLink, elHref);
585
- result.push(...nested);
586
- } else if (el.type === "image") {
587
- const maxH = store.options.page.maxContentHeight - store.options.page.topmargin;
588
- const { finalWidth, finalHeight } = calculateImageDimensions(doc, el, store.options.page.maxContentWidth - store.options.page.indent * 0, maxH, store.options.page.unit || "mm");
634
+ if (el.type === "image") {
635
+ const { finalWidth, finalHeight } = calculateImageDimensions(doc, el, store.options.page.maxContentWidth, store.options.page.maxContentHeight - store.options.page.topmargin, store.options.page.unit || "mm");
636
+ result.push({
637
+ text: "",
638
+ width: finalWidth,
639
+ style,
640
+ isLink: elIsLink,
641
+ href: elHref,
642
+ isImage: true,
643
+ imageElement: el,
644
+ imageHeight: finalHeight
645
+ });
646
+ continue;
647
+ }
648
+ if (el.items && el.items.length > 0) {
649
+ result.push(...flattenToWords(doc, el.items, store, style, elIsLink, elHref));
650
+ continue;
651
+ }
652
+ const text = el.content || el.text || "";
653
+ if (!text) continue;
654
+ if (/^\s/.test(text) && result.length > 0) result[result.length - 1].hasTrailingSpace = true;
655
+ if (style === "codespan") {
656
+ const trimmed = text.trim();
657
+ if (trimmed) result.push({
658
+ text: trimmed,
659
+ width: measureStyledWidth(doc, trimmed, style, store),
660
+ style,
661
+ isLink: elIsLink,
662
+ href: elHref,
663
+ linkColor: elIsLink ? store.options.link?.linkColor || [
664
+ 0,
665
+ 0,
666
+ 255
667
+ ] : void 0,
668
+ hasTrailingSpace: /\s$/.test(text)
669
+ });
670
+ continue;
671
+ }
672
+ const lines = text.split("\n");
673
+ for (let li = 0; li < lines.length; li++) {
674
+ const words = lines[li].trim().split(/[ \t\r\v\f]+/).filter(Boolean);
675
+ for (let wi = 0; wi < words.length; wi++) {
676
+ const isLastInLine = wi === words.length - 1;
589
677
  result.push({
590
- text: "",
591
- width: finalWidth,
678
+ text: words[wi],
679
+ width: measureStyledWidth(doc, words[wi], style, store),
592
680
  style,
593
681
  isLink: elIsLink,
594
682
  href: elHref,
@@ -597,338 +685,253 @@ var JustifiedTextRenderer = class {
597
685
  0,
598
686
  255
599
687
  ] : void 0,
600
- isImage: true,
601
- imageElement: el,
602
- imageHeight: finalHeight
688
+ hasTrailingSpace: !isLastInLine || /[ \t]$/.test(lines[li])
603
689
  });
604
- } else if (el.type === "br") result.push({
690
+ }
691
+ if (li < lines.length - 1) result.push({
605
692
  text: "",
606
693
  width: 0,
607
694
  style,
608
695
  isBr: true
609
696
  });
610
- else {
611
- const text = el.content || el.text || "";
612
- if (!text) continue;
613
- if (/^\s/.test(text) && result.length > 0) result[result.length - 1].hasTrailingSpace = true;
614
- if (style === "codespan") {
615
- const trimmedText = text.trim();
616
- if (trimmedText) result.push({
617
- text: trimmedText,
618
- width: this.measureWordWidth(doc, trimmedText, style, store),
619
- style,
620
- isLink: elIsLink,
621
- href: elHref,
622
- linkColor: elIsLink ? store.options.link?.linkColor || [
623
- 0,
624
- 0,
625
- 255
626
- ] : void 0,
627
- hasTrailingSpace: /\s$/.test(text)
628
- });
629
- continue;
630
- }
631
- const lines = text.split("\n");
632
- for (let partIndex = 0; partIndex < lines.length; partIndex++) {
633
- const lineStr = lines[partIndex];
634
- const words = lineStr.trim().split(/[ \t\r\v\f]+/).filter((w) => w.length > 0);
635
- for (let i = 0; i < words.length; i++) {
636
- const hasTrailingSpace = !(i === words.length - 1) || /[ \t\r\v\f]$/.test(lineStr);
637
- result.push({
638
- text: words[i],
639
- width: this.measureWordWidth(doc, words[i], style, store),
640
- style,
641
- isLink: elIsLink,
642
- href: elHref,
643
- linkColor: elIsLink ? store.options.link?.linkColor || [
644
- 0,
645
- 0,
646
- 255
647
- ] : void 0,
648
- hasTrailingSpace
649
- });
650
- }
651
- if (partIndex < lines.length - 1) result.push({
652
- text: "",
653
- width: 0,
654
- style,
655
- isBr: true
656
- });
657
- }
658
- }
659
697
  }
660
- return result;
661
698
  }
662
- /**
663
- * Break a flat list of words into lines that fit within maxWidth.
664
- * Correctly tracks totalTextWidth (sum of word widths only) for justification.
665
- */
666
- static breakIntoLines(doc, words, maxWidth, store) {
667
- const lines = [];
668
- let currentLine = [];
669
- let currentTextWidth = 0;
670
- let currentLineWidth = 0;
671
- let currentLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
672
- const spaceWidth = doc.getTextWidth(" ");
673
- for (let i = 0; i < words.length; i++) {
674
- const word = words[i];
675
- const neededWidthWithSpace = currentLine[currentLine.length - 1]?.hasTrailingSpace ? spaceWidth + word.width : word.width;
676
- const itemHeight = word.isImage && word.imageHeight ? word.imageHeight : getCharHight(doc) * store.options.page.defaultLineHeightFactor;
677
- if (word.isBr) {
678
- lines.push({
679
- words: currentLine,
680
- totalTextWidth: currentTextWidth,
681
- isLastLine: true,
682
- lineHeight: currentLineHeight
683
- });
684
- currentLine = [];
685
- currentTextWidth = 0;
686
- currentLineWidth = 0;
687
- currentLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
688
- continue;
689
- }
690
- if (currentLineWidth + neededWidthWithSpace > maxWidth && currentLine.length > 0) {
691
- lines.push({
692
- words: currentLine,
693
- totalTextWidth: currentTextWidth,
694
- isLastLine: false,
695
- lineHeight: currentLineHeight
696
- });
697
- currentLine = [word];
698
- currentTextWidth = word.width;
699
- currentLineWidth = word.width;
700
- currentLineHeight = itemHeight;
701
- } else {
702
- currentLine.push(word);
703
- currentTextWidth += word.width;
704
- currentLineWidth += neededWidthWithSpace;
705
- currentLineHeight = Math.max(currentLineHeight, itemHeight);
706
- }
699
+ return result;
700
+ };
701
+ //#endregion
702
+ //#region src/layout/lineBreaker.ts
703
+ const splitOversizedWord = (doc, word, maxWidth, store) => {
704
+ if (!word.text || word.width <= maxWidth || word.isImage) return [word];
705
+ const chunks = [];
706
+ let remaining = word.text;
707
+ while (remaining.length > 0) {
708
+ let chunk = "";
709
+ for (const ch of remaining) {
710
+ const candidate = chunk + ch;
711
+ const candidateWidth = measureStyledWidth(doc, candidate, word.style, store);
712
+ if (candidateWidth > maxWidth && chunk.length > 0) break;
713
+ chunk = candidate;
714
+ if (candidateWidth >= maxWidth) break;
707
715
  }
716
+ if (!chunk) chunk = remaining.charAt(0);
717
+ chunks.push({
718
+ ...word,
719
+ text: chunk,
720
+ width: measureStyledWidth(doc, chunk, word.style, store),
721
+ hasTrailingSpace: false
722
+ });
723
+ remaining = remaining.slice(chunk.length);
724
+ }
725
+ if (chunks.length > 0) chunks[chunks.length - 1].hasTrailingSpace = word.hasTrailingSpace;
726
+ return chunks;
727
+ };
728
+ const breakIntoLines = (doc, words, maxWidth, store) => {
729
+ const normalizedWords = [];
730
+ for (const word of words) normalizedWords.push(...splitOversizedWord(doc, word, maxWidth, store));
731
+ const lines = [];
732
+ let currentLine = [];
733
+ let currentTextWidth = 0;
734
+ let currentLineWidth = 0;
735
+ const baseLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
736
+ let currentLineHeight = baseLineHeight;
737
+ const spaceWidth = doc.getTextWidth(" ");
738
+ const pushLine = (isLast) => {
708
739
  if (currentLine.length > 0) lines.push({
709
740
  words: currentLine,
710
741
  totalTextWidth: currentTextWidth,
711
- isLastLine: true,
742
+ isLastLine: isLast,
712
743
  lineHeight: currentLineHeight
713
744
  });
714
- return lines;
715
- }
716
- /**
717
- * Render a single word with its style applied.
718
- */
719
- static renderWord(doc, word, x, y, store) {
720
- const savedFont = doc.getFont();
721
- const savedSize = doc.getFontSize();
722
- const savedColor = doc.getTextColor();
723
- this.applyStyle(doc, word.style, store);
724
- if (word.isLink && word.linkColor) doc.setTextColor(...word.linkColor);
725
- if (word.isImage && word.imageElement && word.imageElement.data) try {
726
- let imgFormat = "JPEG";
727
- if (word.imageElement.data.startsWith("data:image/png")) imgFormat = "PNG";
728
- else if (word.imageElement.data.startsWith("data:image/webp")) imgFormat = "WEBP";
729
- else if (word.imageElement.data.startsWith("data:image/gif")) imgFormat = "GIF";
730
- else if (word.imageElement.src) {
731
- const ext = word.imageElement.src.split("?")[0].split("#")[0].split(".").pop()?.toUpperCase();
732
- if (ext && [
733
- "PNG",
734
- "JPEG",
735
- "JPG",
736
- "WEBP",
737
- "GIF"
738
- ].includes(ext)) imgFormat = ext === "JPG" ? "JPEG" : ext;
739
- }
740
- if (word.width > 0 && (word.imageHeight || 0) > 0) {
741
- const imgH = word.imageHeight || 0;
742
- const imgY = y;
743
- doc.addImage(word.imageElement.data, imgFormat, x, imgY, word.width, imgH);
744
- }
745
- } catch (e) {
746
- console.warn("Failed to render inline image", e);
747
- }
748
- else {
749
- if (word.style === "codespan") {
750
- const codespanOpts = this.getCodespanOptions(store);
751
- if (codespanOpts.showBackground) {
752
- const h = getCharHight(doc);
753
- const pad = codespanOpts.padding;
754
- doc.setFillColor(codespanOpts.backgroundColor);
755
- doc.rect(x - pad, y - pad, word.width + pad * 2, h + pad * 2, "F");
756
- doc.setFillColor("#000000");
757
- }
758
- }
759
- doc.text(word.text, x, y, { baseline: "top" });
745
+ };
746
+ for (const word of normalizedWords) {
747
+ if (word.isBr) {
748
+ pushLine(true);
749
+ currentLine = [];
750
+ currentTextWidth = 0;
751
+ currentLineWidth = 0;
752
+ currentLineHeight = baseLineHeight;
753
+ continue;
760
754
  }
761
- if (word.isLink && word.href) {
762
- const h = word.isImage && word.imageHeight ? word.imageHeight : getCharHight(doc);
763
- doc.link(x, y, word.width, h, { url: word.href });
755
+ const neededWidth = (currentLine[currentLine.length - 1]?.hasTrailingSpace ? spaceWidth : 0) + word.width;
756
+ const itemHeight = word.isImage && word.imageHeight ? word.imageHeight : baseLineHeight;
757
+ if (currentLineWidth + neededWidth > maxWidth && currentLine.length > 0) {
758
+ pushLine(false);
759
+ currentLine = [word];
760
+ currentTextWidth = word.width;
761
+ currentLineWidth = word.width;
762
+ currentLineHeight = itemHeight;
763
+ } else {
764
+ currentLine.push(word);
765
+ currentTextWidth += word.width;
766
+ currentLineWidth += neededWidth;
767
+ currentLineHeight = Math.max(currentLineHeight, itemHeight);
764
768
  }
765
- doc.setFont(savedFont.fontName, savedFont.fontStyle);
766
- doc.setFontSize(savedSize);
767
- doc.setTextColor(savedColor);
768
769
  }
769
- /**
770
- * Render a single line with specified alignment.
771
- */
772
- static renderAlignedLine(doc, line, x, y, maxWidth, store, alignment = "left") {
773
- const { words, totalTextWidth, isLastLine } = line;
774
- if (words.length === 0) return;
775
- const normalSpaceWidth = doc.getTextWidth(" ");
776
- let startX = x;
777
- let wordSpacing = normalSpaceWidth;
778
- let lineWidthWithNormalSpaces = totalTextWidth;
779
- let expandableSpacesCount = 0;
780
- for (let i = 0; i < words.length - 1; i++) if (words[i].hasTrailingSpace) {
781
- lineWidthWithNormalSpaces += normalSpaceWidth;
782
- expandableSpacesCount++;
783
- }
784
- switch (alignment) {
785
- case "right":
786
- startX = x + maxWidth - lineWidthWithNormalSpaces;
787
- break;
788
- case "center":
789
- startX = x + (maxWidth - lineWidthWithNormalSpaces) / 2;
790
- break;
791
- case "justify":
792
- if (!isLastLine && expandableSpacesCount > 0) wordSpacing = (maxWidth - totalTextWidth) / expandableSpacesCount;
793
- break;
794
- default: break;
795
- }
796
- let currentX = startX;
797
- const textHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
798
- for (let i = 0; i < words.length; i++) {
799
- const word = words[i];
800
- let drawY = y;
801
- const elementHeight = word.isImage && word.imageHeight ? word.imageHeight : textHeight;
802
- if (word.isImage) drawY = y;
803
- else if (elementHeight < line.lineHeight) drawY = y + (line.lineHeight - elementHeight);
804
- this.renderWord(doc, word, currentX, drawY, store);
805
- currentX += word.width;
806
- if (i < words.length - 1 && word.hasTrailingSpace) currentX += wordSpacing;
807
- }
770
+ pushLine(true);
771
+ return lines;
772
+ };
773
+ //#endregion
774
+ //#region src/layout/lineRenderer.ts
775
+ const renderLine = (doc, line, x, y, maxWidth, store, alignment = "left") => {
776
+ const { words, totalTextWidth, isLastLine } = line;
777
+ if (words.length === 0) return;
778
+ const normalSpaceWidth = doc.getTextWidth(" ");
779
+ let lineWidthWithSpaces = totalTextWidth;
780
+ let expandableSpaces = 0;
781
+ for (let i = 0; i < words.length - 1; i++) if (words[i].hasTrailingSpace) {
782
+ lineWidthWithSpaces += normalSpaceWidth;
783
+ expandableSpaces++;
808
784
  }
809
- /**
810
- * Main entry point: Render a paragraph with mixed inline elements.
811
- * Respects user's textAlignment option from store.
812
- *
813
- * @param doc jsPDF instance
814
- * @param elements Array of ParsedElement (inline items in a paragraph)
815
- * @param x Starting X coordinate
816
- * @param y Starting Y coordinate
817
- * @param maxWidth Maximum width for text wrapping
818
- * @param store RenderStore instance to use
819
- * @param alignment Optional alignment override (defaults to store option)
820
- */
821
- static renderStyledParagraph(doc, elements, x, y, maxWidth, store, alignment) {
822
- const textAlignment = alignment ?? store.options.content?.textAlignment ?? "left";
823
- const words = this.flattenToWords(doc, elements, store);
824
- if (words.length === 0) return;
825
- const lines = this.breakIntoLines(doc, words, maxWidth, store);
826
- let currentY = y;
827
- for (const line of lines) {
828
- if (currentY + line.lineHeight > store.options.page.maxContentHeight) {
829
- HandlePageBreaks(doc, store);
830
- currentY = store.Y;
831
- }
832
- this.renderAlignedLine(doc, line, x, currentY, maxWidth, store, textAlignment);
833
- store.recordContentY(currentY + line.lineHeight);
834
- currentY += line.lineHeight;
835
- store.updateY(line.lineHeight, "add");
836
- }
837
- const lastLine = lines[lines.length - 1];
838
- if (lastLine) {
839
- let actualSpacesCount = 0;
840
- for (let i = 0; i < lastLine.words.length - 1; i++) if (lastLine.words[i].hasTrailingSpace) actualSpacesCount++;
841
- const lastLineWidth = lastLine.totalTextWidth + actualSpacesCount * doc.getTextWidth(" ");
842
- store.updateX(x + lastLineWidth, "set");
843
- }
785
+ let startX = x;
786
+ let wordSpacing = normalSpaceWidth;
787
+ switch (alignment) {
788
+ case "right":
789
+ startX = x + maxWidth - lineWidthWithSpaces;
790
+ break;
791
+ case "center":
792
+ startX = x + (maxWidth - lineWidthWithSpaces) / 2;
793
+ break;
794
+ case "justify":
795
+ if (!isLastLine && expandableSpaces > 0) wordSpacing = (maxWidth - totalTextWidth) / expandableSpaces;
796
+ break;
844
797
  }
845
- /**
846
- * @deprecated Use renderStyledParagraph instead
847
- */
848
- static renderJustifiedParagraph(doc, elements, x, y, maxWidth, store) {
849
- this.renderStyledParagraph(doc, elements, x, y, maxWidth, store);
798
+ const textLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
799
+ let currentX = startX;
800
+ for (let i = 0; i < words.length; i++) {
801
+ const word = words[i];
802
+ const elementHeight = word.isImage && word.imageHeight ? word.imageHeight : textLineHeight;
803
+ const drawY = word.isImage ? y : y + (line.lineHeight - elementHeight);
804
+ renderSingleWord(doc, word, currentX, drawY, store);
805
+ currentX += word.width;
806
+ if (i < words.length - 1 && word.hasTrailingSpace) currentX += wordSpacing;
850
807
  }
851
808
  };
809
+ const renderSingleWord = (doc, word, x, y, store) => {
810
+ const savedFont = doc.getFont();
811
+ const savedSize = doc.getFontSize();
812
+ const savedColor = doc.getTextColor();
813
+ applyStyleToDoc(doc, word.style, store);
814
+ if (word.isLink && word.linkColor) doc.setTextColor(...word.linkColor);
815
+ if (word.isImage && word.imageElement?.data) renderInlineImage(doc, word, x, y);
816
+ else {
817
+ if (word.style === "codespan") renderCodespanBackground(doc, word, x, y, store);
818
+ doc.text(word.text, x, y, { baseline: "top" });
819
+ }
820
+ if (word.isLink && word.href) {
821
+ const h = word.isImage && word.imageHeight ? word.imageHeight : doc.getTextDimensions("H").h;
822
+ doc.link(x, y, word.width, h, { url: word.href });
823
+ }
824
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
825
+ doc.setFontSize(savedSize);
826
+ doc.setTextColor(savedColor);
827
+ };
828
+ const renderCodespanBackground = (doc, word, x, y, store) => {
829
+ const opts = store.options.codespan ?? {};
830
+ if (opts.showBackground === false) return;
831
+ const bg = opts.backgroundColor ?? "#EEEEEE";
832
+ const pad = opts.padding ?? .8;
833
+ const h = doc.getTextDimensions("H").h;
834
+ doc.setFillColor(bg);
835
+ doc.rect(x - pad, y - pad, word.width + pad * 2, h + pad * 2, "F");
836
+ doc.setFillColor("#000000");
837
+ };
838
+ const renderInlineImage = (doc, word, x, y) => {
839
+ const el = word.imageElement;
840
+ let fmt = "JPEG";
841
+ if (el.data.startsWith("data:image/png")) fmt = "PNG";
842
+ else if (el.data.startsWith("data:image/webp")) fmt = "WEBP";
843
+ else if (el.data.startsWith("data:image/gif")) fmt = "GIF";
844
+ if (word.width > 0 && (word.imageHeight || 0) > 0) doc.addImage(el.data, fmt, x, y, word.width, word.imageHeight);
845
+ };
852
846
  //#endregion
853
- //#region src/renderer/components/heading.ts
847
+ //#region src/layout/layoutEngine.ts
854
848
  /**
855
- * Renders heading elements.
849
+ * THE single entry point for rendering any mixed inline content.
850
+ * All paragraph, heading, list item, and blockquote text must go through here.
851
+ *
852
+ * Handles: word splitting, line breaking, page breaks, styled rendering.
853
+ * Returns the final Y position after rendering.
856
854
  */
857
- const renderHeading = (doc, element, indent, store, parentElementRenderer) => {
858
- const size = 6 - (element?.depth ?? 0) > 0 ? 6 - (element?.depth ?? 0) : 1;
859
- doc.setFontSize(store.options.page.defaultFontSize + size);
860
- if (element?.items && element?.items.length > 0) {
861
- const originalLineHeightFactor = store.options.page.defaultLineHeightFactor;
862
- store.options.page.defaultLineHeightFactor = 1;
863
- JustifiedTextRenderer.renderStyledParagraph(doc, element.items, store.X + indent, store.Y, store.options.page.maxContentWidth - indent, store, "left");
864
- store.options.page.defaultLineHeightFactor = originalLineHeightFactor;
865
- } else {
866
- const charHeight = getCharHight(doc);
867
- doc.text(element?.content ?? "", store.X + indent, store.Y, {
868
- align: "left",
869
- maxWidth: store.options.page.maxContentWidth - indent,
870
- baseline: "top"
871
- });
872
- store.recordContentY(store.Y + charHeight);
873
- store.updateY(getCharHight(doc), "add");
855
+ const renderInlineContent = (doc, elements, x, y, maxWidth, store, opts = {}) => {
856
+ const alignment = opts.alignment ?? store.options.content?.textAlignment ?? "left";
857
+ const trimLastLine = opts.trimLastLine ?? false;
858
+ const words = flattenToWords(doc, elements, store);
859
+ if (words.length === 0) return y;
860
+ const lines = breakIntoLines(doc, words, maxWidth, store);
861
+ let currentY = y;
862
+ for (let i = 0; i < lines.length; i++) {
863
+ const line = lines[i];
864
+ if (currentY + line.lineHeight > store.options.page.maxContentHeight) {
865
+ HandlePageBreaks(doc, store);
866
+ currentY = store.Y;
867
+ }
868
+ renderLine(doc, line, x, currentY, maxWidth, store, alignment);
869
+ const yAdvance = i === lines.length - 1 && trimLastLine && !line.words.some((w) => w.isImage && w.imageHeight) ? getCharHight(doc) : line.lineHeight;
870
+ store.recordContentY(currentY + yAdvance);
871
+ currentY += yAdvance;
872
+ store.updateY(yAdvance, "add");
874
873
  }
875
- doc.setFontSize(store.options.page.defaultFontSize);
876
- store.updateX(store.options.page.xpading, "set");
874
+ const lastLine = lines[lines.length - 1];
875
+ if (lastLine) {
876
+ let spaceCount = 0;
877
+ for (let i = 0; i < lastLine.words.length - 1; i++) if (lastLine.words[i].hasTrailingSpace) spaceCount++;
878
+ const lastLineW = lastLine.totalTextWidth + spaceCount * doc.getTextWidth(" ");
879
+ store.updateX(x + lastLineW, "set");
880
+ }
881
+ return currentY;
882
+ };
883
+ /**
884
+ * Render a single string of plain (unstyled) text with wrapping and page breaks.
885
+ * Use this for simple text in list items and raw items where there are no inline styles.
886
+ */
887
+ const renderPlainText = (doc, text, x, y, maxWidth, store, opts = {}) => {
888
+ return renderInlineContent(doc, [{
889
+ type: "text",
890
+ content: text
891
+ }], x, y, maxWidth, store, opts);
877
892
  };
878
893
  //#endregion
879
- //#region src/utils/text-renderer.ts
880
- var TextRenderer = class {
881
- /**
882
- * Renders text with automatic line wrapping and page breaking.
883
- * @param doc jsPDF instance
884
- * @param text Text to render
885
- * @param store RenderStore instance to use
886
- * @param x X coordinate (if not provided, uses store.X)
887
- * @param y Y coordinate (if not provided, uses store.Y)
888
- * @param maxWidth Max width for text wrapping
889
- * @param justify Whether to justify the text
890
- */
891
- static renderText(doc, text, store, x = store.X, y = store.Y, maxWidth, justify = false) {
892
- const lines = doc.splitTextToSize(text, maxWidth);
893
- const charHeight = getCharHight(doc);
894
- const lineHeight = charHeight * store.options.page.defaultLineHeightFactor;
895
- let currentY = y;
896
- for (let i = 0; i < lines.length; i++) {
897
- const line = lines[i];
898
- if (currentY + lineHeight > store.options.page.maxContentHeight) {
899
- HandlePageBreaks(doc, store);
900
- currentY = store.Y;
901
- }
902
- if (justify) if (i === lines.length - 1) doc.text(line, x, currentY, { baseline: "top" });
903
- else doc.text(line, x, currentY, {
904
- maxWidth,
905
- align: "justify",
906
- baseline: "top"
907
- });
908
- else doc.text(line, x, currentY, { baseline: "top" });
909
- store.recordContentY(currentY + charHeight);
910
- currentY += lineHeight;
911
- store.updateY(lineHeight, "add");
912
- }
913
- return currentY;
914
- }
894
+ //#region src/renderer/components/heading.ts
895
+ const renderHeading = (doc, element, indent, store) => {
896
+ const savedColor = doc.getTextColor();
897
+ const headingKey = `h${element?.depth ?? 1}`;
898
+ const fontSize = store.options.heading?.[headingKey] ?? store.options.page.defaultFontSize;
899
+ const headingColor = store.options.heading?.[`${headingKey}Color`] ?? store.options.heading?.color ?? "#000000";
900
+ const savedSize = doc.getFontSize();
901
+ doc.setFontSize(fontSize);
902
+ doc.setFont(store.options.font.bold.name, store.options.font.bold.style || "bold");
903
+ doc.setTextColor(headingColor);
904
+ breakIfOverflow(doc, store, getCharHight(doc) * 1.8);
905
+ const maxWidth = store.options.page.maxContentWidth - indent;
906
+ if (element.items && element.items.length > 0) renderInlineContent(doc, element.items, store.X + indent, store.Y, maxWidth, store, {
907
+ alignment: "left",
908
+ trimLastLine: true
909
+ });
910
+ else renderPlainText(doc, element?.content ?? "", store.X + indent, store.Y, maxWidth, store, {
911
+ alignment: "left",
912
+ trimLastLine: true
913
+ });
914
+ const bottomSpacing = store.options.heading?.bottomSpacing ?? store.options.spacing?.afterHeading ?? 2;
915
+ store.updateY(bottomSpacing, "add");
916
+ doc.setFontSize(savedSize);
917
+ doc.setFont(store.options.font.regular.name, store.options.font.regular.style);
918
+ doc.setTextColor(savedColor);
919
+ store.updateX(store.options.page.xpading, "set");
915
920
  };
916
921
  //#endregion
917
922
  //#region src/renderer/components/paragraph.ts
918
- /**
919
- * Renders paragraph elements with proper text alignment.
920
- * Handles mixed inline styles (bold, italic, codespan) and links.
921
- * Respects user's textAlignment option from RenderStore.
922
- */
923
923
  const renderParagraph = (doc, element, indent, store, parentElementRenderer) => {
924
- store.activateInlineLock();
924
+ const indentLevel = indent / store.options.page.indent;
925
+ const savedColor = doc.getTextColor();
925
926
  doc.setFontSize(store.options.page.defaultFontSize);
927
+ doc.setFont(store.options.font.regular.name, store.options.font.regular.style);
928
+ doc.setTextColor(store.options.paragraph?.color ?? "#000000");
926
929
  const maxWidth = store.options.page.maxContentWidth - indent;
927
- if (element?.items && element?.items.length > 0) {
930
+ if (element.items && element.items.length > 0) {
928
931
  if (element.items.length === 1 && element.items[0].type === "image") {
929
- parentElementRenderer(element.items[0], indent, store, false);
932
+ parentElementRenderer(element.items[0], indentLevel, store, false);
930
933
  store.updateX(store.options.page.xpading);
931
- store.deactivateInlineLock();
934
+ doc.setTextColor(savedColor);
932
935
  return;
933
936
  }
934
937
  const inlineTypes = [
@@ -942,35 +945,38 @@ const renderParagraph = (doc, element, indent, store, parentElementRenderer) =>
942
945
  ];
943
946
  if (element.items.some((item) => !inlineTypes.includes(item.type))) {
944
947
  const inlineBuffer = [];
945
- const flushInlineBuffer = () => {
948
+ const flush = () => {
946
949
  if (inlineBuffer.length > 0) {
947
- JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, store.X + indent, store.Y, maxWidth, store);
950
+ renderInlineContent(doc, inlineBuffer, store.X + indent, store.Y, maxWidth, store, { trimLastLine: true });
948
951
  inlineBuffer.length = 0;
949
952
  }
950
953
  };
951
954
  for (const item of element.items) if (inlineTypes.includes(item.type)) inlineBuffer.push(item);
952
955
  else {
953
- flushInlineBuffer();
954
- parentElementRenderer(item, indent, store, false);
956
+ flush();
957
+ parentElementRenderer(item, indentLevel, store, false);
955
958
  }
956
- flushInlineBuffer();
957
- } else JustifiedTextRenderer.renderStyledParagraph(doc, element.items, store.X + indent, store.Y, maxWidth, store);
958
- } else {
959
- const content = element.content ?? "";
960
- const textAlignment = store.options.content?.textAlignment ?? "left";
961
- if (content.trim()) TextRenderer.renderText(doc, content, store, store.X + indent, store.Y, maxWidth, textAlignment === "justify");
962
- }
959
+ flush();
960
+ } else renderInlineContent(doc, element.items, store.X + indent, store.Y, maxWidth, store, { trimLastLine: true });
961
+ } else if (element.content?.trim()) renderInlineContent(doc, [{
962
+ type: "text",
963
+ content: element.content
964
+ }], store.X + indent, store.Y, maxWidth, store, { trimLastLine: true });
965
+ const bottomSpacing = store.options.paragraph?.bottomSpacing ?? store.options.spacing?.afterParagraph ?? store.options.page.lineSpace;
966
+ store.updateY(bottomSpacing, "add");
963
967
  store.updateX(store.options.page.xpading);
964
- store.deactivateInlineLock();
968
+ doc.setTextColor(savedColor);
965
969
  };
966
970
  //#endregion
967
971
  //#region src/renderer/components/list.ts
968
972
  const renderList = (doc, element, indentLevel, store, parentElementRenderer) => {
969
973
  doc.setFontSize(store.options.page.defaultFontSize);
970
974
  for (const [i, point] of element?.items?.entries() ?? []) {
971
- const _start = element.ordered ? (element.start ?? 0) + i : element.start;
975
+ const _start = element.ordered ? (element.start ?? 1) + i : void 0;
972
976
  parentElementRenderer(point, indentLevel + 1, store, true, _start, element.ordered);
977
+ if (i < (element.items?.length ?? 0) - 1) store.updateY(store.options.spacing?.betweenListItems ?? 0, "add");
973
978
  }
979
+ store.updateY(store.options.spacing?.afterList ?? 3, "add");
974
980
  };
975
981
  //#endregion
976
982
  //#region src/renderer/components/listItem.ts
@@ -978,22 +984,46 @@ const renderList = (doc, element, indentLevel, store, parentElementRenderer) =>
978
984
  * Render a single list item, including bullets/numbering, inline text, and any nested lists.
979
985
  */
980
986
  const renderListItem = (doc, element, indentLevel, store, parentElementRenderer, start, ordered) => {
981
- if (store.Y + getCharHight(doc) >= store.options.page.maxContentHeight) HandlePageBreaks(doc, store);
987
+ breakIfOverflow(doc, store, getCharHight(doc));
982
988
  const options = store.options;
983
- const baseIndent = indentLevel * options.page.indent;
984
- const bullet = ordered ? `${start}. ` : "• ";
989
+ const listOpts = store.options.list ?? {};
990
+ const baseIndent = indentLevel * (listOpts.indentSize ?? options.page.indent);
991
+ const bullet = ordered ? `${start}. ` : listOpts.bulletChar ?? "• ";
985
992
  const xLeft = options.page.xpading;
986
993
  store.updateX(xLeft, "set");
987
994
  doc.setFont(options.font.regular.name, options.font.regular.style);
988
- doc.text(bullet, xLeft + baseIndent, store.Y, { baseline: "top" });
989
- const bulletWidth = doc.getTextWidth(bullet);
990
- const contentX = xLeft + baseIndent + bulletWidth;
995
+ let contentX = xLeft + baseIndent;
996
+ let bulletWidth;
997
+ if (element.task) {
998
+ const cbSize = doc.getFontSize() * .5;
999
+ const cbX = xLeft + baseIndent;
1000
+ const cbY = store.Y + (getCharHight(doc) - cbSize) / 2;
1001
+ doc.setDrawColor("#555555");
1002
+ doc.setLineWidth(.4);
1003
+ doc.rect(cbX, cbY, cbSize, cbSize);
1004
+ if (element.checked) {
1005
+ doc.setDrawColor("#2B6CB0");
1006
+ doc.setLineWidth(.5);
1007
+ const pad = cbSize * .2;
1008
+ doc.line(cbX + pad, cbY + cbSize * .55, cbX + cbSize * .4, cbY + cbSize - pad);
1009
+ doc.line(cbX + cbSize * .4, cbY + cbSize - pad, cbX + cbSize - pad, cbY + pad);
1010
+ doc.setDrawColor("#000000");
1011
+ }
1012
+ doc.setLineWidth(.1);
1013
+ bulletWidth = cbSize + 2;
1014
+ } else {
1015
+ doc.text(bullet, xLeft + baseIndent, store.Y, { baseline: "top" });
1016
+ bulletWidth = doc.getTextWidth(bullet);
1017
+ }
1018
+ contentX += bulletWidth;
991
1019
  const textMaxWidth = options.page.maxContentWidth - baseIndent - bulletWidth;
1020
+ const originalTextColor = doc.getTextColor();
1021
+ if (element.checked) doc.setTextColor(150, 150, 150);
992
1022
  if (element.items && element.items.length > 0) {
993
1023
  const inlineBuffer = [];
994
1024
  const flushInlineBuffer = () => {
995
1025
  if (inlineBuffer.length > 0) {
996
- JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, contentX, store.Y, textMaxWidth, store);
1026
+ renderInlineContent(doc, inlineBuffer, contentX, store.Y, textMaxWidth, store);
997
1027
  inlineBuffer.length = 0;
998
1028
  store.updateX(xLeft, "set");
999
1029
  }
@@ -1006,10 +1036,8 @@ const renderListItem = (doc, element, indentLevel, store, parentElementRenderer,
1006
1036
  parentElementRenderer(subItem, indentLevel, store, true, start, ordered);
1007
1037
  } else inlineBuffer.push(subItem);
1008
1038
  flushInlineBuffer();
1009
- } else if (element.content) {
1010
- const textAlignment = options.content?.textAlignment ?? "left";
1011
- TextRenderer.renderText(doc, element.content, store, contentX, store.Y, textMaxWidth, textAlignment === "justify");
1012
- }
1039
+ } else if (element.content) renderPlainText(doc, element.content, contentX, store.Y, textMaxWidth, store);
1040
+ doc.setTextColor(originalTextColor);
1013
1041
  };
1014
1042
  //#endregion
1015
1043
  //#region src/renderer/components/rawItem.ts
@@ -1018,19 +1046,18 @@ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentEle
1018
1046
  else {
1019
1047
  const options = store.options;
1020
1048
  const indent = indentLevel * options.page.indent;
1021
- const bullet = hasRawBullet ? ordered ? `${start}. ` : "• " : "";
1049
+ const listOpts = store.options.list ?? {};
1050
+ const bullet = hasRawBullet ? ordered ? `${start}. ` : listOpts.bulletChar ?? "• " : "";
1022
1051
  const content = element.content || "";
1023
1052
  const xLeft = options.page.xpading;
1024
1053
  if (!content && !bullet) return;
1025
1054
  if (!content.trim() && !bullet) {
1026
1055
  const newlines = (content.match(/\n/g) || []).length;
1027
1056
  if (newlines > 1) {
1028
- const addedHeight = (newlines - 1) * (doc.getTextDimensions("A").h * options.page.defaultLineHeightFactor);
1029
- if (store.Y + addedHeight > options.page.maxContentHeight) HandlePageBreaks(doc, store);
1030
- else {
1031
- store.updateY(addedHeight, "add");
1032
- store.recordContentY(store.Y);
1033
- }
1057
+ const addedHeight = (newlines - 1) * (getCharHight(doc) * options.page.defaultLineHeightFactor);
1058
+ breakIfOverflow(doc, store, addedHeight);
1059
+ store.updateY(addedHeight, "add");
1060
+ store.recordContentY(store.Y);
1034
1061
  }
1035
1062
  return;
1036
1063
  }
@@ -1040,10 +1067,10 @@ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentEle
1040
1067
  const textMaxWidth = options.page.maxContentWidth - indent - bulletWidth;
1041
1068
  doc.setFont(options.font.regular.name, options.font.regular.style);
1042
1069
  doc.text(bullet, xLeft + indent, store.Y, { baseline: "top" });
1043
- TextRenderer.renderText(doc, content, store, xLeft + indent + bulletWidth, store.Y, textMaxWidth, justify);
1070
+ renderPlainText(doc, content, xLeft + indent + bulletWidth, store.Y, textMaxWidth, store, { alignment: justify ? "justify" : "left" });
1044
1071
  } else {
1045
1072
  const textMaxWidth = options.page.maxContentWidth - indent;
1046
- TextRenderer.renderText(doc, content, store, xLeft + indent, store.Y, textMaxWidth, justify);
1073
+ renderPlainText(doc, content, xLeft + indent, store.Y, textMaxWidth, store, { alignment: justify ? "justify" : "left" });
1047
1074
  }
1048
1075
  store.updateX(xLeft, "set");
1049
1076
  }
@@ -1051,6 +1078,9 @@ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentEle
1051
1078
  //#endregion
1052
1079
  //#region src/renderer/components/hr.ts
1053
1080
  const renderHR = (doc, store) => {
1081
+ const savedDrawColor = doc.getDrawColor();
1082
+ const savedLineWidth = doc.getLineWidth();
1083
+ breakIfOverflow(doc, store, getCharHight(doc));
1054
1084
  const pageWidth = doc.internal.pageSize.getWidth();
1055
1085
  doc.setLineDashPattern([1, 1], 0);
1056
1086
  doc.setLineWidth(.1);
@@ -1058,21 +1088,30 @@ const renderHR = (doc, store) => {
1058
1088
  doc.setLineWidth(.1);
1059
1089
  doc.setLineDashPattern([], 0);
1060
1090
  store.updateY(getCharHight(doc), "add");
1091
+ store.updateY(store.options.spacing?.afterHR ?? 2, "add");
1092
+ doc.setDrawColor(savedDrawColor);
1093
+ doc.setLineWidth(savedLineWidth);
1061
1094
  };
1062
1095
  //#endregion
1063
1096
  //#region src/renderer/components/code.ts
1064
1097
  const renderCodeBlock = (doc, element, indentLevel, store) => {
1065
1098
  const savedFont = doc.getFont();
1066
1099
  const savedFontSize = doc.getFontSize();
1100
+ const savedTextColor = doc.getTextColor();
1101
+ const savedDrawColor = doc.getDrawColor();
1102
+ const savedFillColor = doc.getFillColor();
1067
1103
  const codeFont = store.options.font.code || {
1068
1104
  name: "courier",
1069
1105
  style: "normal"
1070
1106
  };
1071
1107
  doc.setFont(codeFont.name, codeFont.style);
1072
- const codeFontSize = store.options.page.defaultFontSize * .9;
1108
+ const codeOpts = store.options.codeBlock ?? {};
1109
+ const codeFontSizeScale = codeOpts.fontSizeScale ?? .9;
1110
+ const codeFontSize = store.options.page.defaultFontSize * codeFontSizeScale;
1073
1111
  doc.setFontSize(codeFontSize);
1074
1112
  const indent = indentLevel * store.options.page.indent;
1075
- const maxWidth = store.options.page.maxContentWidth - indent - 8;
1113
+ const padding = codeOpts.padding ?? 4;
1114
+ const maxWidth = store.options.page.maxContentWidth - indent - padding * 2;
1076
1115
  const lineHeightFactor = doc.getLineHeightFactor();
1077
1116
  const lineHeight = codeFontSize / doc.internal.scaleFactor * lineHeightFactor;
1078
1117
  const content = (element.code ?? "").replace(/[\r\n\s]+$/, "");
@@ -1088,9 +1127,9 @@ const renderCodeBlock = (doc, element, indentLevel, store) => {
1088
1127
  doc.setFontSize(savedFontSize);
1089
1128
  return;
1090
1129
  }
1091
- const padding = 4;
1092
- const bgColor = "#EEEEEE";
1093
- const drawColor = "#DDDDDD";
1130
+ const bgColor = codeOpts.backgroundColor ?? "#F6F8FA";
1131
+ const drawColor = codeOpts.borderColor ?? "#E1E4E8";
1132
+ const radius = codeOpts.borderRadius ?? 2;
1094
1133
  let currentLineIndex = 0;
1095
1134
  while (currentLineIndex < lines.length) {
1096
1135
  const availableHeight = store.options.page.maxContentHeight - store.Y;
@@ -1109,20 +1148,22 @@ const renderCodeBlock = (doc, element, indentLevel, store) => {
1109
1148
  if (isFirstChunk) store.updateY(padding, "add");
1110
1149
  doc.setFillColor(bgColor);
1111
1150
  doc.setDrawColor(drawColor);
1112
- doc.roundedRect(store.X, store.Y - padding, store.options.page.maxContentWidth, textBlockHeight + (isFirstChunk ? padding : 0) + (isLastChunk ? padding : 0), 2, 2, "FD");
1113
- if (isFirstChunk && element.lang) {
1151
+ doc.roundedRect(store.X, store.Y - padding, store.options.page.maxContentWidth, textBlockHeight + (isFirstChunk ? padding : 0) + (isLastChunk ? padding : 0), radius, radius, "FD");
1152
+ if (isFirstChunk && element.lang && codeOpts.showLanguageLabel !== false) {
1114
1153
  const savedCodeFontSize = doc.getFontSize();
1115
1154
  doc.setFontSize(10);
1116
- doc.setTextColor("#666666");
1155
+ doc.setTextColor(codeOpts.labelColor ?? "#666666");
1117
1156
  doc.text(element.lang, store.X + store.options.page.maxContentWidth - doc.getTextWidth(element.lang) - 4, store.Y, { baseline: "top" });
1118
1157
  doc.setFontSize(savedCodeFontSize);
1119
- doc.setTextColor("#000000");
1158
+ doc.setTextColor(savedTextColor);
1120
1159
  }
1121
1160
  let yPos = store.Y;
1161
+ doc.setTextColor(codeOpts.textColor ?? "#000000");
1122
1162
  for (const line of linesToRender) {
1123
1163
  doc.text(line, store.X + 4, yPos, { baseline: "top" });
1124
1164
  yPos += lineHeight;
1125
1165
  }
1166
+ doc.setTextColor(savedTextColor);
1126
1167
  store.updateY(textBlockHeight, "add");
1127
1168
  store.recordContentY(store.Y + (isLastChunk ? padding : 0));
1128
1169
  if (isLastChunk) store.updateY(padding, "add");
@@ -1131,199 +1172,17 @@ const renderCodeBlock = (doc, element, indentLevel, store) => {
1131
1172
  }
1132
1173
  doc.setFont(savedFont.fontName, savedFont.fontStyle);
1133
1174
  doc.setFontSize(savedFontSize);
1134
- };
1135
- //#endregion
1136
- //#region src/renderer/components/inlineText.ts
1137
- /**
1138
- * Renders inline text elements (Strong, Em, and Text) with proper inline styling.
1139
- */
1140
- const renderInlineText = (doc, element, indent, store) => {
1141
- const currentFont = doc.getFont().fontName;
1142
- const currentFontStyle = doc.getFont().fontStyle;
1143
- const currentFontSize = doc.getFontSize();
1144
- const spaceMultiplier = (style) => {
1145
- switch (style) {
1146
- case "normal": return 0;
1147
- case "bold": return 1;
1148
- case "italic": return 1.5;
1149
- case "bolditalic": return 1.5;
1150
- case "codespan": return .5;
1151
- default: return 0;
1152
- }
1153
- };
1154
- const renderTextWithStyle = (text, style) => {
1155
- 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");
1156
- else if (style === "italic") doc.setFont(store.options.font.regular.name, "italic");
1157
- else if (style === "bolditalic") doc.setFont(store.options.font.bold.name && store.options.font.bold.name !== "" ? store.options.font.bold.name : currentFont, "bolditalic");
1158
- else if (style === "codespan") {
1159
- const codeFont = store.options.font.code || {
1160
- name: "courier",
1161
- style: "normal"
1162
- };
1163
- doc.setFont(codeFont.name, codeFont.style);
1164
- doc.setFontSize(currentFontSize * .9);
1165
- } else doc.setFont(store.options.font.regular.name, currentFontStyle);
1166
- const availableWidth = store.options.page.maxContentWidth - indent - store.X;
1167
- const textLines = doc.splitTextToSize(text, availableWidth);
1168
- const isCodeSpan = style === "codespan";
1169
- const codePadding = 1;
1170
- const codeBgColor = "#EEEEEE";
1171
- if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
1172
- if (isCodeSpan) {
1173
- const lineWidth = doc.getTextWidth(textLines[i]) + getCharWidth(doc);
1174
- const lineHeight = getCharHight(doc);
1175
- doc.setFillColor(codeBgColor);
1176
- doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, lineWidth + codePadding * 2, lineHeight + codePadding * 2, 2, 2, "F");
1177
- doc.setFillColor("#000000");
1178
- }
1179
- doc.text(textLines[i], store.X + indent, store.Y, {
1180
- baseline: "top",
1181
- maxWidth: availableWidth
1182
- });
1183
- store.updateX(doc.getTextDimensions(textLines[i]).w + (isCodeSpan ? codePadding * 2 : 1), "add");
1184
- if (i < textLines.length - 1) {
1185
- store.updateY(getCharHight(doc), "add");
1186
- store.updateX(store.options.page.xpading, "set");
1187
- }
1188
- }
1189
- else if (textLines.length > 1) {
1190
- const firstLine = textLines[0];
1191
- const restContent = textLines?.slice(1)?.join(" ");
1192
- if (isCodeSpan) {
1193
- const w = doc.getTextWidth(firstLine) + getCharWidth(doc);
1194
- const h = getCharHight(doc);
1195
- doc.setFillColor(codeBgColor);
1196
- doc.roundedRect(store.X + (indent >= 2 ? indent + 2 : 0) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
1197
- doc.setFillColor("#000000");
1198
- }
1199
- doc.text(firstLine, store.X + (indent >= 2 ? indent + 2 * spaceMultiplier(style) : 0), store.Y, {
1200
- baseline: "top",
1201
- maxWidth: availableWidth
1202
- });
1203
- store.updateX(store.options.page.xpading + indent);
1204
- store.updateY(getCharHight(doc), "add");
1205
- const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
1206
- doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
1207
- if (isCodeSpan) {
1208
- const w = doc.getTextWidth(line) + getCharWidth(doc);
1209
- const h = getCharHight(doc);
1210
- doc.setFillColor(codeBgColor);
1211
- doc.roundedRect(store.X + getCharWidth(doc) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
1212
- doc.setFillColor("#000000");
1213
- }
1214
- doc.text(line, store.X + getCharWidth(doc), store.Y, {
1215
- baseline: "top",
1216
- maxWidth: maxWidthForRest
1217
- });
1218
- });
1219
- } else {
1220
- if (isCodeSpan) {
1221
- const w = doc.getTextWidth(text) + getCharWidth(doc);
1222
- const h = getCharHight(doc);
1223
- doc.setFillColor(codeBgColor);
1224
- doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
1225
- doc.setFillColor("#000000");
1226
- }
1227
- doc.text(text, store.X + indent, store.Y, {
1228
- baseline: "top",
1229
- maxWidth: availableWidth
1230
- });
1231
- store.updateX(doc.getTextDimensions(text).w + (indent >= 2 ? text.split(" ").length + 2 : 2) * spaceMultiplier(style) * .5 + (isCodeSpan ? codePadding * 2 : 0), "add");
1232
- }
1233
- };
1234
- if (element.type === "text" && element.items && element.items.length > 0) for (const item of element.items) if (item.type === "codespan") renderTextWithStyle(item.content || "", "codespan");
1235
- else if (item.type === "em" || item.type === "strong") {
1236
- const baseStyle = item.type === "em" ? "italic" : "bold";
1237
- if (item.items && item.items.length > 0) for (const subItem of item.items) if (subItem.type === "strong" && baseStyle === "italic") renderTextWithStyle(subItem.content || "", "bolditalic");
1238
- else if (subItem.type === "em" && baseStyle === "bold") renderTextWithStyle(subItem.content || "", "bolditalic");
1239
- else renderTextWithStyle(subItem.content || "", baseStyle);
1240
- else renderTextWithStyle(item.content || "", baseStyle);
1241
- } else renderTextWithStyle(item.content || "", "normal");
1242
- else if (element.type === "em") renderTextWithStyle(element.content || "", "italic");
1243
- else if (element.type === "strong") renderTextWithStyle(element.content || "", "bold");
1244
- else if (element.type === "codespan") renderTextWithStyle(element.content || "", "codespan");
1245
- else renderTextWithStyle(element.content || "", "normal");
1246
- doc.setFont(currentFont, currentFontStyle);
1247
- doc.setFontSize(currentFontSize);
1248
- };
1249
- //#endregion
1250
- //#region src/renderer/components/link.ts
1251
- /**
1252
- * Renders link elements with proper styling and URL handling.
1253
- * Links are rendered in blue color and underlined to distinguish them from regular text.
1254
- */
1255
- const renderLink = (doc, element, indent, store) => {
1256
- const currentFont = doc.getFont().fontName;
1257
- const currentFontStyle = doc.getFont().fontStyle;
1258
- const currentFontSize = doc.getFontSize();
1259
- const currentTextColor = doc.getTextColor();
1260
- const linkColor = store.options.link?.linkColor || [
1261
- 0,
1262
- 0,
1263
- 255
1264
- ];
1265
- doc.setTextColor(...linkColor);
1266
- const availableWidth = store.options.page.maxContentWidth - indent - store.X;
1267
- const linkText = element.text || element.content || "";
1268
- const linkUrl = element.href || "";
1269
- const textLines = doc.splitTextToSize(linkText, availableWidth);
1270
- if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
1271
- const textWidth = doc.getTextDimensions(textLines[i]).w;
1272
- const textHeight = getCharHight(doc) / 2;
1273
- doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
1274
- doc.text(textLines[i], store.X + indent, store.Y, {
1275
- baseline: "top",
1276
- maxWidth: availableWidth
1277
- });
1278
- store.updateX(textWidth + 1, "add");
1279
- if (store.X + textWidth > store.options.page.maxContentWidth - indent) {
1280
- store.updateY(textHeight, "add");
1281
- store.updateX(store.options.page.xpading + indent, "set");
1282
- }
1283
- if (i < textLines.length - 1) {
1284
- store.updateY(textHeight, "add");
1285
- store.updateX(store.options.page.xpading + indent, "set");
1286
- }
1287
- }
1288
- else if (textLines.length > 1) {
1289
- const firstLine = textLines[0];
1290
- const restContent = textLines?.slice(1)?.join(" ");
1291
- const firstLineWidth = doc.getTextDimensions(firstLine).w;
1292
- const textHeight = getCharHight(doc) / 2;
1293
- doc.link(store.X + indent, store.Y, firstLineWidth, textHeight, { url: linkUrl });
1294
- doc.text(firstLine, store.X + indent, store.Y, {
1295
- baseline: "top",
1296
- maxWidth: availableWidth
1297
- });
1298
- store.updateX(store.options.page.xpading + indent);
1299
- store.updateY(textHeight, "add");
1300
- const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
1301
- doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
1302
- const lineWidth = doc.getTextDimensions(line).w;
1303
- doc.link(store.X + getCharWidth(doc), store.Y, lineWidth, textHeight, { url: linkUrl });
1304
- doc.text(line, store.X + getCharWidth(doc), store.Y, {
1305
- baseline: "top",
1306
- maxWidth: maxWidthForRest
1307
- });
1308
- });
1309
- } else {
1310
- const textWidth = doc.getTextDimensions(linkText).w;
1311
- const textHeight = getCharHight(doc) / 2;
1312
- doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
1313
- doc.text(linkText, store.X + indent, store.Y, {
1314
- baseline: "top",
1315
- maxWidth: availableWidth
1316
- });
1317
- store.updateX(textWidth + 2, "add");
1318
- }
1319
- doc.setFont(currentFont, currentFontStyle);
1320
- doc.setFontSize(currentFontSize);
1321
- doc.setTextColor(currentTextColor);
1175
+ doc.setTextColor(savedTextColor);
1176
+ doc.setDrawColor(savedDrawColor);
1177
+ doc.setFillColor(savedFillColor);
1178
+ store.updateY(store.options.spacing?.afterCodeBlock ?? 3, "add");
1322
1179
  };
1323
1180
  //#endregion
1324
1181
  //#region src/renderer/components/blockquote.ts
1325
1182
  const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
1326
1183
  const options = store.options;
1184
+ const savedDrawColor = doc.getDrawColor();
1185
+ const savedLineWidth = doc.getLineWidth();
1327
1186
  const blockquoteIndent = indentLevel + 1;
1328
1187
  const currentX = store.X + indentLevel * options.page.indent;
1329
1188
  const currentY = store.Y;
@@ -1333,10 +1192,13 @@ const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
1333
1192
  if (element.items && element.items.length > 0) element.items.forEach((item) => {
1334
1193
  renderElement(item, blockquoteIndent, store);
1335
1194
  });
1336
- const endY = store.Y;
1195
+ const endY = store.lastContentY || store.Y;
1337
1196
  const endPage = doc.internal.getCurrentPageInfo().pageNumber;
1338
- doc.setDrawColor(100);
1339
- doc.setLineWidth(1);
1197
+ const bqOpts = store.options.blockquote ?? {};
1198
+ const barColor = bqOpts.barColor ?? "#AAAAAA";
1199
+ const barWidth = bqOpts.barWidth ?? 1;
1200
+ doc.setDrawColor(barColor);
1201
+ doc.setLineWidth(barWidth);
1340
1202
  for (let p = startPage; p <= endPage; p++) {
1341
1203
  doc.setPage(p);
1342
1204
  const isStart = p === startPage;
@@ -1347,6 +1209,10 @@ const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
1347
1209
  }
1348
1210
  store.recordContentY();
1349
1211
  doc.setPage(endPage);
1212
+ const bqBottomSpacing = bqOpts.bottomSpacing ?? options.spacing?.afterBlockquote ?? options.page.lineSpace;
1213
+ store.updateY(bqBottomSpacing, "add");
1214
+ doc.setDrawColor(savedDrawColor);
1215
+ doc.setLineWidth(savedLineWidth);
1350
1216
  };
1351
1217
  //#endregion
1352
1218
  //#region src/renderer/components/image.ts
@@ -1416,9 +1282,10 @@ const renderImage = (doc, element, indentLevel, store) => {
1416
1282
  const imgFormat = detectImageFormat(element);
1417
1283
  if (finalWidth > 0 && finalHeight > 0) doc.addImage(element.data, imgFormat, drawX, currentY, finalWidth, finalHeight);
1418
1284
  store.updateY(finalHeight, "add");
1285
+ store.updateY(store.options.spacing?.afterImage ?? 2, "add");
1419
1286
  store.recordContentY();
1420
1287
  } catch (e) {
1421
- console.warn("Failed to render image", e);
1288
+ console.warn("[jspdf-md-renderer] Failed to render image", e);
1422
1289
  }
1423
1290
  };
1424
1291
  //#endregion
@@ -1431,34 +1298,56 @@ const resolveAutoTable = () => {
1431
1298
  throw new Error("Could not resolve jspdf-autotable export. Expected a callable export.");
1432
1299
  };
1433
1300
  const renderTable = (doc, element, indentLevel, store) => {
1434
- if (!element.header || !element.rows) return;
1301
+ if (!element.header || element.header.length === 0) {
1302
+ console.warn("[jspdf-md-renderer] Table skipped: no header row");
1303
+ return;
1304
+ }
1435
1305
  const options = store.options;
1436
1306
  const marginLeft = options.page.xmargin + indentLevel * options.page.indent;
1307
+ ensureSpace(doc, store, 20);
1308
+ const columnCount = element.header.length;
1309
+ const rows = (element.rows ?? []).map((row) => {
1310
+ const normalized = [...row];
1311
+ while (normalized.length < columnCount) normalized.push({
1312
+ type: "table_cell",
1313
+ content: ""
1314
+ });
1315
+ return normalized.slice(0, columnCount).map((cell) => cell.content || "");
1316
+ });
1437
1317
  const head = [element.header.map((h) => h.content || "")];
1438
- const body = element.rows.map((row) => row.map((cell) => cell.content || ""));
1439
1318
  const userTableOptions = options.table || {};
1319
+ const safeDidDrawPage = (data) => {
1320
+ try {
1321
+ if (userTableOptions.didDrawPage) userTableOptions.didDrawPage(data);
1322
+ } catch (e) {
1323
+ console.warn("[jspdf-md-renderer] table.didDrawPage callback threw:", e);
1324
+ }
1325
+ };
1326
+ const safeDidDrawCell = (data) => {
1327
+ try {
1328
+ if (userTableOptions.didDrawCell) userTableOptions.didDrawCell(data);
1329
+ } catch (e) {
1330
+ console.warn("[jspdf-md-renderer] table.didDrawCell callback threw:", e);
1331
+ }
1332
+ };
1440
1333
  resolveAutoTable()(doc, {
1441
1334
  head,
1442
- body,
1335
+ body: rows,
1443
1336
  startY: store.Y,
1444
1337
  margin: {
1445
1338
  left: marginLeft,
1446
1339
  right: options.page.xmargin
1447
1340
  },
1448
1341
  ...userTableOptions,
1449
- didDrawPage: (data) => {
1450
- if (userTableOptions.didDrawPage) userTableOptions.didDrawPage(data);
1451
- },
1452
- didDrawCell: (data) => {
1453
- if (userTableOptions.didDrawCell) userTableOptions.didDrawCell(data);
1454
- }
1342
+ didDrawPage: safeDidDrawPage,
1343
+ didDrawCell: safeDidDrawCell
1455
1344
  });
1456
1345
  const finalY = doc.lastAutoTable?.finalY;
1457
1346
  if (typeof finalY === "number") {
1458
- store.updateY(finalY + options.page.lineSpace, "set");
1347
+ store.updateY(finalY + (options.spacing?.afterTable ?? 3), "set");
1459
1348
  store.updateX(options.page.xpading, "set");
1460
1349
  store.recordContentY();
1461
- }
1350
+ } else console.warn("[jspdf-md-renderer] autoTable did not return a finalY. Y position may be incorrect.");
1462
1351
  };
1463
1352
  //#endregion
1464
1353
  //#region src/store/renderStore.ts
@@ -1539,65 +1428,220 @@ var RenderStore = class {
1539
1428
  };
1540
1429
  //#endregion
1541
1430
  //#region src/utils/options-validation.ts
1542
- const defaultOptions = {
1543
- page: {
1544
- indent: 10,
1545
- maxContentWidth: 190,
1546
- maxContentHeight: 277,
1547
- lineSpace: 1.5,
1548
- defaultLineHeightFactor: 1.2,
1549
- defaultFontSize: 12,
1550
- defaultTitleFontSize: 14,
1551
- topmargin: 10,
1552
- xpading: 10,
1553
- xmargin: 10,
1554
- format: "a4",
1555
- orientation: "p"
1431
+ const DEFAULT_HEADING_SIZES = {
1432
+ h1: 24,
1433
+ h2: 20,
1434
+ h3: 17,
1435
+ h4: 15,
1436
+ h5: 13,
1437
+ h6: 12,
1438
+ bottomSpacing: 2
1439
+ };
1440
+ const DEFAULT_FONT = {
1441
+ bold: {
1442
+ name: "helvetica",
1443
+ style: "bold"
1556
1444
  },
1557
- font: {
1558
- bold: {
1559
- name: "helvetica",
1560
- style: "bold"
1561
- },
1562
- regular: {
1563
- name: "helvetica",
1564
- style: "normal"
1565
- },
1566
- light: {
1567
- name: "helvetica",
1568
- style: "light"
1569
- },
1570
- code: {
1571
- name: "courier",
1572
- style: "normal"
1573
- }
1445
+ regular: {
1446
+ name: "helvetica",
1447
+ style: "normal"
1574
1448
  },
1575
- image: { defaultAlign: "left" }
1449
+ light: {
1450
+ name: "helvetica",
1451
+ style: "light"
1452
+ },
1453
+ code: {
1454
+ name: "courier",
1455
+ style: "normal"
1456
+ }
1457
+ };
1458
+ const DEFAULT_PAGE = {
1459
+ format: "a4",
1460
+ unit: "mm",
1461
+ orientation: "portrait",
1462
+ maxContentWidth: 190,
1463
+ maxContentHeight: 277,
1464
+ lineSpace: 3,
1465
+ defaultLineHeightFactor: 1.4,
1466
+ defaultFontSize: 11,
1467
+ defaultTitleFontSize: 14,
1468
+ topmargin: 10,
1469
+ xpading: 10,
1470
+ xmargin: 10,
1471
+ indent: 8
1576
1472
  };
1577
1473
  const validateOptions = (options) => {
1578
- if (!options) throw new Error("RenderOption is required");
1579
- const mergedPage = {
1580
- ...defaultOptions.page,
1474
+ if (!options) throw new Error("[jspdf-md-renderer] RenderOption is required");
1475
+ const page = {
1476
+ ...DEFAULT_PAGE,
1581
1477
  ...options.page
1582
1478
  };
1583
- const mergedFont = {
1584
- ...defaultOptions.font,
1479
+ if (page.maxContentWidth <= 0) throw new Error("[jspdf-md-renderer] page.maxContentWidth must be > 0");
1480
+ if (page.maxContentHeight <= 0) throw new Error("[jspdf-md-renderer] page.maxContentHeight must be > 0");
1481
+ if (page.indent < 0) throw new Error("[jspdf-md-renderer] page.indent must be >= 0");
1482
+ if (page.defaultFontSize < 1) throw new Error("[jspdf-md-renderer] page.defaultFontSize must be >= 1");
1483
+ if (page.defaultLineHeightFactor < 1) page.defaultLineHeightFactor = 1.4;
1484
+ if (!options.font?.regular?.name) throw new Error("[jspdf-md-renderer] font.regular.name is required");
1485
+ const font = {
1486
+ ...DEFAULT_FONT,
1585
1487
  ...options.font
1586
1488
  };
1587
- const mergedImage = {
1588
- ...defaultOptions.image,
1589
- ...options.image
1489
+ if (!font.bold?.name) font.bold = DEFAULT_FONT.bold;
1490
+ if (!font.code?.name) font.code = DEFAULT_FONT.code;
1491
+ const heading = {
1492
+ ...DEFAULT_HEADING_SIZES,
1493
+ ...options.heading ?? {}
1494
+ };
1495
+ [
1496
+ "h1",
1497
+ "h2",
1498
+ "h3",
1499
+ "h4",
1500
+ "h5",
1501
+ "h6"
1502
+ ].forEach((k) => {
1503
+ if (heading[k] < 6 || heading[k] > 72) heading[k] = DEFAULT_HEADING_SIZES[k];
1504
+ });
1505
+ const codespan = {
1506
+ backgroundColor: "#EEEEEE",
1507
+ padding: .8,
1508
+ showBackground: true,
1509
+ fontSizeScale: .88,
1510
+ ...options.codespan ?? {}
1511
+ };
1512
+ const blockquote = {
1513
+ barColor: "#AAAAAA",
1514
+ barWidth: 1,
1515
+ paddingLeft: 4,
1516
+ ...options.blockquote ?? {}
1517
+ };
1518
+ const list = {
1519
+ bulletChar: "• ",
1520
+ indentSize: page.indent,
1521
+ itemSpacing: 0,
1522
+ ...options.list ?? {}
1523
+ };
1524
+ const paragraph = {
1525
+ bottomSpacing: page.lineSpace,
1526
+ ...options.paragraph ?? {}
1527
+ };
1528
+ const codeBlock = {
1529
+ backgroundColor: "#F6F8FA",
1530
+ borderColor: "#E1E4E8",
1531
+ borderRadius: 2,
1532
+ padding: 4,
1533
+ fontSizeScale: .9,
1534
+ showLanguageLabel: true,
1535
+ ...options.codeBlock ?? {}
1536
+ };
1537
+ const spacing = {
1538
+ afterHeading: 2,
1539
+ afterParagraph: 3,
1540
+ afterCodeBlock: 3,
1541
+ afterBlockquote: 3,
1542
+ afterImage: 2,
1543
+ afterHR: 2,
1544
+ betweenListItems: 0,
1545
+ afterList: 3,
1546
+ afterTable: 3,
1547
+ ...options.spacing ?? {}
1590
1548
  };
1591
- if (!mergedPage.maxContentWidth) mergedPage.maxContentWidth = 190;
1592
- if (!mergedPage.maxContentHeight) mergedPage.maxContentHeight = 277;
1549
+ [
1550
+ "afterHeading",
1551
+ "afterParagraph",
1552
+ "afterCodeBlock",
1553
+ "afterBlockquote",
1554
+ "afterImage",
1555
+ "afterHR",
1556
+ "betweenListItems",
1557
+ "afterList",
1558
+ "afterTable"
1559
+ ].forEach((key) => {
1560
+ if ((spacing[key] ?? 0) < 0) spacing[key] = 0;
1561
+ });
1562
+ if ((heading.bottomSpacing ?? 0) < 0) heading.bottomSpacing = 0;
1563
+ if ((paragraph.bottomSpacing ?? 0) < 0) paragraph.bottomSpacing = 0;
1564
+ if ((blockquote.bottomSpacing ?? 0) < 0) blockquote.bottomSpacing = 0;
1565
+ const image = {
1566
+ defaultAlign: "left",
1567
+ ...options.image ?? {}
1568
+ };
1569
+ const endCursorYHandler = options.endCursorYHandler ?? (() => {});
1593
1570
  return {
1594
1571
  ...options,
1595
- page: mergedPage,
1596
- font: mergedFont,
1597
- image: mergedImage
1572
+ page,
1573
+ font,
1574
+ heading,
1575
+ codespan,
1576
+ blockquote,
1577
+ list,
1578
+ paragraph,
1579
+ codeBlock,
1580
+ spacing,
1581
+ image,
1582
+ endCursorYHandler
1598
1583
  };
1599
1584
  };
1600
1585
  //#endregion
1586
+ //#region src/utils/pageDecorations.ts
1587
+ const applyPageDecorations = (doc, options) => {
1588
+ const totalPages = doc.internal.getNumberOfPages();
1589
+ for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
1590
+ doc.setPage(pageNum);
1591
+ applyHeader(doc, options, pageNum, totalPages);
1592
+ applyFooter(doc, options, pageNum, totalPages);
1593
+ }
1594
+ };
1595
+ const applyHeader = (doc, options, pageNum, totalPages) => {
1596
+ const hOpts = options.header;
1597
+ if (!hOpts) return;
1598
+ const text = typeof hOpts.text === "function" ? hOpts.text(pageNum, totalPages) : hOpts.text ?? "";
1599
+ if (!text.trim()) return;
1600
+ const savedFont = doc.getFont();
1601
+ const savedSize = doc.getFontSize();
1602
+ const savedColor = doc.getTextColor();
1603
+ doc.setFontSize(hOpts.fontSize ?? 9);
1604
+ doc.setTextColor(hOpts.color ?? "#666666");
1605
+ const y = hOpts.y ?? 5;
1606
+ const align = hOpts.align ?? "center";
1607
+ const pageWidth = doc.internal.pageSize.getWidth();
1608
+ let x = pageWidth / 2;
1609
+ if (align === "left") x = options.page.xmargin;
1610
+ if (align === "right") x = pageWidth - options.page.xmargin;
1611
+ doc.text(text, x, y, {
1612
+ align,
1613
+ baseline: "top"
1614
+ });
1615
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
1616
+ doc.setFontSize(savedSize);
1617
+ doc.setTextColor(savedColor);
1618
+ };
1619
+ const applyFooter = (doc, options, pageNum, totalPages) => {
1620
+ const fOpts = options.footer;
1621
+ if (!fOpts) return;
1622
+ const text = fOpts.showPageNumbers ? `Page ${pageNum} of ${totalPages}` : typeof fOpts.text === "function" ? fOpts.text(pageNum, totalPages) : fOpts.text ?? "";
1623
+ if (!text.trim()) return;
1624
+ const savedFont = doc.getFont();
1625
+ const savedSize = doc.getFontSize();
1626
+ const savedColor = doc.getTextColor();
1627
+ doc.setFontSize(fOpts.fontSize ?? 9);
1628
+ doc.setTextColor(fOpts.color ?? "#666666");
1629
+ const pageHeight = doc.internal.pageSize.getHeight();
1630
+ const y = fOpts.y ?? pageHeight - 5;
1631
+ const align = fOpts.align ?? "right";
1632
+ const pageWidth = doc.internal.pageSize.getWidth();
1633
+ let x = pageWidth / 2;
1634
+ if (align === "left") x = options.page.xmargin;
1635
+ if (align === "right") x = pageWidth - options.page.xmargin;
1636
+ doc.text(text, x, y, {
1637
+ align,
1638
+ baseline: "bottom"
1639
+ });
1640
+ doc.setFont(savedFont.fontName, savedFont.fontStyle);
1641
+ doc.setFontSize(savedSize);
1642
+ doc.setTextColor(savedColor);
1643
+ };
1644
+ //#endregion
1601
1645
  //#region src/renderer/MdTextRender.ts
1602
1646
  /**
1603
1647
  * Renders parsed markdown text into jsPDF document.
@@ -1615,7 +1659,7 @@ const MdTextRender = async (doc, text, options) => {
1615
1659
  const indent = indentLevel * validOptions.page.indent;
1616
1660
  switch (element.type) {
1617
1661
  case "heading":
1618
- renderHeading(doc, element, indent, store, renderElement);
1662
+ renderHeading(doc, element, indent, store);
1619
1663
  break;
1620
1664
  case "paragraph":
1621
1665
  renderParagraph(doc, element, indent, store, renderElement);
@@ -1635,10 +1679,8 @@ const MdTextRender = async (doc, text, options) => {
1635
1679
  case "strong":
1636
1680
  case "em":
1637
1681
  case "codespan":
1638
- renderInlineText(doc, element, indent, store);
1639
- break;
1640
1682
  case "link":
1641
- renderLink(doc, element, indent, store);
1683
+ renderInlineContent(doc, [element], store.X + indent, store.Y, validOptions.page.maxContentWidth - indent, store);
1642
1684
  break;
1643
1685
  case "blockquote":
1644
1686
  renderBlockquote(doc, element, indentLevel, store, renderElement);
@@ -1670,7 +1712,8 @@ const MdTextRender = async (doc, text, options) => {
1670
1712
  }
1671
1713
  };
1672
1714
  for (const item of parsedElements) renderElement(item, 0, store);
1715
+ applyPageDecorations(doc, validOptions);
1673
1716
  validOptions.endCursorYHandler(store.Y);
1674
1717
  };
1675
1718
  //#endregion
1676
- export { MdTextParser, MdTextRender };
1719
+ export { MdTextParser, MdTextRender, MdTokenType, renderInlineContent, renderPlainText, validateOptions };