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.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*(\
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
310
|
-
if (/^<br\s*\/?>$/i.test(
|
|
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:
|
|
371
|
+
content: ""
|
|
317
372
|
};
|
|
318
373
|
},
|
|
319
374
|
["br"]: () => ({
|
|
@@ -322,14 +377,6 @@ 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;
|
|
328
|
-
};
|
|
329
|
-
const getCharWidth = (doc) => {
|
|
330
|
-
return doc.getTextDimensions("H").w;
|
|
331
|
-
};
|
|
332
|
-
//#endregion
|
|
333
380
|
//#region src/utils/handlePageBreak.ts
|
|
334
381
|
const HandlePageBreaks = (doc, store) => {
|
|
335
382
|
if (typeof store.options.pageBreakHandler === "function") store.options.pageBreakHandler(doc);
|
|
@@ -337,6 +384,35 @@ const HandlePageBreaks = (doc, store) => {
|
|
|
337
384
|
store.updateY(store.options.page.topmargin);
|
|
338
385
|
store.updateX(store.options.page.xpading);
|
|
339
386
|
};
|
|
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;
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Checks if we will overflow, and if so, breaks the page first.
|
|
395
|
+
* Returns true if a page break was performed.
|
|
396
|
+
*/
|
|
397
|
+
const breakIfOverflow = (doc, store, height) => {
|
|
398
|
+
if (willOverflow(store, height)) {
|
|
399
|
+
HandlePageBreaks(doc, store);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
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);
|
|
410
|
+
};
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/utils/doc-helpers.ts
|
|
413
|
+
const getCharHight = (doc) => {
|
|
414
|
+
return doc.getFontSize() / doc.internal.scaleFactor;
|
|
415
|
+
};
|
|
340
416
|
//#endregion
|
|
341
417
|
//#region src/utils/image-utils.ts
|
|
342
418
|
/**
|
|
@@ -504,115 +580,127 @@ const prefetchImages = async (elements) => {
|
|
|
504
580
|
}
|
|
505
581
|
};
|
|
506
582
|
//#endregion
|
|
507
|
-
//#region src/
|
|
583
|
+
//#region src/layout/wordSplitter.ts
|
|
508
584
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* Features:
|
|
512
|
-
* - Handles bold, italic, codespan, links mixed in paragraph
|
|
513
|
-
* - Proper word spacing distribution for justified alignment
|
|
514
|
-
* - Supports left, right, center, and justify alignments
|
|
515
|
-
* - Page break handling
|
|
516
|
-
* - Preserves link clickability
|
|
517
|
-
* - Codespan background rendering
|
|
585
|
+
* Maps a ParsedElement type string to a TextStyle.
|
|
518
586
|
*/
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
return
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
showBackground: opts.showBackground !== false,
|
|
526
|
-
fontSizeScale: opts.fontSizeScale ?? .9
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Apply font style to the jsPDF document.
|
|
531
|
-
*/
|
|
532
|
-
static applyStyle(doc, style, store) {
|
|
533
|
-
const currentFont = doc.getFont().fontName;
|
|
534
|
-
const currentFontSize = doc.getFontSize();
|
|
535
|
-
const getBoldFont = () => {
|
|
536
|
-
const boldName = store.options.font.bold?.name;
|
|
537
|
-
return boldName && boldName !== "" ? boldName : currentFont;
|
|
538
|
-
};
|
|
539
|
-
const getRegularFont = () => {
|
|
540
|
-
const regularName = store.options.font.regular?.name;
|
|
541
|
-
return regularName && regularName !== "" ? regularName : currentFont;
|
|
542
|
-
};
|
|
543
|
-
switch (style) {
|
|
544
|
-
case "bold":
|
|
545
|
-
doc.setFont(getBoldFont(), store.options.font.bold?.style || "bold");
|
|
546
|
-
break;
|
|
547
|
-
case "italic":
|
|
548
|
-
doc.setFont(getRegularFont(), "italic");
|
|
549
|
-
break;
|
|
550
|
-
case "bolditalic":
|
|
551
|
-
doc.setFont(getBoldFont(), "bolditalic");
|
|
552
|
-
break;
|
|
553
|
-
case "codespan":
|
|
554
|
-
const codeFont = store.options.font.code || {
|
|
555
|
-
name: "courier",
|
|
556
|
-
style: "normal"
|
|
557
|
-
};
|
|
558
|
-
doc.setFont(codeFont.name, codeFont.style);
|
|
559
|
-
doc.setFontSize(currentFontSize * this.getCodespanOptions(store).fontSizeScale);
|
|
560
|
-
break;
|
|
561
|
-
default:
|
|
562
|
-
doc.setFont(getRegularFont(), doc.getFont().fontStyle);
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
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";
|
|
565
593
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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;
|
|
581
637
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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;
|
|
595
657
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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;
|
|
613
701
|
result.push({
|
|
614
|
-
text:
|
|
615
|
-
width:
|
|
702
|
+
text: words[wi],
|
|
703
|
+
width: measureStyledWidth(doc, words[wi], style, store),
|
|
616
704
|
style,
|
|
617
705
|
isLink: elIsLink,
|
|
618
706
|
href: elHref,
|
|
@@ -621,338 +709,253 @@ var JustifiedTextRenderer = class {
|
|
|
621
709
|
0,
|
|
622
710
|
255
|
|
623
711
|
] : void 0,
|
|
624
|
-
|
|
625
|
-
imageElement: el,
|
|
626
|
-
imageHeight: finalHeight
|
|
712
|
+
hasTrailingSpace: !isLastInLine || /[ \t]$/.test(lines[li])
|
|
627
713
|
});
|
|
628
|
-
}
|
|
714
|
+
}
|
|
715
|
+
if (li < lines.length - 1) result.push({
|
|
629
716
|
text: "",
|
|
630
717
|
width: 0,
|
|
631
718
|
style,
|
|
632
719
|
isBr: true
|
|
633
720
|
});
|
|
634
|
-
else {
|
|
635
|
-
const text = el.content || el.text || "";
|
|
636
|
-
if (!text) continue;
|
|
637
|
-
if (/^\s/.test(text) && result.length > 0) result[result.length - 1].hasTrailingSpace = true;
|
|
638
|
-
if (style === "codespan") {
|
|
639
|
-
const trimmedText = text.trim();
|
|
640
|
-
if (trimmedText) result.push({
|
|
641
|
-
text: trimmedText,
|
|
642
|
-
width: this.measureWordWidth(doc, trimmedText, style, store),
|
|
643
|
-
style,
|
|
644
|
-
isLink: elIsLink,
|
|
645
|
-
href: elHref,
|
|
646
|
-
linkColor: elIsLink ? store.options.link?.linkColor || [
|
|
647
|
-
0,
|
|
648
|
-
0,
|
|
649
|
-
255
|
|
650
|
-
] : void 0,
|
|
651
|
-
hasTrailingSpace: /\s$/.test(text)
|
|
652
|
-
});
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
const lines = text.split("\n");
|
|
656
|
-
for (let partIndex = 0; partIndex < lines.length; partIndex++) {
|
|
657
|
-
const lineStr = lines[partIndex];
|
|
658
|
-
const words = lineStr.trim().split(/[ \t\r\v\f]+/).filter((w) => w.length > 0);
|
|
659
|
-
for (let i = 0; i < words.length; i++) {
|
|
660
|
-
const hasTrailingSpace = !(i === words.length - 1) || /[ \t\r\v\f]$/.test(lineStr);
|
|
661
|
-
result.push({
|
|
662
|
-
text: words[i],
|
|
663
|
-
width: this.measureWordWidth(doc, words[i], style, store),
|
|
664
|
-
style,
|
|
665
|
-
isLink: elIsLink,
|
|
666
|
-
href: elHref,
|
|
667
|
-
linkColor: elIsLink ? store.options.link?.linkColor || [
|
|
668
|
-
0,
|
|
669
|
-
0,
|
|
670
|
-
255
|
|
671
|
-
] : void 0,
|
|
672
|
-
hasTrailingSpace
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
if (partIndex < lines.length - 1) result.push({
|
|
676
|
-
text: "",
|
|
677
|
-
width: 0,
|
|
678
|
-
style,
|
|
679
|
-
isBr: true
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
721
|
}
|
|
684
|
-
return result;
|
|
685
722
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
let
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
lines.push({
|
|
703
|
-
words: currentLine,
|
|
704
|
-
totalTextWidth: currentTextWidth,
|
|
705
|
-
isLastLine: true,
|
|
706
|
-
lineHeight: currentLineHeight
|
|
707
|
-
});
|
|
708
|
-
currentLine = [];
|
|
709
|
-
currentTextWidth = 0;
|
|
710
|
-
currentLineWidth = 0;
|
|
711
|
-
currentLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
if (currentLineWidth + neededWidthWithSpace > maxWidth && currentLine.length > 0) {
|
|
715
|
-
lines.push({
|
|
716
|
-
words: currentLine,
|
|
717
|
-
totalTextWidth: currentTextWidth,
|
|
718
|
-
isLastLine: false,
|
|
719
|
-
lineHeight: currentLineHeight
|
|
720
|
-
});
|
|
721
|
-
currentLine = [word];
|
|
722
|
-
currentTextWidth = word.width;
|
|
723
|
-
currentLineWidth = word.width;
|
|
724
|
-
currentLineHeight = itemHeight;
|
|
725
|
-
} else {
|
|
726
|
-
currentLine.push(word);
|
|
727
|
-
currentTextWidth += word.width;
|
|
728
|
-
currentLineWidth += neededWidthWithSpace;
|
|
729
|
-
currentLineHeight = Math.max(currentLineHeight, itemHeight);
|
|
730
|
-
}
|
|
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;
|
|
731
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) => {
|
|
732
763
|
if (currentLine.length > 0) lines.push({
|
|
733
764
|
words: currentLine,
|
|
734
765
|
totalTextWidth: currentTextWidth,
|
|
735
|
-
isLastLine:
|
|
766
|
+
isLastLine: isLast,
|
|
736
767
|
lineHeight: currentLineHeight
|
|
737
768
|
});
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
this.applyStyle(doc, word.style, store);
|
|
748
|
-
if (word.isLink && word.linkColor) doc.setTextColor(...word.linkColor);
|
|
749
|
-
if (word.isImage && word.imageElement && word.imageElement.data) try {
|
|
750
|
-
let imgFormat = "JPEG";
|
|
751
|
-
if (word.imageElement.data.startsWith("data:image/png")) imgFormat = "PNG";
|
|
752
|
-
else if (word.imageElement.data.startsWith("data:image/webp")) imgFormat = "WEBP";
|
|
753
|
-
else if (word.imageElement.data.startsWith("data:image/gif")) imgFormat = "GIF";
|
|
754
|
-
else if (word.imageElement.src) {
|
|
755
|
-
const ext = word.imageElement.src.split("?")[0].split("#")[0].split(".").pop()?.toUpperCase();
|
|
756
|
-
if (ext && [
|
|
757
|
-
"PNG",
|
|
758
|
-
"JPEG",
|
|
759
|
-
"JPG",
|
|
760
|
-
"WEBP",
|
|
761
|
-
"GIF"
|
|
762
|
-
].includes(ext)) imgFormat = ext === "JPG" ? "JPEG" : ext;
|
|
763
|
-
}
|
|
764
|
-
if (word.width > 0 && (word.imageHeight || 0) > 0) {
|
|
765
|
-
const imgH = word.imageHeight || 0;
|
|
766
|
-
const imgY = y;
|
|
767
|
-
doc.addImage(word.imageElement.data, imgFormat, x, imgY, word.width, imgH);
|
|
768
|
-
}
|
|
769
|
-
} catch (e) {
|
|
770
|
-
console.warn("Failed to render inline image", e);
|
|
771
|
-
}
|
|
772
|
-
else {
|
|
773
|
-
if (word.style === "codespan") {
|
|
774
|
-
const codespanOpts = this.getCodespanOptions(store);
|
|
775
|
-
if (codespanOpts.showBackground) {
|
|
776
|
-
const h = getCharHight(doc);
|
|
777
|
-
const pad = codespanOpts.padding;
|
|
778
|
-
doc.setFillColor(codespanOpts.backgroundColor);
|
|
779
|
-
doc.rect(x - pad, y - pad, word.width + pad * 2, h + pad * 2, "F");
|
|
780
|
-
doc.setFillColor("#000000");
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
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;
|
|
784
778
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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);
|
|
788
792
|
}
|
|
789
|
-
doc.setFont(savedFont.fontName, savedFont.fontStyle);
|
|
790
|
-
doc.setFontSize(savedSize);
|
|
791
|
-
doc.setTextColor(savedColor);
|
|
792
793
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
}
|
|
808
|
-
switch (alignment) {
|
|
809
|
-
case "right":
|
|
810
|
-
startX = x + maxWidth - lineWidthWithNormalSpaces;
|
|
811
|
-
break;
|
|
812
|
-
case "center":
|
|
813
|
-
startX = x + (maxWidth - lineWidthWithNormalSpaces) / 2;
|
|
814
|
-
break;
|
|
815
|
-
case "justify":
|
|
816
|
-
if (!isLastLine && expandableSpacesCount > 0) wordSpacing = (maxWidth - totalTextWidth) / expandableSpacesCount;
|
|
817
|
-
break;
|
|
818
|
-
default: break;
|
|
819
|
-
}
|
|
820
|
-
let currentX = startX;
|
|
821
|
-
const textHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
|
|
822
|
-
for (let i = 0; i < words.length; i++) {
|
|
823
|
-
const word = words[i];
|
|
824
|
-
let drawY = y;
|
|
825
|
-
const elementHeight = word.isImage && word.imageHeight ? word.imageHeight : textHeight;
|
|
826
|
-
if (word.isImage) drawY = y;
|
|
827
|
-
else if (elementHeight < line.lineHeight) drawY = y + (line.lineHeight - elementHeight);
|
|
828
|
-
this.renderWord(doc, word, currentX, drawY, store);
|
|
829
|
-
currentX += word.width;
|
|
830
|
-
if (i < words.length - 1 && word.hasTrailingSpace) currentX += wordSpacing;
|
|
831
|
-
}
|
|
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++;
|
|
832
808
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
static renderStyledParagraph(doc, elements, x, y, maxWidth, store, alignment) {
|
|
846
|
-
const textAlignment = alignment ?? store.options.content?.textAlignment ?? "left";
|
|
847
|
-
const words = this.flattenToWords(doc, elements, store);
|
|
848
|
-
if (words.length === 0) return;
|
|
849
|
-
const lines = this.breakIntoLines(doc, words, maxWidth, store);
|
|
850
|
-
let currentY = y;
|
|
851
|
-
for (const line of lines) {
|
|
852
|
-
if (currentY + line.lineHeight > store.options.page.maxContentHeight) {
|
|
853
|
-
HandlePageBreaks(doc, store);
|
|
854
|
-
currentY = store.Y;
|
|
855
|
-
}
|
|
856
|
-
this.renderAlignedLine(doc, line, x, currentY, maxWidth, store, textAlignment);
|
|
857
|
-
store.recordContentY(currentY + line.lineHeight);
|
|
858
|
-
currentY += line.lineHeight;
|
|
859
|
-
store.updateY(line.lineHeight, "add");
|
|
860
|
-
}
|
|
861
|
-
const lastLine = lines[lines.length - 1];
|
|
862
|
-
if (lastLine) {
|
|
863
|
-
let actualSpacesCount = 0;
|
|
864
|
-
for (let i = 0; i < lastLine.words.length - 1; i++) if (lastLine.words[i].hasTrailingSpace) actualSpacesCount++;
|
|
865
|
-
const lastLineWidth = lastLine.totalTextWidth + actualSpacesCount * doc.getTextWidth(" ");
|
|
866
|
-
store.updateX(x + lastLineWidth, "set");
|
|
867
|
-
}
|
|
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;
|
|
868
821
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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;
|
|
874
831
|
}
|
|
875
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
|
+
};
|
|
876
870
|
//#endregion
|
|
877
|
-
//#region src/
|
|
871
|
+
//#region src/layout/layoutEngine.ts
|
|
878
872
|
/**
|
|
879
|
-
*
|
|
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.
|
|
880
878
|
*/
|
|
881
|
-
const
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
store.recordContentY(
|
|
897
|
-
|
|
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;
|
|
891
|
+
}
|
|
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");
|
|
898
897
|
}
|
|
899
|
-
|
|
900
|
-
|
|
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;
|
|
906
|
+
};
|
|
907
|
+
/**
|
|
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.
|
|
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);
|
|
901
916
|
};
|
|
902
917
|
//#endregion
|
|
903
|
-
//#region src/
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
align: "justify",
|
|
930
|
-
baseline: "top"
|
|
931
|
-
});
|
|
932
|
-
else doc.text(line, x, currentY, { baseline: "top" });
|
|
933
|
-
store.recordContentY(currentY + charHeight);
|
|
934
|
-
currentY += lineHeight;
|
|
935
|
-
store.updateY(lineHeight, "add");
|
|
936
|
-
}
|
|
937
|
-
return currentY;
|
|
938
|
-
}
|
|
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");
|
|
939
944
|
};
|
|
940
945
|
//#endregion
|
|
941
946
|
//#region src/renderer/components/paragraph.ts
|
|
942
|
-
/**
|
|
943
|
-
* Renders paragraph elements with proper text alignment.
|
|
944
|
-
* Handles mixed inline styles (bold, italic, codespan) and links.
|
|
945
|
-
* Respects user's textAlignment option from RenderStore.
|
|
946
|
-
*/
|
|
947
947
|
const renderParagraph = (doc, element, indent, store, parentElementRenderer) => {
|
|
948
|
-
store.
|
|
948
|
+
const indentLevel = indent / store.options.page.indent;
|
|
949
|
+
const savedColor = doc.getTextColor();
|
|
949
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");
|
|
950
953
|
const maxWidth = store.options.page.maxContentWidth - indent;
|
|
951
|
-
if (element
|
|
954
|
+
if (element.items && element.items.length > 0) {
|
|
952
955
|
if (element.items.length === 1 && element.items[0].type === "image") {
|
|
953
|
-
parentElementRenderer(element.items[0],
|
|
956
|
+
parentElementRenderer(element.items[0], indentLevel, store, false);
|
|
954
957
|
store.updateX(store.options.page.xpading);
|
|
955
|
-
|
|
958
|
+
doc.setTextColor(savedColor);
|
|
956
959
|
return;
|
|
957
960
|
}
|
|
958
961
|
const inlineTypes = [
|
|
@@ -966,35 +969,38 @@ const renderParagraph = (doc, element, indent, store, parentElementRenderer) =>
|
|
|
966
969
|
];
|
|
967
970
|
if (element.items.some((item) => !inlineTypes.includes(item.type))) {
|
|
968
971
|
const inlineBuffer = [];
|
|
969
|
-
const
|
|
972
|
+
const flush = () => {
|
|
970
973
|
if (inlineBuffer.length > 0) {
|
|
971
|
-
|
|
974
|
+
renderInlineContent(doc, inlineBuffer, store.X + indent, store.Y, maxWidth, store, { trimLastLine: true });
|
|
972
975
|
inlineBuffer.length = 0;
|
|
973
976
|
}
|
|
974
977
|
};
|
|
975
978
|
for (const item of element.items) if (inlineTypes.includes(item.type)) inlineBuffer.push(item);
|
|
976
979
|
else {
|
|
977
|
-
|
|
978
|
-
parentElementRenderer(item,
|
|
980
|
+
flush();
|
|
981
|
+
parentElementRenderer(item, indentLevel, store, false);
|
|
979
982
|
}
|
|
980
|
-
|
|
981
|
-
} else
|
|
982
|
-
} else {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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");
|
|
987
991
|
store.updateX(store.options.page.xpading);
|
|
988
|
-
|
|
992
|
+
doc.setTextColor(savedColor);
|
|
989
993
|
};
|
|
990
994
|
//#endregion
|
|
991
995
|
//#region src/renderer/components/list.ts
|
|
992
996
|
const renderList = (doc, element, indentLevel, store, parentElementRenderer) => {
|
|
993
997
|
doc.setFontSize(store.options.page.defaultFontSize);
|
|
994
998
|
for (const [i, point] of element?.items?.entries() ?? []) {
|
|
995
|
-
const _start = element.ordered ? (element.start ??
|
|
999
|
+
const _start = element.ordered ? (element.start ?? 1) + i : void 0;
|
|
996
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");
|
|
997
1002
|
}
|
|
1003
|
+
store.updateY(store.options.spacing?.afterList ?? 3, "add");
|
|
998
1004
|
};
|
|
999
1005
|
//#endregion
|
|
1000
1006
|
//#region src/renderer/components/listItem.ts
|
|
@@ -1002,22 +1008,46 @@ const renderList = (doc, element, indentLevel, store, parentElementRenderer) =>
|
|
|
1002
1008
|
* Render a single list item, including bullets/numbering, inline text, and any nested lists.
|
|
1003
1009
|
*/
|
|
1004
1010
|
const renderListItem = (doc, element, indentLevel, store, parentElementRenderer, start, ordered) => {
|
|
1005
|
-
|
|
1011
|
+
breakIfOverflow(doc, store, getCharHight(doc));
|
|
1006
1012
|
const options = store.options;
|
|
1007
|
-
const
|
|
1008
|
-
const
|
|
1013
|
+
const listOpts = store.options.list ?? {};
|
|
1014
|
+
const baseIndent = indentLevel * (listOpts.indentSize ?? options.page.indent);
|
|
1015
|
+
const bullet = ordered ? `${start}. ` : listOpts.bulletChar ?? "• ";
|
|
1009
1016
|
const xLeft = options.page.xpading;
|
|
1010
1017
|
store.updateX(xLeft, "set");
|
|
1011
1018
|
doc.setFont(options.font.regular.name, options.font.regular.style);
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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;
|
|
1015
1043
|
const textMaxWidth = options.page.maxContentWidth - baseIndent - bulletWidth;
|
|
1044
|
+
const originalTextColor = doc.getTextColor();
|
|
1045
|
+
if (element.checked) doc.setTextColor(150, 150, 150);
|
|
1016
1046
|
if (element.items && element.items.length > 0) {
|
|
1017
1047
|
const inlineBuffer = [];
|
|
1018
1048
|
const flushInlineBuffer = () => {
|
|
1019
1049
|
if (inlineBuffer.length > 0) {
|
|
1020
|
-
|
|
1050
|
+
renderInlineContent(doc, inlineBuffer, contentX, store.Y, textMaxWidth, store);
|
|
1021
1051
|
inlineBuffer.length = 0;
|
|
1022
1052
|
store.updateX(xLeft, "set");
|
|
1023
1053
|
}
|
|
@@ -1030,10 +1060,8 @@ const renderListItem = (doc, element, indentLevel, store, parentElementRenderer,
|
|
|
1030
1060
|
parentElementRenderer(subItem, indentLevel, store, true, start, ordered);
|
|
1031
1061
|
} else inlineBuffer.push(subItem);
|
|
1032
1062
|
flushInlineBuffer();
|
|
1033
|
-
} else if (element.content)
|
|
1034
|
-
|
|
1035
|
-
TextRenderer.renderText(doc, element.content, store, contentX, store.Y, textMaxWidth, textAlignment === "justify");
|
|
1036
|
-
}
|
|
1063
|
+
} else if (element.content) renderPlainText(doc, element.content, contentX, store.Y, textMaxWidth, store);
|
|
1064
|
+
doc.setTextColor(originalTextColor);
|
|
1037
1065
|
};
|
|
1038
1066
|
//#endregion
|
|
1039
1067
|
//#region src/renderer/components/rawItem.ts
|
|
@@ -1042,19 +1070,18 @@ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentEle
|
|
|
1042
1070
|
else {
|
|
1043
1071
|
const options = store.options;
|
|
1044
1072
|
const indent = indentLevel * options.page.indent;
|
|
1045
|
-
const
|
|
1073
|
+
const listOpts = store.options.list ?? {};
|
|
1074
|
+
const bullet = hasRawBullet ? ordered ? `${start}. ` : listOpts.bulletChar ?? "• " : "";
|
|
1046
1075
|
const content = element.content || "";
|
|
1047
1076
|
const xLeft = options.page.xpading;
|
|
1048
1077
|
if (!content && !bullet) return;
|
|
1049
1078
|
if (!content.trim() && !bullet) {
|
|
1050
1079
|
const newlines = (content.match(/\n/g) || []).length;
|
|
1051
1080
|
if (newlines > 1) {
|
|
1052
|
-
const addedHeight = (newlines - 1) * (doc
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
store.recordContentY(store.Y);
|
|
1057
|
-
}
|
|
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);
|
|
1058
1085
|
}
|
|
1059
1086
|
return;
|
|
1060
1087
|
}
|
|
@@ -1064,10 +1091,10 @@ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentEle
|
|
|
1064
1091
|
const textMaxWidth = options.page.maxContentWidth - indent - bulletWidth;
|
|
1065
1092
|
doc.setFont(options.font.regular.name, options.font.regular.style);
|
|
1066
1093
|
doc.text(bullet, xLeft + indent, store.Y, { baseline: "top" });
|
|
1067
|
-
|
|
1094
|
+
renderPlainText(doc, content, xLeft + indent + bulletWidth, store.Y, textMaxWidth, store, { alignment: justify ? "justify" : "left" });
|
|
1068
1095
|
} else {
|
|
1069
1096
|
const textMaxWidth = options.page.maxContentWidth - indent;
|
|
1070
|
-
|
|
1097
|
+
renderPlainText(doc, content, xLeft + indent, store.Y, textMaxWidth, store, { alignment: justify ? "justify" : "left" });
|
|
1071
1098
|
}
|
|
1072
1099
|
store.updateX(xLeft, "set");
|
|
1073
1100
|
}
|
|
@@ -1075,6 +1102,9 @@ const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentEle
|
|
|
1075
1102
|
//#endregion
|
|
1076
1103
|
//#region src/renderer/components/hr.ts
|
|
1077
1104
|
const renderHR = (doc, store) => {
|
|
1105
|
+
const savedDrawColor = doc.getDrawColor();
|
|
1106
|
+
const savedLineWidth = doc.getLineWidth();
|
|
1107
|
+
breakIfOverflow(doc, store, getCharHight(doc));
|
|
1078
1108
|
const pageWidth = doc.internal.pageSize.getWidth();
|
|
1079
1109
|
doc.setLineDashPattern([1, 1], 0);
|
|
1080
1110
|
doc.setLineWidth(.1);
|
|
@@ -1082,21 +1112,30 @@ const renderHR = (doc, store) => {
|
|
|
1082
1112
|
doc.setLineWidth(.1);
|
|
1083
1113
|
doc.setLineDashPattern([], 0);
|
|
1084
1114
|
store.updateY(getCharHight(doc), "add");
|
|
1115
|
+
store.updateY(store.options.spacing?.afterHR ?? 2, "add");
|
|
1116
|
+
doc.setDrawColor(savedDrawColor);
|
|
1117
|
+
doc.setLineWidth(savedLineWidth);
|
|
1085
1118
|
};
|
|
1086
1119
|
//#endregion
|
|
1087
1120
|
//#region src/renderer/components/code.ts
|
|
1088
1121
|
const renderCodeBlock = (doc, element, indentLevel, store) => {
|
|
1089
1122
|
const savedFont = doc.getFont();
|
|
1090
1123
|
const savedFontSize = doc.getFontSize();
|
|
1124
|
+
const savedTextColor = doc.getTextColor();
|
|
1125
|
+
const savedDrawColor = doc.getDrawColor();
|
|
1126
|
+
const savedFillColor = doc.getFillColor();
|
|
1091
1127
|
const codeFont = store.options.font.code || {
|
|
1092
1128
|
name: "courier",
|
|
1093
1129
|
style: "normal"
|
|
1094
1130
|
};
|
|
1095
1131
|
doc.setFont(codeFont.name, codeFont.style);
|
|
1096
|
-
const
|
|
1132
|
+
const codeOpts = store.options.codeBlock ?? {};
|
|
1133
|
+
const codeFontSizeScale = codeOpts.fontSizeScale ?? .9;
|
|
1134
|
+
const codeFontSize = store.options.page.defaultFontSize * codeFontSizeScale;
|
|
1097
1135
|
doc.setFontSize(codeFontSize);
|
|
1098
1136
|
const indent = indentLevel * store.options.page.indent;
|
|
1099
|
-
const
|
|
1137
|
+
const padding = codeOpts.padding ?? 4;
|
|
1138
|
+
const maxWidth = store.options.page.maxContentWidth - indent - padding * 2;
|
|
1100
1139
|
const lineHeightFactor = doc.getLineHeightFactor();
|
|
1101
1140
|
const lineHeight = codeFontSize / doc.internal.scaleFactor * lineHeightFactor;
|
|
1102
1141
|
const content = (element.code ?? "").replace(/[\r\n\s]+$/, "");
|
|
@@ -1112,9 +1151,9 @@ const renderCodeBlock = (doc, element, indentLevel, store) => {
|
|
|
1112
1151
|
doc.setFontSize(savedFontSize);
|
|
1113
1152
|
return;
|
|
1114
1153
|
}
|
|
1115
|
-
const
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1154
|
+
const bgColor = codeOpts.backgroundColor ?? "#F6F8FA";
|
|
1155
|
+
const drawColor = codeOpts.borderColor ?? "#E1E4E8";
|
|
1156
|
+
const radius = codeOpts.borderRadius ?? 2;
|
|
1118
1157
|
let currentLineIndex = 0;
|
|
1119
1158
|
while (currentLineIndex < lines.length) {
|
|
1120
1159
|
const availableHeight = store.options.page.maxContentHeight - store.Y;
|
|
@@ -1133,20 +1172,22 @@ const renderCodeBlock = (doc, element, indentLevel, store) => {
|
|
|
1133
1172
|
if (isFirstChunk) store.updateY(padding, "add");
|
|
1134
1173
|
doc.setFillColor(bgColor);
|
|
1135
1174
|
doc.setDrawColor(drawColor);
|
|
1136
|
-
doc.roundedRect(store.X, store.Y - padding, store.options.page.maxContentWidth, textBlockHeight + (isFirstChunk ? padding : 0) + (isLastChunk ? padding : 0),
|
|
1137
|
-
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) {
|
|
1138
1177
|
const savedCodeFontSize = doc.getFontSize();
|
|
1139
1178
|
doc.setFontSize(10);
|
|
1140
|
-
doc.setTextColor("#666666");
|
|
1179
|
+
doc.setTextColor(codeOpts.labelColor ?? "#666666");
|
|
1141
1180
|
doc.text(element.lang, store.X + store.options.page.maxContentWidth - doc.getTextWidth(element.lang) - 4, store.Y, { baseline: "top" });
|
|
1142
1181
|
doc.setFontSize(savedCodeFontSize);
|
|
1143
|
-
doc.setTextColor(
|
|
1182
|
+
doc.setTextColor(savedTextColor);
|
|
1144
1183
|
}
|
|
1145
1184
|
let yPos = store.Y;
|
|
1185
|
+
doc.setTextColor(codeOpts.textColor ?? "#000000");
|
|
1146
1186
|
for (const line of linesToRender) {
|
|
1147
1187
|
doc.text(line, store.X + 4, yPos, { baseline: "top" });
|
|
1148
1188
|
yPos += lineHeight;
|
|
1149
1189
|
}
|
|
1190
|
+
doc.setTextColor(savedTextColor);
|
|
1150
1191
|
store.updateY(textBlockHeight, "add");
|
|
1151
1192
|
store.recordContentY(store.Y + (isLastChunk ? padding : 0));
|
|
1152
1193
|
if (isLastChunk) store.updateY(padding, "add");
|
|
@@ -1155,199 +1196,17 @@ const renderCodeBlock = (doc, element, indentLevel, store) => {
|
|
|
1155
1196
|
}
|
|
1156
1197
|
doc.setFont(savedFont.fontName, savedFont.fontStyle);
|
|
1157
1198
|
doc.setFontSize(savedFontSize);
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
* Renders inline text elements (Strong, Em, and Text) with proper inline styling.
|
|
1163
|
-
*/
|
|
1164
|
-
const renderInlineText = (doc, element, indent, store) => {
|
|
1165
|
-
const currentFont = doc.getFont().fontName;
|
|
1166
|
-
const currentFontStyle = doc.getFont().fontStyle;
|
|
1167
|
-
const currentFontSize = doc.getFontSize();
|
|
1168
|
-
const spaceMultiplier = (style) => {
|
|
1169
|
-
switch (style) {
|
|
1170
|
-
case "normal": return 0;
|
|
1171
|
-
case "bold": return 1;
|
|
1172
|
-
case "italic": return 1.5;
|
|
1173
|
-
case "bolditalic": return 1.5;
|
|
1174
|
-
case "codespan": return .5;
|
|
1175
|
-
default: return 0;
|
|
1176
|
-
}
|
|
1177
|
-
};
|
|
1178
|
-
const renderTextWithStyle = (text, style) => {
|
|
1179
|
-
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");
|
|
1180
|
-
else if (style === "italic") doc.setFont(store.options.font.regular.name, "italic");
|
|
1181
|
-
else if (style === "bolditalic") doc.setFont(store.options.font.bold.name && store.options.font.bold.name !== "" ? store.options.font.bold.name : currentFont, "bolditalic");
|
|
1182
|
-
else if (style === "codespan") {
|
|
1183
|
-
const codeFont = store.options.font.code || {
|
|
1184
|
-
name: "courier",
|
|
1185
|
-
style: "normal"
|
|
1186
|
-
};
|
|
1187
|
-
doc.setFont(codeFont.name, codeFont.style);
|
|
1188
|
-
doc.setFontSize(currentFontSize * .9);
|
|
1189
|
-
} else doc.setFont(store.options.font.regular.name, currentFontStyle);
|
|
1190
|
-
const availableWidth = store.options.page.maxContentWidth - indent - store.X;
|
|
1191
|
-
const textLines = doc.splitTextToSize(text, availableWidth);
|
|
1192
|
-
const isCodeSpan = style === "codespan";
|
|
1193
|
-
const codePadding = 1;
|
|
1194
|
-
const codeBgColor = "#EEEEEE";
|
|
1195
|
-
if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
|
|
1196
|
-
if (isCodeSpan) {
|
|
1197
|
-
const lineWidth = doc.getTextWidth(textLines[i]) + getCharWidth(doc);
|
|
1198
|
-
const lineHeight = getCharHight(doc);
|
|
1199
|
-
doc.setFillColor(codeBgColor);
|
|
1200
|
-
doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, lineWidth + codePadding * 2, lineHeight + codePadding * 2, 2, 2, "F");
|
|
1201
|
-
doc.setFillColor("#000000");
|
|
1202
|
-
}
|
|
1203
|
-
doc.text(textLines[i], store.X + indent, store.Y, {
|
|
1204
|
-
baseline: "top",
|
|
1205
|
-
maxWidth: availableWidth
|
|
1206
|
-
});
|
|
1207
|
-
store.updateX(doc.getTextDimensions(textLines[i]).w + (isCodeSpan ? codePadding * 2 : 1), "add");
|
|
1208
|
-
if (i < textLines.length - 1) {
|
|
1209
|
-
store.updateY(getCharHight(doc), "add");
|
|
1210
|
-
store.updateX(store.options.page.xpading, "set");
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
else if (textLines.length > 1) {
|
|
1214
|
-
const firstLine = textLines[0];
|
|
1215
|
-
const restContent = textLines?.slice(1)?.join(" ");
|
|
1216
|
-
if (isCodeSpan) {
|
|
1217
|
-
const w = doc.getTextWidth(firstLine) + getCharWidth(doc);
|
|
1218
|
-
const h = getCharHight(doc);
|
|
1219
|
-
doc.setFillColor(codeBgColor);
|
|
1220
|
-
doc.roundedRect(store.X + (indent >= 2 ? indent + 2 : 0) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
|
|
1221
|
-
doc.setFillColor("#000000");
|
|
1222
|
-
}
|
|
1223
|
-
doc.text(firstLine, store.X + (indent >= 2 ? indent + 2 * spaceMultiplier(style) : 0), store.Y, {
|
|
1224
|
-
baseline: "top",
|
|
1225
|
-
maxWidth: availableWidth
|
|
1226
|
-
});
|
|
1227
|
-
store.updateX(store.options.page.xpading + indent);
|
|
1228
|
-
store.updateY(getCharHight(doc), "add");
|
|
1229
|
-
const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
|
|
1230
|
-
doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
|
|
1231
|
-
if (isCodeSpan) {
|
|
1232
|
-
const w = doc.getTextWidth(line) + getCharWidth(doc);
|
|
1233
|
-
const h = getCharHight(doc);
|
|
1234
|
-
doc.setFillColor(codeBgColor);
|
|
1235
|
-
doc.roundedRect(store.X + getCharWidth(doc) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
|
|
1236
|
-
doc.setFillColor("#000000");
|
|
1237
|
-
}
|
|
1238
|
-
doc.text(line, store.X + getCharWidth(doc), store.Y, {
|
|
1239
|
-
baseline: "top",
|
|
1240
|
-
maxWidth: maxWidthForRest
|
|
1241
|
-
});
|
|
1242
|
-
});
|
|
1243
|
-
} else {
|
|
1244
|
-
if (isCodeSpan) {
|
|
1245
|
-
const w = doc.getTextWidth(text) + getCharWidth(doc);
|
|
1246
|
-
const h = getCharHight(doc);
|
|
1247
|
-
doc.setFillColor(codeBgColor);
|
|
1248
|
-
doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
|
|
1249
|
-
doc.setFillColor("#000000");
|
|
1250
|
-
}
|
|
1251
|
-
doc.text(text, store.X + indent, store.Y, {
|
|
1252
|
-
baseline: "top",
|
|
1253
|
-
maxWidth: availableWidth
|
|
1254
|
-
});
|
|
1255
|
-
store.updateX(doc.getTextDimensions(text).w + (indent >= 2 ? text.split(" ").length + 2 : 2) * spaceMultiplier(style) * .5 + (isCodeSpan ? codePadding * 2 : 0), "add");
|
|
1256
|
-
}
|
|
1257
|
-
};
|
|
1258
|
-
if (element.type === "text" && element.items && element.items.length > 0) for (const item of element.items) if (item.type === "codespan") renderTextWithStyle(item.content || "", "codespan");
|
|
1259
|
-
else if (item.type === "em" || item.type === "strong") {
|
|
1260
|
-
const baseStyle = item.type === "em" ? "italic" : "bold";
|
|
1261
|
-
if (item.items && item.items.length > 0) for (const subItem of item.items) if (subItem.type === "strong" && baseStyle === "italic") renderTextWithStyle(subItem.content || "", "bolditalic");
|
|
1262
|
-
else if (subItem.type === "em" && baseStyle === "bold") renderTextWithStyle(subItem.content || "", "bolditalic");
|
|
1263
|
-
else renderTextWithStyle(subItem.content || "", baseStyle);
|
|
1264
|
-
else renderTextWithStyle(item.content || "", baseStyle);
|
|
1265
|
-
} else renderTextWithStyle(item.content || "", "normal");
|
|
1266
|
-
else if (element.type === "em") renderTextWithStyle(element.content || "", "italic");
|
|
1267
|
-
else if (element.type === "strong") renderTextWithStyle(element.content || "", "bold");
|
|
1268
|
-
else if (element.type === "codespan") renderTextWithStyle(element.content || "", "codespan");
|
|
1269
|
-
else renderTextWithStyle(element.content || "", "normal");
|
|
1270
|
-
doc.setFont(currentFont, currentFontStyle);
|
|
1271
|
-
doc.setFontSize(currentFontSize);
|
|
1272
|
-
};
|
|
1273
|
-
//#endregion
|
|
1274
|
-
//#region src/renderer/components/link.ts
|
|
1275
|
-
/**
|
|
1276
|
-
* Renders link elements with proper styling and URL handling.
|
|
1277
|
-
* Links are rendered in blue color and underlined to distinguish them from regular text.
|
|
1278
|
-
*/
|
|
1279
|
-
const renderLink = (doc, element, indent, store) => {
|
|
1280
|
-
const currentFont = doc.getFont().fontName;
|
|
1281
|
-
const currentFontStyle = doc.getFont().fontStyle;
|
|
1282
|
-
const currentFontSize = doc.getFontSize();
|
|
1283
|
-
const currentTextColor = doc.getTextColor();
|
|
1284
|
-
const linkColor = store.options.link?.linkColor || [
|
|
1285
|
-
0,
|
|
1286
|
-
0,
|
|
1287
|
-
255
|
|
1288
|
-
];
|
|
1289
|
-
doc.setTextColor(...linkColor);
|
|
1290
|
-
const availableWidth = store.options.page.maxContentWidth - indent - store.X;
|
|
1291
|
-
const linkText = element.text || element.content || "";
|
|
1292
|
-
const linkUrl = element.href || "";
|
|
1293
|
-
const textLines = doc.splitTextToSize(linkText, availableWidth);
|
|
1294
|
-
if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
|
|
1295
|
-
const textWidth = doc.getTextDimensions(textLines[i]).w;
|
|
1296
|
-
const textHeight = getCharHight(doc) / 2;
|
|
1297
|
-
doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
|
|
1298
|
-
doc.text(textLines[i], store.X + indent, store.Y, {
|
|
1299
|
-
baseline: "top",
|
|
1300
|
-
maxWidth: availableWidth
|
|
1301
|
-
});
|
|
1302
|
-
store.updateX(textWidth + 1, "add");
|
|
1303
|
-
if (store.X + textWidth > store.options.page.maxContentWidth - indent) {
|
|
1304
|
-
store.updateY(textHeight, "add");
|
|
1305
|
-
store.updateX(store.options.page.xpading + indent, "set");
|
|
1306
|
-
}
|
|
1307
|
-
if (i < textLines.length - 1) {
|
|
1308
|
-
store.updateY(textHeight, "add");
|
|
1309
|
-
store.updateX(store.options.page.xpading + indent, "set");
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
else if (textLines.length > 1) {
|
|
1313
|
-
const firstLine = textLines[0];
|
|
1314
|
-
const restContent = textLines?.slice(1)?.join(" ");
|
|
1315
|
-
const firstLineWidth = doc.getTextDimensions(firstLine).w;
|
|
1316
|
-
const textHeight = getCharHight(doc) / 2;
|
|
1317
|
-
doc.link(store.X + indent, store.Y, firstLineWidth, textHeight, { url: linkUrl });
|
|
1318
|
-
doc.text(firstLine, store.X + indent, store.Y, {
|
|
1319
|
-
baseline: "top",
|
|
1320
|
-
maxWidth: availableWidth
|
|
1321
|
-
});
|
|
1322
|
-
store.updateX(store.options.page.xpading + indent);
|
|
1323
|
-
store.updateY(textHeight, "add");
|
|
1324
|
-
const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
|
|
1325
|
-
doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
|
|
1326
|
-
const lineWidth = doc.getTextDimensions(line).w;
|
|
1327
|
-
doc.link(store.X + getCharWidth(doc), store.Y, lineWidth, textHeight, { url: linkUrl });
|
|
1328
|
-
doc.text(line, store.X + getCharWidth(doc), store.Y, {
|
|
1329
|
-
baseline: "top",
|
|
1330
|
-
maxWidth: maxWidthForRest
|
|
1331
|
-
});
|
|
1332
|
-
});
|
|
1333
|
-
} else {
|
|
1334
|
-
const textWidth = doc.getTextDimensions(linkText).w;
|
|
1335
|
-
const textHeight = getCharHight(doc) / 2;
|
|
1336
|
-
doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
|
|
1337
|
-
doc.text(linkText, store.X + indent, store.Y, {
|
|
1338
|
-
baseline: "top",
|
|
1339
|
-
maxWidth: availableWidth
|
|
1340
|
-
});
|
|
1341
|
-
store.updateX(textWidth + 2, "add");
|
|
1342
|
-
}
|
|
1343
|
-
doc.setFont(currentFont, currentFontStyle);
|
|
1344
|
-
doc.setFontSize(currentFontSize);
|
|
1345
|
-
doc.setTextColor(currentTextColor);
|
|
1199
|
+
doc.setTextColor(savedTextColor);
|
|
1200
|
+
doc.setDrawColor(savedDrawColor);
|
|
1201
|
+
doc.setFillColor(savedFillColor);
|
|
1202
|
+
store.updateY(store.options.spacing?.afterCodeBlock ?? 3, "add");
|
|
1346
1203
|
};
|
|
1347
1204
|
//#endregion
|
|
1348
1205
|
//#region src/renderer/components/blockquote.ts
|
|
1349
1206
|
const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
|
|
1350
1207
|
const options = store.options;
|
|
1208
|
+
const savedDrawColor = doc.getDrawColor();
|
|
1209
|
+
const savedLineWidth = doc.getLineWidth();
|
|
1351
1210
|
const blockquoteIndent = indentLevel + 1;
|
|
1352
1211
|
const currentX = store.X + indentLevel * options.page.indent;
|
|
1353
1212
|
const currentY = store.Y;
|
|
@@ -1357,10 +1216,13 @@ const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
|
|
|
1357
1216
|
if (element.items && element.items.length > 0) element.items.forEach((item) => {
|
|
1358
1217
|
renderElement(item, blockquoteIndent, store);
|
|
1359
1218
|
});
|
|
1360
|
-
const endY = store.Y;
|
|
1219
|
+
const endY = store.lastContentY || store.Y;
|
|
1361
1220
|
const endPage = doc.internal.getCurrentPageInfo().pageNumber;
|
|
1362
|
-
|
|
1363
|
-
|
|
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);
|
|
1364
1226
|
for (let p = startPage; p <= endPage; p++) {
|
|
1365
1227
|
doc.setPage(p);
|
|
1366
1228
|
const isStart = p === startPage;
|
|
@@ -1371,6 +1233,10 @@ const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
|
|
|
1371
1233
|
}
|
|
1372
1234
|
store.recordContentY();
|
|
1373
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);
|
|
1374
1240
|
};
|
|
1375
1241
|
//#endregion
|
|
1376
1242
|
//#region src/renderer/components/image.ts
|
|
@@ -1440,9 +1306,10 @@ const renderImage = (doc, element, indentLevel, store) => {
|
|
|
1440
1306
|
const imgFormat = detectImageFormat(element);
|
|
1441
1307
|
if (finalWidth > 0 && finalHeight > 0) doc.addImage(element.data, imgFormat, drawX, currentY, finalWidth, finalHeight);
|
|
1442
1308
|
store.updateY(finalHeight, "add");
|
|
1309
|
+
store.updateY(store.options.spacing?.afterImage ?? 2, "add");
|
|
1443
1310
|
store.recordContentY();
|
|
1444
1311
|
} catch (e) {
|
|
1445
|
-
console.warn("Failed to render image", e);
|
|
1312
|
+
console.warn("[jspdf-md-renderer] Failed to render image", e);
|
|
1446
1313
|
}
|
|
1447
1314
|
};
|
|
1448
1315
|
//#endregion
|
|
@@ -1455,34 +1322,56 @@ const resolveAutoTable = () => {
|
|
|
1455
1322
|
throw new Error("Could not resolve jspdf-autotable export. Expected a callable export.");
|
|
1456
1323
|
};
|
|
1457
1324
|
const renderTable = (doc, element, indentLevel, store) => {
|
|
1458
|
-
if (!element.header ||
|
|
1325
|
+
if (!element.header || element.header.length === 0) {
|
|
1326
|
+
console.warn("[jspdf-md-renderer] Table skipped: no header row");
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1459
1329
|
const options = store.options;
|
|
1460
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
|
+
});
|
|
1461
1341
|
const head = [element.header.map((h) => h.content || "")];
|
|
1462
|
-
const body = element.rows.map((row) => row.map((cell) => cell.content || ""));
|
|
1463
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
|
+
};
|
|
1464
1357
|
resolveAutoTable()(doc, {
|
|
1465
1358
|
head,
|
|
1466
|
-
body,
|
|
1359
|
+
body: rows,
|
|
1467
1360
|
startY: store.Y,
|
|
1468
1361
|
margin: {
|
|
1469
1362
|
left: marginLeft,
|
|
1470
1363
|
right: options.page.xmargin
|
|
1471
1364
|
},
|
|
1472
1365
|
...userTableOptions,
|
|
1473
|
-
didDrawPage:
|
|
1474
|
-
|
|
1475
|
-
},
|
|
1476
|
-
didDrawCell: (data) => {
|
|
1477
|
-
if (userTableOptions.didDrawCell) userTableOptions.didDrawCell(data);
|
|
1478
|
-
}
|
|
1366
|
+
didDrawPage: safeDidDrawPage,
|
|
1367
|
+
didDrawCell: safeDidDrawCell
|
|
1479
1368
|
});
|
|
1480
1369
|
const finalY = doc.lastAutoTable?.finalY;
|
|
1481
1370
|
if (typeof finalY === "number") {
|
|
1482
|
-
store.updateY(finalY + options.
|
|
1371
|
+
store.updateY(finalY + (options.spacing?.afterTable ?? 3), "set");
|
|
1483
1372
|
store.updateX(options.page.xpading, "set");
|
|
1484
1373
|
store.recordContentY();
|
|
1485
|
-
}
|
|
1374
|
+
} else console.warn("[jspdf-md-renderer] autoTable did not return a finalY. Y position may be incorrect.");
|
|
1486
1375
|
};
|
|
1487
1376
|
//#endregion
|
|
1488
1377
|
//#region src/store/renderStore.ts
|
|
@@ -1563,65 +1452,220 @@ var RenderStore = class {
|
|
|
1563
1452
|
};
|
|
1564
1453
|
//#endregion
|
|
1565
1454
|
//#region src/utils/options-validation.ts
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
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"
|
|
1580
1468
|
},
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
style: "bold"
|
|
1585
|
-
},
|
|
1586
|
-
regular: {
|
|
1587
|
-
name: "helvetica",
|
|
1588
|
-
style: "normal"
|
|
1589
|
-
},
|
|
1590
|
-
light: {
|
|
1591
|
-
name: "helvetica",
|
|
1592
|
-
style: "light"
|
|
1593
|
-
},
|
|
1594
|
-
code: {
|
|
1595
|
-
name: "courier",
|
|
1596
|
-
style: "normal"
|
|
1597
|
-
}
|
|
1469
|
+
regular: {
|
|
1470
|
+
name: "helvetica",
|
|
1471
|
+
style: "normal"
|
|
1598
1472
|
},
|
|
1599
|
-
|
|
1473
|
+
light: {
|
|
1474
|
+
name: "helvetica",
|
|
1475
|
+
style: "light"
|
|
1476
|
+
},
|
|
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
|
|
1600
1496
|
};
|
|
1601
1497
|
const validateOptions = (options) => {
|
|
1602
|
-
if (!options) throw new Error("RenderOption is required");
|
|
1603
|
-
const
|
|
1604
|
-
...
|
|
1498
|
+
if (!options) throw new Error("[jspdf-md-renderer] RenderOption is required");
|
|
1499
|
+
const page = {
|
|
1500
|
+
...DEFAULT_PAGE,
|
|
1605
1501
|
...options.page
|
|
1606
1502
|
};
|
|
1607
|
-
|
|
1608
|
-
|
|
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,
|
|
1609
1511
|
...options.font
|
|
1610
1512
|
};
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
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 ?? {}
|
|
1560
|
+
};
|
|
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 ?? {}
|
|
1614
1572
|
};
|
|
1615
|
-
|
|
1616
|
-
|
|
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 ?? (() => {});
|
|
1617
1594
|
return {
|
|
1618
1595
|
...options,
|
|
1619
|
-
page
|
|
1620
|
-
font
|
|
1621
|
-
|
|
1596
|
+
page,
|
|
1597
|
+
font,
|
|
1598
|
+
heading,
|
|
1599
|
+
codespan,
|
|
1600
|
+
blockquote,
|
|
1601
|
+
list,
|
|
1602
|
+
paragraph,
|
|
1603
|
+
codeBlock,
|
|
1604
|
+
spacing,
|
|
1605
|
+
image,
|
|
1606
|
+
endCursorYHandler
|
|
1622
1607
|
};
|
|
1623
1608
|
};
|
|
1624
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
|
|
1625
1669
|
//#region src/renderer/MdTextRender.ts
|
|
1626
1670
|
/**
|
|
1627
1671
|
* Renders parsed markdown text into jsPDF document.
|
|
@@ -1639,7 +1683,7 @@ const MdTextRender = async (doc, text, options) => {
|
|
|
1639
1683
|
const indent = indentLevel * validOptions.page.indent;
|
|
1640
1684
|
switch (element.type) {
|
|
1641
1685
|
case "heading":
|
|
1642
|
-
renderHeading(doc, element, indent, store
|
|
1686
|
+
renderHeading(doc, element, indent, store);
|
|
1643
1687
|
break;
|
|
1644
1688
|
case "paragraph":
|
|
1645
1689
|
renderParagraph(doc, element, indent, store, renderElement);
|
|
@@ -1659,10 +1703,8 @@ const MdTextRender = async (doc, text, options) => {
|
|
|
1659
1703
|
case "strong":
|
|
1660
1704
|
case "em":
|
|
1661
1705
|
case "codespan":
|
|
1662
|
-
renderInlineText(doc, element, indent, store);
|
|
1663
|
-
break;
|
|
1664
1706
|
case "link":
|
|
1665
|
-
|
|
1707
|
+
renderInlineContent(doc, [element], store.X + indent, store.Y, validOptions.page.maxContentWidth - indent, store);
|
|
1666
1708
|
break;
|
|
1667
1709
|
case "blockquote":
|
|
1668
1710
|
renderBlockquote(doc, element, indentLevel, store, renderElement);
|
|
@@ -1694,8 +1736,13 @@ const MdTextRender = async (doc, text, options) => {
|
|
|
1694
1736
|
}
|
|
1695
1737
|
};
|
|
1696
1738
|
for (const item of parsedElements) renderElement(item, 0, store);
|
|
1739
|
+
applyPageDecorations(doc, validOptions);
|
|
1697
1740
|
validOptions.endCursorYHandler(store.Y);
|
|
1698
1741
|
};
|
|
1699
1742
|
//#endregion
|
|
1700
1743
|
exports.MdTextParser = MdTextParser;
|
|
1701
1744
|
exports.MdTextRender = MdTextRender;
|
|
1745
|
+
exports.MdTokenType = MdTokenType;
|
|
1746
|
+
exports.renderInlineContent = renderInlineContent;
|
|
1747
|
+
exports.renderPlainText = renderPlainText;
|
|
1748
|
+
exports.validateOptions = validateOptions;
|