#!/usr/bin/env python3 """ Export a horizontal-vertical research report from Markdown to PDF. Usage: python md_to_pdf.py input.md output.pdf --title "Title" --author "Cell 细胞" python md_to_pdf.py input.md output.pdf --html-only Dependencies: pip install markdown weasyprint --break-system-packages """ from __future__ import annotations import argparse import html import os import re import sys from pathlib import Path import markdown CSS = """ @page { size: A4; margin: 22mm 18mm 18mm 18mm; @top-center { content: string(report-title); font-family: "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; font-size: 8pt; color: #64748b; border-bottom: 0.5pt solid #cbd5e1; padding-bottom: 2.5mm; } @bottom-center { content: counter(page); font-family: "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; font-size: 8pt; color: #64748b; border-top: 0.5pt solid #cbd5e1; padding-top: 2.5mm; } } @page :first { @top-center { content: none; } @bottom-center { content: none; } } body { font-family: "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; color: #0f172a; font-size: 10.5pt; line-height: 1.75; text-align: justify; } .cover { page-break-after: always; text-align: center; padding-top: 40%; } .cover h1 { border: none; color: #0f172a; font-size: 26pt; margin-bottom: 6mm; page-break-before: avoid; } .cover .subtitle { color: #166534; font-size: 13pt; margin-bottom: 4mm; } .cover .meta, .cover .author { color: #64748b; font-size: 10.5pt; } .cover .rule { width: 58%; margin: 8mm auto; border: none; border-top: 1.5pt solid #166534; } h1 { string-set: report-title content(); color: #0f172a; font-size: 19pt; margin-top: 14mm; margin-bottom: 5mm; padding-bottom: 2mm; border-bottom: 1.5pt solid #166534; page-break-before: always; } h2 { color: #166534; font-size: 13.5pt; margin-top: 8mm; margin-bottom: 3mm; } h3 { color: #0f766e; font-size: 11.5pt; margin-top: 5mm; margin-bottom: 2mm; } h4 { color: #334155; font-size: 10.5pt; margin-top: 4mm; margin-bottom: 1mm; } p { margin: 1.5mm 0; orphans: 3; widows: 3; } blockquote { margin: 4mm 0; padding: 3mm 4mm 3mm 8mm; background: #f8fafc; border-left: 3pt solid #166534; color: #334155; } table { width: 100%; border-collapse: collapse; margin: 4mm 0; font-size: 9.5pt; } thead th { background: #166534; color: #ffffff; text-align: left; padding: 2.5mm; } tbody td { border-bottom: 0.5pt solid #cbd5e1; padding: 2.3mm 2.5mm; } tbody tr:nth-child(even) { background: #f8fafc; } code { font-family: "SFMono-Regular", "Menlo", monospace; font-size: 9pt; background: #eff6ff; color: #1d4ed8; padding: 0.4mm 1.2mm; border-radius: 2pt; } pre code { display: block; white-space: pre-wrap; padding: 3mm; } ul, ol { margin: 2mm 0; padding-left: 7mm; } li { margin-bottom: 1mm; } a { color: #0f766e; text-decoration: none; } hr { border: none; border-top: 0.5pt solid #cbd5e1; margin: 4mm 0; } """ def extract_title(markdown_text: str, fallback: str) -> tuple[str, str]: match = re.search(r"^\s*#\s+(.+?)\s*$", markdown_text, flags=re.MULTILINE) if not match: return fallback, markdown_text title = match.group(1).strip() remaining = markdown_text[: match.start()] + markdown_text[match.end() :] return title, remaining.lstrip("\n") def extract_meta_line(markdown_text: str) -> str: for line in markdown_text.splitlines(): stripped = line.strip() if stripped.startswith(">"): candidate = stripped.lstrip(">").strip() if "研究时间" in candidate or "所属领域" in candidate or "研究对象类型" in candidate: return html.escape(candidate) return "" def build_cover(title: str, meta_line: str, author: str) -> str: meta_html = f"
" if meta_line else "" return f"""