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