Skip to content
Clone
cb06060
102 lines 3.5 KB Rust
use export::http::{Path, Query, Response, State};
use maud::html;
use crate::handler::html::{HtmlRepoPath, archived_class, error, resolve, response_html};
use crate::service::GitService;
use ::session::Session;
use crate::share::validate;
#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
pub struct BreadcrumbsQuery {
#[serde(default)]
pub revision: Option<String>,
#[serde(default)]
pub path: Option<String>,
}
#[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<GitService>,
Path(path): Path<HtmlRepoPath>,
Query(q): Query<BreadcrumbsQuery>,
session: Session,
) -> Response {
let ctx = match resolve(service, path, session).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<String> = 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)
}