jspdf-md-renderer 3.5.0 → 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,42 +353,41 @@ 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;
356
+ //#region src/utils/handlePageBreak.ts
357
+ const HandlePageBreaks = (doc, store) => {
358
+ if (typeof store.options.pageBreakHandler === "function") store.options.pageBreakHandler(doc);
359
+ else doc.addPage(store.options.page?.format, store.options.page?.orientation);
360
+ store.updateY(store.options.page.topmargin);
361
+ store.updateX(store.options.page.xpading);
304
362
  };
305
- const getCharWidth = (doc) => {
306
- return doc.getTextDimensions("H").w;
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;
307
368
  };
308
- //#endregion
309
- //#region src/renderer/components/heading.ts
310
369
  /**
311
- * Renders heading elements.
370
+ * Checks if we will overflow, and if so, breaks the page first.
371
+ * Returns true if a page break was performed.
312
372
  */
313
- const renderHeading = (doc, element, indent, store, parentElementRenderer) => {
314
- const size = 6 - (element?.depth ?? 0) > 0 ? 6 - (element?.depth ?? 0) : 1;
315
- doc.setFontSize(store.options.page.defaultFontSize + size);
316
- if (element?.items && element?.items.length > 0) for (const item of element?.items ?? []) parentElementRenderer(item, indent, store, false);
317
- else {
318
- const charHeight = getCharHight(doc);
319
- doc.text(element?.content ?? "", store.X + indent, store.Y, {
320
- align: "left",
321
- maxWidth: store.options.page.maxContentWidth - indent,
322
- baseline: "top"
323
- });
324
- store.recordContentY(store.Y + charHeight);
325
- store.updateY(getCharHight(doc), "add");
373
+ const breakIfOverflow = (doc, store, height) => {
374
+ if (willOverflow(store, height)) {
375
+ HandlePageBreaks(doc, store);
376
+ return true;
326
377
  }
327
- doc.setFontSize(store.options.page.defaultFontSize);
328
- store.updateX(store.options.page.xpading, "set");
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);
329
386
  };
330
387
  //#endregion
331
- //#region src/utils/handlePageBreak.ts
332
- const HandlePageBreaks = (doc, store) => {
333
- if (typeof store.options.pageBreakHandler === "function") store.options.pageBreakHandler(doc);
334
- else doc.addPage(store.options.page?.format, store.options.page?.orientation);
335
- store.updateY(store.options.page.topmargin);
336
- store.updateX(store.options.page.xpading);
388
+ //#region src/utils/doc-helpers.ts
389
+ const getCharHight = (doc) => {
390
+ return doc.getFontSize() / doc.internal.scaleFactor;
337
391
  };
338
392
  //#endregion
339
393
  //#region src/utils/image-utils.ts
@@ -502,115 +556,127 @@ const prefetchImages = async (elements) => {
502
556
  }
503
557
  };
504
558
  //#endregion
505
- //#region src/utils/justifiedTextRenderer.ts
559
+ //#region src/layout/wordSplitter.ts
506
560
  /**
507
- * JustifiedTextRenderer - Renders mixed inline elements with proper alignment.
508
- *
509
- * Features:
510
- * - Handles bold, italic, codespan, links mixed in paragraph
511
- * - Proper word spacing distribution for justified alignment
512
- * - Supports left, right, center, and justify alignments
513
- * - Page break handling
514
- * - Preserves link clickability
515
- * - Codespan background rendering
561
+ * Maps a ParsedElement type string to a TextStyle.
516
562
  */
517
- var JustifiedTextRenderer = class {
518
- static getCodespanOptions(store) {
519
- const opts = store.options.codespan ?? {};
520
- return {
521
- backgroundColor: opts.backgroundColor ?? "#EEEEEE",
522
- padding: opts.padding ?? .5,
523
- showBackground: opts.showBackground !== false,
524
- fontSizeScale: opts.fontSizeScale ?? .9
525
- };
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";
526
569
  }
527
- /**
528
- * Apply font style to the jsPDF document.
529
- */
530
- static applyStyle(doc, style, store) {
531
- const currentFont = doc.getFont().fontName;
532
- const currentFontSize = doc.getFontSize();
533
- const getBoldFont = () => {
534
- const boldName = store.options.font.bold?.name;
535
- return boldName && boldName !== "" ? boldName : currentFont;
536
- };
537
- const getRegularFont = () => {
538
- const regularName = store.options.font.regular?.name;
539
- return regularName && regularName !== "" ? regularName : currentFont;
540
- };
541
- switch (style) {
542
- case "bold":
543
- doc.setFont(getBoldFont(), store.options.font.bold?.style || "bold");
544
- break;
545
- case "italic":
546
- doc.setFont(getRegularFont(), "italic");
547
- break;
548
- case "bolditalic":
549
- doc.setFont(getBoldFont(), "bolditalic");
550
- break;
551
- case "codespan":
552
- const codeFont = store.options.font.code || {
553
- name: "courier",
554
- style: "normal"
555
- };
556
- doc.setFont(codeFont.name, codeFont.style);
557
- doc.setFontSize(currentFontSize * this.getCodespanOptions(store).fontSizeScale);
558
- break;
559
- default:
560
- doc.setFont(getRegularFont(), doc.getFont().fontStyle);
561
- break;
562
- }
563
- }
564
- /**
565
- * Measure word width with a specific style applied.
566
- * NOTE: jsPDF's getTextWidth() does NOT include charSpace in its calculation,
567
- * so we must manually add it: effectiveWidth = getTextWidth(text) + (text.length * charSpace)
568
- */
569
- static measureWordWidth(doc, text, style, store) {
570
- const savedFont = doc.getFont();
571
- const savedSize = doc.getFontSize();
572
- this.applyStyle(doc, style, store);
573
- const baseWidth = doc.getTextWidth(text);
574
- const charSpace = doc.getCharSpace?.() ?? 0;
575
- const effectiveWidth = baseWidth + text.length * charSpace;
576
- doc.setFont(savedFont.fontName, savedFont.fontStyle);
577
- doc.setFontSize(savedSize);
578
- 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;
579
613
  }
580
- /**
581
- * Extract style from element type string.
582
- */
583
- static getStyleFromType(type, parentStyle) {
584
- switch (type) {
585
- case "strong":
586
- if (parentStyle === "italic") return "bolditalic";
587
- return "bold";
588
- case "em":
589
- if (parentStyle === "bold") return "bolditalic";
590
- return "italic";
591
- case "codespan": return "codespan";
592
- 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;
593
633
  }
594
- }
595
- /**
596
- * Flatten ParsedElement tree into an array of StyledWordInfo.
597
- * Handles nested inline elements.
598
- */
599
- static flattenToWords(doc, elements, store, parentStyle = "normal", isLink = false, href) {
600
- const result = [];
601
- for (const el of elements) {
602
- const style = this.getStyleFromType(el.type, parentStyle);
603
- const elIsLink = el.type === "link" || isLink;
604
- const elHref = el.href || href;
605
- if (el.items && el.items.length > 0) {
606
- const nested = this.flattenToWords(doc, el.items, store, style, elIsLink, elHref);
607
- result.push(...nested);
608
- } else if (el.type === "image") {
609
- const maxH = store.options.page.maxContentHeight - store.options.page.topmargin;
610
- 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;
611
677
  result.push({
612
- text: "",
613
- width: finalWidth,
678
+ text: words[wi],
679
+ width: measureStyledWidth(doc, words[wi], style, store),
614
680
  style,
615
681
  isLink: elIsLink,
616
682
  href: elHref,
@@ -619,302 +685,253 @@ var JustifiedTextRenderer = class {
619
685
  0,
620
686
  255
621
687
  ] : void 0,
622
- isImage: true,
623
- imageElement: el,
624
- imageHeight: finalHeight
688
+ hasTrailingSpace: !isLastInLine || /[ \t]$/.test(lines[li])
625
689
  });
626
- } else if (el.type === "br") result.push({
690
+ }
691
+ if (li < lines.length - 1) result.push({
627
692
  text: "",
628
693
  width: 0,
629
694
  style,
630
695
  isBr: true
631
696
  });
632
- else {
633
- const text = el.content || el.text || "";
634
- if (!text) continue;
635
- if (/^\s/.test(text) && result.length > 0) result[result.length - 1].hasTrailingSpace = true;
636
- if (style === "codespan") {
637
- const trimmedText = text.trim();
638
- if (trimmedText) result.push({
639
- text: trimmedText,
640
- width: this.measureWordWidth(doc, trimmedText, style, store),
641
- style,
642
- isLink: elIsLink,
643
- href: elHref,
644
- linkColor: elIsLink ? store.options.link?.linkColor || [
645
- 0,
646
- 0,
647
- 255
648
- ] : void 0,
649
- hasTrailingSpace: /\s$/.test(text)
650
- });
651
- continue;
652
- }
653
- const words = text.trim().split(/\s+/).filter((w) => w.length > 0);
654
- for (let i = 0; i < words.length; i++) {
655
- const hasTrailingSpace = !(i === words.length - 1) || /\s$/.test(text);
656
- result.push({
657
- text: words[i],
658
- width: this.measureWordWidth(doc, words[i], style, store),
659
- style,
660
- isLink: elIsLink,
661
- href: elHref,
662
- linkColor: elIsLink ? store.options.link?.linkColor || [
663
- 0,
664
- 0,
665
- 255
666
- ] : void 0,
667
- hasTrailingSpace
668
- });
669
- }
670
- }
671
697
  }
672
- return result;
673
698
  }
674
- /**
675
- * Break a flat list of words into lines that fit within maxWidth.
676
- * Correctly tracks totalTextWidth (sum of word widths only) for justification.
677
- */
678
- static breakIntoLines(doc, words, maxWidth, store) {
679
- const lines = [];
680
- let currentLine = [];
681
- let currentTextWidth = 0;
682
- let currentLineWidth = 0;
683
- let currentLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
684
- const spaceWidth = doc.getTextWidth(" ");
685
- for (let i = 0; i < words.length; i++) {
686
- const word = words[i];
687
- const neededWidthWithSpace = currentLine[currentLine.length - 1]?.hasTrailingSpace ? spaceWidth + word.width : word.width;
688
- const itemHeight = word.isImage && word.imageHeight ? word.imageHeight : getCharHight(doc) * store.options.page.defaultLineHeightFactor;
689
- if (word.isBr) {
690
- lines.push({
691
- words: currentLine,
692
- totalTextWidth: currentTextWidth,
693
- isLastLine: true,
694
- lineHeight: currentLineHeight
695
- });
696
- currentLine = [];
697
- currentTextWidth = 0;
698
- currentLineWidth = 0;
699
- currentLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
700
- continue;
701
- }
702
- if (currentLineWidth + neededWidthWithSpace > maxWidth && currentLine.length > 0) {
703
- lines.push({
704
- words: currentLine,
705
- totalTextWidth: currentTextWidth,
706
- isLastLine: false,
707
- lineHeight: currentLineHeight
708
- });
709
- currentLine = [word];
710
- currentTextWidth = word.width;
711
- currentLineWidth = word.width;
712
- currentLineHeight = itemHeight;
713
- } else {
714
- currentLine.push(word);
715
- currentTextWidth += word.width;
716
- currentLineWidth += neededWidthWithSpace;
717
- currentLineHeight = Math.max(currentLineHeight, itemHeight);
718
- }
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;
719
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) => {
720
739
  if (currentLine.length > 0) lines.push({
721
740
  words: currentLine,
722
741
  totalTextWidth: currentTextWidth,
723
- isLastLine: true,
742
+ isLastLine: isLast,
724
743
  lineHeight: currentLineHeight
725
744
  });
726
- return lines;
727
- }
728
- /**
729
- * Render a single word with its style applied.
730
- */
731
- static renderWord(doc, word, x, y, store) {
732
- const savedFont = doc.getFont();
733
- const savedSize = doc.getFontSize();
734
- const savedColor = doc.getTextColor();
735
- this.applyStyle(doc, word.style, store);
736
- if (word.isLink && word.linkColor) doc.setTextColor(...word.linkColor);
737
- if (word.isImage && word.imageElement && word.imageElement.data) try {
738
- let imgFormat = "JPEG";
739
- if (word.imageElement.data.startsWith("data:image/png")) imgFormat = "PNG";
740
- else if (word.imageElement.data.startsWith("data:image/webp")) imgFormat = "WEBP";
741
- else if (word.imageElement.data.startsWith("data:image/gif")) imgFormat = "GIF";
742
- else if (word.imageElement.src) {
743
- const ext = word.imageElement.src.split("?")[0].split("#")[0].split(".").pop()?.toUpperCase();
744
- if (ext && [
745
- "PNG",
746
- "JPEG",
747
- "JPG",
748
- "WEBP",
749
- "GIF"
750
- ].includes(ext)) imgFormat = ext === "JPG" ? "JPEG" : ext;
751
- }
752
- if (word.width > 0 && (word.imageHeight || 0) > 0) {
753
- const imgH = word.imageHeight || 0;
754
- const imgY = y;
755
- doc.addImage(word.imageElement.data, imgFormat, x, imgY, word.width, imgH);
756
- }
757
- } catch (e) {
758
- console.warn("Failed to render inline image", e);
759
- }
760
- else {
761
- if (word.style === "codespan") {
762
- const codespanOpts = this.getCodespanOptions(store);
763
- if (codespanOpts.showBackground) {
764
- const h = getCharHight(doc);
765
- const pad = codespanOpts.padding;
766
- doc.setFillColor(codespanOpts.backgroundColor);
767
- doc.rect(x - pad, y - pad, word.width + pad * 2, h + pad * 2, "F");
768
- doc.setFillColor("#000000");
769
- }
770
- }
771
- 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;
772
754
  }
773
- if (word.isLink && word.href) {
774
- const h = word.isImage && word.imageHeight ? word.imageHeight : getCharHight(doc);
775
- 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);
776
768
  }
777
- doc.setFont(savedFont.fontName, savedFont.fontStyle);
778
- doc.setFontSize(savedSize);
779
- doc.setTextColor(savedColor);
780
769
  }
781
- /**
782
- * Render a single line with specified alignment.
783
- */
784
- static renderAlignedLine(doc, line, x, y, maxWidth, store, alignment = "left") {
785
- const { words, totalTextWidth, isLastLine } = line;
786
- if (words.length === 0) return;
787
- const normalSpaceWidth = doc.getTextWidth(" ");
788
- let startX = x;
789
- let wordSpacing = normalSpaceWidth;
790
- let lineWidthWithNormalSpaces = totalTextWidth;
791
- let expandableSpacesCount = 0;
792
- for (let i = 0; i < words.length - 1; i++) if (words[i].hasTrailingSpace) {
793
- lineWidthWithNormalSpaces += normalSpaceWidth;
794
- expandableSpacesCount++;
795
- }
796
- switch (alignment) {
797
- case "right":
798
- startX = x + maxWidth - lineWidthWithNormalSpaces;
799
- break;
800
- case "center":
801
- startX = x + (maxWidth - lineWidthWithNormalSpaces) / 2;
802
- break;
803
- case "justify":
804
- if (!isLastLine && expandableSpacesCount > 0) wordSpacing = (maxWidth - totalTextWidth) / expandableSpacesCount;
805
- break;
806
- default: break;
807
- }
808
- let currentX = startX;
809
- const textHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
810
- for (let i = 0; i < words.length; i++) {
811
- const word = words[i];
812
- let drawY = y;
813
- const elementHeight = word.isImage && word.imageHeight ? word.imageHeight : textHeight;
814
- if (word.isImage) drawY = y;
815
- else if (elementHeight < line.lineHeight) drawY = y + (line.lineHeight - elementHeight);
816
- this.renderWord(doc, word, currentX, drawY, store);
817
- currentX += word.width;
818
- if (i < words.length - 1 && word.hasTrailingSpace) currentX += wordSpacing;
819
- }
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++;
820
784
  }
821
- /**
822
- * Main entry point: Render a paragraph with mixed inline elements.
823
- * Respects user's textAlignment option from store.
824
- *
825
- * @param doc jsPDF instance
826
- * @param elements Array of ParsedElement (inline items in a paragraph)
827
- * @param x Starting X coordinate
828
- * @param y Starting Y coordinate
829
- * @param maxWidth Maximum width for text wrapping
830
- * @param store RenderStore instance to use
831
- * @param alignment Optional alignment override (defaults to store option)
832
- */
833
- static renderStyledParagraph(doc, elements, x, y, maxWidth, store, alignment) {
834
- const textAlignment = alignment ?? store.options.content?.textAlignment ?? "left";
835
- const words = this.flattenToWords(doc, elements, store);
836
- if (words.length === 0) return;
837
- const lines = this.breakIntoLines(doc, words, maxWidth, store);
838
- let currentY = y;
839
- for (const line of lines) {
840
- if (currentY + line.lineHeight > store.options.page.maxContentHeight) {
841
- HandlePageBreaks(doc, store);
842
- currentY = store.Y;
843
- }
844
- this.renderAlignedLine(doc, line, x, currentY, maxWidth, store, textAlignment);
845
- store.recordContentY(currentY + line.lineHeight);
846
- currentY += line.lineHeight;
847
- store.updateY(line.lineHeight, "add");
848
- }
849
- const lastLine = lines[lines.length - 1];
850
- if (lastLine) {
851
- let actualSpacesCount = 0;
852
- for (let i = 0; i < lastLine.words.length - 1; i++) if (lastLine.words[i].hasTrailingSpace) actualSpacesCount++;
853
- const lastLineWidth = lastLine.totalTextWidth + actualSpacesCount * doc.getTextWidth(" ");
854
- store.updateX(x + lastLineWidth, "set");
855
- }
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;
856
797
  }
857
- /**
858
- * @deprecated Use renderStyledParagraph instead
859
- */
860
- static renderJustifiedParagraph(doc, elements, x, y, maxWidth, store) {
861
- 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;
862
807
  }
863
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
+ };
864
846
  //#endregion
865
- //#region src/utils/text-renderer.ts
866
- var TextRenderer = class {
867
- /**
868
- * Renders text with automatic line wrapping and page breaking.
869
- * @param doc jsPDF instance
870
- * @param text Text to render
871
- * @param store RenderStore instance to use
872
- * @param x X coordinate (if not provided, uses store.X)
873
- * @param y Y coordinate (if not provided, uses store.Y)
874
- * @param maxWidth Max width for text wrapping
875
- * @param justify Whether to justify the text
876
- */
877
- static renderText(doc, text, store, x = store.X, y = store.Y, maxWidth, justify = false) {
878
- const lines = doc.splitTextToSize(text, maxWidth);
879
- const charHeight = getCharHight(doc);
880
- const lineHeight = charHeight * store.options.page.defaultLineHeightFactor;
881
- let currentY = y;
882
- for (let i = 0; i < lines.length; i++) {
883
- const line = lines[i];
884
- if (currentY + lineHeight > store.options.page.maxContentHeight) {
885
- HandlePageBreaks(doc, store);
886
- currentY = store.Y;
887
- }
888
- if (justify) if (i === lines.length - 1) doc.text(line, x, currentY, { baseline: "top" });
889
- else doc.text(line, x, currentY, {
890
- maxWidth,
891
- align: "justify",
892
- baseline: "top"
893
- });
894
- else doc.text(line, x, currentY, { baseline: "top" });
895
- store.recordContentY(currentY + charHeight);
896
- currentY += lineHeight;
897
- store.updateY(lineHeight, "add");
847
+ //#region src/layout/layoutEngine.ts
848
+ /**
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.
854
+ */
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;
898
867
  }
899
- return currentY;
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");
900
873
  }
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;
901
882
  };
902
- //#endregion
903
- //#region src/renderer/components/paragraph.ts
904
883
  /**
905
- * Renders paragraph elements with proper text alignment.
906
- * Handles mixed inline styles (bold, italic, codespan) and links.
907
- * Respects user's textAlignment option from RenderStore.
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.
908
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);
892
+ };
893
+ //#endregion
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");
920
+ };
921
+ //#endregion
922
+ //#region src/renderer/components/paragraph.ts
909
923
  const renderParagraph = (doc, element, indent, store, parentElementRenderer) => {
910
- store.activateInlineLock();
924
+ const indentLevel = indent / store.options.page.indent;
925
+ const savedColor = doc.getTextColor();
911
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");
912
929
  const maxWidth = store.options.page.maxContentWidth - indent;
913
- if (element?.items && element?.items.length > 0) {
930
+ if (element.items && element.items.length > 0) {
914
931
  if (element.items.length === 1 && element.items[0].type === "image") {
915
- parentElementRenderer(element.items[0], indent, store, false);
932
+ parentElementRenderer(element.items[0], indentLevel, store, false);
916
933
  store.updateX(store.options.page.xpading);
917
- store.deactivateInlineLock();
934
+ doc.setTextColor(savedColor);
918
935
  return;
919
936
  }
920
937
  const inlineTypes = [
@@ -928,35 +945,38 @@ const renderParagraph = (doc, element, indent, store, parentElementRenderer) =>
928
945
  ];
929
946
  if (element.items.some((item) => !inlineTypes.includes(item.type))) {
930
947
  const inlineBuffer = [];
931
- const flushInlineBuffer = () => {
948
+ const flush = () => {
932
949
  if (inlineBuffer.length > 0) {
933
- JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, store.X + indent, store.Y, maxWidth, store);
950
+ renderInlineContent(doc, inlineBuffer, store.X + indent, store.Y, maxWidth, store, { trimLastLine: true });
934
951
  inlineBuffer.length = 0;
935
952
  }
936
953
  };
937
954
  for (const item of element.items) if (inlineTypes.includes(item.type)) inlineBuffer.push(item);
938
955
  else {
939
- flushInlineBuffer();
940
- parentElementRenderer(item, indent, store, false);
956
+ flush();
957
+ parentElementRenderer(item, indentLevel, store, false);
941
958
  }
942
- flushInlineBuffer();
943
- } else JustifiedTextRenderer.renderStyledParagraph(doc, element.items, store.X + indent, store.Y, maxWidth, store);
944
- } else {
945
- const content = element.content ?? "";
946
- const textAlignment = store.options.content?.textAlignment ?? "left";
947
- if (content.trim()) TextRenderer.renderText(doc, content, store, store.X + indent, store.Y, maxWidth, textAlignment === "justify");
948
- }
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");
949
967
  store.updateX(store.options.page.xpading);
950
- store.deactivateInlineLock();
968
+ doc.setTextColor(savedColor);
951
969
  };
952
970
  //#endregion
953
971
  //#region src/renderer/components/list.ts
954
972
  const renderList = (doc, element, indentLevel, store, parentElementRenderer) => {
955
973
  doc.setFontSize(store.options.page.defaultFontSize);
956
974
  for (const [i, point] of element?.items?.entries() ?? []) {
957
- const _start = element.ordered ? (element.start ?? 0) + i : element.start;
975
+ const _start = element.ordered ? (element.start ?? 1) + i : void 0;
958
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");
959
978
  }
979
+ store.updateY(store.options.spacing?.afterList ?? 3, "add");
960
980
  };
961
981
  //#endregion
962
982
  //#region src/renderer/components/listItem.ts
@@ -964,22 +984,46 @@ const renderList = (doc, element, indentLevel, store, parentElementRenderer) =>
964
984
  * Render a single list item, including bullets/numbering, inline text, and any nested lists.
965
985
  */
966
986
  const renderListItem = (doc, element, indentLevel, store, parentElementRenderer, start, ordered) => {
967
- if (store.Y + getCharHight(doc) >= store.options.page.maxContentHeight) HandlePageBreaks(doc, store);
987
+ breakIfOverflow(doc, store, getCharHight(doc));
968
988
  const options = store.options;
969
- const baseIndent = indentLevel * options.page.indent;
970
- 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 ?? "• ";
971
992
  const xLeft = options.page.xpading;
972
993
  store.updateX(xLeft, "set");
973
994
  doc.setFont(options.font.regular.name, options.font.regular.style);
974
- doc.text(bullet, xLeft + baseIndent, store.Y, { baseline: "top" });
975
- const bulletWidth = doc.getTextWidth(bullet);
976
- 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;
977
1019
  const textMaxWidth = options.page.maxContentWidth - baseIndent - bulletWidth;
1020
+ const originalTextColor = doc.getTextColor();
1021
+ if (element.checked) doc.setTextColor(150, 150, 150);
978
1022
  if (element.items && element.items.length > 0) {
979
1023
  const inlineBuffer = [];
980
1024
  const flushInlineBuffer = () => {
981
1025
  if (inlineBuffer.length > 0) {
982
- JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, contentX, store.Y, textMaxWidth, store);
1026
+ renderInlineContent(doc, inlineBuffer, contentX, store.Y, textMaxWidth, store);
983
1027
  inlineBuffer.length = 0;
984
1028
  store.updateX(xLeft, "set");
985
1029
  }
@@ -992,10 +1036,8 @@ const renderListItem = (doc, element, indentLevel, store, parentElementRenderer,
992
1036
  parentElementRenderer(subItem, indentLevel, store, true, start, ordered);
993
1037
  } else inlineBuffer.push(subItem);
994
1038
  flushInlineBuffer();
995
- } else if (element.content) {
996
- const textAlignment = options.content?.textAlignment ?? "left";
997
- TextRenderer.renderText(doc, element.content, store, contentX, store.Y, textMaxWidth, textAlignment === "justify");
998
- }
1039
+ } else if (element.content) renderPlainText(doc, element.content, contentX, store.Y, textMaxWidth, store);
1040
+ doc.setTextColor(originalTextColor);
999
1041
  };
1000
1042
  //#endregion
1001
1043
  //#region src/renderer/components/rawItem.ts
@@ -1004,19 +1046,18 @@ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentEle
1004
1046
  else {
1005
1047
  const options = store.options;
1006
1048
  const indent = indentLevel * options.page.indent;
1007
- const bullet = hasRawBullet ? ordered ? `${start}. ` : "• " : "";
1049
+ const listOpts = store.options.list ?? {};
1050
+ const bullet = hasRawBullet ? ordered ? `${start}. ` : listOpts.bulletChar ?? "• " : "";
1008
1051
  const content = element.content || "";
1009
1052
  const xLeft = options.page.xpading;
1010
1053
  if (!content && !bullet) return;
1011
1054
  if (!content.trim() && !bullet) {
1012
1055
  const newlines = (content.match(/\n/g) || []).length;
1013
1056
  if (newlines > 1) {
1014
- const addedHeight = (newlines - 1) * (doc.getTextDimensions("A").h * options.page.defaultLineHeightFactor);
1015
- if (store.Y + addedHeight > options.page.maxContentHeight) HandlePageBreaks(doc, store);
1016
- else {
1017
- store.updateY(addedHeight, "add");
1018
- store.recordContentY(store.Y);
1019
- }
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);
1020
1061
  }
1021
1062
  return;
1022
1063
  }
@@ -1026,10 +1067,10 @@ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentEle
1026
1067
  const textMaxWidth = options.page.maxContentWidth - indent - bulletWidth;
1027
1068
  doc.setFont(options.font.regular.name, options.font.regular.style);
1028
1069
  doc.text(bullet, xLeft + indent, store.Y, { baseline: "top" });
1029
- 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" });
1030
1071
  } else {
1031
1072
  const textMaxWidth = options.page.maxContentWidth - indent;
1032
- 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" });
1033
1074
  }
