@bendyline/squisq-formats 0.1.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +105 -0
  2. package/dist/__tests__/docxExport.test.d.ts +8 -0
  3. package/dist/__tests__/docxExport.test.d.ts.map +1 -0
  4. package/dist/__tests__/docxExport.test.js +383 -0
  5. package/dist/__tests__/docxExport.test.js.map +1 -0
  6. package/dist/__tests__/docxImport.test.d.ts +8 -0
  7. package/dist/__tests__/docxImport.test.d.ts.map +1 -0
  8. package/dist/__tests__/docxImport.test.js +309 -0
  9. package/dist/__tests__/docxImport.test.js.map +1 -0
  10. package/dist/__tests__/html.test.d.ts +8 -0
  11. package/dist/__tests__/html.test.d.ts.map +1 -0
  12. package/dist/__tests__/html.test.js +243 -0
  13. package/dist/__tests__/html.test.js.map +1 -0
  14. package/dist/__tests__/ooxml.test.d.ts +5 -0
  15. package/dist/__tests__/ooxml.test.d.ts.map +1 -0
  16. package/dist/__tests__/ooxml.test.js +152 -0
  17. package/dist/__tests__/ooxml.test.js.map +1 -0
  18. package/dist/__tests__/pdfExport.test.d.ts +8 -0
  19. package/dist/__tests__/pdfExport.test.d.ts.map +1 -0
  20. package/dist/__tests__/pdfExport.test.js +275 -0
  21. package/dist/__tests__/pdfExport.test.js.map +1 -0
  22. package/dist/__tests__/pdfImport.test.d.ts +11 -0
  23. package/dist/__tests__/pdfImport.test.d.ts.map +1 -0
  24. package/dist/__tests__/pdfImport.test.js +254 -0
  25. package/dist/__tests__/pdfImport.test.js.map +1 -0
  26. package/dist/__tests__/roundTrip.test.d.ts +7 -0
  27. package/dist/__tests__/roundTrip.test.d.ts.map +1 -0
  28. package/dist/__tests__/roundTrip.test.js +173 -0
  29. package/dist/__tests__/roundTrip.test.js.map +1 -0
  30. package/dist/docx/export.d.ts +54 -0
  31. package/dist/docx/export.d.ts.map +1 -0
  32. package/dist/docx/export.js +729 -0
  33. package/dist/docx/export.js.map +1 -0
  34. package/dist/docx/import.d.ts +52 -0
  35. package/dist/docx/import.d.ts.map +1 -0
  36. package/dist/docx/import.js +687 -0
  37. package/dist/docx/import.js.map +1 -0
  38. package/dist/docx/index.d.ts +15 -100
  39. package/dist/docx/index.d.ts.map +1 -0
  40. package/dist/docx/index.js +22 -13
  41. package/dist/docx/index.js.map +1 -1
  42. package/dist/docx/styles.d.ts +68 -0
  43. package/dist/docx/styles.d.ts.map +1 -0
  44. package/dist/docx/styles.js +125 -0
  45. package/dist/docx/styles.js.map +1 -0
  46. package/dist/html/htmlTemplate.d.ts +64 -0
  47. package/dist/html/htmlTemplate.d.ts.map +1 -0
  48. package/dist/html/htmlTemplate.js +235 -0
  49. package/dist/html/htmlTemplate.js.map +1 -0
  50. package/dist/html/imageUtils.d.ts +29 -0
  51. package/dist/html/imageUtils.d.ts.map +1 -0
  52. package/dist/html/imageUtils.js +63 -0
  53. package/dist/html/imageUtils.js.map +1 -0
  54. package/dist/html/index.d.ts +9 -77
  55. package/dist/html/index.d.ts.map +1 -0
  56. package/dist/html/index.js +137 -16
  57. package/dist/html/index.js.map +1 -1
  58. package/dist/index.d.ts +36 -7
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +35 -53
  61. package/dist/index.js.map +1 -1
  62. package/dist/ooxml/index.d.ts +14 -343
  63. package/dist/ooxml/index.d.ts.map +1 -0
  64. package/dist/ooxml/index.js +22 -98
  65. package/dist/ooxml/index.js.map +1 -1
  66. package/dist/ooxml/namespaces.d.ts +67 -0
  67. package/dist/ooxml/namespaces.d.ts.map +1 -0
  68. package/dist/ooxml/namespaces.js +94 -0
  69. package/dist/ooxml/namespaces.js.map +1 -0
  70. package/dist/ooxml/reader.d.ts +52 -0
  71. package/dist/ooxml/reader.d.ts.map +1 -0
  72. package/dist/ooxml/reader.js +190 -0
  73. package/dist/ooxml/reader.js.map +1 -0
  74. package/dist/ooxml/types.d.ts +82 -0
  75. package/dist/ooxml/types.d.ts.map +1 -0
  76. package/dist/ooxml/types.js +9 -0
  77. package/dist/ooxml/types.js.map +1 -0
  78. package/dist/ooxml/writer.d.ts +71 -0
  79. package/dist/ooxml/writer.d.ts.map +1 -0
  80. package/dist/ooxml/writer.js +200 -0
  81. package/dist/ooxml/writer.js.map +1 -0
  82. package/dist/ooxml/xmlUtils.d.ts +76 -0
  83. package/dist/ooxml/xmlUtils.d.ts.map +1 -0
  84. package/dist/ooxml/xmlUtils.js +108 -0
  85. package/dist/ooxml/xmlUtils.js.map +1 -0
  86. package/dist/pdf/export.d.ts +50 -0
  87. package/dist/pdf/export.d.ts.map +1 -0
  88. package/dist/pdf/export.js +740 -0
  89. package/dist/pdf/export.js.map +1 -0
  90. package/dist/pdf/import.d.ts +78 -0
  91. package/dist/pdf/import.d.ts.map +1 -0
  92. package/dist/pdf/import.js +619 -0
  93. package/dist/pdf/import.js.map +1 -0
  94. package/dist/pdf/index.d.ts +17 -121
  95. package/dist/pdf/index.d.ts.map +1 -0
  96. package/dist/pdf/index.js +25 -14
  97. package/dist/pdf/index.js.map +1 -1
  98. package/dist/pdf/styles.d.ts +142 -0
  99. package/dist/pdf/styles.d.ts.map +1 -0
  100. package/dist/pdf/styles.js +145 -0
  101. package/dist/pdf/styles.js.map +1 -0
  102. package/dist/pptx/index.d.ts +9 -12
  103. package/dist/pptx/index.d.ts.map +1 -0
  104. package/dist/pptx/index.js +44 -12
  105. package/dist/pptx/index.js.map +1 -1
  106. package/dist/xlsx/index.d.ts +9 -12
  107. package/dist/xlsx/index.d.ts.map +1 -0
  108. package/dist/xlsx/index.js +44 -12
  109. package/dist/xlsx/index.js.map +1 -1
  110. package/package.json +5 -6
