ba6fe0f
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<String>,
#[serde(default)]
pub path: Option<String>,
}
#[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<GitService>,
Path(path): Path<HtmlRepoPath>,
Query(q): Query<ReadmeQuery>,
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.space_name.clone(),
ctx.repo_name.clone(),
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!(
"<pre class=\"overflow-x-auto p-4 text-xs font-mono text-zinc-800 whitespace-pre-wrap\">{}</pre>",
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)
}