1034
1075
  store.updateX(xLeft, "set");
1035
1076
  }
@@ -1037,6 +1078,9 @@ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentEle
1037
1078
  //#endregion
1038
1079
  //#region src/renderer/components/hr.ts
1039
1080
  const renderHR = (doc, store) => {
1081
+ const savedDrawColor = doc.getDrawColor();
1082
+ const savedLineWidth = doc.getLineWidth();
1083
+ breakIfOverflow(doc, store, getCharHight(doc));
1040
1084
  const pageWidth = doc.internal.pageSize.getWidth();
1041
1085
  doc.setLineDashPattern([1, 1], 0);
1042
1086
  doc.setLineWidth(.1);
@@ -1044,21 +1088,30 @@ const renderHR = (doc, store) => {
1044
1088
  doc.setLineWidth(.1);
1045
1089
  doc.setLineDashPattern([], 0);
1046
1090
  store.updateY(getCharHight(doc), "add");
1091
+ store.updateY(store.options.spacing?.afterHR ?? 2, "add");
1092
+ doc.setDrawColor(savedDrawColor);
1093
+ doc.setLineWidth(savedLineWidth);
1047
1094
  };
1048
1095
  //#endregion
1049
1096
  //#region src/renderer/components/code.ts
1050
1097
  const renderCodeBlock = (doc, element, indentLevel, store) => {
1051
1098
  const savedFont = doc.getFont();
1052
1099
  const savedFontSize = doc.getFontSize();
1100
+ const savedTextColor = doc.getTextColor();
1101
+ const savedDrawColor = doc.getDrawColor();
1102
+ const savedFillColor = doc.getFillColor();
1053
1103
  const codeFont = store.options.font.code || {
1054
1104
  name: "courier",
1055
1105
  style: "normal"
1056
1106
  };
1057
1107
  doc.setFont(codeFont.name, codeFont.style);
1058
- 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;
1059
1111
  doc.setFontSize(codeFontSize);
1060
1112
  const indent = indentLevel * store.options.page.indent;
1061
- const maxWidth = store.options.page.maxContentWidth - indent - 8;
1113
+ const padding = codeOpts.padding ?? 4;
1114
+ const maxWidth = store.options.page.maxContentWidth - indent - padding * 2;
1062
1115
  const lineHeightFactor = doc.getLineHeightFactor();
1063
1116
  const lineHeight = codeFontSize / doc.internal.scaleFactor * lineHeightFactor;
1064
1117
  const content = (element.code ?? "").replace(/[\r\n\s]+$/, "");
@@ -1074,9 +1127,9 @@ const renderCodeBlock = (doc, element, indentLevel, store) => {
1074
1127
  doc.setFontSize(savedFontSize);
1075
1128
  return;
1076
1129
  }
1077
- const padding = 4;
1078
- const bgColor = "#EEEEEE";
1079
- const drawColor = "#DDDDDD";
1130
+ const bgColor = codeOpts.backgroundColor ?? "#F6F8FA";
1131
+ const drawColor = codeOpts.borderColor ?? "#E1E4E8";
1132
+ const radius = codeOpts.borderRadius ?? 2;
1080
1133
  let currentLineIndex = 0;
1081
1134
  while (currentLineIndex < lines.length) {
1082
1135
  const availableHeight = store.options.page.maxContentHeight - store.Y;
@@ -1095,20 +1148,22 @@ const renderCodeBlock = (doc, element, indentLevel, store) => {
1095
1148
  if (isFirstChunk) store.updateY(padding, "add");
1096
1149
  doc.setFillColor(bgColor);
1097
1150
  doc.setDrawColor(drawColor);
1098
- doc.roundedRect(store.X, store.Y - padding, store.options.page.maxContentWidth, textBlockHeight + (isFirstChunk ? padding : 0) + (isLastChunk ? padding : 0), 2, 2, "FD");
1099
- 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) {
1100
1153
  const savedCodeFontSize = doc.getFontSize();
1101
1154
  doc.setFontSize(10);
1102
- doc.setTextColor("#666666");
1155
+ doc.setTextColor(codeOpts.labelColor ?? "#666666");
1103
1156
  doc.text(element.lang, store.X + store.options.page.maxContentWidth - doc.getTextWidth(element.lang) - 4, store.Y, { baseline: "top" });
1104
1157
  doc.setFontSize(savedCodeFontSize);
1105
- doc.setTextColor("#000000");
1158
+ doc.setTextColor(savedTextColor);
1106
1159
  }
1107
1160
  let yPos = store.Y;
1161
+ doc.setTextColor(codeOpts.textColor ?? "#000000");
1108
1162
  for (const line of linesToRender) {
1109
1163
  doc.text(line, store.X + 4, yPos, { baseline: "top" });
1110
1164
  yPos += lineHeight;
1111
1165
  }
1166
+ doc.setTextColor(savedTextColor);
1112
1167
  store.updateY(textBlockHeight, "add");
1113
1168
  store.recordContentY(store.Y + (isLastChunk ? padding : 0));
1114
1169
  if (isLastChunk) store.updateY(padding, "add");
@@ -1117,199 +1172,17 @@ const renderCodeBlock = (doc, element, indentLevel, store) => {
1117
1172
  }
1118
1173
  doc.setFont(savedFont.fontName, savedFont.fontStyle);
1119
1174
  doc.setFontSize(savedFontSize);
1120
- };
1121
- //#endregion
1122
- //#region src/renderer/components/inlineText.ts
1123
- /**
1124
- * Renders inline text elements (Strong, Em, and Text) with proper inline styling.
1125
- */
1126
- const renderInlineText = (doc, element, indent, store) => {
1127
- const currentFont = doc.getFont().fontName;
1128
- const currentFontStyle = doc.getFont().fontStyle;
1129
- const currentFontSize = doc.getFontSize();
1130
- const spaceMultiplier = (style) => {
1131
- switch (style) {
1132
- case "normal": return 0;
1133
- case "bold": return 1;
1134
- case "italic": return 1.5;
1135
- case "bolditalic": return 1.5;
1136
- case "codespan": return .5;
1137
- default: return 0;
1138
- }
1139
- };
1140
- const renderTextWithStyle = (text, style) => {
1141
- 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");
1142
- else if (style === "italic") doc.setFont(store.options.font.regular.name, "italic");
1143
- else if (style === "bolditalic") doc.setFont(store.options.font.bold.name && store.options.font.bold.name !== "" ? store.options.font.bold.name : currentFont, "bolditalic");
1144
- else if (style === "codespan") {
1145
- const codeFont = store.options.font.code || {
1146
- name: "courier",
1147
- style: "normal"
1148
- };
1149
- doc.setFont(codeFont.name, codeFont.style);
1150
- doc.setFontSize(currentFontSize * .9);
1151
- } else doc.setFont(store.options.font.regular.name, currentFontStyle);
1152
- const availableWidth = store.options.page.maxContentWidth - indent - store.X;
1153
- const textLines = doc.splitTextToSize(text, availableWidth);
1154
- const isCodeSpan = style === "codespan";
1155
- const codePadding = 1;
1156
- const codeBgColor = "#EEEEEE";
1157
- if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
1158
- if (isCodeSpan) {
1159
- const lineWidth = doc.getTextWidth(textLines[i]) + getCharWidth(doc);
1160
- const lineHeight = getCharHight(doc);
1161
- doc.setFillColor(codeBgColor);
1162
- doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, lineWidth + codePadding * 2, lineHeight + codePadding * 2, 2, 2, "F");
1163
- doc.setFillColor("#000000");
1164
- }
1165
- doc.text(textLines[i], store.X + indent, store.Y, {
1166
- baseline: "top",
1167
- maxWidth: availableWidth
1168
- });
1169
- store.updateX(doc.getTextDimensions(textLines[i]).w + (isCodeSpan ? codePadding * 2 : 1), "add");
1170
- if (i < textLines.length - 1) {
1171
- store.updateY(getCharHight(doc), "add");
1172
- store.updateX(store.options.page.xpading, "set");
1173
- }
1174
- }
1175
- else if (textLines.length > 1) {
1176
- const firstLine = textLines[0];
1177
- const restContent = textLines?.slice(1)?.join(" ");
1178
- if (isCodeSpan) {
1179
- const w = doc.getTextWidth(firstLine) + getCharWidth(doc);
1180
- const h = getCharHight(doc);
1181
- doc.setFillColor(codeBgColor);
1182
- doc.roundedRect(store.X + (indent >= 2 ? indent + 2 : 0) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
1183
- doc.setFillColor("#000000");
1184
- }
1185
- doc.text(firstLine, store.X + (indent >= 2 ? indent + 2 * spaceMultiplier(style) : 0), store.Y, {
1186
- baseline: "top",
1187
- maxWidth: availableWidth
1188
- });
1189
- store.updateX(store.options.page.xpading + indent);
1190
- store.updateY(getCharHight(doc), "add");
1191
- const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
1192
- doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
1193
- if (isCodeSpan) {
1194
- const w = doc.getTextWidth(line) + getCharWidth(doc);
1195
- const h = getCharHight(doc);
1196
- doc.setFillColor(codeBgColor);
1197
- doc.roundedRect(store.X + getCharWidth(doc) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
1198
- doc.setFillColor("#000000");
1199
- }
1200
- doc.text(line, store.X + getCharWidth(doc), store.Y, {
1201
- baseline: "top",
1202
- maxWidth: maxWidthForRest
1203
- });
1204
- });
1205
- } else {
1206
- if (isCodeSpan) {
1207
- const w = doc.getTextWidth(text) + getCharWidth(doc);
1208
- const h = getCharHight(doc);
1209
- doc.setFillColor(codeBgColor);
1210
- doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
1211
- doc.setFillColor("#000000");
1212
- }
1213
- doc.text(text, store.X + indent, store.Y, {
1214
- baseline: "top",
1215
- maxWidth: availableWidth
1216
- });
1217
- store.updateX(doc.getTextDimensions(text).w + (indent >= 2 ? text.split(" ").length + 2 : 2) * spaceMultiplier(style) * .5 + (isCodeSpan ? codePadding * 2 : 0), "add");
1218
- }
1219
- };
1220
- if (element.type === "text" && element.items && element.items.length > 0) for (const item of element.items) if (item.type === "codespan") renderTextWithStyle(item.content || "", "codespan");
1221
- else if (item.type === "em" || item.type === "strong") {
1222
- const baseStyle = item.type === "em" ? "italic" : "bold";
1223
- if (item.items && item.items.length > 0) for (const subItem of item.items) if (subItem.type === "strong" && baseStyle === "italic") renderTextWithStyle(subItem.content || "", "bolditalic");
1224
- else if (subItem.type === "em" && baseStyle === "bold") renderTextWithStyle(subItem.content || "", "bolditalic");
1225
- else renderTextWithStyle(subItem.content || "", baseStyle);
1226
- else renderTextWithStyle(item.content || "", baseStyle);
1227
- } else renderTextWithStyle(item.content || "", "normal");
1228
- else if (element.type === "em") renderTextWithStyle(element.content || "", "italic");
1229
- else if (element.type === "strong") renderTextWithStyle(element.content || "", "bold");
1230
- else if (element.type === "codespan") renderTextWithStyle(element.content || "", "codespan");
1231
- else renderTextWithStyle(element.content || "", "normal");
1232
- doc.setFont(currentFont, currentFontStyle);
1233
- doc.setFontSize(currentFontSize);
1234
- };
1235
- //#endregion
1236
- //#region src/renderer/components/link.ts
1237
- /**
1238
- * Renders link elements with proper styling and URL handling.
1239
- * Links are rendered in blue color and underlined to distinguish them from regular text.
1240
- */
1241
- const renderLink = (doc, element, indent, store) => {
1242
- const currentFont = doc.getFont().fontName;
1243
- const currentFontStyle = doc.getFont().fontStyle;
1244
- const currentFontSize = doc.getFontSize();
1245
- const currentTextColor = doc.getTextColor();
1246
- const linkColor = store.options.link?.linkColor || [
1247
- 0,
1248
- 0,
1249
- 255
1250
- ];
1251
- doc.setTextColor(...linkColor);
1252
- const availableWidth = store.options.page.maxContentWidth - indent - store.X;
1253
- const linkText = element.text || element.content || "";
1254
- const linkUrl = element.href || "";
1255
- const textLines = doc.splitTextToSize(linkText, availableWidth);
1256
- if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
1257
- const textWidth = doc.getTextDimensions(textLines[i]).w;
1258
- const textHeight = getCharHight(doc) / 2;
1259
- doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
1260
- doc.text(textLines[i], store.X + indent, store.Y, {
1261
- baseline: "top",
1262
- maxWidth: availableWidth
1263
- });
1264
- store.updateX(textWidth + 1, "add");
1265
- if (store.X + textWidth > store.options.page.maxContentWidth - indent) {
1266
- store.updateY(textHeight, "add");
1267
- store.updateX(store.options.page.xpading + indent, "set");
1268
- }
1269
- if (i < textLines.length - 1) {
1270
- store.updateY(textHeight, "add");
1271
- store.updateX(store.options.page.xpading + indent, "set");
1272
- }
1273
- }
1274
- else if (textLines.length > 1) {
1275
- const firstLine = textLines[0];
1276
- const restContent = textLines?.slice(1)?.join(" ");
1277
- const firstLineWidth = doc.getTextDimensions(firstLine).w;
1278
- const textHeight = getCharHight(doc) / 2;
1279
- doc.link(store.X + indent, store.Y, firstLineWidth, textHeight, { url: linkUrl });
1280
- doc.text(firstLine, store.X + indent, store.Y, {
1281
- baseline: "top",
1282
- maxWidth: availableWidth
1283
- });
1284
- store.updateX(store.options.page.xpading + indent);
1285
- store.updateY(textHeight, "add");
1286
- const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
1287
- doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
1288
- const lineWidth = doc.getTextDimensions(line).w;
1289
- doc.link(store.X + getCharWidth(doc), store.Y, lineWidth, textHeight, { url: linkUrl });
1290
- doc.text(line, store.X + getCharWidth(doc), store.Y, {
1291
- baseline: "top",
1292
- maxWidth: maxWidthForRest
1293
- });
1294
- });
1295
- } else {
1296
- const textWidth = doc.getTextDimensions(linkText).w;
1297
- const textHeight = getCharHight(doc) / 2;
1298
- doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
1299
- doc.text(linkText, store.X + indent, store.Y, {
1300
- baseline: "top",
1301
- maxWidth: availableWidth
1302
- });
1303
- store.updateX(textWidth + 2, "add");
1304
- }
1305
- doc.setFont(currentFont, currentFontStyle);
1306
- doc.setFontSize(currentFontSize);
1307
- doc.setTextColor(currentTextColor);
1175
+ doc.setTextColor(savedTextColor);
1176
+ doc.setDrawColor(savedDrawColor);
1177
+ doc.setFillColor(savedFillColor);
1178
+ store.updateY(store.options.spacing?.afterCodeBlock ?? 3, "add");
1308
1179
  };
