use crate::rpcgit::commit::types::TreeEntryMode; use export::http::{Path, Query, Response, State}; use maud::{PreEscaped, html}; use crate::handler::html::{ HtmlRepoPath, archived_class, error, format_size, resolve_view, response_html, }; use crate::service::GitService; use crate::share::access::BasicAuth; const README_CANDIDATES: &[&str] = &[ "README.md", "readme.md", "README.MD", "Readme.md", "README.markdown", "readme.markdown", "README.txt", "readme.txt", "README", "readme", ]; #[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)] pub struct ReadmeQuery { #[serde(default)] pub revision: Option, #[serde(default)] pub path: Option, } #[utoipa::path( get, path = "/repos/{space_name}/{repo_name}/html/readme", tag = "html", description = "查找目录中的 README 并渲染为 HTML 片段。", operation_id = "render_readme", params(HtmlRepoPath, ReadmeQuery), responses( (status = 200, description = "rendered readme html fragment", content_type = "text/html"), (status = 404, description = "repo, ref or readme not found") ) )] pub async fn render_readme( State(service): State, Path(path): Path, Query(q): Query, auth: BasicAuth, ) -> Response { let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, false).await { Ok(v) => v, Err(resp) => return resp, }; let ctx = view.ctx; let revision = view.revision; let clean_path = view.clean_path; let baby = service.baby(&ctx.ctx); let dir_arg = if clean_path.is_empty() { None } else { Some(clean_path.as_str()) }; let entries = match baby.list_dir(&revision, dir_arg).await { Ok(e) => e, Err(_) => return error::html_err(404, "readme not found"), }; let matched_entry = README_CANDIDATES.iter().find_map(|candidate| { entries.iter().find(|e| { (e.mode == TreeEntryMode::Blob || e.mode == TreeEntryMode::Exec) && e.name.eq_ignore_ascii_case(candidate) }) }); let entry = match matched_entry { Some(e) => e, None => return error::html_err(404, "readme not found"), }; let blob = match service.blob_content(&ctx.ctx, entry.oid.to_string()).await { Ok(b) => b, Err(_) => return error::html_err(404, "failed to load readme blob"), }; let text = String::from_utf8_lossy(&blob.data); let is_markdown = entry.name.ends_with(".md") || entry.name.ends_with(".MD") || entry.name.ends_with(".markdown"); let rendered_content = if is_markdown { let mut html_buf = String::with_capacity(text.len() * 3 / 2); let parser = pulldown_cmark::Parser::new_ext(&text, pulldown_cmark::Options::all()); pulldown_cmark::html::push_html(&mut html_buf, parser); PreEscaped(html_buf) } else { PreEscaped(format!( "
{}
", maud::html! { (text) }.into_string() )) }; let base = "git-readme rounded-lg border border-zinc-200 bg-white overflow-hidden text-sm"; let cls = archived_class(&ctx.dto, base); let size_str = format_size(blob.size.max(0) as u64); let markup = html! { div class=(cls) data-component="git-readme" data-space=(ctx.space_name) data-repo=(ctx.repo_name) data-revision=(revision) data-filename=(entry.name) data-size=(blob.size) data-archived=(if ctx.dto.is_archived { "true" } else { "false" }) { div class="flex items-center justify-between border-b border-zinc-200 bg-zinc-50 px-4 py-2" { div class="flex items-center gap-2" { svg class="h-4 w-4 text-zinc-500" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" { path d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8z" {} } span class="font-mono text-xs font-semibold text-zinc-700" { (entry.name) } } span class="font-mono text-xs text-zinc-400" { (size_str) } } div class="markdown-body p-6 text-zinc-900 leading-relaxed max-w-none prose prose-zinc" { (rendered_content) } } }; response_html(markup, 30) }