jspdf-md-renderer 3.4.1 → 3.5.0

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