1309
1180
  //#endregion
1310
1181
  //#region src/renderer/components/blockquote.ts
1311
1182
  const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
1312
1183
  const options = store.options;
1184
+ const savedDrawColor = doc.getDrawColor();
1185
+ const savedLineWidth = doc.getLineWidth();
1313
1186
  const blockquoteIndent = indentLevel + 1;
1314
1187
  const currentX = store.X + indentLevel * options.page.indent;
1315
1188
  const currentY = store.Y;
@@ -1319,10 +1192,13 @@ const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
1319
1192
  if (element.items && element.items.length > 0) element.items.forEach((item) => {
1320
1193
  renderElement(item, blockquoteIndent, store);
1321
1194
  });
1322
- const endY = store.Y;
1195
+ const endY = store.lastContentY || store.Y;
1323
1196
  const endPage = doc.internal.getCurrentPageInfo().pageNumber;
1324
- doc.setDrawColor(100);
1325
- 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);
1326
1202
  for (let p = startPage; p <= endPage; p++) {
1327
1203
  doc.setPage(p);
1328
1204
  const isStart = p === startPage;
@@ -1333,6 +1209,10 @@ const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
1333
1209
  }
1334
1210
  store.recordContentY();
1335
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);
1336
1216
  };
1337
1217
  //#endregion
