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