Add native UI window with tao and wry for preview
This commit is contained in:
439
src/main.rs
439
src/main.rs
@@ -131,13 +131,20 @@ fn wrap_in_html(title: &str, content: &str) -> String {
|
||||
<title>{}</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||
<style>
|
||||
* {{ box-sizing: border-box; }}
|
||||
html, body {{ margin: 0; padding: 0; height: 100%; }}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
overflow-y: auto;
|
||||
}}
|
||||
.content {{
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}}
|
||||
pre {{
|
||||
background: #f4f4f4;
|
||||
@@ -214,206 +221,104 @@ fn wrap_in_html(title: &str, content: &str) -> String {
|
||||
.mermaid-download:hover {{
|
||||
background: #0056b3;
|
||||
}}
|
||||
</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 + '<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 {{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
z-index: 1000;
|
||||
}}
|
||||
}}
|
||||
|
||||
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;
|
||||
.toolbar h1 {{
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
font-size: 12pt;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
h1, h2, h3, h4, h5, h6 {{
|
||||
page-break-after: avoid;
|
||||
}}
|
||||
pre {{
|
||||
background: #f4f4f4;
|
||||
padding: 12px;
|
||||
.toolbar button {{
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
page-break-inside: avoid;
|
||||
font-size: 10pt;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}}
|
||||
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;
|
||||
}}
|
||||
.toolbar button:hover {{
|
||||
background: #2980b9;
|
||||
}}
|
||||
body {{ padding-top: 50px; }}
|
||||
</style>
|
||||
</head>
|
||||
<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>
|
||||
let svgData = [];
|
||||
async function renderMermaidDiagrams() {{
|
||||
const mermaidDivs = document.querySelectorAll('.mermaid');
|
||||
svgData = [];
|
||||
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;
|
||||
svgData.push({{ id: i, svg: svg }});
|
||||
div.innerHTML = svg + '<button class="mermaid-download" onclick="downloadSingleSvg(' + i + ')">Download SVG</button>';
|
||||
}} 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);
|
||||
}}
|
||||
|
||||
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 }});
|
||||
renderMermaidDiagrams();
|
||||
</script>
|
||||
</body>
|
||||
</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> {
|
||||
std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read file: {}", path.display()))
|
||||
@@ -606,27 +511,189 @@ fn list_directory(path: &Path) -> String {
|
||||
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 content = read_file(path)?;
|
||||
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 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());
|
||||
let pdf_html = wrap_for_pdf(title, &html);
|
||||
|
||||
std::fs::write(&temp_file, &full_html)
|
||||
.with_context(|| format!("Failed to write temp file: {}", temp_file.display()))?;
|
||||
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()
|
||||
};
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -677,7 +744,11 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Commands::Export { file, output, format } => {
|
||||
Commands::Export {
|
||||
file,
|
||||
output,
|
||||
format,
|
||||
} => {
|
||||
if format == "pdf" {
|
||||
if let Err(e) = export_pdf(&file, Some(output)) {
|
||||
eprintln!("Export error: {}", e);
|
||||
|
||||
Reference in New Issue
Block a user