use api::http::{Path, Query, Response, State}; use maud::html; use crate::handler::html::{HtmlRepoPath, archived_class, error, resolve, response_html}; use crate::service::GitService; use crate::share::validate; use ::session::OptionalUser; #[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)] pub struct BreadcrumbsQuery { #[serde(default)] pub revision: Option, #[serde(default)] pub path: Option, } #[utoipa::path( get, path = "/repos/{space_name}/{repo_name}/html/breadcrumbs", tag = "html", description = "渲染目录路径面包屑 HTML 片段。", operation_id = "render_breadcrumbs", params(HtmlRepoPath, BreadcrumbsQuery), responses( (status = 200, description = "path breadcrumbs html fragment", content_type = "text/html"), (status = 404, description = "repo or ref not found") ) )] pub async fn render_breadcrumbs( State(service): State, Path(path): Path, Query(q): Query, OptionalUser(viewer): OptionalUser, ) -> Response { let ctx = match resolve(service, path, viewer).await { Ok(c) => c, Err(resp) => return resp, }; let revision = q .revision .as_deref() .unwrap_or(ctx.dto.default_branch.as_str()) .to_string(); if validate::validate_revision(&revision).is_err() { return error::html_err(404, "ref invalid"); } let raw_path = q.path.unwrap_or_default(); let clean_path = crate::handler::html::validate_path(&raw_path).unwrap_or_default(); let segments: Vec<&str> = if clean_path.is_empty() { Vec::new() } else { clean_path.split('/').filter(|s| !s.is_empty()).collect() }; let base = "git-breadcrumbs flex items-center gap-1 text-sm text-zinc-700"; let cls = archived_class(&ctx.dto, base); let space = ctx.space_name.clone(); let repo = ctx.repo_name.clone(); let accumulated: Vec = segments .iter() .scan(String::new(), |acc, seg| { if !acc.is_empty() { acc.push('/'); } acc.push_str(seg); Some(acc.clone()) }) .collect(); let markup = html! { nav class=(cls) data-component="git-breadcrumbs" data-space=(space) data-repo=(repo) data-revision=(revision) data-path=(clean_path.as_str()) data-archived=(if ctx.dto.is_archived { "true" } else { "false" }) aria-label="breadcrumb" { a class="hover:underline text-zinc-500" href=(format!("/{}/{}", ctx.space_name, ctx.repo_name)) { (ctx.repo_name) } @if segments.is_empty() { span class="text-zinc-400" { "/" } } @else { @for (i, seg) in segments.iter().enumerate() { span class="text-zinc-400" { "/" } @let is_last = i + 1 == segments.len(); @if is_last { span class="font-medium text-zinc-800" { (seg) } } @else { a class="hover:underline text-zinc-600" href=(format!("/{}/{}/tree/{}/{}", ctx.space_name, ctx.repo_name, revision, accumulated[i])) { (seg) } } } } } }; response_html(markup, 30) }