1338
1218
  //#region src/renderer/components/image.ts
@@ -1402,9 +1282,10 @@ const renderImage = (doc, element, indentLevel, store) => {
1402
1282
  const imgFormat = detectImageFormat(element);
1403
1283
  if (finalWidth > 0 && finalHeight > 0) doc.addImage(element.data, imgFormat, drawX, currentY, finalWidth, finalHeight);
1404
1284
  store.updateY(finalHeight, "add");
1285
+ store.updateY(store.options.spacing?.afterImage ?? 2, "add");
1405
1286
  store.recordContentY();
1406
1287
  } catch (e) {
1407
- console.warn("Failed to render image", e);
1288
+ console.warn("[jspdf-md-renderer] Failed to render image", e);
1408
1289
  }
1409
1290
  };
1410
1291
  //#endregion
@@ -1417,34 +1298,56 @@ const resolveAutoTable = () => {
1417
1298
  throw new Error("Could not resolve jspdf-autotable export. Expected a callable export.");
1418
1299
  };
1419
1300
  const renderTable = (doc, element, indentLevel, store) => {
1420
- 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
+ }
1421
1305
  const options = store.options;
1422
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
+ });
1423
1317
  const head = [element.header.map((h) => h.content || "")];
1424
- const body = element.rows.map((row) => row.map((cell) => cell.content || ""));
1425
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
+ };
1426
1333
  resolveAutoTable()(doc, {
1427
1334
  head,
1428
- body,
1335
+ body: rows,
1429
1336
  startY: store.Y,
1430
1337
  margin: {
1431
1338
  left: marginLeft,
1432
1339
  right: options.page.xmargin
1433
1340
  },
1434
1341
  ...userTableOptions,
1435
- didDrawPage: (data) => {
1436
- if (userTableOptions.didDrawPage) userTableOptions.didDrawPage(data);
1437
- },
1438
- didDrawCell: (data) => {
1439
- if (userTableOptions.didDrawCell) userTableOptions.didDrawCell(data);
1440
- }
1342
+ didDrawPage: safeDidDrawPage,
1343
+ didDrawCell: safeDidDrawCell
1441
1344
  });
