@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.
- package/lib/analysis/index.js.html +1 -1
- package/lib/{docx-uNAel545.js → docx-CcB3jYd3.js} +9 -1
- package/lib/{docx-uNAel545.js.map → docx-CcB3jYd3.js.map} +1 -1
- package/lib/{html-B5biprN2.js → html-DtsbNARB.js} +2 -2
- package/lib/html-DtsbNARB.js.map +1 -0
- package/lib/index.js +6 -6
- package/lib/{markdown-CdtlFGC0.js → markdown-BYkSLplL.js} +23 -2
- package/lib/markdown-BYkSLplL.js.map +1 -0
- package/package.json +5 -5
- package/src/renderers/docx.ts +32 -0
- package/src/renderers/html.ts +13 -1
- package/src/renderers/markdown.ts +31 -1
- package/src/tests/integration.test.ts +275 -0
- package/lib/html-B5biprN2.js.map +0 -1
- package/lib/markdown-CdtlFGC0.js.map +0 -1
|
@@ -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('<script>')
|
|
364
|
+
// The & < > in subject are also escaped
|
|
365
|
+
expect(html).toContain('Has & < > 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
|
+
})
|
package/lib/html-B5biprN2.js.map
DELETED
|
@@ -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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\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 = `})`\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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"}
|