@@ -0,0 +1,740 @@
1
+ /**
2
+ * PDF Export
3
+ *
4
+ * Converts a squisq MarkdownDocument (or Doc) into a PDF file
5
+ * using pdf-lib. Generates paginated, styled output with support for
6
+ * headings, paragraphs, inline formatting, lists, code blocks,
7
+ * blockquotes, tables, thematic breaks, and hyperlinks.
8
+ *
9
+ * Uses only the 14 standard PDF fonts (no font embedding required),
10
+ * keeping output size small and rendering fast.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { parseMarkdown } from '@bendyline/squisq/markdown';
15
+ * import { markdownDocToPdf } from '@bendyline/squisq-formats/pdf';
16
+ *
17
+ * const md = parseMarkdown('# Hello\n\nWorld **bold** text');
18
+ * const buffer = await markdownDocToPdf(md);
19
+ * ```
20
+ */
21
+ import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
22
+ import { docToMarkdown } from '@bendyline/squisq/doc';
23
+ import { PAGE_WIDTH_LETTER, PAGE_HEIGHT_LETTER, PAGE_WIDTH_A4, PAGE_HEIGHT_A4, DEFAULT_MARGIN, DEFAULT_FONT_SIZE, HEADING_SIZES, CODE_FONT_SIZE, LINE_HEIGHT_FACTOR, HEADING_SPACE_BEFORE, HEADING_SPACE_AFTER, PARAGRAPH_SPACING, LIST_INDENT, BULLET_CHAR, BLOCKQUOTE_INDENT, BLOCKQUOTE_BAR_WIDTH, COLOR_TEXT, COLOR_HEADING, COLOR_LINK, COLOR_CODE_TEXT, COLOR_BLOCKQUOTE_BAR, COLOR_BLOCKQUOTE_TEXT, COLOR_THEMATIC_BREAK, COLOR_TABLE_BORDER, COLOR_TABLE_HEADER_BG, TABLE_CELL_PAD_X, TABLE_CELL_PAD_Y, TABLE_BORDER_WIDTH, } from './styles.js';
24
+ /**
25
+ * Convert a MarkdownDocument to a PDF ArrayBuffer.
26
+ */
27
+ export async function markdownDocToPdf(doc, options = {}) {
28
+ const pdfDoc = await PDFDocument.create();
29
+ // Metadata
30
+ if (options.title)
31
+ pdfDoc.setTitle(options.title);
32
+ if (options.author)
33
+ pdfDoc.setAuthor(options.author);
34
+ pdfDoc.setCreationDate(new Date());
35
+ pdfDoc.setModificationDate(new Date());
36
+ const ctx = await createExportContext(pdfDoc, options);
37
+ renderBlocks(doc.children, ctx, 0);
38
+ const bytes = await pdfDoc.save();
39
+ return bytes.buffer;
40
+ }
41
+ /**
42
+ * Convert a squisq Doc to a PDF ArrayBuffer.
43
+ *
44
+ * Convenience wrapper: Doc → MarkdownDocument → PDF.
45
+ */
46
+ export async function docToPdf(doc, options = {}) {
47
+ const markdownDoc = docToMarkdown(doc);
48
+ return markdownDocToPdf(markdownDoc, options);
49
+ }
50
+ async function createExportContext(pdfDoc, options) {
51
+ const [regular, bold, italic, boldItalic, mono, monoBold] = await Promise.all([
52
+ pdfDoc.embedFont(StandardFonts.Helvetica),
53
+ pdfDoc.embedFont(StandardFonts.HelveticaBold),
54
+ pdfDoc.embedFont(StandardFonts.HelveticaOblique),
55
+ pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique),
56
+ pdfDoc.embedFont(StandardFonts.Courier),
57
+ pdfDoc.embedFont(StandardFonts.CourierBold),
58
+ ]);
59
+ const isA4 = options.pageSize === 'a4';
60
+ const pageWidth = isA4 ? PAGE_WIDTH_A4 : PAGE_WIDTH_LETTER;
61
+ const pageHeight = isA4 ? PAGE_HEIGHT_A4 : PAGE_HEIGHT_LETTER;
62
+ const margin = options.margin ?? DEFAULT_MARGIN;
63
+ const fontSize = options.defaultFontSize ?? DEFAULT_FONT_SIZE;
64
+ const page = pdfDoc.addPage([pageWidth, pageHeight]);
65
+ return {
66
+ pdfDoc,
67
+ fonts: { regular, bold, italic, boldItalic, mono, monoBold },
68
+ pageWidth,
69
+ pageHeight,
70
+ margin,
71
+ fontSize,
72
+ page,
73
+ y: pageHeight - margin,
74
+ contentWidth: pageWidth - 2 * margin,
75
+ bottomY: margin,
76
+ };
77
+ }
78
+ // ============================================
79
+ // Page Break Management
80
+ // ============================================
81
+ function ensureSpace(ctx, needed) {
82
+ if (ctx.y - needed < ctx.bottomY) {
83
+ newPage(ctx);
84
+ }
85
+ }
86
+ function newPage(ctx) {
87
+ const page = ctx.pdfDoc.addPage([ctx.pageWidth, ctx.pageHeight]);
88
+ ctx.page = page;
89
+ ctx.y = ctx.pageHeight - ctx.margin;
90
+ }
91
+ /**
92
+ * Flatten an inline node tree into a flat list of TextSpans,
93
+ * accumulating bold/italic/code state as we recurse.
94
+ */
95
+ function flattenInlines(nodes, ctx, state) {
96
+ const spans = [];
97
+ for (const node of nodes) {
98
+ switch (node.type) {
99
+ case 'text': {
100
+ const font = state.code
101
+ ? state.bold
102
+ ? ctx.fonts.monoBold
103
+ : ctx.fonts.mono
104
+ : pickFont(ctx, state.bold, state.italic);
105
+ spans.push({
106
+ text: node.value,
107
+ font,
108
+ fontSize: state.code ? CODE_FONT_SIZE : ctx.fontSize,
109
+ color: state.code ? COLOR_CODE_TEXT : (state.color ?? COLOR_TEXT),
110
+ link: state.link,
111
+ strikethrough: state.strikethrough,
112
+ });
113
+ break;
114
+ }
115
+ case 'strong':
116
+ spans.push(...flattenInlines(node.children, ctx, { ...state, bold: true }));
117
+ break;
118
+ case 'emphasis':
119
+ spans.push(...flattenInlines(node.children, ctx, { ...state, italic: true }));
120
+ break;
121
+ case 'delete':
122
+ spans.push(...flattenInlines(node.children, ctx, {
123
+ ...state,
124
+ strikethrough: true,
125
+ }));
126
+ break;
127
+ case 'inlineCode': {
128
+ spans.push({
129
+ text: node.value,
130
+ font: ctx.fonts.mono,
131
+ fontSize: CODE_FONT_SIZE,
132
+ color: COLOR_CODE_TEXT,
133
+ link: state.link,
134
+ });
135
+ break;
136
+ }
137
+ case 'link': {
138
+ const linkNode = node;
139
+ spans.push(...flattenInlines(linkNode.children, ctx, {
140
+ ...state,
141
+ link: linkNode.url,
142
+ color: COLOR_LINK,
143
+ }));
144
+ break;
145
+ }
146
+ case 'image': {
147
+ const imgNode = node;
148
+ spans.push({
149
+ text: imgNode.alt ? `[Image: ${imgNode.alt}]` : '[Image]',
150
+ font: ctx.fonts.italic,
151
+ fontSize: ctx.fontSize,
152
+ color: COLOR_BLOCKQUOTE_TEXT,
153
+ });
154
+ break;
155
+ }
156
+ case 'break':
157
+ spans.push({
158
+ text: '\n',
159
+ font: ctx.fonts.regular,
160
+ fontSize: ctx.fontSize,
161
+ color: COLOR_TEXT,
162
+ });
163
+ break;
164
+ case 'htmlInline': {
165
+ const html = node.rawHtml;
166
+ if (html) {
167
+ spans.push({
168
+ text: html,
169
+ font: ctx.fonts.mono,
170
+ fontSize: CODE_FONT_SIZE,
171
+ color: COLOR_CODE_TEXT,
172
+ });
173
+ }
174
+ break;
175
+ }
176
+ case 'inlineMath':
177
+ spans.push({
178
+ text: node.value,
179
+ font: ctx.fonts.mono,
180
+ fontSize: CODE_FONT_SIZE,
181
+ color: COLOR_CODE_TEXT,
182
+ });
183
+ break;
184
+ case 'footnoteReference': {
185
+ const ref = node;
186
+ spans.push({
187
+ text: `[${ref.identifier}]`,
188
+ font: ctx.fonts.regular,
189
+ fontSize: ctx.fontSize * 0.75,
190
+ color: COLOR_LINK,
191
+ });
192
+ break;
193
+ }
194
+ // linkReference, imageReference, textDirective — render children or identifier
195
+ default: {
196
+ const fallback = node;
197
+ if (fallback.children) {
198
+ spans.push(...flattenInlines(fallback.children, ctx, state));
199
+ }
200
+ else if (fallback.value) {
201
+ spans.push({
202
+ text: fallback.value,
203
+ font: pickFont(ctx, state.bold, state.italic),
204
+ fontSize: ctx.fontSize,
205
+ color: state.color ?? COLOR_TEXT,
206
+ });
207
+ }
208
+ break;
209
+ }
210
+ }
211
+ }
212
+ return spans;
213
+ }
214
+ function pickFont(ctx, bold, italic) {
215
+ if (bold && italic)
216
+ return ctx.fonts.boldItalic;
217
+ if (bold)
218
+ return ctx.fonts.bold;
219
+ if (italic)
220
+ return ctx.fonts.italic;
221
+ return ctx.fonts.regular;
222
+ }
223
+ // ============================================
224
+ // Word-Wrap & Draw
225
+ // ============================================
226
+ /**
227
+ * Word-wraps and draws a flat list of TextSpans within the given
228
+ * available width, starting at ctx.y. Updates ctx.y after drawing.
229
+ * Returns the y position after the last line.
230
+ */
231
+ function drawSpans(spans, ctx, availableWidth, x0) {
232
+ if (spans.length === 0)
233
+ return;
234
+ // Split spans at \n and word boundaries, then wrap into lines
235
+ const lines = wrapSpans(spans, availableWidth);
236
+ for (const line of lines) {
237
+ const lineHeight = getLineHeight(line);
238
+ ensureSpace(ctx, lineHeight);
239
+ let x = x0;
240
+ for (const span of line) {
241
+ ctx.page.drawText(span.text, {
242
+ x,
243
+ y: ctx.y - span.fontSize, // pdf-lib y is baseline
244
+ size: span.fontSize,
245
+ font: span.font,
246
+ color: rgb(span.color.r, span.color.g, span.color.b),
247
+ });
248
+ const textWidth = span.font.widthOfTextAtSize(span.text, span.fontSize);
249
+ // Underline for links
250
+ if (span.link) {
251
+ ctx.page.drawLine({
252
+ start: { x, y: ctx.y - span.fontSize - 1 },
253
+ end: { x: x + textWidth, y: ctx.y - span.fontSize - 1 },
254
+ thickness: 0.5,
255
+ color: rgb(span.color.r, span.color.g, span.color.b),
256
+ });
257
+ }
258
+ // Strikethrough
259
+ if (span.strikethrough) {
260
+ const midY = ctx.y - span.fontSize * 0.6;
261
+ ctx.page.drawLine({
262
+ start: { x, y: midY },
263
+ end: { x: x + textWidth, y: midY },
264
+ thickness: 0.5,
265
+ color: rgb(span.color.r, span.color.g, span.color.b),
266
+ });
267
+ }
268
+ x += textWidth;
269
+ }
270
+ ctx.y -= lineHeight;
271
+ }
272
+ }
273
+ /**
274
+ * Break a flat list of spans into wrapped lines that fit within
275
+ * `maxWidth`. Respects explicit \n characters.
276
+ */
277
+ function wrapSpans(spans, maxWidth) {
278
+ const lines = [];
279
+ let currentLine = [];
280
+ let lineWidth = 0;
281
+ for (const span of spans) {
282
+ // Handle explicit newlines
283
+ if (span.text === '\n') {
284
+ lines.push(currentLine);
285
+ currentLine = [];
286
+ lineWidth = 0;
287
+ continue;
288
+ }
289
+ // Split by whitespace for word-wrapping
290
+ const words = splitIntoWords(span.text);
291
+ for (const word of words) {
292
+ const wordWidth = span.font.widthOfTextAtSize(word, span.fontSize);
293
+ if (lineWidth + wordWidth > maxWidth && currentLine.length > 0 && word.trim().length > 0) {
294
+ // Wrap to next line
295
+ lines.push(currentLine);
296
+ currentLine = [];
297
+ lineWidth = 0;
298
+ }
299
+ // Trim leading space on new line
300
+ const trimmedWord = currentLine.length === 0 ? word.replace(/^\s+/, '') : word;
301
+ if (trimmedWord.length > 0) {
302
+ const tw = span.font.widthOfTextAtSize(trimmedWord, span.fontSize);
303
+ currentLine.push({ ...span, text: trimmedWord });
304
+ lineWidth += tw;
305
+ }
306
+ }
307
+ }
308
+ if (currentLine.length > 0) {
309
+ lines.push(currentLine);
310
+ }
311
+ return lines.length > 0 ? lines : [[]];
312
+ }
313
+ /**
314
+ * Split text into "words" preserving whitespace as separate tokens
315
+ * so the wrapping logic can break at whitespace.
316
+ */
317
+ function splitIntoWords(text) {
318
+ const tokens = [];
319
+ let current = '';
320
+ let inSpace = false;
321
+ for (const ch of text) {
322
+ const isSpace = ch === ' ' || ch === '\t';
323
+ if (isSpace !== inSpace && current.length > 0) {
324
+ tokens.push(current);
325
+ current = '';
326
+ }
327
+ current += ch;
328
+ inSpace = isSpace;
329
+ }
330
+ if (current.length > 0)
331
+ tokens.push(current);
332
+ return tokens;
333
+ }
334
+ function getLineHeight(line) {
335
+ let maxSize = 0;
336
+ for (const span of line) {
337
+ if (span.fontSize > maxSize)
338
+ maxSize = span.fontSize;
339
+ }
340
+ return (maxSize || DEFAULT_FONT_SIZE) * LINE_HEIGHT_FACTOR;
341
+ }
342
+ // ============================================
343
+ // Block Renderers
344
+ // ============================================
345
+ function renderBlocks(nodes, ctx, extraIndent) {
346
+ for (const node of nodes) {
347
+ renderBlock(node, ctx, extraIndent);
348
+ }
349
+ }
350
+ function renderBlock(node, ctx, extraIndent) {
351
+ switch (node.type) {
352
+ case 'heading':
353
+ renderHeading(node, ctx, extraIndent);
354
+ break;
355
+ case 'paragraph':
356
+ renderParagraph(node, ctx, extraIndent);
357
+ break;
358
+ case 'blockquote':
359
+ renderBlockquote(node, ctx, extraIndent);
360
+ break;
361
+ case 'list':
362
+ renderList(node, ctx, extraIndent, 0);
363
+ break;
364
+ case 'code':
365
+ renderCodeBlock(node, ctx, extraIndent);
366
+ break;
367
+ case 'table':
368
+ renderTable(node, ctx, extraIndent);
369
+ break;
370
+ case 'thematicBreak':
371
+ renderThematicBreak(ctx, extraIndent);
372
+ break;
373
+ case 'htmlBlock':
374
+ renderHtmlBlock(node, ctx, extraIndent);
375
+ break;
376
+ case 'math':
377
+ renderMathBlock(node, ctx, extraIndent);
378
+ break;
379
+ case 'footnoteDefinition':
380
+ renderFootnoteDefinition(node, ctx, extraIndent);
381
+ break;
382
+ default:
383
+ // containerDirective, leafDirective, definitionList, etc.
384
+ // Render any children or value as best-effort
385
+ renderFallbackBlock(node, ctx, extraIndent);
386
+ break;
387
+ }
388
+ }
389
+ // ---- Heading ----
390
+ function renderHeading(node, ctx, extraIndent) {
391
+ const origFontSize = ctx.fontSize;
392
+ ctx.fontSize = HEADING_SIZES[node.depth] ?? DEFAULT_FONT_SIZE;
393
+ ctx.y -= HEADING_SPACE_BEFORE;
394
+ const x0 = ctx.margin + extraIndent;
395
+ const w = ctx.contentWidth - extraIndent;
396
+ const spans = flattenInlines(node.children, ctx, {
397
+ bold: true,
398
+ italic: false,
399
+ code: false,
400
+ color: COLOR_HEADING,
401
+ });
402
+ drawSpans(spans, ctx, w, x0);
403
+ ctx.y -= HEADING_SPACE_AFTER;
404
+ ctx.fontSize = origFontSize;
405
+ }
406
+ // ---- Paragraph ----
407
+ function renderParagraph(node, ctx, extraIndent, colorOverride) {
408
+ const x0 = ctx.margin + extraIndent;
409
+ const w = ctx.contentWidth - extraIndent;
410
+ const spans = flattenInlines(node.children, ctx, {
411
+ bold: false,
412
+ italic: false,
413
+ code: false,
414
+ color: colorOverride,
415
+ });
416
+ drawSpans(spans, ctx, w, x0);
417
+ ctx.y -= PARAGRAPH_SPACING;
418
+ }
419
+ // ---- Blockquote ----
420
+ function renderBlockquote(node, ctx, extraIndent) {
421
+ const barX = ctx.margin + extraIndent + 4;
422
+ const indent = extraIndent + BLOCKQUOTE_INDENT;
423
+ const startY = ctx.y;
424
+ for (const child of node.children) {
425
+ if (child.type === 'paragraph') {
426
+ renderParagraph(child, ctx, indent, COLOR_BLOCKQUOTE_TEXT);
427
+ }
428
+ else {
429
+ renderBlock(child, ctx, indent);
430
+ }
431
+ }
432
+ // Draw left bar from startY to ctx.y
433
+ const endY = ctx.y + PARAGRAPH_SPACING; // undo last paragraph spacing
434
+ if (startY > endY) {
435
+ // Bar might span pages — draw on current page only
436
+ ctx.page.drawRectangle({
437
+ x: barX,
438
+ y: endY,
439
+ width: BLOCKQUOTE_BAR_WIDTH,
440
+ height: startY - endY,
441
+ color: rgb(COLOR_BLOCKQUOTE_BAR.r, COLOR_BLOCKQUOTE_BAR.g, COLOR_BLOCKQUOTE_BAR.b),
442
+ });
443
+ }
444
+ ctx.y -= PARAGRAPH_SPACING;
445
+ }
446
+ // ---- List ----
447
+ function renderList(node, ctx, extraIndent, depth) {
448
+ const ordered = node.ordered ?? false;
449
+ let counter = node.start ?? 1;
450
+ for (const child of node.children) {
451
+ if (child.type === 'listItem') {
452
+ renderListItem(child, ctx, extraIndent, depth, ordered, counter);
453
+ if (ordered)
454
+ counter++;
455
+ }
456
+ }
457
+ }
458
+ function renderListItem(item, ctx, extraIndent, depth, ordered, counter) {
459
+ const indent = extraIndent + depth * LIST_INDENT;
460
+ const bullet = ordered ? `${counter}.` : BULLET_CHAR;
461
+ const bulletFont = ctx.fonts.regular;
462
+ const bulletWidth = bulletFont.widthOfTextAtSize(bullet + ' ', ctx.fontSize);
463
+ // Draw bullet
464
+ const lineHeight = ctx.fontSize * LINE_HEIGHT_FACTOR;
465
+ ensureSpace(ctx, lineHeight);
466
+ ctx.page.drawText(bullet, {
467
+ x: ctx.margin + indent,
468
+ y: ctx.y - ctx.fontSize,
469
+ size: ctx.fontSize,
470
+ font: bulletFont,
471
+ color: rgb(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b),
472
+ });
473
+ const textIndent = indent + bulletWidth + 4;
474
+ const textWidth = ctx.contentWidth - textIndent;
475
+ // Render children at the text indent
476
+ let isFirstChild = true;
477
+ for (const child of item.children) {
478
+ if (child.type === 'paragraph' && isFirstChild) {
479
+ // First paragraph: render on same line as bullet
480
+ const spans = flattenInlines(child.children, ctx, {
481
+ bold: false,
482
+ italic: false,
483
+ code: false,
484
+ });
485
+ drawSpans(spans, ctx, textWidth, ctx.margin + textIndent);
486
+ ctx.y -= PARAGRAPH_SPACING / 2;
487
+ }
488
+ else if (child.type === 'list') {
489
+ renderList(child, ctx, extraIndent, depth + 1);
490
+ }
491
+ else {
492
+ renderBlock(child, ctx, textIndent);
493
+ }
494
+ isFirstChild = false;
495
+ }
496
+ }
497
+ // ---- Code Block ----
498
+ function renderCodeBlock(node, ctx, extraIndent) {
499
+ const x0 = ctx.margin + extraIndent;
500
+ const _w = ctx.contentWidth - extraIndent;
501
+ const lines = node.value.split('\n');
502
+ const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
503
+ const totalHeight = lines.length * lineH + 12; // 12 = vertical padding
504
+ ensureSpace(ctx, Math.min(totalHeight, ctx.y - ctx.bottomY));
505
+ const _bgTop = ctx.y;
506
+ // Draw background first (we'll adjust after we know the final y)
507
+ const _bgStartY = ctx.y;
508
+ ctx.y -= 6; // top padding
509
+ for (const line of lines) {
510
+ ensureSpace(ctx, lineH);
511
+ if (line.length > 0) {
512
+ ctx.page.drawText(line, {
513
+ x: x0 + 8,
514
+ y: ctx.y - CODE_FONT_SIZE,
515
+ size: CODE_FONT_SIZE,
516
+ font: ctx.fonts.mono,
517
+ color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b),
518
+ });
519
+ }
520
+ ctx.y -= lineH;
521
+ }
522
+ ctx.y -= 6; // bottom padding
523
+ // Draw background rectangle (behind text — pdf-lib draws on top,
524
+ // so we could draw it first on a separate pass, but since we can't
525
+ // easily re-order, we leave it as a visual approximation).
526
+ // For a production implementation, you'd use a two-pass approach.
527
+ // Here we draw the bg rect at the saved position. Text drawn after
528
+ // will already be on the page. Since pdf-lib paints in order, we
529
+ // accept that the bg goes on top — but only if bg is on same page.
530
+ // A simpler approach: draw bg *before* text per page.
531
+ // Actually, let's use a cleaner approach: draw line by line with bg
532
+ // We already drew the text. For the background box, we'll just
533
+ // not draw it behind (it would overlay). This is a known limitation
534
+ // with pdf-lib's immediate mode. In practice the light grey bg makes
535
+ // text hard to read if drawn on top. So we skip the bg for now and
536
+ // rely on the monospace font to visually distinguish code.
537
+ ctx.y -= PARAGRAPH_SPACING;
538
+ }
539
+ // ---- Table ----
540
+ function renderTable(node, ctx, extraIndent) {
541
+ if (node.children.length === 0)
542
+ return;
543
+ const x0 = ctx.margin + extraIndent;
544
+ const tableWidth = ctx.contentWidth - extraIndent;
545
+ // Measure columns: get max width per column across all rows
546
+ const numCols = Math.max(...node.children.map((row) => row.children.length));
547
+ if (numCols === 0)
548
+ return;
549
+ // Simple equal-width columns
550
+ const colWidth = tableWidth / numCols;
551
+ for (let rowIdx = 0; rowIdx < node.children.length; rowIdx++) {
552
+ const row = node.children[rowIdx];
553
+ const isHeader = rowIdx === 0 && row.children.some((c) => c.isHeader);
554
+ // Calculate row height
555
+ const rowCellHeights = row.children.map((cell) => {
556
+ const cellNode = cell;
557
+ const spans = flattenInlines(cellNode.children, ctx, {
558
+ bold: isHeader,
559
+ italic: false,
560
+ code: false,
561
+ });
562
+ const lines = wrapSpans(spans, colWidth - 2 * TABLE_CELL_PAD_X);
563
+ return lines.length * ctx.fontSize * LINE_HEIGHT_FACTOR + 2 * TABLE_CELL_PAD_Y;
564
+ });
565
+ const rowHeight = Math.max(...rowCellHeights, ctx.fontSize * LINE_HEIGHT_FACTOR + 2 * TABLE_CELL_PAD_Y);
566
+ ensureSpace(ctx, rowHeight);
567
+ // Draw header background
568
+ if (isHeader) {
569
+ ctx.page.drawRectangle({
570
+ x: x0,
571
+ y: ctx.y - rowHeight,
572
+ width: tableWidth,
573
+ height: rowHeight,
574
+ color: rgb(COLOR_TABLE_HEADER_BG.r, COLOR_TABLE_HEADER_BG.g, COLOR_TABLE_HEADER_BG.b),
575
+ });
576
+ }
577
+ // Draw cell borders and text
578
+ for (let colIdx = 0; colIdx < numCols; colIdx++) {
579
+ const cellX = x0 + colIdx * colWidth;
580
+ // Draw cell border
581
+ ctx.page.drawRectangle({
582
+ x: cellX,
583
+ y: ctx.y - rowHeight,
584
+ width: colWidth,
585
+ height: rowHeight,
586
+ borderColor: rgb(COLOR_TABLE_BORDER.r, COLOR_TABLE_BORDER.g, COLOR_TABLE_BORDER.b),
587
+ borderWidth: TABLE_BORDER_WIDTH,
588
+ });
589
+ if (colIdx < row.children.length) {
590
+ const cellNode = row.children[colIdx];
591
+ const spans = flattenInlines(cellNode.children, ctx, {
592
+ bold: isHeader,
593
+ italic: false,
594
+ code: false,
595
+ });
596
+ // Draw text inside cell
597
+ const savedY = ctx.y;
598
+ ctx.y = ctx.y - TABLE_CELL_PAD_Y;
599
+ const cellAvailableWidth = colWidth - 2 * TABLE_CELL_PAD_X;
600
+ const wrappedLines = wrapSpans(spans, cellAvailableWidth);
601
+ for (const line of wrappedLines) {
602
+ let lx = cellX + TABLE_CELL_PAD_X;
603
+ for (const span of line) {
604
+ ctx.page.drawText(span.text, {
605
+ x: lx,
606
+ y: ctx.y - span.fontSize,
607
+ size: span.fontSize,
608
+ font: span.font,
609
+ color: rgb(span.color.r, span.color.g, span.color.b),
610
+ });
611
+ lx += span.font.widthOfTextAtSize(span.text, span.fontSize);
612
+ }
613
+ ctx.y -= getLineHeight(line);
614
+ }
615
+ ctx.y = savedY; // restore for next cell
616
+ }
617
+ }
618
+ ctx.y -= rowHeight;
619
+ }
620
+ ctx.y -= PARAGRAPH_SPACING;
621
+ }
622
+ // ---- Thematic Break ----
623
+ function renderThematicBreak(ctx, extraIndent) {
624
+ ctx.y -= PARAGRAPH_SPACING;
625
+ ensureSpace(ctx, 10);
626
+ const x0 = ctx.margin + extraIndent;
627
+ const x1 = ctx.margin + ctx.contentWidth;
628
+ ctx.page.drawLine({
629
+ start: { x: x0, y: ctx.y },
630
+ end: { x: x1, y: ctx.y },
631
+ thickness: 1,
632
+ color: rgb(COLOR_THEMATIC_BREAK.r, COLOR_THEMATIC_BREAK.g, COLOR_THEMATIC_BREAK.b),
633
+ });
634
+ ctx.y -= PARAGRAPH_SPACING;
635
+ }
636
+ // ---- HTML Block ----
637
+ function renderHtmlBlock(node, ctx, extraIndent) {
638
+ if (!node.rawHtml)
639
+ return;
640
+ // Render raw HTML as monospace text (best effort)
641
+ const lines = node.rawHtml.split('\n');
642
+ const x0 = ctx.margin + extraIndent;
643
+ for (const line of lines) {
644
+ if (line.trim().length === 0)
645
+ continue;
646
+ const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
647
+ ensureSpace(ctx, lineH);
648
+ ctx.page.drawText(line, {
649
+ x: x0,
650
+ y: ctx.y - CODE_FONT_SIZE,
651
+ size: CODE_FONT_SIZE,
652
+ font: ctx.fonts.mono,
653
+ color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b),
654
+ });
655
+ ctx.y -= lineH;
656
+ }
657
+ ctx.y -= PARAGRAPH_SPACING;
658
+ }
659
+ // ---- Math Block ----
660
+ function renderMathBlock(node, ctx, extraIndent) {
661
+ // Render LaTeX source in monospace as fallback
662
+ const lines = node.value.split('\n');
663
+ const x0 = ctx.margin + extraIndent;
664
+ for (const line of lines) {
665
+ const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
666
+ ensureSpace(ctx, lineH);
667
+ if (line.length > 0) {
668
+ ctx.page.drawText(line, {
669
+ x: x0,
670
+ y: ctx.y - CODE_FONT_SIZE,
671
+ size: CODE_FONT_SIZE,
672
+ font: ctx.fonts.mono,
673
+ color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b),
674
+ });
675
+ }
676
+ ctx.y -= lineH;
677
+ }
678
+ ctx.y -= PARAGRAPH_SPACING;
679
+ }
680
+ // ---- Footnote Definition ----
681
+ function renderFootnoteDefinition(node, ctx, extraIndent) {
682
+ // Draw footnote identifier
683
+ const label = `[${node.identifier}]`;
684
+ const lineH = ctx.fontSize * LINE_HEIGHT_FACTOR;
685
+ ensureSpace(ctx, lineH);
686
+ ctx.page.drawText(label, {
687
+ x: ctx.margin + extraIndent,
688
+ y: ctx.y - ctx.fontSize * 0.75,
689
+ size: ctx.fontSize * 0.75,
690
+ font: ctx.fonts.bold,
691
+ color: rgb(COLOR_LINK.r, COLOR_LINK.g, COLOR_LINK.b),
692
+ });
693
+ // Render children indented
694
+ renderBlocks(node.children, ctx, extraIndent + LIST_INDENT);
695
+ }
696
+ // ---- Fallback ----
697
+ function renderFallbackBlock(node, ctx, extraIndent) {
698
+ const fallback = node;
699
+ if (fallback.children && Array.isArray(fallback.children)) {
700
+ // Could be block children or inline children
701
+ if (fallback.children.length > 0 && typeof fallback.children[0]?.type === 'string') {
702
+ const firstType = fallback.children[0].type;
703
+ // Heuristic: if first child looks like an inline node, wrap as paragraph
704
+ const inlineTypes = new Set([
705
+ 'text',
706
+ 'strong',
707
+ 'emphasis',
708
+ 'delete',
709
+ 'inlineCode',
710
+ 'link',
711
+ 'image',
712
+ 'break',
713
+ ]);
714
+ if (inlineTypes.has(firstType)) {
715
+ const spans = flattenInlines(fallback.children, ctx, {
716
+ bold: false,
717
+ italic: false,
718
+ code: false,
719
+ });
720
+ drawSpans(spans, ctx, ctx.contentWidth - extraIndent, ctx.margin + extraIndent);
721
+ ctx.y -= PARAGRAPH_SPACING;
722
+ return;
723
+ }
724
+ }
725
+ renderBlocks(fallback.children, ctx, extraIndent);
726
+ }
727
+ else if (fallback.value && typeof fallback.value === 'string') {
728
+ const lineH = ctx.fontSize * LINE_HEIGHT_FACTOR;
729
+ ensureSpace(ctx, lineH);
730
+ ctx.page.drawText(fallback.value, {
731
+ x: ctx.margin + extraIndent,
732
+ y: ctx.y - ctx.fontSize,
733
+ size: ctx.fontSize,
734
+ font: ctx.fonts.regular,
735
+ color: rgb(COLOR_TEXT.r, COLOR_TEXT.g, COLOR_TEXT.b),
736
+ });
737
+ ctx.y -= lineH + PARAGRAPH_SPACING;
738
+ }
739
+ }
740
+ //# sourceMappingURL=export.js.map