jspdf-md-renderer 3.4.1 → 3.5.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/dist/index.d.mts +124 -0
- package/dist/index.d.ts +124 -1
- package/dist/index.js +1699 -4
- package/dist/index.mjs +1369 -667
- package/dist/index.umd.js +1705 -4
- package/package.json +4 -6
package/dist/index.mjs
CHANGED
|
@@ -1,641 +1,1148 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/*!
|
|
2
|
+
* jspdf-md-renderer
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Jeel Gajera
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
import { marked } from "marked";
|
|
28
|
+
import autoTable from "jspdf-autotable";
|
|
29
|
+
//#region src/parser/imageExtension.ts
|
|
30
|
+
/**
|
|
31
|
+
* Internal hash prefix used to encode image attributes in the URL fragment.
|
|
32
|
+
* This is stripped during token conversion and never reaches the image fetcher.
|
|
33
|
+
*/
|
|
34
|
+
const ATTR_HASH_PREFIX = "__jmr_";
|
|
35
|
+
/**
|
|
36
|
+
* Regex to match an image tag followed by an attribute block.
|
|
37
|
+
* Captures:
|
|
38
|
+
* Group 1: Everything before the closing `)` (i.e., `
|
|
39
|
+
* Group 2: The image URL inside the parentheses
|
|
40
|
+
* Group 3: The attribute block content (e.g., `width=200 height=150 align=center`)
|
|
41
|
+
*
|
|
42
|
+
* Pattern: {key=value ...}
|
|
43
|
+
*/
|
|
44
|
+
const IMAGE_WITH_ATTRS_REGEX = /(!\[[^\]]*\]\()([^)]+)(\))\s*\{([^}]+)\}/g;
|
|
45
|
+
/**
|
|
46
|
+
* Regex to extract individual key=value pairs from the attribute block.
|
|
47
|
+
*/
|
|
48
|
+
const ATTR_PAIR_REGEX = /(\w+)\s*=\s*(\w+)/g;
|
|
49
|
+
/** Valid alignment values */
|
|
50
|
+
const VALID_ALIGNMENTS = [
|
|
7
51
|
"left",
|
|
8
52
|
"center",
|
|
9
53
|
"right"
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Encodes image attributes into a URL hash fragment.
|
|
57
|
+
* Example: {width: 200, height: 100, align: 'center'} → '#__jmr_w=200&h=100&a=center'
|
|
58
|
+
*/
|
|
59
|
+
const encodeAttrsToFragment = (attrs) => {
|
|
60
|
+
const parts = [];
|
|
61
|
+
if (attrs.width !== void 0) parts.push(`w=${attrs.width}`);
|
|
62
|
+
if (attrs.height !== void 0) parts.push(`h=${attrs.height}`);
|
|
63
|
+
if (attrs.align) parts.push(`a=${attrs.align}`);
|
|
64
|
+
return parts.length > 0 ? `#${ATTR_HASH_PREFIX}${parts.join("&")}` : "";
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Parses an attribute string like "width=200 height=150 align=center"
|
|
68
|
+
* into a structured object.
|
|
69
|
+
*/
|
|
70
|
+
const parseRawAttributes = (attrString) => {
|
|
71
|
+
const attrs = {};
|
|
72
|
+
let match;
|
|
73
|
+
while ((match = ATTR_PAIR_REGEX.exec(attrString)) !== null) {
|
|
74
|
+
const key = match[1].toLowerCase();
|
|
75
|
+
const value = match[2];
|
|
76
|
+
switch (key) {
|
|
18
77
|
case "width":
|
|
19
78
|
case "w": {
|
|
20
|
-
|
|
21
|
-
!isNaN(
|
|
79
|
+
const num = parseInt(value, 10);
|
|
80
|
+
if (!isNaN(num) && num > 0) attrs.width = num;
|
|
22
81
|
break;
|
|
23
82
|
}
|
|
24
83
|
case "height":
|
|
25
84
|
case "h": {
|
|
26
|
-
|
|
27
|
-
!isNaN(
|
|
85
|
+
const num = parseInt(value, 10);
|
|
86
|
+
if (!isNaN(num) && num > 0) attrs.height = num;
|
|
28
87
|
break;
|
|
29
88
|
}
|
|
30
89
|
case "align": {
|
|
31
|
-
|
|
32
|
-
|
|
90
|
+
const alignVal = value.toLowerCase();
|
|
91
|
+
if (VALID_ALIGNMENTS.includes(alignVal)) attrs.align = alignVal;
|
|
33
92
|
break;
|
|
34
93
|
}
|
|
35
94
|
}
|
|
36
95
|
}
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
96
|
+
return attrs;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Pre-processes markdown text to embed image attributes into URL fragments.
|
|
100
|
+
*
|
|
101
|
+
* Transforms `{width=200 align=center}` into
|
|
102
|
+
* `` so that each image token
|
|
103
|
+
* carries its own attributes — no shared state needed.
|
|
104
|
+
*
|
|
105
|
+
* Supported attributes:
|
|
106
|
+
* - `width` or `w`: Image width in pixels (number)
|
|
107
|
+
* - `height` or `h`: Image height in pixels (number)
|
|
108
|
+
* - `align`: Image alignment - 'left', 'center', or 'right'
|
|
109
|
+
*
|
|
110
|
+
* @param text - The raw markdown text
|
|
111
|
+
* @returns The cleaned markdown text with attributes encoded in URLs
|
|
112
|
+
*/
|
|
113
|
+
const preprocessImageAttributes = (text) => {
|
|
114
|
+
return text.replace(IMAGE_WITH_ATTRS_REGEX, (_fullMatch, before, url, closeParen, attrsContent) => {
|
|
115
|
+
return `${before}${url}${encodeAttrsToFragment(parseRawAttributes(attrsContent))}${closeParen}`;
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Extracts image attributes from a URL that may contain an encoded fragment.
|
|
120
|
+
* Returns the clean URL (without the attribute fragment) and parsed attributes.
|
|
121
|
+
*
|
|
122
|
+
* @param href - The image URL, possibly containing `#__jmr_...` fragment
|
|
123
|
+
* @returns Object with cleanHref and parsed attrs
|
|
124
|
+
*/
|
|
125
|
+
const parseImageAttrsFromHref = (href) => {
|
|
126
|
+
const fragmentIdx = href.indexOf(`#${ATTR_HASH_PREFIX}`);
|
|
127
|
+
if (fragmentIdx === -1) return {
|
|
128
|
+
cleanHref: href,
|
|
42
129
|
attrs: {}
|
|
43
130
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
131
|
+
const cleanHref = href.substring(0, fragmentIdx);
|
|
132
|
+
const fragment = href.substring(fragmentIdx + 1 + 6);
|
|
133
|
+
const attrs = {};
|
|
134
|
+
const pairs = fragment.split("&");
|
|
135
|
+
for (const pair of pairs) {
|
|
136
|
+
const [key, value] = pair.split("=");
|
|
137
|
+
switch (key) {
|
|
48
138
|
case "w": {
|
|
49
|
-
|
|
50
|
-
!isNaN(
|
|
139
|
+
const num = parseInt(value, 10);
|
|
140
|
+
if (!isNaN(num) && num > 0) attrs.width = num;
|
|
51
141
|
break;
|
|
52
142
|
}
|
|
53
143
|
case "h": {
|
|
54
|
-
|
|
55
|
-
!isNaN(
|
|
144
|
+
const num = parseInt(value, 10);
|
|
145
|
+
if (!isNaN(num) && num > 0) attrs.height = num;
|
|
56
146
|
break;
|
|
57
147
|
}
|
|
58
148
|
case "a":
|
|
59
|
-
|
|
149
|
+
if (VALID_ALIGNMENTS.includes(value)) attrs.align = value;
|
|
60
150
|
break;
|
|
61
151
|
}
|
|
62
152
|
}
|
|
63
153
|
return {
|
|
64
|
-
cleanHref
|
|
65
|
-
attrs
|
|
154
|
+
cleanHref,
|
|
155
|
+
attrs
|
|
66
156
|
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/parser/MdTextParser.ts
|
|
160
|
+
/**
|
|
161
|
+
* Parses markdown into tokens and converts to a custom parsed structure.
|
|
162
|
+
*
|
|
163
|
+
* @param text - The markdown content to parse.
|
|
164
|
+
* @returns Parsed markdown elements.
|
|
165
|
+
*/
|
|
166
|
+
const MdTextParser = async (text) => {
|
|
167
|
+
const processedText = preprocessImageAttributes(text);
|
|
168
|
+
return convertTokens(await marked.lexer(processedText, {
|
|
169
|
+
async: true,
|
|
170
|
+
gfm: true
|
|
72
171
|
}));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Convert the markdown tokens to ParsedElements.
|
|
175
|
+
*
|
|
176
|
+
* @param tokens - The list of markdown tokens.
|
|
177
|
+
* @returns Parsed elements in a custom structure.
|
|
178
|
+
*/
|
|
179
|
+
const convertTokens = (tokens) => {
|
|
180
|
+
const parsedElements = [];
|
|
181
|
+
tokens.forEach((token) => {
|
|
76
182
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
183
|
+
const handler = tokenHandlers[token.type];
|
|
184
|
+
if (handler) parsedElements.push(handler(token));
|
|
185
|
+
else parsedElements.push({
|
|
186
|
+
type: "raw",
|
|
187
|
+
content: token.raw
|
|
81
188
|
});
|
|
82
|
-
} catch (
|
|
83
|
-
console.error("Failed to handle token ==>",
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error("Failed to handle token ==>", token, error);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return parsedElements;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Map each token type to its handler function.
|
|
197
|
+
*/
|
|
198
|
+
const tokenHandlers = {
|
|
199
|
+
["heading"]: (token) => ({
|
|
200
|
+
type: "heading",
|
|
201
|
+
depth: token.depth,
|
|
202
|
+
content: token.text,
|
|
203
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
92
204
|
}),
|
|
93
|
-
[
|
|
94
|
-
type:
|
|
95
|
-
content:
|
|
96
|
-
items:
|
|
205
|
+
["paragraph"]: (token) => ({
|
|
206
|
+
type: "paragraph",
|
|
207
|
+
content: token.text,
|
|
208
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
97
209
|
}),
|
|
98
|
-
[
|
|
99
|
-
type:
|
|
100
|
-
ordered:
|
|
101
|
-
start:
|
|
102
|
-
items:
|
|
210
|
+
["list"]: (token) => ({
|
|
211
|
+
type: "list",
|
|
212
|
+
ordered: token.ordered,
|
|
213
|
+
start: token.start,
|
|
214
|
+
items: token.items ? convertTokens(token.items) : []
|
|
103
215
|
}),
|
|
104
|
-
[
|
|
105
|
-
type:
|
|
106
|
-
content:
|
|
107
|
-
items:
|
|
216
|
+
["list_item"]: (token) => ({
|
|
217
|
+
type: "list_item",
|
|
218
|
+
content: token.text,
|
|
219
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
108
220
|
}),
|
|
109
|
-
[
|
|
110
|
-
type:
|
|
111
|
-
lang:
|
|
112
|
-
code:
|
|
221
|
+
["code"]: (token) => ({
|
|
222
|
+
type: "code",
|
|
223
|
+
lang: token.lang,
|
|
224
|
+
code: token.text
|
|
113
225
|
}),
|
|
114
|
-
[
|
|
115
|
-
type:
|
|
116
|
-
header:
|
|
117
|
-
type:
|
|
118
|
-
content:
|
|
226
|
+
["table"]: (token) => ({
|
|
227
|
+
type: "table",
|
|
228
|
+
header: token.header.map((header) => ({
|
|
229
|
+
type: "table_header",
|
|
230
|
+
content: header.text
|
|
119
231
|
})),
|
|
120
|
-
rows:
|
|
121
|
-
type:
|
|
122
|
-
content:
|
|
232
|
+
rows: token.rows.map((row) => row.map((cell) => ({
|
|
233
|
+
type: "table_cell",
|
|
234
|
+
content: cell.text
|
|
123
235
|
})))
|
|
124
236
|
}),
|
|
125
|
-
[
|
|
126
|
-
|
|
237
|
+
["image"]: (token) => {
|
|
238
|
+
const { cleanHref, attrs } = parseImageAttrsFromHref(token.href);
|
|
127
239
|
return {
|
|
128
|
-
type:
|
|
129
|
-
src:
|
|
130
|
-
alt:
|
|
131
|
-
width:
|
|
132
|
-
height:
|
|
133
|
-
align:
|
|
240
|
+
type: "image",
|
|
241
|
+
src: cleanHref,
|
|
242
|
+
alt: token.text,
|
|
243
|
+
width: attrs.width,
|
|
244
|
+
height: attrs.height,
|
|
245
|
+
align: attrs.align
|
|
134
246
|
};
|
|
135
247
|
},
|
|
136
|
-
[
|
|
137
|
-
type:
|
|
138
|
-
href:
|
|
139
|
-
text:
|
|
140
|
-
items:
|
|
248
|
+
["link"]: (token) => ({
|
|
249
|
+
type: "link",
|
|
250
|
+
href: token.href,
|
|
251
|
+
text: token.text,
|
|
252
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
141
253
|
}),
|
|
142
|
-
[
|
|
143
|
-
type:
|
|
144
|
-
content:
|
|
145
|
-
items:
|
|
254
|
+
["strong"]: (token) => ({
|
|
255
|
+
type: "strong",
|
|
256
|
+
content: token.text,
|
|
257
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
146
258
|
}),
|
|
147
|
-
[
|
|
148
|
-
type:
|
|
149
|
-
content:
|
|
150
|
-
items:
|
|
259
|
+
["em"]: (token) => ({
|
|
260
|
+
type: "em",
|
|
261
|
+
content: token.text,
|
|
262
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
151
263
|
}),
|
|
152
|
-
[
|
|
153
|
-
type:
|
|
154
|
-
content:
|
|
155
|
-
items:
|
|
264
|
+
["text"]: (token) => ({
|
|
265
|
+
type: "text",
|
|
266
|
+
content: token.text,
|
|
267
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
156
268
|
}),
|
|
157
|
-
[
|
|
158
|
-
type:
|
|
159
|
-
content:
|
|
160
|
-
items:
|
|
269
|
+
["hr"]: (token) => ({
|
|
270
|
+
type: "hr",
|
|
271
|
+
content: token.raw,
|
|
272
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
161
273
|
}),
|
|
162
|
-
[
|
|
163
|
-
type:
|
|
164
|
-
content:
|
|
165
|
-
items:
|
|
274
|
+
["codespan"]: (token) => ({
|
|
275
|
+
type: "codespan",
|
|
276
|
+
content: token.text,
|
|
277
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
166
278
|
}),
|
|
167
|
-
[
|
|
168
|
-
type:
|
|
169
|
-
content:
|
|
170
|
-
items:
|
|
279
|
+
["blockquote"]: (token) => ({
|
|
280
|
+
type: "blockquote",
|
|
281
|
+
content: token.text,
|
|
282
|
+
items: token.tokens ? convertTokens(token.tokens) : []
|
|
171
283
|
}),
|
|
172
|
-
[
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
type:
|
|
284
|
+
["html"]: (token) => {
|
|
285
|
+
const rawHtml = String(token.raw ?? token.text ?? "").trim();
|
|
286
|
+
if (/^<br\s*\/?>$/i.test(rawHtml)) return {
|
|
287
|
+
type: "br",
|
|
176
288
|
content: "\n"
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
type: "raw",
|
|
292
|
+
content: token.raw ?? token.text ?? ""
|
|
180
293
|
};
|
|
181
294
|
},
|
|
182
|
-
[
|
|
183
|
-
type:
|
|
295
|
+
["br"]: () => ({
|
|
296
|
+
type: "br",
|
|
184
297
|
content: "\n"
|
|
185
298
|
})
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
299
|
+
};
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/utils/doc-helpers.ts
|
|
302
|
+
const getCharHight = (doc) => {
|
|
303
|
+
return doc.getTextDimensions("H").h;
|
|
304
|
+
};
|
|
305
|
+
const getCharWidth = (doc) => {
|
|
306
|
+
return doc.getTextDimensions("H").w;
|
|
307
|
+
};
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/utils/handlePageBreak.ts
|
|
310
|
+
const HandlePageBreaks = (doc, store) => {
|
|
311
|
+
if (typeof store.options.pageBreakHandler === "function") store.options.pageBreakHandler(doc);
|
|
312
|
+
else doc.addPage(store.options.page?.format, store.options.page?.orientation);
|
|
313
|
+
store.updateY(store.options.page.topmargin);
|
|
314
|
+
store.updateX(store.options.page.xpading);
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/utils/image-utils.ts
|
|
318
|
+
/**
|
|
319
|
+
* Standard DPI for web/screen pixels.
|
|
320
|
+
*/
|
|
321
|
+
const DEFAULT_DPI = 96;
|
|
322
|
+
/**
|
|
323
|
+
* Converts pixel values to the document's unit system.
|
|
324
|
+
* Uses 96 DPI as the standard web pixel density.
|
|
325
|
+
*
|
|
326
|
+
* @param px - Value in pixels
|
|
327
|
+
* @param unit - The document unit ('mm' | 'pt' | 'in' | 'px')
|
|
328
|
+
* @returns Value in document units
|
|
329
|
+
*/
|
|
330
|
+
const pxToDocUnit = (px, unit = "mm") => {
|
|
331
|
+
switch (unit) {
|
|
332
|
+
case "pt": return px * 72 / DEFAULT_DPI;
|
|
333
|
+
case "in": return px / DEFAULT_DPI;
|
|
334
|
+
case "px": return px;
|
|
335
|
+
default: return px * 25.4 / DEFAULT_DPI;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Extracts width and height from an SVG data URI if possible.
|
|
340
|
+
*/
|
|
341
|
+
const extractSvgDimensions = (dataUri) => {
|
|
208
342
|
try {
|
|
209
|
-
let
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
343
|
+
let svgString = "";
|
|
344
|
+
if (dataUri.includes("base64,")) {
|
|
345
|
+
const base64 = dataUri.split("base64,")[1];
|
|
346
|
+
if (typeof window !== "undefined" && typeof window.atob === "function") svgString = decodeURIComponent(escape(window.atob(base64)));
|
|
347
|
+
else if (typeof Buffer !== "undefined") svgString = Buffer.from(base64, "base64").toString("utf-8");
|
|
348
|
+
else svgString = decodeURIComponent(escape(atob(base64)));
|
|
349
|
+
} else svgString = decodeURIComponent(dataUri.split(",")[1] || "");
|
|
350
|
+
const widthMatch = svgString.match(/<svg[^>]*\swidth=(?:'|")([0-9.]+)[a-zA-Z]*(?:'|")/i);
|
|
351
|
+
const heightMatch = svgString.match(/<svg[^>]*\sheight=(?:'|")([0-9.]+)[a-zA-Z]*(?:'|")/i);
|
|
352
|
+
const viewBoxMatch = svgString.match(/<svg[^>]*\sviewBox=(?:'|")[^'"]*(?:'|")/i);
|
|
353
|
+
let w = widthMatch ? parseFloat(widthMatch[1]) : 0;
|
|
354
|
+
let h = heightMatch ? parseFloat(heightMatch[1]) : 0;
|
|
355
|
+
if ((!w || !h) && viewBoxMatch) {
|
|
356
|
+
const viewBoxStr = viewBoxMatch[0].match(/viewBox=(?:'|")([^'"]+)(?:'|")/i);
|
|
357
|
+
if (viewBoxStr) {
|
|
358
|
+
const parts = viewBoxStr[1].split(/[ ,]+/).filter(Boolean).map(parseFloat);
|
|
359
|
+
if (parts.length >= 4) {
|
|
360
|
+
w = w || parts[2];
|
|
361
|
+
h = h || parts[3];
|
|
362
|
+
}
|
|
220
363
|
}
|
|
221
364
|
}
|
|
222
|
-
if (
|
|
223
|
-
width:
|
|
224
|
-
height:
|
|
365
|
+
if (w > 0 && h > 0) return {
|
|
366
|
+
width: w,
|
|
367
|
+
height: h
|
|
225
368
|
};
|
|
226
369
|
} catch (e) {
|
|
227
370
|
console.warn("Failed to extract SVG dimensions:", e);
|
|
228
371
|
}
|
|
229
372
|
return null;
|
|
230
|
-
}
|
|
231
|
-
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* Calculates final dimensions for an image, respecting intrinsic size,
|
|
376
|
+
* user-specified attributes, and page bounds.
|
|
377
|
+
*/
|
|
378
|
+
const calculateImageDimensions = (doc, element, maxWidth, maxHeight, docUnit = "mm") => {
|
|
379
|
+
if (!element.data) return {
|
|
232
380
|
finalWidth: 0,
|
|
233
381
|
finalHeight: 0
|
|
234
382
|
};
|
|
235
|
-
let
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
a = n.width, o = n.height;
|
|
383
|
+
let intrinsicPxW = element.naturalWidth || 0;
|
|
384
|
+
let intrinsicPxH = element.naturalHeight || 0;
|
|
385
|
+
if (!intrinsicPxW || !intrinsicPxH) if (!element.data.startsWith("data:image/svg")) try {
|
|
386
|
+
const props = doc.getImageProperties(element.data);
|
|
387
|
+
intrinsicPxW = props.width;
|
|
388
|
+
intrinsicPxH = props.height;
|
|
242
389
|
} catch (e) {
|
|
243
390
|
console.warn("Failed to get image properties for intrinsic sizing:", e);
|
|
244
391
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
392
|
+
else {
|
|
393
|
+
const svgDims = extractSvgDimensions(element.data);
|
|
394
|
+
if (svgDims) {
|
|
395
|
+
intrinsicPxW = svgDims.width;
|
|
396
|
+
intrinsicPxH = svgDims.height;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const aspectRatio = intrinsicPxH > 0 ? intrinsicPxW / intrinsicPxH : 1;
|
|
400
|
+
let finalWidth;
|
|
401
|
+
let finalHeight;
|
|
402
|
+
if (element.width && element.height) {
|
|
403
|
+
finalWidth = pxToDocUnit(element.width, docUnit);
|
|
404
|
+
finalHeight = pxToDocUnit(element.height, docUnit);
|
|
405
|
+
} else if (element.width) {
|
|
406
|
+
finalWidth = pxToDocUnit(element.width, docUnit);
|
|
407
|
+
finalHeight = finalWidth / aspectRatio;
|
|
408
|
+
} else if (element.height) {
|
|
409
|
+
finalHeight = pxToDocUnit(element.height, docUnit);
|
|
410
|
+
finalWidth = finalHeight * aspectRatio;
|
|
411
|
+
} else {
|
|
412
|
+
finalWidth = pxToDocUnit(intrinsicPxW, docUnit);
|
|
413
|
+
finalHeight = pxToDocUnit(intrinsicPxH, docUnit);
|
|
414
|
+
}
|
|
415
|
+
if (finalWidth > maxWidth) {
|
|
416
|
+
const scale = maxWidth / finalWidth;
|
|
417
|
+
finalWidth = maxWidth;
|
|
418
|
+
finalHeight = finalHeight * scale;
|
|
249
419
|
}
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
420
|
+
if (finalHeight > maxHeight) {
|
|
421
|
+
const scale = maxHeight / finalHeight;
|
|
422
|
+
finalHeight = maxHeight;
|
|
423
|
+
finalWidth = finalWidth * scale;
|
|
253
424
|
}
|
|
254
425
|
return {
|
|
255
|
-
finalWidth
|
|
256
|
-
finalHeight
|
|
426
|
+
finalWidth,
|
|
427
|
+
finalHeight
|
|
257
428
|
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
429
|
+
};
|
|
430
|
+
/**
|
|
431
|
+
* Recursively traverses parsed elements and loads image data for Image tokens.
|
|
432
|
+
* @param elements - The parsed elements to process.
|
|
433
|
+
*/
|
|
434
|
+
const prefetchImages = async (elements) => {
|
|
435
|
+
for (const element of elements) {
|
|
436
|
+
if (element.type === "image" && element.src) try {
|
|
437
|
+
if (element.src.startsWith("data:")) element.data = element.src;
|
|
262
438
|
else {
|
|
263
|
-
|
|
264
|
-
if (!
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
typeof
|
|
270
|
-
|
|
439
|
+
const response = await fetch(element.src);
|
|
440
|
+
if (!response.ok) throw new Error(`Failed to fetch image: ${response.statusText}`);
|
|
441
|
+
const blob = await response.blob();
|
|
442
|
+
element.data = await new Promise((resolve, reject) => {
|
|
443
|
+
const reader = new FileReader();
|
|
444
|
+
reader.onloadend = () => {
|
|
445
|
+
if (typeof reader.result === "string") resolve(reader.result);
|
|
446
|
+
else reject(/* @__PURE__ */ new Error("Failed to convert image to base64 string"));
|
|
447
|
+
};
|
|
448
|
+
reader.onerror = reject;
|
|
449
|
+
reader.readAsDataURL(blob);
|
|
271
450
|
});
|
|
272
451
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
452
|
+
if (element.data && element.data.startsWith("data:image/svg")) {
|
|
453
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") element.data = await new Promise((resolve) => {
|
|
454
|
+
const img = new Image();
|
|
455
|
+
img.onload = () => {
|
|
456
|
+
const canvas = document.createElement("canvas");
|
|
457
|
+
const dims = extractSvgDimensions(element.data);
|
|
458
|
+
const w = dims ? dims.width : img.width || 300;
|
|
459
|
+
const h = dims ? dims.height : img.height || 150;
|
|
460
|
+
element.naturalWidth = w;
|
|
461
|
+
element.naturalHeight = h;
|
|
462
|
+
const scale = 4;
|
|
463
|
+
canvas.width = w * scale;
|
|
464
|
+
canvas.height = h * scale;
|
|
465
|
+
const ctx = canvas.getContext("2d");
|
|
466
|
+
if (ctx) {
|
|
467
|
+
ctx.scale(scale, scale);
|
|
468
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
469
|
+
resolve(canvas.toDataURL("image/png"));
|
|
470
|
+
} else resolve(element.data);
|
|
471
|
+
};
|
|
472
|
+
img.onerror = () => resolve(element.data);
|
|
473
|
+
img.src = element.data;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.warn(`[jspdf-md-renderer] Warning: Failed to load image at ${element.src}. It will be skipped.`, error);
|
|
284
478
|
}
|
|
285
|
-
|
|
479
|
+
if (element.items && element.items.length > 0) await prefetchImages(element.items);
|
|
286
480
|
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
481
|
+
};
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/utils/justifiedTextRenderer.ts
|
|
484
|
+
/**
|
|
485
|
+
* JustifiedTextRenderer - Renders mixed inline elements with proper alignment.
|
|
486
|
+
*
|
|
487
|
+
* Features:
|
|
488
|
+
* - Handles bold, italic, codespan, links mixed in paragraph
|
|
489
|
+
* - Proper word spacing distribution for justified alignment
|
|
490
|
+
* - Supports left, right, center, and justify alignments
|
|
491
|
+
* - Page break handling
|
|
492
|
+
* - Preserves link clickability
|
|
493
|
+
* - Codespan background rendering
|
|
494
|
+
*/
|
|
495
|
+
var JustifiedTextRenderer = class {
|
|
496
|
+
static getCodespanOptions(store) {
|
|
497
|
+
const opts = store.options.codespan ?? {};
|
|
290
498
|
return {
|
|
291
|
-
backgroundColor:
|
|
292
|
-
padding:
|
|
293
|
-
showBackground:
|
|
294
|
-
fontSizeScale:
|
|
499
|
+
backgroundColor: opts.backgroundColor ?? "#EEEEEE",
|
|
500
|
+
padding: opts.padding ?? .5,
|
|
501
|
+
showBackground: opts.showBackground !== false,
|
|
502
|
+
fontSizeScale: opts.fontSizeScale ?? .9
|
|
295
503
|
};
|
|
296
504
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Apply font style to the jsPDF document.
|
|
507
|
+
*/
|
|
508
|
+
static applyStyle(doc, style, store) {
|
|
509
|
+
const currentFont = doc.getFont().fontName;
|
|
510
|
+
const currentFontSize = doc.getFontSize();
|
|
511
|
+
const getBoldFont = () => {
|
|
512
|
+
const boldName = store.options.font.bold?.name;
|
|
513
|
+
return boldName && boldName !== "" ? boldName : currentFont;
|
|
514
|
+
};
|
|
515
|
+
const getRegularFont = () => {
|
|
516
|
+
const regularName = store.options.font.regular?.name;
|
|
517
|
+
return regularName && regularName !== "" ? regularName : currentFont;
|
|
304
518
|
};
|
|
305
|
-
switch (
|
|
519
|
+
switch (style) {
|
|
306
520
|
case "bold":
|
|
307
|
-
|
|
521
|
+
doc.setFont(getBoldFont(), store.options.font.bold?.style || "bold");
|
|
308
522
|
break;
|
|
309
523
|
case "italic":
|
|
310
|
-
|
|
524
|
+
doc.setFont(getRegularFont(), "italic");
|
|
311
525
|
break;
|
|
312
526
|
case "bolditalic":
|
|
313
|
-
|
|
527
|
+
doc.setFont(getBoldFont(), "bolditalic");
|
|
314
528
|
break;
|
|
315
529
|
case "codespan":
|
|
316
|
-
|
|
530
|
+
const codeFont = store.options.font.code || {
|
|
531
|
+
name: "courier",
|
|
532
|
+
style: "normal"
|
|
533
|
+
};
|
|
534
|
+
doc.setFont(codeFont.name, codeFont.style);
|
|
535
|
+
doc.setFontSize(currentFontSize * this.getCodespanOptions(store).fontSizeScale);
|
|
317
536
|
break;
|
|
318
537
|
default:
|
|
319
|
-
|
|
538
|
+
doc.setFont(getRegularFont(), doc.getFont().fontStyle);
|
|
320
539
|
break;
|
|
321
540
|
}
|
|
322
541
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
542
|
+
/**
|
|
543
|
+
* Measure word width with a specific style applied.
|
|
544
|
+
* NOTE: jsPDF's getTextWidth() does NOT include charSpace in its calculation,
|
|
545
|
+
* so we must manually add it: effectiveWidth = getTextWidth(text) + (text.length * charSpace)
|
|
546
|
+
*/
|
|
547
|
+
static measureWordWidth(doc, text, style, store) {
|
|
548
|
+
const savedFont = doc.getFont();
|
|
549
|
+
const savedSize = doc.getFontSize();
|
|
550
|
+
this.applyStyle(doc, style, store);
|
|
551
|
+
const baseWidth = doc.getTextWidth(text);
|
|
552
|
+
const charSpace = doc.getCharSpace?.() ?? 0;
|
|
553
|
+
const effectiveWidth = baseWidth + text.length * charSpace;
|
|
554
|
+
doc.setFont(savedFont.fontName, savedFont.fontStyle);
|
|
555
|
+
doc.setFontSize(savedSize);
|
|
556
|
+
return effectiveWidth;
|
|
328
557
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
558
|
+
/**
|
|
559
|
+
* Extract style from element type string.
|
|
560
|
+
*/
|
|
561
|
+
static getStyleFromType(type, parentStyle) {
|
|
562
|
+
switch (type) {
|
|
563
|
+
case "strong":
|
|
564
|
+
if (parentStyle === "italic") return "bolditalic";
|
|
565
|
+
return "bold";
|
|
566
|
+
case "em":
|
|
567
|
+
if (parentStyle === "bold") return "bolditalic";
|
|
568
|
+
return "italic";
|
|
333
569
|
case "codespan": return "codespan";
|
|
334
|
-
default: return
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
570
|
+
default: return parentStyle || "normal";
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Flatten ParsedElement tree into an array of StyledWordInfo.
|
|
575
|
+
* Handles nested inline elements.
|
|
576
|
+
*/
|
|
577
|
+
static flattenToWords(doc, elements, store, parentStyle = "normal", isLink = false, href) {
|
|
578
|
+
const result = [];
|
|
579
|
+
for (const el of elements) {
|
|
580
|
+
const style = this.getStyleFromType(el.type, parentStyle);
|
|
581
|
+
const elIsLink = el.type === "link" || isLink;
|
|
582
|
+
const elHref = el.href || href;
|
|
583
|
+
if (el.items && el.items.length > 0) {
|
|
584
|
+
const nested = this.flattenToWords(doc, el.items, store, style, elIsLink, elHref);
|
|
585
|
+
result.push(...nested);
|
|
586
|
+
} else if (el.type === "image") {
|
|
587
|
+
const maxH = store.options.page.maxContentHeight - store.options.page.topmargin;
|
|
588
|
+
const { finalWidth, finalHeight } = calculateImageDimensions(doc, el, store.options.page.maxContentWidth - store.options.page.indent * 0, maxH, store.options.page.unit || "mm");
|
|
589
|
+
result.push({
|
|
347
590
|
text: "",
|
|
348
|
-
width:
|
|
349
|
-
style
|
|
350
|
-
isLink:
|
|
351
|
-
href:
|
|
352
|
-
linkColor:
|
|
591
|
+
width: finalWidth,
|
|
592
|
+
style,
|
|
593
|
+
isLink: elIsLink,
|
|
594
|
+
href: elHref,
|
|
595
|
+
linkColor: elIsLink ? store.options.link?.linkColor || [
|
|
353
596
|
0,
|
|
354
597
|
0,
|
|
355
598
|
255
|
|
356
599
|
] : void 0,
|
|
357
|
-
isImage:
|
|
358
|
-
imageElement:
|
|
359
|
-
imageHeight:
|
|
600
|
+
isImage: true,
|
|
601
|
+
imageElement: el,
|
|
602
|
+
imageHeight: finalHeight
|
|
360
603
|
});
|
|
361
|
-
} else if (
|
|
604
|
+
} else if (el.type === "br") result.push({
|
|
362
605
|
text: "",
|
|
363
606
|
width: 0,
|
|
364
|
-
style
|
|
365
|
-
isBr:
|
|
607
|
+
style,
|
|
608
|
+
isBr: true
|
|
366
609
|
});
|
|
367
610
|
else {
|
|
368
|
-
|
|
369
|
-
if (!
|
|
370
|
-
if (/^\s/.test(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
611
|
+
const text = el.content || el.text || "";
|
|
612
|
+
if (!text) continue;
|
|
613
|
+
if (/^\s/.test(text) && result.length > 0) result[result.length - 1].hasTrailingSpace = true;
|
|
614
|
+
if (style === "codespan") {
|
|
615
|
+
const trimmedText = text.trim();
|
|
616
|
+
if (trimmedText) result.push({
|
|
617
|
+
text: trimmedText,
|
|
618
|
+
width: this.measureWordWidth(doc, trimmedText, style, store),
|
|
619
|
+
style,
|
|
620
|
+
isLink: elIsLink,
|
|
621
|
+
href: elHref,
|
|
622
|
+
linkColor: elIsLink ? store.options.link?.linkColor || [
|
|
379
623
|
0,
|
|
380
624
|
0,
|
|
381
625
|
255
|
|
382
626
|
] : void 0,
|
|
383
|
-
hasTrailingSpace: /\s$/.test(
|
|
627
|
+
hasTrailingSpace: /\s$/.test(text)
|
|
384
628
|
});
|
|
385
629
|
continue;
|
|
386
630
|
}
|
|
387
|
-
|
|
388
|
-
for (let
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
631
|
+
const lines = text.split("\n");
|
|
632
|
+
for (let partIndex = 0; partIndex < lines.length; partIndex++) {
|
|
633
|
+
const lineStr = lines[partIndex];
|
|
634
|
+
const words = lineStr.trim().split(/[ \t\r\v\f]+/).filter((w) => w.length > 0);
|
|
635
|
+
for (let i = 0; i < words.length; i++) {
|
|
636
|
+
const hasTrailingSpace = !(i === words.length - 1) || /[ \t\r\v\f]$/.test(lineStr);
|
|
637
|
+
result.push({
|
|
638
|
+
text: words[i],
|
|
639
|
+
width: this.measureWordWidth(doc, words[i], style, store),
|
|
640
|
+
style,
|
|
641
|
+
isLink: elIsLink,
|
|
642
|
+
href: elHref,
|
|
643
|
+
linkColor: elIsLink ? store.options.link?.linkColor || [
|
|
644
|
+
0,
|
|
645
|
+
0,
|
|
646
|
+
255
|
|
647
|
+
] : void 0,
|
|
648
|
+
hasTrailingSpace
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
if (partIndex < lines.length - 1) result.push({
|
|
652
|
+
text: "",
|
|
653
|
+
width: 0,
|
|
654
|
+
style,
|
|
655
|
+
isBr: true
|
|
402
656
|
});
|
|
403
657
|
}
|
|
404
658
|
}
|
|
405
659
|
}
|
|
406
|
-
return
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
660
|
+
return result;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Break a flat list of words into lines that fit within maxWidth.
|
|
664
|
+
* Correctly tracks totalTextWidth (sum of word widths only) for justification.
|
|
665
|
+
*/
|
|
666
|
+
static breakIntoLines(doc, words, maxWidth, store) {
|
|
667
|
+
const lines = [];
|
|
668
|
+
let currentLine = [];
|
|
669
|
+
let currentTextWidth = 0;
|
|
670
|
+
let currentLineWidth = 0;
|
|
671
|
+
let currentLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
|
|
672
|
+
const spaceWidth = doc.getTextWidth(" ");
|
|
673
|
+
for (let i = 0; i < words.length; i++) {
|
|
674
|
+
const word = words[i];
|
|
675
|
+
const neededWidthWithSpace = currentLine[currentLine.length - 1]?.hasTrailingSpace ? spaceWidth + word.width : word.width;
|
|
676
|
+
const itemHeight = word.isImage && word.imageHeight ? word.imageHeight : getCharHight(doc) * store.options.page.defaultLineHeightFactor;
|
|
677
|
+
if (word.isBr) {
|
|
678
|
+
lines.push({
|
|
679
|
+
words: currentLine,
|
|
680
|
+
totalTextWidth: currentTextWidth,
|
|
681
|
+
isLastLine: true,
|
|
682
|
+
lineHeight: currentLineHeight
|
|
683
|
+
});
|
|
684
|
+
currentLine = [];
|
|
685
|
+
currentTextWidth = 0;
|
|
686
|
+
currentLineWidth = 0;
|
|
687
|
+
currentLineHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
|
|
419
688
|
continue;
|
|
420
689
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
690
|
+
if (currentLineWidth + neededWidthWithSpace > maxWidth && currentLine.length > 0) {
|
|
691
|
+
lines.push({
|
|
692
|
+
words: currentLine,
|
|
693
|
+
totalTextWidth: currentTextWidth,
|
|
694
|
+
isLastLine: false,
|
|
695
|
+
lineHeight: currentLineHeight
|
|
696
|
+
});
|
|
697
|
+
currentLine = [word];
|
|
698
|
+
currentTextWidth = word.width;
|
|
699
|
+
currentLineWidth = word.width;
|
|
700
|
+
currentLineHeight = itemHeight;
|
|
701
|
+
} else {
|
|
702
|
+
currentLine.push(word);
|
|
703
|
+
currentTextWidth += word.width;
|
|
704
|
+
currentLineWidth += neededWidthWithSpace;
|
|
705
|
+
currentLineHeight = Math.max(currentLineHeight, itemHeight);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (currentLine.length > 0) lines.push({
|
|
709
|
+
words: currentLine,
|
|
710
|
+
totalTextWidth: currentTextWidth,
|
|
711
|
+
isLastLine: true,
|
|
712
|
+
lineHeight: currentLineHeight
|
|
713
|
+
});
|
|
714
|
+
return lines;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Render a single word with its style applied.
|
|
718
|
+
*/
|
|
719
|
+
static renderWord(doc, word, x, y, store) {
|
|
720
|
+
const savedFont = doc.getFont();
|
|
721
|
+
const savedSize = doc.getFontSize();
|
|
722
|
+
const savedColor = doc.getTextColor();
|
|
723
|
+
this.applyStyle(doc, word.style, store);
|
|
724
|
+
if (word.isLink && word.linkColor) doc.setTextColor(...word.linkColor);
|
|
725
|
+
if (word.isImage && word.imageElement && word.imageElement.data) try {
|
|
726
|
+
let imgFormat = "JPEG";
|
|
727
|
+
if (word.imageElement.data.startsWith("data:image/png")) imgFormat = "PNG";
|
|
728
|
+
else if (word.imageElement.data.startsWith("data:image/webp")) imgFormat = "WEBP";
|
|
729
|
+
else if (word.imageElement.data.startsWith("data:image/gif")) imgFormat = "GIF";
|
|
730
|
+
else if (word.imageElement.src) {
|
|
731
|
+
const ext = word.imageElement.src.split("?")[0].split("#")[0].split(".").pop()?.toUpperCase();
|
|
732
|
+
if (ext && [
|
|
445
733
|
"PNG",
|
|
446
734
|
"JPEG",
|
|
447
735
|
"JPG",
|
|
448
736
|
"WEBP",
|
|
449
737
|
"GIF"
|
|
450
|
-
].includes(
|
|
738
|
+
].includes(ext)) imgFormat = ext === "JPG" ? "JPEG" : ext;
|
|
451
739
|
}
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
740
|
+
if (word.width > 0 && (word.imageHeight || 0) > 0) {
|
|
741
|
+
const imgH = word.imageHeight || 0;
|
|
742
|
+
const imgY = y;
|
|
743
|
+
doc.addImage(word.imageElement.data, imgFormat, x, imgY, word.width, imgH);
|
|
455
744
|
}
|
|
456
745
|
} catch (e) {
|
|
457
746
|
console.warn("Failed to render inline image", e);
|
|
458
747
|
}
|
|
459
748
|
else {
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
|
|
749
|
+
if (word.style === "codespan") {
|
|
750
|
+
const codespanOpts = this.getCodespanOptions(store);
|
|
751
|
+
if (codespanOpts.showBackground) {
|
|
752
|
+
const h = getCharHight(doc);
|
|
753
|
+
const pad = codespanOpts.padding;
|
|
754
|
+
doc.setFillColor(codespanOpts.backgroundColor);
|
|
755
|
+
doc.rect(x - pad, y - pad, word.width + pad * 2, h + pad * 2, "F");
|
|
756
|
+
doc.setFillColor("#000000");
|
|
465
757
|
}
|
|
466
758
|
}
|
|
467
|
-
|
|
759
|
+
doc.text(word.text, x, y, { baseline: "top" });
|
|
468
760
|
}
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
761
|
+
if (word.isLink && word.href) {
|
|
762
|
+
const h = word.isImage && word.imageHeight ? word.imageHeight : getCharHight(doc);
|
|
763
|
+
doc.link(x, y, word.width, h, { url: word.href });
|
|
472
764
|
}
|
|
473
|
-
|
|
765
|
+
doc.setFont(savedFont.fontName, savedFont.fontStyle);
|
|
766
|
+
doc.setFontSize(savedSize);
|
|
767
|
+
doc.setTextColor(savedColor);
|
|
474
768
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
769
|
+
/**
|
|
770
|
+
* Render a single line with specified alignment.
|
|
771
|
+
*/
|
|
772
|
+
static renderAlignedLine(doc, line, x, y, maxWidth, store, alignment = "left") {
|
|
773
|
+
const { words, totalTextWidth, isLastLine } = line;
|
|
774
|
+
if (words.length === 0) return;
|
|
775
|
+
const normalSpaceWidth = doc.getTextWidth(" ");
|
|
776
|
+
let startX = x;
|
|
777
|
+
let wordSpacing = normalSpaceWidth;
|
|
778
|
+
let lineWidthWithNormalSpaces = totalTextWidth;
|
|
779
|
+
let expandableSpacesCount = 0;
|
|
780
|
+
for (let i = 0; i < words.length - 1; i++) if (words[i].hasTrailingSpace) {
|
|
781
|
+
lineWidthWithNormalSpaces += normalSpaceWidth;
|
|
782
|
+
expandableSpacesCount++;
|
|
783
|
+
}
|
|
784
|
+
switch (alignment) {
|
|
481
785
|
case "right":
|
|
482
|
-
|
|
786
|
+
startX = x + maxWidth - lineWidthWithNormalSpaces;
|
|
483
787
|
break;
|
|
484
788
|
case "center":
|
|
485
|
-
|
|
789
|
+
startX = x + (maxWidth - lineWidthWithNormalSpaces) / 2;
|
|
486
790
|
break;
|
|
487
791
|
case "justify":
|
|
488
|
-
!
|
|
792
|
+
if (!isLastLine && expandableSpacesCount > 0) wordSpacing = (maxWidth - totalTextWidth) / expandableSpacesCount;
|
|
489
793
|
break;
|
|
490
794
|
default: break;
|
|
491
795
|
}
|
|
492
|
-
let
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
796
|
+
let currentX = startX;
|
|
797
|
+
const textHeight = getCharHight(doc) * store.options.page.defaultLineHeightFactor;
|
|
798
|
+
for (let i = 0; i < words.length; i++) {
|
|
799
|
+
const word = words[i];
|
|
800
|
+
let drawY = y;
|
|
801
|
+
const elementHeight = word.isImage && word.imageHeight ? word.imageHeight : textHeight;
|
|
802
|
+
if (word.isImage) drawY = y;
|
|
803
|
+
else if (elementHeight < line.lineHeight) drawY = y + (line.lineHeight - elementHeight);
|
|
804
|
+
this.renderWord(doc, word, currentX, drawY, store);
|
|
805
|
+
currentX += word.width;
|
|
806
|
+
if (i < words.length - 1 && word.hasTrailingSpace) currentX += wordSpacing;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Main entry point: Render a paragraph with mixed inline elements.
|
|
811
|
+
* Respects user's textAlignment option from store.
|
|
812
|
+
*
|
|
813
|
+
* @param doc jsPDF instance
|
|
814
|
+
* @param elements Array of ParsedElement (inline items in a paragraph)
|
|
815
|
+
* @param x Starting X coordinate
|
|
816
|
+
* @param y Starting Y coordinate
|
|
817
|
+
* @param maxWidth Maximum width for text wrapping
|
|
818
|
+
* @param store RenderStore instance to use
|
|
819
|
+
* @param alignment Optional alignment override (defaults to store option)
|
|
820
|
+
*/
|
|
821
|
+
static renderStyledParagraph(doc, elements, x, y, maxWidth, store, alignment) {
|
|
822
|
+
const textAlignment = alignment ?? store.options.content?.textAlignment ?? "left";
|
|
823
|
+
const words = this.flattenToWords(doc, elements, store);
|
|
824
|
+
if (words.length === 0) return;
|
|
825
|
+
const lines = this.breakIntoLines(doc, words, maxWidth, store);
|
|
826
|
+
let currentY = y;
|
|
827
|
+
for (const line of lines) {
|
|
828
|
+
if (currentY + line.lineHeight > store.options.page.maxContentHeight) {
|
|
829
|
+
HandlePageBreaks(doc, store);
|
|
830
|
+
currentY = store.Y;
|
|
831
|
+
}
|
|
832
|
+
this.renderAlignedLine(doc, line, x, currentY, maxWidth, store, textAlignment);
|
|
833
|
+
store.recordContentY(currentY + line.lineHeight);
|
|
834
|
+
currentY += line.lineHeight;
|
|
835
|
+
store.updateY(line.lineHeight, "add");
|
|
836
|
+
}
|
|
837
|
+
const lastLine = lines[lines.length - 1];
|
|
838
|
+
if (lastLine) {
|
|
839
|
+
let actualSpacesCount = 0;
|
|
840
|
+
for (let i = 0; i < lastLine.words.length - 1; i++) if (lastLine.words[i].hasTrailingSpace) actualSpacesCount++;
|
|
841
|
+
const lastLineWidth = lastLine.totalTextWidth + actualSpacesCount * doc.getTextWidth(" ");
|
|
842
|
+
store.updateX(x + lastLineWidth, "set");
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* @deprecated Use renderStyledParagraph instead
|
|
847
|
+
*/
|
|
848
|
+
static renderJustifiedParagraph(doc, elements, x, y, maxWidth, store) {
|
|
849
|
+
this.renderStyledParagraph(doc, elements, x, y, maxWidth, store);
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
//#endregion
|
|
853
|
+
//#region src/renderer/components/heading.ts
|
|
854
|
+
/**
|
|
855
|
+
* Renders heading elements.
|
|
856
|
+
*/
|
|
857
|
+
const renderHeading = (doc, element, indent, store, parentElementRenderer) => {
|
|
858
|
+
const size = 6 - (element?.depth ?? 0) > 0 ? 6 - (element?.depth ?? 0) : 1;
|
|
859
|
+
doc.setFontSize(store.options.page.defaultFontSize + size);
|
|
860
|
+
if (element?.items && element?.items.length > 0) {
|
|
861
|
+
const originalLineHeightFactor = store.options.page.defaultLineHeightFactor;
|
|
862
|
+
store.options.page.defaultLineHeightFactor = 1;
|
|
863
|
+
JustifiedTextRenderer.renderStyledParagraph(doc, element.items, store.X + indent, store.Y, store.options.page.maxContentWidth - indent, store, "left");
|
|
864
|
+
store.options.page.defaultLineHeightFactor = originalLineHeightFactor;
|
|
865
|
+
} else {
|
|
866
|
+
const charHeight = getCharHight(doc);
|
|
867
|
+
doc.text(element?.content ?? "", store.X + indent, store.Y, {
|
|
868
|
+
align: "left",
|
|
869
|
+
maxWidth: store.options.page.maxContentWidth - indent,
|
|
870
|
+
baseline: "top"
|
|
871
|
+
});
|
|
872
|
+
store.recordContentY(store.Y + charHeight);
|
|
873
|
+
store.updateY(getCharHight(doc), "add");
|
|
874
|
+
}
|
|
875
|
+
doc.setFontSize(store.options.page.defaultFontSize);
|
|
876
|
+
store.updateX(store.options.page.xpading, "set");
|
|
877
|
+
};
|
|
878
|
+
//#endregion
|
|
879
|
+
//#region src/utils/text-renderer.ts
|
|
880
|
+
var TextRenderer = class {
|
|
881
|
+
/**
|
|
882
|
+
* Renders text with automatic line wrapping and page breaking.
|
|
883
|
+
* @param doc jsPDF instance
|
|
884
|
+
* @param text Text to render
|
|
885
|
+
* @param store RenderStore instance to use
|
|
886
|
+
* @param x X coordinate (if not provided, uses store.X)
|
|
887
|
+
* @param y Y coordinate (if not provided, uses store.Y)
|
|
888
|
+
* @param maxWidth Max width for text wrapping
|
|
889
|
+
* @param justify Whether to justify the text
|
|
890
|
+
*/
|
|
891
|
+
static renderText(doc, text, store, x = store.X, y = store.Y, maxWidth, justify = false) {
|
|
892
|
+
const lines = doc.splitTextToSize(text, maxWidth);
|
|
893
|
+
const charHeight = getCharHight(doc);
|
|
894
|
+
const lineHeight = charHeight * store.options.page.defaultLineHeightFactor;
|
|
895
|
+
let currentY = y;
|
|
896
|
+
for (let i = 0; i < lines.length; i++) {
|
|
897
|
+
const line = lines[i];
|
|
898
|
+
if (currentY + lineHeight > store.options.page.maxContentHeight) {
|
|
899
|
+
HandlePageBreaks(doc, store);
|
|
900
|
+
currentY = store.Y;
|
|
901
|
+
}
|
|
902
|
+
if (justify) if (i === lines.length - 1) doc.text(line, x, currentY, { baseline: "top" });
|
|
903
|
+
else doc.text(line, x, currentY, {
|
|
904
|
+
maxWidth,
|
|
521
905
|
align: "justify",
|
|
522
906
|
baseline: "top"
|
|
523
|
-
})
|
|
907
|
+
});
|
|
908
|
+
else doc.text(line, x, currentY, { baseline: "top" });
|
|
909
|
+
store.recordContentY(currentY + charHeight);
|
|
910
|
+
currentY += lineHeight;
|
|
911
|
+
store.updateY(lineHeight, "add");
|
|
524
912
|
}
|
|
525
|
-
return
|
|
913
|
+
return currentY;
|
|
526
914
|
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
915
|
+
};
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/renderer/components/paragraph.ts
|
|
918
|
+
/**
|
|
919
|
+
* Renders paragraph elements with proper text alignment.
|
|
920
|
+
* Handles mixed inline styles (bold, italic, codespan) and links.
|
|
921
|
+
* Respects user's textAlignment option from RenderStore.
|
|
922
|
+
*/
|
|
923
|
+
const renderParagraph = (doc, element, indent, store, parentElementRenderer) => {
|
|
924
|
+
store.activateInlineLock();
|
|
925
|
+
doc.setFontSize(store.options.page.defaultFontSize);
|
|
926
|
+
const maxWidth = store.options.page.maxContentWidth - indent;
|
|
927
|
+
if (element?.items && element?.items.length > 0) {
|
|
928
|
+
if (element.items.length === 1 && element.items[0].type === "image") {
|
|
929
|
+
parentElementRenderer(element.items[0], indent, store, false);
|
|
930
|
+
store.updateX(store.options.page.xpading);
|
|
931
|
+
store.deactivateInlineLock();
|
|
533
932
|
return;
|
|
534
933
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
934
|
+
const inlineTypes = [
|
|
935
|
+
"strong",
|
|
936
|
+
"em",
|
|
937
|
+
"text",
|
|
938
|
+
"codespan",
|
|
939
|
+
"link",
|
|
940
|
+
"image",
|
|
941
|
+
"br"
|
|
543
942
|
];
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
943
|
+
if (element.items.some((item) => !inlineTypes.includes(item.type))) {
|
|
944
|
+
const inlineBuffer = [];
|
|
945
|
+
const flushInlineBuffer = () => {
|
|
946
|
+
if (inlineBuffer.length > 0) {
|
|
947
|
+
JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, store.X + indent, store.Y, maxWidth, store);
|
|
948
|
+
inlineBuffer.length = 0;
|
|
949
|
+
}
|
|
547
950
|
};
|
|
548
|
-
for (
|
|
549
|
-
|
|
550
|
-
|
|
951
|
+
for (const item of element.items) if (inlineTypes.includes(item.type)) inlineBuffer.push(item);
|
|
952
|
+
else {
|
|
953
|
+
flushInlineBuffer();
|
|
954
|
+
parentElementRenderer(item, indent, store, false);
|
|
955
|
+
}
|
|
956
|
+
flushInlineBuffer();
|
|
957
|
+
} else JustifiedTextRenderer.renderStyledParagraph(doc, element.items, store.X + indent, store.Y, maxWidth, store);
|
|
551
958
|
} else {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
959
|
+
const content = element.content ?? "";
|
|
960
|
+
const textAlignment = store.options.content?.textAlignment ?? "left";
|
|
961
|
+
if (content.trim()) TextRenderer.renderText(doc, content, store, store.X + indent, store.Y, maxWidth, textAlignment === "justify");
|
|
962
|
+
}
|
|
963
|
+
store.updateX(store.options.page.xpading);
|
|
964
|
+
store.deactivateInlineLock();
|
|
965
|
+
};
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/renderer/components/list.ts
|
|
968
|
+
const renderList = (doc, element, indentLevel, store, parentElementRenderer) => {
|
|
969
|
+
doc.setFontSize(store.options.page.defaultFontSize);
|
|
970
|
+
for (const [i, point] of element?.items?.entries() ?? []) {
|
|
971
|
+
const _start = element.ordered ? (element.start ?? 0) + i : element.start;
|
|
972
|
+
parentElementRenderer(point, indentLevel + 1, store, true, _start, element.ordered);
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
//#endregion
|
|
976
|
+
//#region src/renderer/components/listItem.ts
|
|
977
|
+
/**
|
|
978
|
+
* Render a single list item, including bullets/numbering, inline text, and any nested lists.
|
|
979
|
+
*/
|
|
980
|
+
const renderListItem = (doc, element, indentLevel, store, parentElementRenderer, start, ordered) => {
|
|
981
|
+
if (store.Y + getCharHight(doc) >= store.options.page.maxContentHeight) HandlePageBreaks(doc, store);
|
|
982
|
+
const options = store.options;
|
|
983
|
+
const baseIndent = indentLevel * options.page.indent;
|
|
984
|
+
const bullet = ordered ? `${start}. ` : "• ";
|
|
985
|
+
const xLeft = options.page.xpading;
|
|
986
|
+
store.updateX(xLeft, "set");
|
|
987
|
+
doc.setFont(options.font.regular.name, options.font.regular.style);
|
|
988
|
+
doc.text(bullet, xLeft + baseIndent, store.Y, { baseline: "top" });
|
|
989
|
+
const bulletWidth = doc.getTextWidth(bullet);
|
|
990
|
+
const contentX = xLeft + baseIndent + bulletWidth;
|
|
991
|
+
const textMaxWidth = options.page.maxContentWidth - baseIndent - bulletWidth;
|
|
992
|
+
if (element.items && element.items.length > 0) {
|
|
993
|
+
const inlineBuffer = [];
|
|
994
|
+
const flushInlineBuffer = () => {
|
|
995
|
+
if (inlineBuffer.length > 0) {
|
|
996
|
+
JustifiedTextRenderer.renderStyledParagraph(doc, inlineBuffer, contentX, store.Y, textMaxWidth, store);
|
|
997
|
+
inlineBuffer.length = 0;
|
|
998
|
+
store.updateX(xLeft, "set");
|
|
999
|
+
}
|
|
570
1000
|
};
|
|
571
|
-
for (
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
1001
|
+
for (const subItem of element.items) if (subItem.type === "list") {
|
|
1002
|
+
flushInlineBuffer();
|
|
1003
|
+
parentElementRenderer(subItem, indentLevel, store, true, start, subItem.ordered ?? false);
|
|
1004
|
+
} else if (subItem.type === "list_item") {
|
|
1005
|
+
flushInlineBuffer();
|
|
1006
|
+
parentElementRenderer(subItem, indentLevel, store, true, start, ordered);
|
|
1007
|
+
} else inlineBuffer.push(subItem);
|
|
1008
|
+
flushInlineBuffer();
|
|
1009
|
+
} else if (element.content) {
|
|
1010
|
+
const textAlignment = options.content?.textAlignment ?? "left";
|
|
1011
|
+
TextRenderer.renderText(doc, element.content, store, contentX, store.Y, textMaxWidth, textAlignment === "justify");
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
//#endregion
|
|
1015
|
+
//#region src/renderer/components/rawItem.ts
|
|
1016
|
+
const renderRawItem = (doc, element, indentLevel, store, hasRawBullet, parentElementRenderer, start, ordered, justify = true) => {
|
|
1017
|
+
if (element?.items && element?.items.length > 0) for (const item of element?.items ?? []) parentElementRenderer(item, indentLevel, store, hasRawBullet, start, ordered, justify);
|
|
579
1018
|
else {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1019
|
+
const options = store.options;
|
|
1020
|
+
const indent = indentLevel * options.page.indent;
|
|
1021
|
+
const bullet = hasRawBullet ? ordered ? `${start}. ` : "• " : "";
|
|
1022
|
+
const content = element.content || "";
|
|
1023
|
+
const xLeft = options.page.xpading;
|
|
1024
|
+
if (!content && !bullet) return;
|
|
1025
|
+
if (!content.trim() && !bullet) {
|
|
1026
|
+
const newlines = (content.match(/\n/g) || []).length;
|
|
1027
|
+
if (newlines > 1) {
|
|
1028
|
+
const addedHeight = (newlines - 1) * (doc.getTextDimensions("A").h * options.page.defaultLineHeightFactor);
|
|
1029
|
+
if (store.Y + addedHeight > options.page.maxContentHeight) HandlePageBreaks(doc, store);
|
|
1030
|
+
else {
|
|
1031
|
+
store.updateY(addedHeight, "add");
|
|
1032
|
+
store.recordContentY(store.Y);
|
|
1033
|
+
}
|
|
587
1034
|
}
|
|
588
1035
|
return;
|
|
589
1036
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1037
|
+
store.updateX(xLeft, "set");
|
|
1038
|
+
if (hasRawBullet && bullet) {
|
|
1039
|
+
const bulletWidth = doc.getTextWidth(bullet);
|
|
1040
|
+
const textMaxWidth = options.page.maxContentWidth - indent - bulletWidth;
|
|
1041
|
+
doc.setFont(options.font.regular.name, options.font.regular.style);
|
|
1042
|
+
doc.text(bullet, xLeft + indent, store.Y, { baseline: "top" });
|
|
1043
|
+
TextRenderer.renderText(doc, content, store, xLeft + indent + bulletWidth, store.Y, textMaxWidth, justify);
|
|
593
1044
|
} else {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1045
|
+
const textMaxWidth = options.page.maxContentWidth - indent;
|
|
1046
|
+
TextRenderer.renderText(doc, content, store, xLeft + indent, store.Y, textMaxWidth, justify);
|
|
1047
|
+
}
|
|
1048
|
+
store.updateX(xLeft, "set");
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/renderer/components/hr.ts
|
|
1053
|
+
const renderHR = (doc, store) => {
|
|
1054
|
+
const pageWidth = doc.internal.pageSize.getWidth();
|
|
1055
|
+
doc.setLineDashPattern([1, 1], 0);
|
|
1056
|
+
doc.setLineWidth(.1);
|
|
1057
|
+
doc.line(store.options.page.xpading, store.Y, pageWidth - store.options.page.xpading, store.Y);
|
|
1058
|
+
doc.setLineWidth(.1);
|
|
1059
|
+
doc.setLineDashPattern([], 0);
|
|
1060
|
+
store.updateY(getCharHight(doc), "add");
|
|
1061
|
+
};
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region src/renderer/components/code.ts
|
|
1064
|
+
const renderCodeBlock = (doc, element, indentLevel, store) => {
|
|
1065
|
+
const savedFont = doc.getFont();
|
|
1066
|
+
const savedFontSize = doc.getFontSize();
|
|
1067
|
+
const codeFont = store.options.font.code || {
|
|
1068
|
+
name: "courier",
|
|
1069
|
+
style: "normal"
|
|
1070
|
+
};
|
|
1071
|
+
doc.setFont(codeFont.name, codeFont.style);
|
|
1072
|
+
const codeFontSize = store.options.page.defaultFontSize * .9;
|
|
1073
|
+
doc.setFontSize(codeFontSize);
|
|
1074
|
+
const indent = indentLevel * store.options.page.indent;
|
|
1075
|
+
const maxWidth = store.options.page.maxContentWidth - indent - 8;
|
|
1076
|
+
const lineHeightFactor = doc.getLineHeightFactor();
|
|
1077
|
+
const lineHeight = codeFontSize / doc.internal.scaleFactor * lineHeightFactor;
|
|
1078
|
+
const content = (element.code ?? "").replace(/[\r\n\s]+$/, "");
|
|
1079
|
+
if (!content) {
|
|
1080
|
+
doc.setFont(savedFont.fontName, savedFont.fontStyle);
|
|
1081
|
+
doc.setFontSize(savedFontSize);
|
|
610
1082
|
return;
|
|
611
1083
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
|
|
1084
|
+
const lines = doc.splitTextToSize(content, maxWidth);
|
|
1085
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
1086
|
+
if (lines.length === 0) {
|
|
1087
|
+
doc.setFont(savedFont.fontName, savedFont.fontStyle);
|
|
1088
|
+
doc.setFontSize(savedFontSize);
|
|
616
1089
|
return;
|
|
617
1090
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
1091
|
+
const padding = 4;
|
|
1092
|
+
const bgColor = "#EEEEEE";
|
|
1093
|
+
const drawColor = "#DDDDDD";
|
|
1094
|
+
let currentLineIndex = 0;
|
|
1095
|
+
while (currentLineIndex < lines.length) {
|
|
1096
|
+
const availableHeight = store.options.page.maxContentHeight - store.Y;
|
|
1097
|
+
const remainingLines = lines.length - currentLineIndex;
|
|
1098
|
+
const effectiveAvailable = availableHeight - padding * 2;
|
|
1099
|
+
let linesToRenderCount = Math.floor(effectiveAvailable / lineHeight);
|
|
1100
|
+
if (linesToRenderCount <= 0) {
|
|
1101
|
+
HandlePageBreaks(doc, store);
|
|
623
1102
|
continue;
|
|
624
1103
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1104
|
+
if (linesToRenderCount > remainingLines) linesToRenderCount = remainingLines;
|
|
1105
|
+
const linesToRender = lines.slice(currentLineIndex, currentLineIndex + linesToRenderCount);
|
|
1106
|
+
const isFirstChunk = currentLineIndex === 0;
|
|
1107
|
+
const isLastChunk = currentLineIndex + linesToRenderCount >= lines.length;
|
|
1108
|
+
const textBlockHeight = linesToRenderCount * lineHeight;
|
|
1109
|
+
if (isFirstChunk) store.updateY(padding, "add");
|
|
1110
|
+
doc.setFillColor(bgColor);
|
|
1111
|
+
doc.setDrawColor(drawColor);
|
|
1112
|
+
doc.roundedRect(store.X, store.Y - padding, store.options.page.maxContentWidth, textBlockHeight + (isFirstChunk ? padding : 0) + (isLastChunk ? padding : 0), 2, 2, "FD");
|
|
1113
|
+
if (isFirstChunk && element.lang) {
|
|
1114
|
+
const savedCodeFontSize = doc.getFontSize();
|
|
1115
|
+
doc.setFontSize(10);
|
|
1116
|
+
doc.setTextColor("#666666");
|
|
1117
|
+
doc.text(element.lang, store.X + store.options.page.maxContentWidth - doc.getTextWidth(element.lang) - 4, store.Y, { baseline: "top" });
|
|
1118
|
+
doc.setFontSize(savedCodeFontSize);
|
|
1119
|
+
doc.setTextColor("#000000");
|
|
630
1120
|
}
|
|
631
|
-
let
|
|
632
|
-
for (
|
|
633
|
-
|
|
1121
|
+
let yPos = store.Y;
|
|
1122
|
+
for (const line of linesToRender) {
|
|
1123
|
+
doc.text(line, store.X + 4, yPos, { baseline: "top" });
|
|
1124
|
+
yPos += lineHeight;
|
|
1125
|
+
}
|
|
1126
|
+
store.updateY(textBlockHeight, "add");
|
|
1127
|
+
store.recordContentY(store.Y + (isLastChunk ? padding : 0));
|
|
1128
|
+
if (isLastChunk) store.updateY(padding, "add");
|
|
1129
|
+
currentLineIndex += linesToRenderCount;
|
|
1130
|
+
if (currentLineIndex < lines.length) HandlePageBreaks(doc, store);
|
|
634
1131
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1132
|
+
doc.setFont(savedFont.fontName, savedFont.fontStyle);
|
|
1133
|
+
doc.setFontSize(savedFontSize);
|
|
1134
|
+
};
|
|
1135
|
+
//#endregion
|
|
1136
|
+
//#region src/renderer/components/inlineText.ts
|
|
1137
|
+
/**
|
|
1138
|
+
* Renders inline text elements (Strong, Em, and Text) with proper inline styling.
|
|
1139
|
+
*/
|
|
1140
|
+
const renderInlineText = (doc, element, indent, store) => {
|
|
1141
|
+
const currentFont = doc.getFont().fontName;
|
|
1142
|
+
const currentFontStyle = doc.getFont().fontStyle;
|
|
1143
|
+
const currentFontSize = doc.getFontSize();
|
|
1144
|
+
const spaceMultiplier = (style) => {
|
|
1145
|
+
switch (style) {
|
|
639
1146
|
case "normal": return 0;
|
|
640
1147
|
case "bold": return 1;
|
|
641
1148
|
case "italic": return 1.5;
|
|
@@ -643,192 +1150,338 @@ var n = /* @__PURE__ */ function(e) {
|
|
|
643
1150
|
case "codespan": return .5;
|
|
644
1151
|
default: return 0;
|
|
645
1152
|
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1153
|
+
};
|
|
1154
|
+
const renderTextWithStyle = (text, style) => {
|
|
1155
|
+
if (style === "bold") doc.setFont(store.options.font.bold.name && store.options.font.bold.name !== "" ? store.options.font.bold.name : currentFont, store.options.font.bold.style || "bold");
|
|
1156
|
+
else if (style === "italic") doc.setFont(store.options.font.regular.name, "italic");
|
|
1157
|
+
else if (style === "bolditalic") doc.setFont(store.options.font.bold.name && store.options.font.bold.name !== "" ? store.options.font.bold.name : currentFont, "bolditalic");
|
|
1158
|
+
else if (style === "codespan") {
|
|
1159
|
+
const codeFont = store.options.font.code || {
|
|
1160
|
+
name: "courier",
|
|
1161
|
+
style: "normal"
|
|
1162
|
+
};
|
|
1163
|
+
doc.setFont(codeFont.name, codeFont.style);
|
|
1164
|
+
doc.setFontSize(currentFontSize * .9);
|
|
1165
|
+
} else doc.setFont(store.options.font.regular.name, currentFontStyle);
|
|
1166
|
+
const availableWidth = store.options.page.maxContentWidth - indent - store.X;
|
|
1167
|
+
const textLines = doc.splitTextToSize(text, availableWidth);
|
|
1168
|
+
const isCodeSpan = style === "codespan";
|
|
1169
|
+
const codePadding = 1;
|
|
1170
|
+
const codeBgColor = "#EEEEEE";
|
|
1171
|
+
if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
|
|
1172
|
+
if (isCodeSpan) {
|
|
1173
|
+
const lineWidth = doc.getTextWidth(textLines[i]) + getCharWidth(doc);
|
|
1174
|
+
const lineHeight = getCharHight(doc);
|
|
1175
|
+
doc.setFillColor(codeBgColor);
|
|
1176
|
+
doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, lineWidth + codePadding * 2, lineHeight + codePadding * 2, 2, 2, "F");
|
|
1177
|
+
doc.setFillColor("#000000");
|
|
653
1178
|
}
|
|
654
|
-
|
|
1179
|
+
doc.text(textLines[i], store.X + indent, store.Y, {
|
|
655
1180
|
baseline: "top",
|
|
656
|
-
maxWidth:
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1181
|
+
maxWidth: availableWidth
|
|
1182
|
+
});
|
|
1183
|
+
store.updateX(doc.getTextDimensions(textLines[i]).w + (isCodeSpan ? codePadding * 2 : 1), "add");
|
|
1184
|
+
if (i < textLines.length - 1) {
|
|
1185
|
+
store.updateY(getCharHight(doc), "add");
|
|
1186
|
+
store.updateX(store.options.page.xpading, "set");
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
else if (textLines.length > 1) {
|
|
1190
|
+
const firstLine = textLines[0];
|
|
1191
|
+
const restContent = textLines?.slice(1)?.join(" ");
|
|
1192
|
+
if (isCodeSpan) {
|
|
1193
|
+
const w = doc.getTextWidth(firstLine) + getCharWidth(doc);
|
|
1194
|
+
const h = getCharHight(doc);
|
|
1195
|
+
doc.setFillColor(codeBgColor);
|
|
1196
|
+
doc.roundedRect(store.X + (indent >= 2 ? indent + 2 : 0) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
|
|
1197
|
+
doc.setFillColor("#000000");
|
|
664
1198
|
}
|
|
665
|
-
|
|
1199
|
+
doc.text(firstLine, store.X + (indent >= 2 ? indent + 2 * spaceMultiplier(style) : 0), store.Y, {
|
|
666
1200
|
baseline: "top",
|
|
667
|
-
maxWidth:
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1201
|
+
maxWidth: availableWidth
|
|
1202
|
+
});
|
|
1203
|
+
store.updateX(store.options.page.xpading + indent);
|
|
1204
|
+
store.updateY(getCharHight(doc), "add");
|
|
1205
|
+
const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
|
|
1206
|
+
doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
|
|
1207
|
+
if (isCodeSpan) {
|
|
1208
|
+
const w = doc.getTextWidth(line) + getCharWidth(doc);
|
|
1209
|
+
const h = getCharHight(doc);
|
|
1210
|
+
doc.setFillColor(codeBgColor);
|
|
1211
|
+
doc.roundedRect(store.X + getCharWidth(doc) - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
|
|
1212
|
+
doc.setFillColor("#000000");
|
|
674
1213
|
}
|
|
675
|
-
|
|
1214
|
+
doc.text(line, store.X + getCharWidth(doc), store.Y, {
|
|
676
1215
|
baseline: "top",
|
|
677
|
-
maxWidth:
|
|
1216
|
+
maxWidth: maxWidthForRest
|
|
678
1217
|
});
|
|
679
1218
|
});
|
|
680
1219
|
} else {
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
1220
|
+
if (isCodeSpan) {
|
|
1221
|
+
const w = doc.getTextWidth(text) + getCharWidth(doc);
|
|
1222
|
+
const h = getCharHight(doc);
|
|
1223
|
+
doc.setFillColor(codeBgColor);
|
|
1224
|
+
doc.roundedRect(store.X + indent - codePadding, store.Y - codePadding, w + codePadding * 2, h + codePadding * 2, 2, 2, "F");
|
|
1225
|
+
doc.setFillColor("#000000");
|
|
684
1226
|
}
|
|
685
|
-
|
|
1227
|
+
doc.text(text, store.X + indent, store.Y, {
|
|
686
1228
|
baseline: "top",
|
|
687
|
-
maxWidth:
|
|
688
|
-
})
|
|
1229
|
+
maxWidth: availableWidth
|
|
1230
|
+
});
|
|
1231
|
+
store.updateX(doc.getTextDimensions(text).w + (indent >= 2 ? text.split(" ").length + 2 : 2) * spaceMultiplier(style) * .5 + (isCodeSpan ? codePadding * 2 : 0), "add");
|
|
689
1232
|
}
|
|
690
1233
|
};
|
|
691
|
-
if (
|
|
692
|
-
else if (
|
|
693
|
-
|
|
694
|
-
if (
|
|
695
|
-
else
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1234
|
+
if (element.type === "text" && element.items && element.items.length > 0) for (const item of element.items) if (item.type === "codespan") renderTextWithStyle(item.content || "", "codespan");
|
|
1235
|
+
else if (item.type === "em" || item.type === "strong") {
|
|
1236
|
+
const baseStyle = item.type === "em" ? "italic" : "bold";
|
|
1237
|
+
if (item.items && item.items.length > 0) for (const subItem of item.items) if (subItem.type === "strong" && baseStyle === "italic") renderTextWithStyle(subItem.content || "", "bolditalic");
|
|
1238
|
+
else if (subItem.type === "em" && baseStyle === "bold") renderTextWithStyle(subItem.content || "", "bolditalic");
|
|
1239
|
+
else renderTextWithStyle(subItem.content || "", baseStyle);
|
|
1240
|
+
else renderTextWithStyle(item.content || "", baseStyle);
|
|
1241
|
+
} else renderTextWithStyle(item.content || "", "normal");
|
|
1242
|
+
else if (element.type === "em") renderTextWithStyle(element.content || "", "italic");
|
|
1243
|
+
else if (element.type === "strong") renderTextWithStyle(element.content || "", "bold");
|
|
1244
|
+
else if (element.type === "codespan") renderTextWithStyle(element.content || "", "codespan");
|
|
1245
|
+
else renderTextWithStyle(element.content || "", "normal");
|
|
1246
|
+
doc.setFont(currentFont, currentFontStyle);
|
|
1247
|
+
doc.setFontSize(currentFontSize);
|
|
1248
|
+
};
|
|
1249
|
+
//#endregion
|
|
1250
|
+
//#region src/renderer/components/link.ts
|
|
1251
|
+
/**
|
|
1252
|
+
* Renders link elements with proper styling and URL handling.
|
|
1253
|
+
* Links are rendered in blue color and underlined to distinguish them from regular text.
|
|
1254
|
+
*/
|
|
1255
|
+
const renderLink = (doc, element, indent, store) => {
|
|
1256
|
+
const currentFont = doc.getFont().fontName;
|
|
1257
|
+
const currentFontStyle = doc.getFont().fontStyle;
|
|
1258
|
+
const currentFontSize = doc.getFontSize();
|
|
1259
|
+
const currentTextColor = doc.getTextColor();
|
|
1260
|
+
const linkColor = store.options.link?.linkColor || [
|
|
701
1261
|
0,
|
|
702
1262
|
0,
|
|
703
1263
|
255
|
|
704
1264
|
];
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1265
|
+
doc.setTextColor(...linkColor);
|
|
1266
|
+
const availableWidth = store.options.page.maxContentWidth - indent - store.X;
|
|
1267
|
+
const linkText = element.text || element.content || "";
|
|
1268
|
+
const linkUrl = element.href || "";
|
|
1269
|
+
const textLines = doc.splitTextToSize(linkText, availableWidth);
|
|
1270
|
+
if (store.isInlineLockActive) for (let i = 0; i < textLines.length; i++) {
|
|
1271
|
+
const textWidth = doc.getTextDimensions(textLines[i]).w;
|
|
1272
|
+
const textHeight = getCharHight(doc) / 2;
|
|
1273
|
+
doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
|
|
1274
|
+
doc.text(textLines[i], store.X + indent, store.Y, {
|
|
710
1275
|
baseline: "top",
|
|
711
|
-
maxWidth:
|
|
712
|
-
})
|
|
1276
|
+
maxWidth: availableWidth
|
|
1277
|
+
});
|
|
1278
|
+
store.updateX(textWidth + 1, "add");
|
|
1279
|
+
if (store.X + textWidth > store.options.page.maxContentWidth - indent) {
|
|
1280
|
+
store.updateY(textHeight, "add");
|
|
1281
|
+
store.updateX(store.options.page.xpading + indent, "set");
|
|
1282
|
+
}
|
|
1283
|
+
if (i < textLines.length - 1) {
|
|
1284
|
+
store.updateY(textHeight, "add");
|
|
1285
|
+
store.updateX(store.options.page.xpading + indent, "set");
|
|
1286
|
+
}
|
|
713
1287
|
}
|
|
714
|
-
else if (
|
|
715
|
-
|
|
716
|
-
|
|
1288
|
+
else if (textLines.length > 1) {
|
|
1289
|
+
const firstLine = textLines[0];
|
|
1290
|
+
const restContent = textLines?.slice(1)?.join(" ");
|
|
1291
|
+
const firstLineWidth = doc.getTextDimensions(firstLine).w;
|
|
1292
|
+
const textHeight = getCharHight(doc) / 2;
|
|
1293
|
+
doc.link(store.X + indent, store.Y, firstLineWidth, textHeight, { url: linkUrl });
|
|
1294
|
+
doc.text(firstLine, store.X + indent, store.Y, {
|
|
717
1295
|
baseline: "top",
|
|
718
|
-
maxWidth:
|
|
719
|
-
})
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1296
|
+
maxWidth: availableWidth
|
|
1297
|
+
});
|
|
1298
|
+
store.updateX(store.options.page.xpading + indent);
|
|
1299
|
+
store.updateY(textHeight, "add");
|
|
1300
|
+
const maxWidthForRest = store.options.page.maxContentWidth - indent - store.options.page.xpading;
|
|
1301
|
+
doc.splitTextToSize(restContent, maxWidthForRest).forEach((line) => {
|
|
1302
|
+
const lineWidth = doc.getTextDimensions(line).w;
|
|
1303
|
+
doc.link(store.X + getCharWidth(doc), store.Y, lineWidth, textHeight, { url: linkUrl });
|
|
1304
|
+
doc.text(line, store.X + getCharWidth(doc), store.Y, {
|
|
724
1305
|
baseline: "top",
|
|
725
|
-
maxWidth:
|
|
1306
|
+
maxWidth: maxWidthForRest
|
|
726
1307
|
});
|
|
727
1308
|
});
|
|
728
1309
|
} else {
|
|
729
|
-
|
|
730
|
-
|
|
1310
|
+
const textWidth = doc.getTextDimensions(linkText).w;
|
|
1311
|
+
const textHeight = getCharHight(doc) / 2;
|
|
1312
|
+
doc.link(store.X + indent, store.Y, textWidth, textHeight, { url: linkUrl });
|
|
1313
|
+
doc.text(linkText, store.X + indent, store.Y, {
|
|
731
1314
|
baseline: "top",
|
|
732
|
-
maxWidth:
|
|
733
|
-
})
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1315
|
+
maxWidth: availableWidth
|
|
1316
|
+
});
|
|
1317
|
+
store.updateX(textWidth + 2, "add");
|
|
1318
|
+
}
|
|
1319
|
+
doc.setFont(currentFont, currentFontStyle);
|
|
1320
|
+
doc.setFontSize(currentFontSize);
|
|
1321
|
+
doc.setTextColor(currentTextColor);
|
|
1322
|
+
};
|
|
1323
|
+
//#endregion
|
|
1324
|
+
//#region src/renderer/components/blockquote.ts
|
|
1325
|
+
const renderBlockquote = (doc, element, indentLevel, store, renderElement) => {
|
|
1326
|
+
const options = store.options;
|
|
1327
|
+
const blockquoteIndent = indentLevel + 1;
|
|
1328
|
+
const currentX = store.X + indentLevel * options.page.indent;
|
|
1329
|
+
const currentY = store.Y;
|
|
1330
|
+
const barX = currentX + options.page.indent / 2;
|
|
1331
|
+
const startY = currentY;
|
|
1332
|
+
const startPage = doc.internal.getCurrentPageInfo().pageNumber;
|
|
1333
|
+
if (element.items && element.items.length > 0) element.items.forEach((item) => {
|
|
1334
|
+
renderElement(item, blockquoteIndent, store);
|
|
740
1335
|
});
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1336
|
+
const endY = store.Y;
|
|
1337
|
+
const endPage = doc.internal.getCurrentPageInfo().pageNumber;
|
|
1338
|
+
doc.setDrawColor(100);
|
|
1339
|
+
doc.setLineWidth(1);
|
|
1340
|
+
for (let p = startPage; p <= endPage; p++) {
|
|
1341
|
+
doc.setPage(p);
|
|
1342
|
+
const isStart = p === startPage;
|
|
1343
|
+
const isEnd = p === endPage;
|
|
1344
|
+
const lineTop = isStart ? startY : options.page.topmargin;
|
|
1345
|
+
const lineBottom = isEnd ? endY : options.page.maxContentHeight;
|
|
1346
|
+
doc.line(barX, lineTop, barX, lineBottom);
|
|
1347
|
+
}
|
|
1348
|
+
store.recordContentY();
|
|
1349
|
+
doc.setPage(endPage);
|
|
1350
|
+
};
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/renderer/components/image.ts
|
|
1353
|
+
/**
|
|
1354
|
+
* Detects the image format from element data and source.
|
|
1355
|
+
*/
|
|
1356
|
+
const detectImageFormat = (element) => {
|
|
1357
|
+
if (element.data) {
|
|
1358
|
+
if (element.data.startsWith("data:image/png")) return "PNG";
|
|
1359
|
+
if (element.data.startsWith("data:image/jpeg") || element.data.startsWith("data:image/jpg")) return "JPEG";
|
|
1360
|
+
if (element.data.startsWith("data:image/webp")) return "WEBP";
|
|
1361
|
+
if (element.data.startsWith("data:image/webp")) return "WEBP";
|
|
1362
|
+
if (element.data.startsWith("data:image/gif")) return "GIF";
|
|
1363
|
+
}
|
|
1364
|
+
if (element.src) {
|
|
1365
|
+
const ext = element.src.split("?")[0].split("#")[0].split(".").pop()?.toUpperCase();
|
|
1366
|
+
if (ext && [
|
|
759
1367
|
"PNG",
|
|
760
1368
|
"JPEG",
|
|
761
1369
|
"JPG",
|
|
762
1370
|
"WEBP",
|
|
763
1371
|
"GIF"
|
|
764
|
-
].includes(
|
|
1372
|
+
].includes(ext)) return ext === "JPG" ? "JPEG" : ext;
|
|
765
1373
|
}
|
|
766
1374
|
return "JPEG";
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
|
|
1375
|
+
};
|
|
1376
|
+
/**
|
|
1377
|
+
* Renders an image element into the jsPDF document with smart sizing and alignment.
|
|
1378
|
+
*
|
|
1379
|
+
* Sizing logic (in order of priority):
|
|
1380
|
+
* 1. If both width & height are specified by user → convert from px, use as-is
|
|
1381
|
+
* 2. If only width is specified → convert from px, calculate height from aspect ratio
|
|
1382
|
+
* 3. If only height is specified → convert from px, calculate width from aspect ratio
|
|
1383
|
+
* 4. If nothing specified → use intrinsic dimensions (converted from px to doc units)
|
|
1384
|
+
* 5. Always clamp to page bounds (scale down proportionally if needed)
|
|
1385
|
+
*
|
|
1386
|
+
* Alignment: 'left' (default) | 'center' | 'right'
|
|
1387
|
+
* Can be set per-image via markdown attributes or globally via RenderOption.image.defaultAlign
|
|
1388
|
+
*/
|
|
1389
|
+
const renderImage = (doc, element, indentLevel, store) => {
|
|
1390
|
+
if (!element.data) return;
|
|
1391
|
+
const options = store.options;
|
|
1392
|
+
const docUnit = options.page.unit || "mm";
|
|
1393
|
+
const indent = indentLevel * options.page.indent;
|
|
1394
|
+
const maxWidth = options.page.maxContentWidth - indent;
|
|
1395
|
+
const pageLeftX = store.X + indent;
|
|
1396
|
+
let currentY = store.Y;
|
|
770
1397
|
try {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1398
|
+
const { finalWidth, finalHeight } = calculateImageDimensions(doc, element, maxWidth, options.page.maxContentHeight - options.page.topmargin, docUnit);
|
|
1399
|
+
if (currentY + finalHeight > options.page.maxContentHeight) {
|
|
1400
|
+
HandlePageBreaks(doc, store);
|
|
1401
|
+
currentY = store.Y;
|
|
1402
|
+
}
|
|
1403
|
+
const align = element.align || options.image?.defaultAlign || "left";
|
|
1404
|
+
let drawX;
|
|
1405
|
+
switch (align) {
|
|
775
1406
|
case "right":
|
|
776
|
-
|
|
1407
|
+
drawX = pageLeftX + maxWidth - finalWidth;
|
|
777
1408
|
break;
|
|
778
1409
|
case "center":
|
|
779
|
-
|
|
1410
|
+
drawX = pageLeftX + (maxWidth - finalWidth) / 2;
|
|
780
1411
|
break;
|
|
781
1412
|
default:
|
|
782
|
-
|
|
1413
|
+
drawX = pageLeftX;
|
|
783
1414
|
break;
|
|
784
1415
|
}
|
|
785
|
-
|
|
786
|
-
|
|
1416
|
+
const imgFormat = detectImageFormat(element);
|
|
1417
|
+
if (finalWidth > 0 && finalHeight > 0) doc.addImage(element.data, imgFormat, drawX, currentY, finalWidth, finalHeight);
|
|
1418
|
+
store.updateY(finalHeight, "add");
|
|
1419
|
+
store.recordContentY();
|
|
787
1420
|
} catch (e) {
|
|
788
1421
|
console.warn("Failed to render image", e);
|
|
789
1422
|
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
if (
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1423
|
+
};
|
|
1424
|
+
//#endregion
|
|
1425
|
+
//#region src/renderer/components/table.ts
|
|
1426
|
+
const resolveAutoTable = () => {
|
|
1427
|
+
const autoTableCandidate = autoTable;
|
|
1428
|
+
if (typeof autoTable === "function") return autoTable;
|
|
1429
|
+
if (typeof autoTableCandidate.default === "function") return autoTableCandidate.default;
|
|
1430
|
+
if (typeof autoTableCandidate.autoTable === "function") return autoTableCandidate.autoTable;
|
|
1431
|
+
throw new Error("Could not resolve jspdf-autotable export. Expected a callable export.");
|
|
1432
|
+
};
|
|
1433
|
+
const renderTable = (doc, element, indentLevel, store) => {
|
|
1434
|
+
if (!element.header || !element.rows) return;
|
|
1435
|
+
const options = store.options;
|
|
1436
|
+
const marginLeft = options.page.xmargin + indentLevel * options.page.indent;
|
|
1437
|
+
const head = [element.header.map((h) => h.content || "")];
|
|
1438
|
+
const body = element.rows.map((row) => row.map((cell) => cell.content || ""));
|
|
1439
|
+
const userTableOptions = options.table || {};
|
|
1440
|
+
resolveAutoTable()(doc, {
|
|
1441
|
+
head,
|
|
1442
|
+
body,
|
|
1443
|
+
startY: store.Y,
|
|
803
1444
|
margin: {
|
|
804
|
-
left:
|
|
805
|
-
right:
|
|
1445
|
+
left: marginLeft,
|
|
1446
|
+
right: options.page.xmargin
|
|
806
1447
|
},
|
|
807
|
-
...
|
|
808
|
-
didDrawPage: (
|
|
809
|
-
|
|
1448
|
+
...userTableOptions,
|
|
1449
|
+
didDrawPage: (data) => {
|
|
1450
|
+
if (userTableOptions.didDrawPage) userTableOptions.didDrawPage(data);
|
|
810
1451
|
},
|
|
811
|
-
didDrawCell: (
|
|
812
|
-
|
|
1452
|
+
didDrawCell: (data) => {
|
|
1453
|
+
if (userTableOptions.didDrawCell) userTableOptions.didDrawCell(data);
|
|
813
1454
|
}
|
|
814
1455
|
});
|
|
815
|
-
|
|
816
|
-
typeof
|
|
817
|
-
|
|
818
|
-
|
|
1456
|
+
const finalY = doc.lastAutoTable?.finalY;
|
|
1457
|
+
if (typeof finalY === "number") {
|
|
1458
|
+
store.updateY(finalY + options.page.lineSpace, "set");
|
|
1459
|
+
store.updateX(options.page.xpading, "set");
|
|
1460
|
+
store.recordContentY();
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
//#endregion
|
|
1464
|
+
//#region src/store/renderStore.ts
|
|
1465
|
+
var RenderStore = class {
|
|
1466
|
+
constructor(options) {
|
|
819
1467
|
this.cursor = {
|
|
820
1468
|
x: 0,
|
|
821
1469
|
y: 0
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1470
|
+
};
|
|
1471
|
+
this.lastContentY_ = 0;
|
|
1472
|
+
this.inlineLock = false;
|
|
1473
|
+
this.options_ = options;
|
|
1474
|
+
this.cursor = {
|
|
1475
|
+
x: options.cursor.x,
|
|
1476
|
+
y: options.cursor.y
|
|
1477
|
+
};
|
|
1478
|
+
this.lastContentY_ = options.cursor.y;
|
|
826
1479
|
}
|
|
827
1480
|
getCursor() {
|
|
828
1481
|
return this.cursor;
|
|
829
1482
|
}
|
|
830
|
-
setCursor(
|
|
831
|
-
this.cursor =
|
|
1483
|
+
setCursor(newCursor) {
|
|
1484
|
+
this.cursor = newCursor;
|
|
832
1485
|
}
|
|
833
1486
|
get options() {
|
|
834
1487
|
return this.options_;
|
|
@@ -837,20 +1490,43 @@ var n = /* @__PURE__ */ function(e) {
|
|
|
837
1490
|
return this.inlineLock;
|
|
838
1491
|
}
|
|
839
1492
|
activateInlineLock() {
|
|
840
|
-
this.inlineLock =
|
|
1493
|
+
this.inlineLock = true;
|
|
841
1494
|
}
|
|
842
1495
|
deactivateInlineLock() {
|
|
843
|
-
this.inlineLock =
|
|
1496
|
+
this.inlineLock = false;
|
|
844
1497
|
}
|
|
845
|
-
|
|
846
|
-
|
|
1498
|
+
/**
|
|
1499
|
+
* Updates the x pointer of the cursor.
|
|
1500
|
+
* @param value The value to set or add.
|
|
1501
|
+
* @param operation 'set' to assign a new value, 'add' to increment the current value.
|
|
1502
|
+
* @default operation = 'set'
|
|
1503
|
+
*/
|
|
1504
|
+
updateX(value, operation = "set") {
|
|
1505
|
+
if (operation === "set") this.cursor.x = value;
|
|
1506
|
+
else if (operation === "add") this.cursor.x += value;
|
|
847
1507
|
}
|
|
848
|
-
|
|
849
|
-
|
|
1508
|
+
/**
|
|
1509
|
+
* Updates the y pointer of the cursor.
|
|
1510
|
+
* @param value The value to set or add.
|
|
1511
|
+
* @param operation 'set' to assign a new value, 'add' to increment the current value.
|
|
1512
|
+
* @default operation = 'set'
|
|
1513
|
+
*/
|
|
1514
|
+
updateY(value, operation = "set") {
|
|
1515
|
+
if (operation === "set") this.cursor.y = value;
|
|
1516
|
+
else if (operation === "add") this.cursor.y += value;
|
|
850
1517
|
}
|
|
851
|
-
|
|
852
|
-
|
|
1518
|
+
/**
|
|
1519
|
+
* Records a Y position as the bottom of rendered content.
|
|
1520
|
+
* This is useful for container components (like blockquotes) to know
|
|
1521
|
+
* where their actual text content ends, ignoring any trailing margins.
|
|
1522
|
+
* @param specificY Optional Y value to record. Defaults to current cursor Y.
|
|
1523
|
+
*/
|
|
1524
|
+
recordContentY(specificY) {
|
|
1525
|
+
this.lastContentY_ = specificY !== void 0 ? specificY : this.cursor.y;
|
|
853
1526
|
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Gets the last Y position recorded as content bottom.
|
|
1529
|
+
*/
|
|
854
1530
|
get lastContentY() {
|
|
855
1531
|
return this.lastContentY_;
|
|
856
1532
|
}
|
|
@@ -860,7 +1536,10 @@ var n = /* @__PURE__ */ function(e) {
|
|
|
860
1536
|
get Y() {
|
|
861
1537
|
return this.cursor.y;
|
|
862
1538
|
}
|
|
863
|
-
}
|
|
1539
|
+
};
|
|
1540
|
+
//#endregion
|
|
1541
|
+
//#region src/utils/options-validation.ts
|
|
1542
|
+
const defaultOptions = {
|
|
864
1543
|
page: {
|
|
865
1544
|
indent: 10,
|
|
866
1545
|
maxContentWidth: 190,
|
|
@@ -887,88 +1566,111 @@ var n = /* @__PURE__ */ function(e) {
|
|
|
887
1566
|
light: {
|
|
888
1567
|
name: "helvetica",
|
|
889
1568
|
style: "light"
|
|
1569
|
+
},
|
|
1570
|
+
code: {
|
|
1571
|
+
name: "courier",
|
|
1572
|
+
style: "normal"
|
|
890
1573
|
}
|
|
891
1574
|
},
|
|
892
1575
|
image: { defaultAlign: "left" }
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
...
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
...
|
|
903
|
-
|
|
1576
|
+
};
|
|
1577
|
+
const validateOptions = (options) => {
|
|
1578
|
+
if (!options) throw new Error("RenderOption is required");
|
|
1579
|
+
const mergedPage = {
|
|
1580
|
+
...defaultOptions.page,
|
|
1581
|
+
...options.page
|
|
1582
|
+
};
|
|
1583
|
+
const mergedFont = {
|
|
1584
|
+
...defaultOptions.font,
|
|
1585
|
+
...options.font
|
|
1586
|
+
};
|
|
1587
|
+
const mergedImage = {
|
|
1588
|
+
...defaultOptions.image,
|
|
1589
|
+
...options.image
|
|
904
1590
|
};
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1591
|
+
if (!mergedPage.maxContentWidth) mergedPage.maxContentWidth = 190;
|
|
1592
|
+
if (!mergedPage.maxContentHeight) mergedPage.maxContentHeight = 277;
|
|
1593
|
+
return {
|
|
1594
|
+
...options,
|
|
1595
|
+
page: mergedPage,
|
|
1596
|
+
font: mergedFont,
|
|
1597
|
+
image: mergedImage
|
|
910
1598
|
};
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1599
|
+
};
|
|
1600
|
+
//#endregion
|
|
1601
|
+
//#region src/renderer/MdTextRender.ts
|
|
1602
|
+
/**
|
|
1603
|
+
* Renders parsed markdown text into jsPDF document.
|
|
1604
|
+
*
|
|
1605
|
+
* @param doc - The jsPDF document.
|
|
1606
|
+
* @param text - The markdown content to render.
|
|
1607
|
+
* @param options - The render options (fonts, page margins, etc.).
|
|
1608
|
+
*/
|
|
1609
|
+
const MdTextRender = async (doc, text, options) => {
|
|
1610
|
+
const validOptions = validateOptions(options);
|
|
1611
|
+
const store = new RenderStore(validOptions);
|
|
1612
|
+
const parsedElements = await MdTextParser(text);
|
|
1613
|
+
await prefetchImages(parsedElements);
|
|
1614
|
+
const renderElement = (element, indentLevel = 0, store, hasRawBullet = false, start = 0, ordered = false) => {
|
|
1615
|
+
const indent = indentLevel * validOptions.page.indent;
|
|
1616
|
+
switch (element.type) {
|
|
1617
|
+
case "heading":
|
|
1618
|
+
renderHeading(doc, element, indent, store, renderElement);
|
|
919
1619
|
break;
|
|
920
|
-
case
|
|
921
|
-
|
|
1620
|
+
case "paragraph":
|
|
1621
|
+
renderParagraph(doc, element, indent, store, renderElement);
|
|
922
1622
|
break;
|
|
923
|
-
case
|
|
924
|
-
|
|
1623
|
+
case "list":
|
|
1624
|
+
renderList(doc, element, indentLevel, store, renderElement);
|
|
925
1625
|
break;
|
|
926
|
-
case
|
|
927
|
-
|
|
1626
|
+
case "list_item":
|
|
1627
|
+
renderListItem(doc, element, indentLevel, store, renderElement, start, ordered);
|
|
928
1628
|
break;
|
|
929
|
-
case
|
|
930
|
-
|
|
1629
|
+
case "hr":
|
|
1630
|
+
renderHR(doc, store);
|
|
931
1631
|
break;
|
|
932
|
-
case
|
|
933
|
-
|
|
1632
|
+
case "code":
|
|
1633
|
+
renderCodeBlock(doc, element, indentLevel, store);
|
|
934
1634
|
break;
|
|
935
|
-
case
|
|
936
|
-
case
|
|
937
|
-
case
|
|
938
|
-
|
|
1635
|
+
case "strong":
|
|
1636
|
+
case "em":
|
|
1637
|
+
case "codespan":
|
|
1638
|
+
renderInlineText(doc, element, indent, store);
|
|
939
1639
|
break;
|
|
940
|
-
case
|
|
941
|
-
|
|
1640
|
+
case "link":
|
|
1641
|
+
renderLink(doc, element, indent, store);
|
|
942
1642
|
break;
|
|
943
|
-
case
|
|
944
|
-
|
|
1643
|
+
case "blockquote":
|
|
1644
|
+
renderBlockquote(doc, element, indentLevel, store, renderElement);
|
|
945
1645
|
break;
|
|
946
|
-
case
|
|
947
|
-
|
|
1646
|
+
case "image":
|
|
1647
|
+
renderImage(doc, element, indentLevel, store);
|
|
948
1648
|
break;
|
|
949
|
-
case
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1649
|
+
case "br": {
|
|
1650
|
+
store.updateX(validOptions.page.xpading, "set");
|
|
1651
|
+
const brHeight = getCharHight(doc) * validOptions.page.defaultLineHeightFactor;
|
|
1652
|
+
if (store.Y + brHeight > validOptions.page.maxContentHeight) HandlePageBreaks(doc, store);
|
|
1653
|
+
else store.updateY(brHeight, "add");
|
|
1654
|
+
store.recordContentY();
|
|
953
1655
|
break;
|
|
954
1656
|
}
|
|
955
|
-
case
|
|
956
|
-
|
|
1657
|
+
case "table":
|
|
1658
|
+
renderTable(doc, element, indentLevel, store);
|
|
957
1659
|
break;
|
|
958
|
-
case
|
|
959
|
-
case
|
|
960
|
-
|
|
1660
|
+
case "raw":
|
|
1661
|
+
case "text":
|
|
1662
|
+
renderRawItem(doc, element, indentLevel, store, hasRawBullet, renderElement, start, ordered, validOptions.content?.textAlignment === "justify");
|
|
961
1663
|
break;
|
|
962
1664
|
default:
|
|
963
|
-
console.warn(`Warning: Unsupported element type encountered: ${
|
|
1665
|
+
console.warn(`Warning: Unsupported element type encountered: ${element.type}.
|
|
964
1666
|
If you believe this element type should be supported, please create an issue at:
|
|
965
1667
|
https://github.com/JeelGajera/jspdf-md-renderer/issues
|
|
966
1668
|
with details of the element and expected behavior. Thanks for helping to improve this library!`);
|
|
967
1669
|
break;
|
|
968
1670
|
}
|
|
969
1671
|
};
|
|
970
|
-
for (
|
|
971
|
-
|
|
1672
|
+
for (const item of parsedElements) renderElement(item, 0, store);
|
|
1673
|
+
validOptions.endCursorYHandler(store.Y);
|
|
972
1674
|
};
|
|
973
1675
|
//#endregion
|
|
974
|
-
export {
|
|
1676
|
+
export { MdTextParser, MdTextRender };
|