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