1442
1345
  const finalY = doc.lastAutoTable?.finalY;
1443
1346
  if (typeof finalY === "number") {
1444
- store.updateY(finalY + options.page.lineSpace, "set");
1347
+ store.updateY(finalY + (options.spacing?.afterTable ?? 3), "set");
1445
1348
  store.updateX(options.page.xpading, "set");
1446
1349
  store.recordContentY();
1447
- }
1350
+ } else console.warn("[jspdf-md-renderer] autoTable did not return a finalY. Y position may be incorrect.");
1448
1351
  };
1449
1352
  //#endregion
1450
1353
  //#region src/store/renderStore.ts
@@ -1525,65 +1428,220 @@ var RenderStore = class {
1525
1428
  };
1526
1429
  //#endregion
1527
1430
  //#region src/utils/options-validation.ts
1528
- const defaultOptions = {
1529
- page: {
1530
- indent: 10,
1531
- maxContentWidth: 190,
1532
- maxContentHeight: 277,
1533
- lineSpace: 1.5,
1534
- defaultLineHeightFactor: 1.2,
1535
- defaultFontSize: 12,
1536
- defaultTitleFontSize: 14,
1537
- topmargin: 10,
1538
- xpading: 10,
1539
- xmargin: 10,
1540
- format: "a4",
1541
- 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"
1542
1444
  },
1543
- font: {
1544
- bold: {
1545
- name: "helvetica",
1546
- style: "bold"
1547
- },
1548
- regular: {
1549
- name: "helvetica",
1550
- style: "normal"
1551
- },
1552
- light: {
1553
- name: "helvetica",
1554
- style: "light"
1555
- },
1556
- code: {
1557
- name: "courier",
1558
- style: "normal"
1559
- }
1445
+ regular: {
1446
+ name: "helvetica",
1447
+ style: "normal"
1448
+ },
1449
+ light: {
1450
+ name: "helvetica",
1451
+ style: "light"
1560
1452
  },
1561
- image: { defaultAlign: "left" }
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
1562
1472
  };
1563
1473
  const validateOptions = (options) => {
1564
- if (!options) throw new Error("RenderOption is required");
1565
- const mergedPage = {
1566
- ...defaultOptions.page,
1474
+ if (!options) throw new Error("[jspdf-md-renderer] RenderOption is required");
1475
+ const page = {
1476
+ ...DEFAULT_PAGE,
1567
1477
  ...options.page
1568
1478
  };
1569
- const mergedFont = {
1570
- ...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,
1571
1487
  ...options.font
1572
1488
  };
1573
- const mergedImage = {
1574
- ...defaultOptions.image,
1575
- ...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 ?? {}
1576
1536
  };
1577
- if (!mergedPage.maxContentWidth) mergedPage.maxContentWidth = 190;
1578
- if (!mergedPage.maxContentHeight) mergedPage.maxContentHeight = 277;
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 ?? {}
1548
+ };
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 ?? (() => {});
1579
1570
  return {
1580
1571
  ...options,
1581
- page: mergedPage,
1582
- font: mergedFont,
1583
- image: mergedImage
1572
+ page,
1573
+ font,
1574
+ heading,
1575
+ codespan,
1576
+ blockquote,
1577
+ list,
1578
+ paragraph,
1579
+ codeBlock,
1580
+ spacing,
1581
+ image,
1582
+ endCursorYHandler
1584
1583
  };
1585
1584
  };
1586
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
1587
1645
  //#region src/renderer/MdTextRender.ts
1588
1646
  /**
1589
1647
  * Renders parsed markdown text into jsPDF document.
@@ -1601,7 +1659,7 @@ const MdTextRender = async (doc, text, options) => {
1601
1659
  const indent = indentLevel * validOptions.page.indent;
1602
1660
  switch (element.type) {
1603
1661
  case "heading":
1604
- renderHeading(doc, element, indent, store, renderElement);
1662
+ renderHeading(doc, element, indent, store);
1605
1663
  break;
1606
1664
  case "paragraph":
1607
1665
  renderParagraph(doc, element, indent, store, renderElement);
@@ -1621,10 +1679,8 @@ const MdTextRender = async (doc, text, options) => {
1621
1679
  case "strong":
1622
1680
  case "em":
1623
1681
  case "codespan":
1624
- renderInlineText(doc, element, indent, store);
1625
- break;
1626
1682
  case "link":
1627
- renderLink(doc, element, indent, store);
1683
+ renderInlineContent(doc, [element], store.X + indent, store.Y, validOptions.page.maxContentWidth - indent, store);
1628
1684
  break;
1629
1685
  case "blockquote":
1630
1686
  renderBlockquote(doc, element, indentLevel, store, renderElement);
@@ -1656,7 +1712,8 @@ const MdTextRender = async (doc, text, options) => {
1656
1712
  }
1657
1713
  };
1658
1714
  for (const item of parsedElements) renderElement(item, 0, store);
1715
+ applyPageDecorations(doc, validOptions);
1659
1716
  validOptions.endCursorYHandler(store.Y);
1660
1717
  };
1661
1718
  //#endregion
1662
- export { MdTextParser, MdTextRender };
1719
+ export { MdTextParser, MdTextRender, MdTokenType, renderInlineContent, renderPlainText, validateOptions };