Add native UI window with tao and wry for preview

This commit is contained in:
2026-03-18 22:26:47 +08:00
parent 607c3bd862
commit 48499b697c

View File

@@ -131,13 +131,20 @@ fn wrap_in_html(title: &str, content: &str) -> String {
<title>{}</title> <title>{}</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style> <style>
* {{ box-sizing: border-box; }}
html, body {{ margin: 0; padding: 0; height: 100%; }}
body {{ body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
max-width: 800px; max-width: 100%;
margin: 0 auto; margin: 0;
padding: 20px; padding: 20px;
line-height: 1.6; line-height: 1.6;
color: #333; color: #333;
overflow-y: auto;
}}
.content {{
max-width: 800px;
margin: 0 auto;
}} }}
pre {{ pre {{
background: #f4f4f4; background: #f4f4f4;
@@ -214,206 +221,104 @@ fn wrap_in_html(title: &str, content: &str) -> String {
.mermaid-download:hover {{ .mermaid-download:hover {{
background: #0056b3; background: #0056b3;
}} }}
</style> .toolbar {{
</head> position: fixed;
<body> top: 0;
{} left: 0;
<script> right: 0;
async function renderMermaidDiagrams() {{ background: #2c3e50;
const mermaidDivs = document.querySelectorAll('.mermaid'); color: white;
for (let i = 0; i < mermaidDivs.length; i++) {{ padding: 8px 16px;
const div = mermaidDivs[i]; display: flex;
const code = div.textContent.trim(); align-items: center;
const id = 'mermaid-' + i; gap: 12px;
try {{ z-index: 1000;
const {{ svg }} = await mermaid.render(id, code);
div.innerHTML = svg + '<button class="mermaid-download" onclick="downloadSvg(this)" data-svg="' + encodeURIComponent(svg) + '">Download SVG</button>';
}} catch (err) {{
console.error('Mermaid render error:', err);
div.innerHTML = '<pre style="text-align:left;color:red;">Error: ' + err.message + '</pre>';
}}
}} }}
}} .toolbar h1 {{
function downloadSvg(btn) {{
const svg = decodeURIComponent(btn.getAttribute('data-svg'));
const blob = new Blob([svg], {{ type: 'image/svg+xml' }});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'diagram-' + Date.now() + '.svg';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}}
mermaid.initialize({{ startOnLoad: false }});
renderMermaidDiagrams();
</script>
</body>
</html>"#,
title, content
)
}
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; margin: 0;
padding: 20px; font-size: 16px;
line-height: 1.6; font-weight: 500;
color: #333;
font-size: 12pt;
}} }}
h1, h2, h3, h4, h5, h6 {{ .toolbar button {{
page-break-after: avoid; background: #3498db;
}} color: white;
pre {{ border: none;
background: #f4f4f4; padding: 6px 12px;
padding: 12px;
border-radius: 4px; border-radius: 4px;
overflow-x: auto; cursor: pointer;
page-break-inside: avoid; font-size: 13px;
font-size: 10pt;
}} }}
code {{ .toolbar button:hover {{
background: #f4f4f4; background: #2980b9;
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;
}}
}} }}
body {{ padding-top: 50px; }}
</style> </style>
</head> </head>
<body> <body>
<div class="toolbar">
<h1>{}</h1>
<button onclick="window.print()">Print / Save as PDF</button>
<button onclick="downloadAllSvg()">Download All SVGs</button>
</div>
<div class="content">
{} {}
</div>
<script> <script>
let svgData = [];
async function renderMermaidDiagrams() {{ async function renderMermaidDiagrams() {{
const mermaidDivs = document.querySelectorAll('.mermaid'); const mermaidDivs = document.querySelectorAll('.mermaid');
svgData = [];
for (let i = 0; i < mermaidDivs.length; i++) {{ for (let i = 0; i < mermaidDivs.length; i++) {{
const div = mermaidDivs[i]; const div = mermaidDivs[i];
const code = div.textContent.trim(); const code = div.textContent.trim();
const id = 'mermaid-' + i; const id = 'mermaid-' + i;
try {{ try {{
const {{ svg }} = await mermaid.render(id, code); const {{ svg }} = await mermaid.render(id, code);
div.innerHTML = svg; svgData.push({{ id: i, svg: svg }});
div.innerHTML = svg + '<button class="mermaid-download" onclick="downloadSingleSvg(' + i + ')">Download SVG</button>';
}} catch (err) {{ }} catch (err) {{
console.error('Mermaid render error:', err); console.error('Mermaid render error:', err);
div.innerHTML = '<pre style="text-align:left;color:red;">Error: ' + err.message + '</pre>'; div.innerHTML = '<pre style="text-align:left;color:red;">Error: ' + err.message + '</pre>';
}} }}
}} }}
setTimeout(function() {{ window.print(); }}, 1500);
}} }}
function downloadSingleSvg(index) {{
const data = svgData.find(d => d.id === index);
if (data) {{
const blob = new Blob([data.svg], {{ type: 'image/svg+xml' }});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'diagram-' + index + '-' + Date.now() + '.svg';
a.click();
URL.revokeObjectURL(url);
}}
}}
function downloadAllSvg() {{
svgData.forEach((data, index) => {{
setTimeout(() => {{
const blob = new Blob([data.svg], {{ type: 'image/svg+xml' }});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'diagram-' + index + '-' + Date.now() + '.svg';
a.click();
URL.revokeObjectURL(url);
}}, index * 200);
}});
}}
mermaid.initialize({{ startOnLoad: false }}); mermaid.initialize({{ startOnLoad: false }});
renderMermaidDiagrams(); renderMermaidDiagrams();
</script> </script>
</body> </body>
</html>"#, </html>"#,
title, content title, 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()))
@@ -606,27 +511,189 @@ fn list_directory(path: &Path) -> String {
files.join("\n") files.join("\n")
} }
fn preview_markdown(file_path: &str, title: &str, _width: u32, _height: u32) -> Result<()> { fn preview_markdown(file_path: &str, title: &str, width: u32, height: u32) -> Result<()> {
use tao::event_loop::{EventLoopBuilder, ControlFlow};
use tao::window::WindowBuilder;
use tao::event::{Event, WindowEvent};
use wry::WebViewBuilder;
let path = Path::new(file_path);
let content = read_file(path)?;
let html_content = render_markdown(&content);
let full_html = wrap_in_html(title, &html_content);
let event_loop = EventLoopBuilder::new().build();
let window = WindowBuilder::new()
.with_title(title)
.with_inner_size(tao::dpi::LogicalSize::new(width as f64, height as f64))
.build(&event_loop)?;
let _webview = WebViewBuilder::new()
.with_html(full_html)
.build(&window)?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
if let Event::WindowEvent { event: WindowEvent::CloseRequested, .. } = event {
*control_flow = ControlFlow::Exit;
}
});
}
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 path = Path::new(file_path);
let content = read_file(path)?; let content = read_file(path)?;
let html = render_markdown(&content); let html = render_markdown(&content);
let full_html = wrap_in_html(title, &html); let title = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Markdown");
let temp_dir = std::env::temp_dir(); let pdf_html = wrap_for_pdf(title, &html);
let safe_title: String = title
.replace(' ', "_")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-' || *c == '.')
.collect();
let temp_file = temp_dir.join(format!("{}.html", safe_title));
println!("Temp file: {}", temp_file.display());
std::fs::write(&temp_file, &full_html) let out_path = if let Some(o) = output {
.with_context(|| format!("Failed to write temp file: {}", temp_file.display()))?; 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()
};
open::that(&temp_file).with_context(|| format!("Failed to open file in browser"))?; 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")?;
println!("Opening preview in browser... (close browser window to exit)");
Ok(()) Ok(())
} }
@@ -677,7 +744,11 @@ fn main() {
std::process::exit(1); std::process::exit(1);
} }
} }
Commands::Export { file, output, format } => { Commands::Export {
file,
output,
format,
} => {
if format == "pdf" { if format == "pdf" {
if let Err(e) = export_pdf(&file, Some(output)) { if let Err(e) = export_pdf(&file, Some(output)) {
eprintln!("Export error: {}", e); eprintln!("Export error: {}", e);