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