@pyreon/document 0.12.10 → 0.12.12

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.
@@ -304,3 +304,278 @@ describe('builder — add and section', () => {
304
304
  expect(html).toContain('inside section')
305
305
  })
306
306
  })
307
+
308
+ // ─── Document Metadata Pass-Through (PR #197) ──────────────────────────────
309
+ //
310
+ // Verifies that `title`, `author`, and `subject` from a Document
311
+ // node actually reach the rendered output for each format. Before
312
+ // PR #197, only the PDF renderer consumed these fields — DOCX, HTML
313
+ // (author/subject), and Markdown all silently dropped them. The
314
+ // resume builder shipped with metadata that worked in PDF only.
315
+ //
316
+ // These tests are end-to-end: they construct a Document via the
317
+ // public factory, render it to each format, and assert on the
318
+ // rendered string for the metadata. If any renderer regresses, the
319
+ // corresponding test fails immediately.
320
+
321
+ describe('document metadata pass-through (PR #197)', () => {
322
+ const doc = Document({
323
+ title: 'My Report',
324
+ author: 'Alice Smith',
325
+ subject: 'Q4 Sales Analysis',
326
+ children: Page({
327
+ children: [Heading({ children: 'Sales' }), Text({ children: 'Body content' })],
328
+ }),
329
+ })
330
+
331
+ it('HTML renderer emits <title>, <meta name="author">, and <meta name="description">', async () => {
332
+ const html = (await render(doc, 'html')) as string
333
+ expect(html).toContain('<title>My Report</title>')
334
+ expect(html).toContain('<meta name="author" content="Alice Smith">')
335
+ expect(html).toContain('<meta name="description" content="Q4 Sales Analysis">')
336
+ })
337
+
338
+ it('HTML omits author/description meta tags when fields are missing', async () => {
339
+ const minimal = Document({
340
+ title: 'Just a title',
341
+ children: Page({ children: [Text({ children: 'x' })] }),
342
+ })
343
+ const html = (await render(minimal, 'html')) as string
344
+ expect(html).toContain('<title>Just a title</title>')
345
+ expect(html).not.toContain('<meta name="author"')
346
+ expect(html).not.toContain('<meta name="description"')
347
+ })
348
+
349
+ it('HTML escapes metadata to prevent XSS', async () => {
350
+ // Metadata strings come from user data — they MUST be escaped.
351
+ // The HTML renderer uses escapeHtml on every metadata field.
352
+ const xssDoc = Document({
353
+ title: '<script>alert(1)</script>',
354
+ author: '"><script>alert(2)</script>',
355
+ subject: 'Has & < > characters',
356
+ children: Page({ children: [Text({ children: 'x' })] }),
357
+ })
358
+ const html = (await render(xssDoc, 'html')) as string
359
+ // No raw <script> tags
360
+ expect(html).not.toContain('<script>alert(1)</script>')
361
+ expect(html).not.toContain('<script>alert(2)</script>')
362
+ // The escaped form is present
363
+ expect(html).toContain('&lt;script&gt;')
364
+ // The & < > in subject are also escaped
365
+ expect(html).toContain('Has &amp; &lt; &gt; characters')
366
+ })
367
+
368
+ it('Markdown emits YAML frontmatter when metadata is present', async () => {
369
+ const md = (await render(doc, 'md')) as string
370
+ // YAML frontmatter at the very start
371
+ expect(md.startsWith('---\n')).toBe(true)
372
+ expect(md).toContain('title: "My Report"')
373
+ expect(md).toContain('author: "Alice Smith"')
374
+ expect(md).toContain('description: "Q4 Sales Analysis"')
375
+ // Closes the frontmatter block before the body content
376
+ expect(md).toMatch(/---\n# Sales/)
377
+ })
378
+
379
+ it('Markdown omits frontmatter entirely when no metadata is present', async () => {
380
+ const noMeta = Document({
381
+ children: Page({ children: [Heading({ children: 'No metadata here' })] }),
382
+ })
383
+ const md = (await render(noMeta, 'md')) as string
384
+ expect(md.startsWith('---')).toBe(false)
385
+ expect(md).toContain('# No metadata here')
386
+ })
387
+
388
+ it('Markdown escapes quotes in YAML frontmatter strings', async () => {
389
+ // YAML double-quoted scalars need backslash-escaping for "
390
+ // and \. The yamlString helper handles this.
391
+ const escapeDoc = Document({
392
+ title: 'Has "quotes" and \\backslash',
393
+ children: Page({ children: [Text({ children: 'x' })] }),
394
+ })
395
+ const md = (await render(escapeDoc, 'md')) as string
396
+ expect(md).toContain('title: "Has \\"quotes\\" and \\\\backslash"')
397
+ })
398
+
399
+ it('Markdown emits frontmatter even with only ONE metadata field', async () => {
400
+ const titleOnly = Document({
401
+ title: 'Just a title',
402
+ children: Page({ children: [Text({ children: 'x' })] }),
403
+ })
404
+ const md = (await render(titleOnly, 'md')) as string
405
+ expect(md).toContain('---\ntitle: "Just a title"\n---')
406
+ expect(md).not.toContain('author:')
407
+ expect(md).not.toContain('description:')
408
+ })
409
+
410
+ // Long timeout: pdfmake is ~1MB and lazy-loaded on first render() call.
411
+ // The first PDF generation in a fresh worker can take 10+ seconds on
412
+ // slow CI runners (the default 5000ms timeout fails reliably). The
413
+ // existing renderer-coverage tests in renderers-coverage.test.ts don't
414
+ // hit this because they run after the lazy load is already warm.
415
+ it('PDF renderer writes metadata to the /Info dictionary (binary verified)', { timeout: 30_000 }, async () => {
416
+ // End-to-end binary verification. The PDF renderer was already
417
+ // correct (it consumed these fields before PR #197), but we
418
+ // never proved it by actually inspecting a generated PDF.
419
+ //
420
+ // PDFs store document metadata in an /Info dictionary in the
421
+ // trailer, with each field as an indirect object reference:
422
+ //
423
+ // 11 0 obj
424
+ // << /Title 15 0 R /Author 16 0 R /Subject 17 0 R /Keywords 18 0 R ... >>
425
+ // endobj
426
+ // 15 0 obj (My Title) endobj
427
+ // 16 0 obj (Alice Author) endobj
428
+ //
429
+ // We decode the PDF bytes as Latin-1 (the safe encoding for
430
+ // PDF stream payloads) and assert on the structural patterns
431
+ // AND the literal metadata strings. If pdfmake's `info` config
432
+ // ever stops producing these objects, this test catches it.
433
+ const docWithKeywords = Document({
434
+ title: 'My Report',
435
+ author: 'Alice Smith',
436
+ subject: 'Q4 Sales Analysis',
437
+ keywords: ['sales', 'q4', 'report'],
438
+ children: Page({
439
+ children: [Heading({ children: 'Sales' }), Text({ children: 'Body content' })],
440
+ }),
441
+ })
442
+
443
+ const bytes = (await render(docWithKeywords, 'pdf')) as Uint8Array
444
+ expect(bytes).toBeInstanceOf(Uint8Array)
445
+ expect(bytes.byteLength).toBeGreaterThan(0)
446
+
447
+ // Decode the entire PDF as latin-1 — PDFs are mostly ASCII +
448
+ // FlateDecoded streams. The /Info dictionary references and
449
+ // the string literal objects are in the uncompressed portion.
450
+ const text = new TextDecoder('latin1').decode(bytes)
451
+
452
+ // The /Info dictionary references all four metadata fields
453
+ expect(text).toMatch(/\/Title\s+\d+\s+0\s+R/)
454
+ expect(text).toMatch(/\/Author\s+\d+\s+0\s+R/)
455
+ expect(text).toMatch(/\/Subject\s+\d+\s+0\s+R/)
456
+ expect(text).toMatch(/\/Keywords\s+\d+\s+0\s+R/)
457
+
458
+ // The literal metadata values appear in the indirect objects
459
+ expect(text).toContain('(My Report)')
460
+ expect(text).toContain('(Alice Smith)')
461
+ expect(text).toContain('(Q4 Sales Analysis)')
462
+ expect(text).toContain('(sales, q4, report)')
463
+ })
464
+
465
+ // Long timeout: same reason as the PDF test above — the docx
466
+ // library is lazy-loaded on first render() call, plus this test
467
+ // shells out to `unzip` which adds ~50ms of subprocess overhead.
468
+ // Stays well under 30s on every machine I've tested.
469
+ it('DOCX renderer writes metadata to docProps/core.xml (binary verified)', { timeout: 30_000 }, async () => {
470
+ // End-to-end binary verification. PR #197 added the metadata
471
+ // pass-through to the DOCX renderer, but we shouldn't trust
472
+ // the docx library's docs without proof. This test:
473
+ //
474
+ // 1. Generates a real .docx (which is just a zip)
475
+ // 2. Uses the system `unzip` tool to extract docProps/core.xml
476
+ // 3. Asserts the OOXML CoreProperties XML contains the
477
+ // expected dc:* and cp:* elements
478
+ //
479
+ // The OOXML CoreProperties schema:
480
+ // • <dc:title> — title (Dublin Core)
481
+ // • <dc:creator> — author (DC's term for "creator")
482
+ // • <dc:subject> — subject
483
+ // • <cp:keywords> — keywords (OOXML core-properties extension)
484
+ //
485
+ // If the docx library ever stops writing these to core.xml, or
486
+ // if my mapping (`author → creator`, `keywords[] → joined`)
487
+ // breaks, this test catches it.
488
+ const { spawnSync } = await import('node:child_process')
489
+ const { writeFileSync, mkdtempSync, rmSync } = await import('node:fs')
490
+ const { join } = await import('node:path')
491
+ const { tmpdir } = await import('node:os')
492
+
493
+ const docWithKeywords = Document({
494
+ title: 'My Report',
495
+ author: 'Alice Smith',
496
+ subject: 'Q4 Sales Analysis',
497
+ keywords: ['sales', 'q4', 'report'],
498
+ children: Page({
499
+ children: [Heading({ children: 'Sales' }), Text({ children: 'Body content' })],
500
+ }),
501
+ })
502
+
503
+ const bytes = (await render(docWithKeywords, 'docx')) as Uint8Array
504
+ expect(bytes.byteLength).toBeGreaterThan(0)
505
+
506
+ // Write the .docx to a temp dir and unzip docProps/core.xml.
507
+ // `unzip -p` writes a single entry to stdout — no on-disk
508
+ // extraction of the rest of the archive.
509
+ const tmp = mkdtempSync(join(tmpdir(), 'pyreon-docx-test-'))
510
+ try {
511
+ const docxPath = join(tmp, 'out.docx')
512
+ writeFileSync(docxPath, bytes)
513
+
514
+ const result = spawnSync('unzip', ['-p', docxPath, 'docProps/core.xml'], {
515
+ encoding: 'utf8',
516
+ timeout: 10_000,
517
+ })
518
+
519
+ if (result.error || result.status !== 0) {
520
+ throw new Error(
521
+ `Failed to unzip docProps/core.xml: ${result.error?.message ?? result.stderr}. ` +
522
+ `This test requires the system 'unzip' tool on PATH.`,
523
+ )
524
+ }
525
+
526
+ const coreXml = result.stdout
527
+ expect(coreXml).toContain('<dc:title>My Report</dc:title>')
528
+ expect(coreXml).toContain('<dc:creator>Alice Smith</dc:creator>')
529
+ expect(coreXml).toContain('<dc:subject>Q4 Sales Analysis</dc:subject>')
530
+ expect(coreXml).toContain('<cp:keywords>sales, q4, report</cp:keywords>')
531
+
532
+ // Sanity: the namespace declarations are present
533
+ expect(coreXml).toContain('xmlns:dc="http://purl.org/dc/elements/1.1/"')
534
+ expect(coreXml).toContain(
535
+ 'xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"',
536
+ )
537
+ } finally {
538
+ rmSync(tmp, { recursive: true, force: true })
539
+ }
540
+ })
541
+
542
+ // Same long timeout — second DOCX render in the same suite, but
543
+ // each test gets its own worker so the lazy-load cost still applies.
544
+ it('DOCX renderer omits empty metadata elements when fields are missing', { timeout: 30_000 }, async () => {
545
+ // Regression case for the conditional spread pattern in the
546
+ // renderer: a Document with only `title` set should produce a
547
+ // core.xml that has <dc:title> but NOT empty <dc:creator> or
548
+ // <dc:subject> elements. The docx library is well-behaved
549
+ // about omitting unset fields, but if our renderer code ever
550
+ // changed from `?... :{}` spreads to always-include with
551
+ // empty defaults, this test catches it.
552
+ const { spawnSync } = await import('node:child_process')
553
+ const { writeFileSync, mkdtempSync, rmSync } = await import('node:fs')
554
+ const { join } = await import('node:path')
555
+ const { tmpdir } = await import('node:os')
556
+
557
+ const titleOnly = Document({
558
+ title: 'Just a title',
559
+ children: Page({ children: [Text({ children: 'x' })] }),
560
+ })
561
+
562
+ const bytes = (await render(titleOnly, 'docx')) as Uint8Array
563
+ const tmp = mkdtempSync(join(tmpdir(), 'pyreon-docx-test-'))
564
+ try {
565
+ const docxPath = join(tmp, 'out.docx')
566
+ writeFileSync(docxPath, bytes)
567
+ const result = spawnSync('unzip', ['-p', docxPath, 'docProps/core.xml'], {
568
+ encoding: 'utf8',
569
+ })
570
+ const coreXml = result.stdout
571
+
572
+ expect(coreXml).toContain('<dc:title>Just a title</dc:title>')
573
+ // Empty creator/subject/keywords elements should NOT appear
574
+ expect(coreXml).not.toMatch(/<dc:creator><\/dc:creator>/)
575
+ expect(coreXml).not.toMatch(/<dc:subject><\/dc:subject>/)
576
+ expect(coreXml).not.toMatch(/<cp:keywords><\/cp:keywords>/)
577
+ } finally {
578
+ rmSync(tmp, { recursive: true, force: true })
579
+ }
580
+ })
581
+ })
@@ -1 +0,0 @@
1
- {"version":3,"file":"html-B5biprN2.js","names":[],"sources":["../src/renderers/html.ts"],"sourcesContent":["import { sanitizeColor, sanitizeHref, sanitizeImageSrc, sanitizeStyle } from '../sanitize'\nimport type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\nfunction resolveColumn(col: string | TableColumn): TableColumn {\n return typeof col === 'string' ? { header: col } : col\n}\n\nfunction styleStr(styles: Record<string, string | number | undefined>): string {\n const parts: string[] = []\n for (const [k, v] of Object.entries(styles)) {\n if (v != null && v !== '') {\n const prop = k.replace(/([A-Z])/g, '-$1').toLowerCase()\n parts.push(`${prop}:${typeof v === 'number' ? `${v}px` : v}`)\n }\n }\n return parts.length > 0 ? ` style=\"${parts.join(';')}\"` : ''\n}\n\nfunction padStr(\n pad: number | [number, number] | [number, number, number, number] | undefined,\n): string | undefined {\n if (pad == null) return undefined\n if (typeof pad === 'number') return `${pad}px`\n if (pad.length === 2) return `${pad[0]}px ${pad[1]}px`\n return `${pad[0]}px ${pad[1]}px ${pad[2]}px ${pad[3]}px`\n}\n\nfunction renderChild(child: DocChild): string {\n if (typeof child === 'string') return escapeHtml(child)\n return renderNode(child)\n}\n\nfunction renderChildren(children: DocChild[]): string {\n return children.map(renderChild).join('')\n}\n\nfunction renderNode(node: DocNode): string {\n const p = node.props\n\n switch (node.type) {\n case 'document': {\n const lang = (p.language as string) ?? 'en'\n const title = p.title ? `<title>${escapeHtml(p.title as string)}</title>` : ''\n return `<!DOCTYPE html><html lang=\"${lang}\"><head><meta charset=\"utf-8\">${title}<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head><body>${renderChildren(node.children)}</body></html>`\n }\n\n case 'page': {\n const margin = padStr(p.margin as PageMargin)\n return `<div${styleStr({ maxWidth: '800px', margin: margin ?? '0 auto', padding: margin ?? '40px' })}>${renderChildren(node.children)}</div>`\n }\n\n case 'section': {\n const dir = (p.direction as string) ?? 'column'\n return `<div${styleStr({\n display: dir === 'row' ? 'flex' : 'block',\n flexDirection: dir === 'row' ? 'row' : undefined,\n gap: p.gap as number | undefined,\n padding: padStr(p.padding as PageMargin),\n background: sanitizeColor(p.background as string | undefined),\n borderRadius: p.borderRadius as number | undefined,\n })}>${renderChildren(node.children)}</div>`\n }\n\n case 'row':\n return `<div${styleStr({ display: 'flex', gap: p.gap as number | undefined, alignItems: p.align as string | undefined })}>${renderChildren(node.children)}</div>`\n\n case 'column':\n return `<div${styleStr({ flex: p.width ? undefined : '1', width: p.width as string | undefined, textAlign: p.align as string | undefined })}>${renderChildren(node.children)}</div>`\n\n case 'heading': {\n const level = (p.level as number) ?? 1\n const tag = `h${Math.min(Math.max(level, 1), 6)}`\n return `<${tag}${styleStr({ color: sanitizeColor(p.color as string | undefined), textAlign: p.align as string | undefined })}>${renderChildren(node.children)}</${tag}>`\n }\n\n case 'text': {\n return `<p${styleStr({\n fontSize: p.size as number | undefined,\n color: sanitizeColor(p.color as string | undefined),\n fontWeight: p.bold ? 'bold' : undefined,\n fontStyle: p.italic ? 'italic' : undefined,\n textDecoration: p.underline ? 'underline' : p.strikethrough ? 'line-through' : undefined,\n textAlign: p.align as string | undefined,\n lineHeight: p.lineHeight as number | undefined,\n })}>${renderChildren(node.children)}</p>`\n }\n\n case 'link':\n return `<a href=\"${escapeHtml(sanitizeHref(p.href as string))}\"${styleStr({ color: sanitizeColor(p.color as string | undefined) })}>${renderChildren(node.children)}</a>`\n\n case 'image': {\n const alignStyle =\n p.align === 'center'\n ? 'display:block;margin:0 auto'\n : p.align === 'right'\n ? 'display:block;margin-left:auto'\n : ''\n const img = `<img src=\"${escapeHtml(sanitizeImageSrc(p.src as string))}\"${p.width ? ` width=\"${p.width}\"` : ''}${p.height ? ` height=\"${p.height}\"` : ''}${p.alt ? ` alt=\"${escapeHtml(p.alt as string)}\"` : ''}${alignStyle ? ` style=\"${sanitizeStyle(alignStyle)}\"` : ''} />`\n if (p.caption) {\n return `<figure${p.align === 'center' ? ' style=\"text-align:center\"' : ''}>${img}<figcaption>${escapeHtml(p.caption as string)}</figcaption></figure>`\n }\n return img\n }\n\n case 'table': {\n const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)\n const rows = (p.rows ?? []) as (string | number)[][]\n const hs = p.headerStyle as\n | { background?: string; color?: string; bold?: boolean }\n | undefined\n const striped = p.striped as boolean | undefined\n const bordered = p.bordered as boolean | undefined\n const borderStyle = bordered\n ? 'border:1px solid #ddd;border-collapse:collapse;'\n : 'border-collapse:collapse;'\n\n let html = `<table style=\"width:100%;${borderStyle}\">`\n if (p.caption) html += `<caption>${escapeHtml(p.caption as string)}</caption>`\n\n html += '<thead><tr>'\n for (const col of columns) {\n const cellBorder = bordered ? 'border:1px solid #ddd;' : ''\n const bgStyle = hs?.background ? `background:${sanitizeColor(hs.background)};` : ''\n const colorStyle = hs?.color ? `color:${sanitizeColor(hs.color)};` : ''\n const fontStyle = hs?.bold !== false ? 'font-weight:bold;' : ''\n const alignStyle = col.align ? `text-align:${col.align};` : ''\n const widthStyle = col.width\n ? `width:${typeof col.width === 'number' ? `${col.width}px` : col.width};`\n : ''\n html += `<th style=\"${cellBorder}${bgStyle}${colorStyle}${fontStyle}${alignStyle}${widthStyle}padding:8px\">${escapeHtml(col.header)}</th>`\n }\n html += '</tr></thead>'\n\n html += '<tbody>'\n for (let i = 0; i < rows.length; i++) {\n const rowBg = striped && i % 2 === 1 ? ' style=\"background:#f9f9f9\"' : ''\n html += `<tr${rowBg}>`\n for (let j = 0; j < columns.length; j++) {\n const cellBorder = bordered ? 'border:1px solid #ddd;' : ''\n const col = columns[j]\n const alignStyle = col?.align ? `text-align:${col.align};` : ''\n html += `<td style=\"${cellBorder}${alignStyle}padding:8px\">${escapeHtml(String(rows[i]?.[j] ?? ''))}</td>`\n }\n html += '</tr>'\n }\n html += '</tbody></table>'\n return html\n }\n\n case 'list': {\n const tag = p.ordered ? 'ol' : 'ul'\n return `<${tag}>${renderChildren(node.children)}</${tag}>`\n }\n\n case 'list-item':\n return `<li>${renderChildren(node.children)}</li>`\n\n case 'code':\n return `<pre style=\"background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto\"><code>${escapeHtml(renderChildren(node.children))}</code></pre>`\n\n case 'divider': {\n const color = sanitizeColor((p.color as string) ?? '#ddd')\n const thickness = (p.thickness as number) ?? 1\n return `<hr style=\"border:none;border-top:${thickness}px solid ${color};margin:16px 0\" />`\n }\n\n case 'page-break':\n return '<div style=\"page-break-after:always;break-after:page\"></div>'\n\n case 'spacer':\n return `<div style=\"height:${p.height}px\"></div>`\n\n case 'button': {\n const bg = sanitizeColor((p.background as string) ?? '#4f46e5')\n const color = sanitizeColor((p.color as string) ?? '#fff')\n const radius = (p.borderRadius as number) ?? 4\n const pad = padStr((p.padding ?? [12, 24]) as [number, number])\n const align = (p.align as string) ?? 'left'\n return `<div style=\"text-align:${align}\"><a href=\"${escapeHtml(sanitizeHref(p.href as string))}\" style=\"display:inline-block;background:${bg};color:${color};padding:${pad};border-radius:${radius}px;text-decoration:none;font-weight:bold\">${renderChildren(node.children)}</a></div>`\n }\n\n case 'quote': {\n const borderColor = sanitizeColor((p.borderColor as string) ?? '#ddd')\n return `<blockquote style=\"margin:0;padding:12px 20px;border-left:4px solid ${borderColor};color:#555\">${renderChildren(node.children)}</blockquote>`\n }\n\n default:\n return renderChildren(node.children)\n }\n}\n\ntype PageMargin = number | [number, number] | [number, number, number, number]\n\nexport const htmlRenderer: DocumentRenderer = {\n async render(node: DocNode, options?: RenderOptions): Promise<string> {\n let html = renderNode(node)\n if (options?.direction === 'rtl') {\n html = html.replace('<body>', '<body dir=\"rtl\" style=\"direction:rtl\">')\n }\n return html\n },\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"markdown-CdtlFGC0.js","names":[],"sources":["../src/renderers/markdown.ts"],"sourcesContent":["import { sanitizeHref, sanitizeImageSrc } from '../sanitize'\nimport type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'\n\nfunction resolveColumn(col: string | TableColumn): TableColumn {\n return typeof col === 'string' ? { header: col } : col\n}\n\nfunction renderChild(child: DocChild): string {\n if (typeof child === 'string') return child\n return renderNode(child)\n}\n\nfunction renderChildren(children: DocChild[]): string {\n return children.map(renderChild).join('')\n}\n\nfunction renderInline(children: DocChild[]): string {\n return children.map(renderChild).join('')\n}\n\nfunction renderNode(node: DocNode): string {\n const p = node.props\n\n switch (node.type) {\n case 'document':\n return renderChildren(node.children)\n\n case 'page':\n return renderChildren(node.children)\n\n case 'section':\n return `${renderChildren(node.children)}\\n`\n\n case 'row':\n case 'column':\n return renderChildren(node.children)\n\n case 'heading': {\n const level = (p.level as number) ?? 1\n const prefix = '#'.repeat(Math.min(Math.max(level, 1), 6))\n return `${prefix} ${renderInline(node.children)}\\n\\n`\n }\n\n case 'text': {\n let text = renderInline(node.children)\n if (p.bold) text = `**${text}**`\n if (p.italic) text = `*${text}*`\n if (p.strikethrough) text = `~~${text}~~`\n return `${text}\\n\\n`\n }\n\n case 'link':\n return `[${renderInline(node.children)}](${sanitizeHref(p.href as string)})`\n\n case 'image': {\n const alt = (p.alt as string) ?? ''\n let md = `![${alt}](${sanitizeImageSrc(p.src as string)})`\n if (p.caption) md += `\\n*${p.caption}*`\n return `${md}\\n\\n`\n }\n\n case 'table': {\n const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)\n const rows = (p.rows ?? []) as (string | number)[][]\n\n if (columns.length === 0) return ''\n\n // Header\n const header = `| ${columns.map((c) => c.header).join(' | ')} |`\n\n // Separator with alignment\n const separator = `| ${columns\n .map((c) => {\n const align = c.align ?? 'left'\n if (align === 'center') return ':---:'\n if (align === 'right') return '---:'\n return '---'\n })\n .join(' | ')} |`\n\n // Rows\n const body = rows\n .map((row) => `| ${row.map((cell) => String(cell ?? '')).join(' | ')} |`)\n .join('\\n')\n\n let md = `${header}\\n${separator}\\n${body}\\n\\n`\n if (p.caption) md = `*${p.caption}*\\n\\n${md}`\n return md\n }\n\n case 'list': {\n const ordered = p.ordered as boolean | undefined\n return `${node.children\n .filter((c): c is DocNode => typeof c !== 'string')\n .map((item, i) => {\n const prefix = ordered ? `${i + 1}.` : '-'\n return `${prefix} ${renderInline(item.children)}`\n })\n .join('\\n')}\\n\\n`\n }\n\n case 'list-item':\n return renderInline(node.children)\n\n case 'code': {\n const lang = (p.language as string) ?? ''\n const content = renderInline(node.children)\n return `\\`\\`\\`${lang}\\n${content}\\n\\`\\`\\`\\n\\n`\n }\n\n case 'divider':\n return '---\\n\\n'\n\n case 'page-break':\n return '---\\n\\n'\n\n case 'spacer':\n return '\\n'\n\n case 'button':\n return `[${renderInline(node.children)}](${sanitizeHref(p.href as string)})\\n\\n`\n\n case 'quote':\n return `> ${renderInline(node.children)}\\n\\n`\n\n default:\n return renderChildren(node.children)\n }\n}\n\nexport const markdownRenderer: DocumentRenderer = {\n async render(node: DocNode, _options?: RenderOptions): Promise<string> {\n return `${renderNode(node).trim()}\\n`\n },\n}\n"],"mappings":"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"}