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