#!/usr/bin/env python3 """ Generate a professional PDF from a markdown file. Requirements: pip install markdown weasyprint Usage: python scripts/export-pdf.py [output.pdf] If output is not specified, uses the same name as input with .pdf extension. """ import re import sys from pathlib import Path import markdown from weasyprint import HTML # --- CSS --- CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); @page { size: A4; margin: 25mm 20mm 25mm 20mm; @bottom-center { content: counter(page); font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif; font-size: 8pt; color: #718096; } } @page :first { @bottom-center { content: none; } } * { box-sizing: border-box; } body { font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; font-size: 10.5pt; line-height: 1.6; color: #1a202c; max-width: 100%; } h1 { font-size: 20pt; font-weight: 700; color: #1a365d; margin-top: 32px; margin-bottom: 12px; page-break-after: avoid; } h2 { font-size: 15pt; font-weight: 700; color: #1a365d; margin-top: 28px; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 2px solid #e2e8f0; page-break-after: avoid; } h3 { font-size: 12pt; font-weight: 600; color: #2b6cb0; margin-top: 20px; margin-bottom: 8px; page-break-after: avoid; } h4 { font-size: 10.5pt; font-weight: 600; color: #2d3748; margin-top: 16px; margin-bottom: 6px; } table { width: 100%; border-collapse: collapse; margin: 12px 0 20px 0; font-size: 9pt; page-break-inside: auto; } thead { display: table-header-group; } tr { page-break-inside: avoid; } th { background-color: #2b6cb0; color: white; font-weight: 600; text-align: left; padding: 8px 10px; font-size: 8.5pt; text-transform: uppercase; letter-spacing: 0.3px; } td { padding: 7px 10px; border-bottom: 1px solid #e2e8f0; vertical-align: top; } tr:nth-child(even) td { background-color: #f7fafc; } blockquote { border-left: 3px solid #2b6cb0; margin: 12px 0; padding: 8px 16px; background: #ebf8ff; color: #2a4365; font-size: 10pt; border-radius: 0 4px 4px 0; } code { background: #edf2f7; padding: 1px 4px; border-radius: 3px; font-size: 9pt; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; } hr { border: none; border-top: 2px solid #e2e8f0; margin: 24px 0; } ul, ol { margin: 8px 0 12px 0; padding-left: 24px; } li { margin-bottom: 4px; } strong { font-weight: 600; color: #1a202c; } a { color: #2b6cb0; text-decoration: none; } p { margin: 8px 0; } .section-break { page-break-before: always; } .score-high { color: #276749; font-weight: 700; } .score-medium { color: #d69e2e; font-weight: 700; } .score-low { color: #c53030; font-weight: 700; } """ def postprocess_html(html: str) -> str: """Add CSS classes for scores and risk levels.""" # Section breaks on h2 (except first) h2_count = 0 def add_section_break(match: re.Match) -> str: nonlocal h2_count h2_count += 1 if h2_count > 1: return f'

{match.group(1)}

' return match.group(0) html = re.sub(r"

(.*?)

", add_section_break, html) # Score coloring: 4/5, 5/5 green; 3/5 yellow; 1/5, 2/5 red html = re.sub(r"([45])/5", r'\1/5', html) html = re.sub(r"3/5", '3/5', html) html = re.sub(r"([12])/5", r'\1/5', html) return html def main() -> None: if len(sys.argv) < 2: print("Usage: python export-pdf.py [output.pdf]") sys.exit(1) input_path = Path(sys.argv[1]) if not input_path.exists(): print(f"Error: {input_path} not found") sys.exit(1) output_path = Path(sys.argv[2]) if len(sys.argv) > 2 else input_path.with_suffix(".pdf") md_text = input_path.read_text(encoding="utf-8") body_html = markdown.markdown(md_text, extensions=["tables", "smarty", "sane_lists"]) body_html = postprocess_html(body_html) full_html = f""" {body_html} """ HTML(string=full_html).write_pdf(str(output_path)) print(f"PDF generated: {output_path}") print(f"Size: {output_path.stat().st_size / 1024:.1f} KB") if __name__ == "__main__": main()