feat: render .md files as HTML (pulldown-cmark), ?raw for plain text

This commit is contained in:
Fam Zheng 2026-04-06 20:06:35 +01:00
parent 214d2a2338
commit 902b5192d5

View File

@ -81,6 +81,7 @@ async fn list_root(Path(project_id): Path<String>) -> Result<Json<Vec<FileEntry>
async fn get_file( async fn get_file(
Path((project_id, file_path)): Path<(String, String)>, Path((project_id, file_path)): Path<(String, String)>,
query: axum::extract::Query<std::collections::HashMap<String, String>>,
) -> Response { ) -> Response {
let full = match resolve_path(&project_id, &file_path) { let full = match resolve_path(&project_id, &file_path) {
Ok(p) => p, Ok(p) => p,
@ -95,16 +96,46 @@ async fn get_file(
}; };
} }
// Otherwise serve the file // Read file
match tokio::fs::read(&full).await { let bytes = match tokio::fs::read(&full).await {
Ok(bytes) => { Ok(b) => b,
Err(_) => return (StatusCode::NOT_FOUND, "File not found").into_response(),
};
// Render markdown as HTML if file is .md
let is_md = full.extension().is_some_and(|e| e == "md");
if is_md && !query.contains_key("raw") {
let md_text = String::from_utf8_lossy(&bytes);
let html = render_markdown(&md_text, &file_path);
return ([(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8".to_string())], html).into_response();
}
let mime = mime_guess::from_path(&full) let mime = mime_guess::from_path(&full)
.first_or_octet_stream() .first_or_octet_stream()
.to_string(); .to_string();
([(axum::http::header::CONTENT_TYPE, mime)], bytes).into_response() ([(axum::http::header::CONTENT_TYPE, mime)], bytes).into_response()
} }
Err(_) => (StatusCode::NOT_FOUND, "File not found").into_response(),
} fn render_markdown(md: &str, title: &str) -> String {
use pulldown_cmark::{Parser, Options, html};
let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TASKLISTS;
let parser = Parser::new_ext(md, opts);
let mut html_out = String::new();
html::push_html(&mut html_out, parser);
format!(r#"<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>{title}</title>
<style>
body {{ max-width: 800px; margin: 40px auto; padding: 0 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; }}
h1,h2,h3 {{ border-bottom: 1px solid #eee; padding-bottom: 0.3em; }}
pre {{ background: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto; }}
code {{ background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }}
pre code {{ background: none; padding: 0; }}
table {{ border-collapse: collapse; }} th,td {{ border: 1px solid #ddd; padding: 8px 12px; }}
blockquote {{ border-left: 4px solid #ddd; margin: 0; padding-left: 16px; color: #666; }}
</style>
</head><body>{html_out}</body></html>"#)
} }
async fn do_upload(project_id: &str, rel_dir: &str, mut multipart: Multipart) -> Response { async fn do_upload(project_id: &str, rel_dir: &str, mut multipart: Multipart) -> Response {