feat: render .md files as HTML (pulldown-cmark), ?raw for plain text
This commit is contained in:
@@ -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,
|
||||||
let mime = mime_guess::from_path(&full)
|
Err(_) => return (StatusCode::NOT_FOUND, "File not found").into_response(),
|
||||||
.first_or_octet_stream()
|
};
|
||||||
.to_string();
|
|
||||||
([(axum::http::header::CONTENT_TYPE, mime)], bytes).into_response()
|
// Render markdown as HTML if file is .md
|
||||||
}
|
let is_md = full.extension().is_some_and(|e| e == "md");
|
||||||
Err(_) => (StatusCode::NOT_FOUND, "File not found").into_response(),
|
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)
|
||||||
|
.first_or_octet_stream()
|
||||||
|
.to_string();
|
||||||
|
([(axum::http::header::CONTENT_TYPE, mime)], bytes).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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user