use crate::rpcgit::commit::types::TreeEntryMode; use axum::extract::{Path, Query, State}; use axum::response::Response; use maud::html; use crate::handler::html::error::html_err; use crate::handler::html::{ HtmlRepoPath, archived_class, format_size, resolve_view, response_html, }; use crate::service::GitService; use crate::share::access::BasicAuth; #[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)] pub struct BlobContentQuery { #[serde(default)] pub revision: Option, #[serde(default)] pub path: Option, } #[utoipa::path( get, path = "/repos/{space_name}/{repo_name}/html/blob-content", tag = "html", description = "渲染文件内容代码表、图片或二进制提示片段。", operation_id = "render_blob_content", params(HtmlRepoPath, BlobContentQuery), responses( (status = 200, description = "blob content html fragment", content_type = "text/html"), (status = 404, description = "repo, ref or blob not found") ) )] pub async fn render_blob_content( 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, true).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 entry = match baby .tree_entry_at(&revision, std::path::Path::new(&clean_path)) .await { Ok(Some(e)) if e.mode == TreeEntryMode::Blob || e.mode == TreeEntryMode::Exec || e.mode == TreeEntryMode::Symlink => { e } Ok(_) => return html_err(404, "blob not found"), Err(_) => return html_err(404, "path 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 html_err(404, "failed to load blob content"), }; let base = "git-blob-content overflow-x-auto rounded-b-lg border-x border-b border-zinc-200 bg-white text-xs"; let cls = archived_class(&ctx.dto, base); let raw_url = format!( "/{}/{}/blob/raw/{revision}/{clean_path}", ctx.space_name, ctx.repo_name ); let file_name = clean_path .rsplit('/') .next() .unwrap_or(&clean_path) .to_string(); let is_image = is_image_extension(&file_name); let markup = html! { div class=(cls) data-component="git-blob-content" data-space=(ctx.space_name) data-repo=(ctx.repo_name) data-revision=(revision) data-path=(clean_path.as_str()) data-size=(blob.size) data-binary=(if blob.is_binary { "true" } else { "false" }) data-truncated=(if blob.truncated { "true" } else { "false" }) data-archived=(if ctx.dto.is_archived { "true" } else { "false" }) { @if is_image { div class="flex items-center justify-center p-8 bg-zinc-50" { img class="max-h-96 max-w-full rounded border border-zinc-200 object-contain shadow-sm" src=(raw_url) alt=(file_name.as_str()); } } @else if blob.is_binary { div class="flex flex-col items-center justify-center gap-3 p-12 text-zinc-500" { svg class="h-10 w-10 text-zinc-400" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" { path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16h-9.5A1.75 1.75 0 012 14.25V1.75z" {} } p class="text-sm font-medium text-zinc-700" { "Binary file not shown" } p class="text-xs text-zinc-400" { (format_size(blob.size.max(0) as u64)) } a class="rounded border border-zinc-200 bg-white px-3 py-1 text-xs font-medium text-zinc-700 hover:bg-zinc-50 shadow-sm" href=(raw_url) download { "Download raw binary" } } } @else { @let text = String::from_utf8_lossy(&blob.data); table class="w-full font-mono text-xs leading-5 border-collapse" { tbody { @for (i, line) in text.lines().enumerate() { @let line_no = i + 1; tr class="hover:bg-zinc-50/80 group" id=(format!("L{line_no}")) { td class="w-12 select-none border-r border-zinc-200 bg-zinc-50/50 px-2 py-0 text-right text-zinc-400 group-hover:text-zinc-600 font-mono text-[11px]" data-line=(line_no) { a class="hover:underline text-zinc-400" href=(format!("#L{line_no}")) { (line_no) } } td class="whitespace-pre px-3 py-0 font-mono text-zinc-900 overflow-x-auto" { (line) } } } } } @if blob.truncated { div class="border-t border-amber-200 bg-amber-50 p-2 text-center text-xs text-amber-700" { "File is too large and has been truncated. " a class="font-medium underline" href=(raw_url) { "Download raw file" } } } } } }; response_html(markup, 30) } fn is_image_extension(filename: &str) -> bool { let lower = filename.to_ascii_lowercase(); lower.ends_with(".png") || lower.ends_with(".jpg") || lower.ends_with(".jpeg") || lower.ends_with(".gif") || lower.ends_with(".svg") || lower.ends_with(".webp") || lower.ends_with(".ico") || lower.ends_with(".bmp") || lower.ends_with(".avif") }