Add PDF export feature with print CSS

This commit is contained in:
2026-03-18 21:45:24 +08:00
parent ffd79dbc61
commit 535206726e

View File

@@ -20,6 +20,8 @@ enum Commands {
file: String, file: String,
#[arg(short, long, help = "Output file path")] #[arg(short, long, help = "Output file path")]
output: Option<String>, output: Option<String>,
#[arg(short = 'p', long, help = "Export as PDF")]
pdf: bool,
}, },
Batch { Batch {
#[arg(help = "Directory containing markdown files")] #[arg(help = "Directory containing markdown files")]
@@ -43,6 +45,14 @@ enum Commands {
#[arg(short = 'e', long, default_value = "700")] #[arg(short = 'e', long, default_value = "700")]
height: u32, height: u32,
}, },
Export {
#[arg(help = "Markdown file to export")]
file: String,
#[arg(short, long, help = "Output file path")]
output: String,
#[arg(short, long, default_value = "pdf", value_parser = ["pdf", "html"])]
format: String,
},
} }
fn render_markdown(content: &str) -> String { fn render_markdown(content: &str) -> String {
@@ -247,6 +257,163 @@ fn wrap_in_html(title: &str, content: &str) -> String {
) )
} }
fn wrap_for_pdf(title: &str, content: &str) -> String {
format!(
r#"<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{}</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
@page {{
margin: 20mm;
size: A4;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
max-width: none;
margin: 0;
padding: 20px;
line-height: 1.6;
color: #333;
font-size: 12pt;
}}
h1, h2, h3, h4, h5, h6 {{
page-break-after: avoid;
}}
pre {{
background: #f4f4f4;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
page-break-inside: avoid;
font-size: 10pt;
}}
code {{
background: #f4f4f4;
padding: 2px 4px;
border-radius: 2px;
font-family: "SF Mono", Monaco, Consolas, monospace;
font-size: 10pt;
}}
pre code {{
background: none;
padding: 0;
}}
blockquote {{
border-left: 3px solid #ddd;
margin: 0;
padding-left: 12px;
color: #666;
}}
table {{
border-collapse: collapse;
width: 100%;
page-break-inside: avoid;
}}
th, td {{
border: 1px solid #ddd;
padding: 6px;
text-align: left;
}}
th {{
background: #f4f4f4;
}}
img {{
max-width: 100%;
page-break-inside: avoid;
}}
.mermaid {{
background: white;
text-align: center;
padding: 10px;
page-break-inside: avoid;
}}
.mermaid svg {{
display: block;
margin: 0 auto;
max-width: 100%;
height: auto;
}}
.mermaid-download {{
display: none;
}}
@media print {{
body {{
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}}
.mermaid svg {{
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}}
}}
</style>
</head>
<body>
{}
<script>
async function renderMermaidDiagrams() {{
const mermaidDivs = document.querySelectorAll('.mermaid');
for (let i = 0; i < mermaidDivs.length; i++) {{
const div = mermaidDivs[i];
const code = div.textContent.trim();
const id = 'mermaid-' + i;
try {{
const {{ svg }} = await mermaid.render(id, code);
div.innerHTML = svg;
}} catch (err) {{
console.error('Mermaid render error:', err);
div.innerHTML = '<pre style="text-align:left;color:red;">Error: ' + err.message + '</pre>';
}}
}}
setTimeout(function() {{ window.print(); }}, 1500);
}}
mermaid.initialize({{ startOnLoad: false }});
renderMermaidDiagrams();
</script>
</body>
</html>"#,
title, content
)
}
fn export_pdf(file_path: &str, output: Option<String>) -> Result<()> {
let path = Path::new(file_path);
let content = read_file(path)?;
let html = render_markdown(&content);
let title = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Markdown");
let pdf_html = wrap_for_pdf(title, &html);
let out_path = if let Some(o) = output {
o
} else {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
let out_dir = home.join("docs/pdf");
std::fs::create_dir_all(&out_dir)?;
out_dir
.join(format!("{}.html", title))
.to_string_lossy()
.to_string()
};
std::fs::write(&out_path, &pdf_html)
.with_context(|| format!("Failed to write to {}", out_path))?;
println!("PDF export file: {}", out_path);
println!("Open this file in browser and use File > Print > Save as PDF");
open::that(&out_path).context("Failed to open file in browser")?;
Ok(())
}
fn read_file(path: &Path) -> Result<String> { fn read_file(path: &Path) -> Result<String> {
std::fs::read_to_string(path) std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path.display())) .with_context(|| format!("Failed to read file: {}", path.display()))
@@ -467,12 +634,19 @@ fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { match cli.command {
Commands::Render { file, output } => { Commands::Render { file, output, pdf } => {
if pdf {
if let Err(e) = export_pdf(&file, output) {
eprintln!("Error: {}", e);
std::process::exit(1);
}
} else {
if let Err(e) = process_file(&file, output) { if let Err(e) = process_file(&file, output) {
eprintln!("Error: {}", e); eprintln!("Error: {}", e);
std::process::exit(1); std::process::exit(1);
} }
} }
}
Commands::Batch { directory, output } => { Commands::Batch { directory, output } => {
if let Err(e) = batch_process(&directory, output) { if let Err(e) = batch_process(&directory, output) {
eprintln!("Error: {}", e); eprintln!("Error: {}", e);
@@ -503,5 +677,18 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
} }
Commands::Export { file, output, format } => {
if format == "pdf" {
if let Err(e) = export_pdf(&file, Some(output)) {
eprintln!("Export error: {}", e);
std::process::exit(1);
}
} else {
if let Err(e) = process_file(&file, Some(output)) {
eprintln!("Export error: {}", e);
std::process::exit(1);
}
}
}
} }
} }