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