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