Add PDF export feature with print CSS
This commit is contained in:
189
src/main.rs
189
src/main.rs
@@ -20,6 +20,8 @@ enum Commands {
|
||||
file: String,
|
||||
#[arg(short, long, help = "Output file path")]
|
||||
output: Option<String>,
|
||||
#[arg(short = 'p', long, help = "Export as PDF")]
|
||||
pdf: bool,
|
||||
},
|
||||
Batch {
|
||||
#[arg(help = "Directory containing markdown files")]
|
||||
@@ -43,6 +45,14 @@ enum Commands {
|
||||
#[arg(short = 'e', long, default_value = "700")]
|
||||
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 {
|
||||
@@ -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> {
|
||||
std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read file: {}", path.display()))
|
||||
@@ -467,12 +634,19 @@ fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
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) {
|
||||
eprintln!("Error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Batch { directory, output } => {
|
||||
if let Err(e) = batch_process(&directory, output) {
|
||||
eprintln!("Error: {}", e);
|
||||
@@ -503,5 +677,18 @@ fn main() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user