//! Page: file view at `/{owner}/{repo}/blob/{*rest}`. Text is highlighted with line anchors, //! markdown is rendered (`?plain=1` shows the source), images use the raw origin, and binary, //! symlink and submodule entries get a short notice. use forge_git::browse::EntryKind; use topcoat::Result; use topcoat::context::{Cx, app_context}; use topcoat::router::error::not_found; use topcoat::router::{page, path_param, query_params}; use topcoat::view::{Unescaped, View, component, view}; use crate::i18n::code::{self as g, K}; use crate::i18n::{self}; use crate::lfs::has_object; use crate::lfs_pointer; use crate::pages::media::media_url; use crate::pages::repo::repo_head; use crate::pages::repo_ctx::load_repo; use crate::pages::tree::{link_context, path_crumbs, read_file, resolve}; use crate::raw::raw_url; use crate::render::highlight::highlight_lines; use crate::render::markdown::render_markdown; use crate::state::AppState; use crate::views::{Crumb, breadcrumb, human_size, repo_href, shell}; const IMAGE_EXTS: [&str; 8] = ["png", "jpg", "jpeg", "gif", "webp", "avif", "svg", "ico"]; const MARKDOWN_EXTS: [&str; 2] = ["md", "markdown"]; path_param!(owner); path_param!(repo); path_param!(*rest); #[query_params(error = bad_request)] struct BlobQuery { plain: Option, } /// One line of a text file. `highlighted` is true when `code` is trusted highlighter output. pub struct Line { pub num: String, pub code: String, pub highlighted: bool, } /// What the page shows for the file. `kind` is one of: image, submodule, symlink, binary, /// markdown, text. struct Viewer { kind: &'static str, /// Notice text: the symlink target, the submodule commit, or the binary size. note: String, /// Rendered markdown (trusted output of `render_markdown`). html: String, lines: Vec, truncated: bool, } fn extension(name: &str) -> String { name.rsplit_once('.') .map(|(_, ext)| ext.to_ascii_lowercase()) .unwrap_or_default() } fn text_lines(text: &str, file_name: &str) -> Vec { let plain: Vec<&str> = text.lines().collect(); let highlighted = highlight_lines(file_name, text).filter(|h| h.len() == plain.len()); plain .iter() .enumerate() .map(|(i, line)| match &highlighted { Some(html) => Line { num: (i + 1).to_string(), code: html[i].clone(), highlighted: true, }, None => Line { num: (i + 1).to_string(), code: (*line).to_owned(), highlighted: false, }, }) .collect() } #[component] async fn file_table(lines: Vec) -> Result { Ok(view! {
for line in lines.into_iter() { }
(line.num) if line.highlighted { (Unescaped::new_unchecked(line.code)) } else { (line.code) }
}) } #[page("/{owner}/{repo}/blob/{*rest}")] async fn blob_page(cx: &Cx) -> Result { let owner = path_param::(cx); let name = path_param::(cx); let rest = path_param::(cx).collect::>().join("/"); let plain = query_params::(cx)?.plain.as_deref() == Some("1"); let ctx = load_repo(cx, owner, name).await?; let state = app_context::(cx); let lang = i18n::lang(cx, ctx.viewer.as_ref().map(|(user, _)| user)); let repo = &ctx.repo; let Some((ref_name, commit, file)) = resolve(ctx.path.clone(), rest).await? else { return Err(not_found().into()); }; let Some(blob) = read_file(ctx.path.clone(), commit, file.clone()).await? else { return Err(not_found().into()); }; let file_name = file.rsplit('/').next().unwrap_or(&file).to_owned(); let dir = file .rsplit_once('/') .map(|(d, _)| d) .unwrap_or("") .to_owned(); let ext = extension(&file_name); let is_markdown = MARKDOWN_EXTS.contains(&ext.as_str()); // A Git LFS pointer is a small text blob that names an object the forge stores. let pointer = match blob.kind { EntryKind::Submodule | EntryKind::Symlink => None, _ => lfs_pointer::parse(&blob.data), }; let (lfs_note, lfs_href) = match &pointer { Some(pointer) => { let size = human_size(lang, pointer.size); match has_object(state, &repo.id, &pointer.oid).await { Ok(Some(_)) => ( Some(g::lfs_stored(lang, &size)), Some(media_url(state, repo, &ref_name, &file)), ), Ok(None) => (Some(g::lfs_missing(lang, &size)), None), Err(e) => { tracing::warn!(error = %e, "blob: LFS object link lookup failed"); (Some(g::lfs_stored(lang, &size)), None) } } } None => (None, None), }; let viewer = match blob.kind { EntryKind::Submodule => Viewer { kind: "submodule", note: blob.oid.clone(), html: String::new(), lines: Vec::new(), truncated: false, }, EntryKind::Symlink => Viewer { kind: "symlink", note: String::from_utf8_lossy(&blob.data).into_owned(), html: String::new(), lines: Vec::new(), truncated: false, }, _ if lfs_note.is_some() => Viewer { kind: "lfs", note: lfs_note.clone().unwrap_or_default(), html: String::new(), lines: Vec::new(), truncated: false, }, _ if IMAGE_EXTS.contains(&ext.as_str()) => Viewer { kind: "image", note: String::new(), html: String::new(), lines: Vec::new(), truncated: blob.truncated, }, _ if blob.is_binary => Viewer { kind: "binary", note: g::binary_note(lang, blob.size), html: String::new(), lines: Vec::new(), truncated: false, }, _ => { let text = String::from_utf8_lossy(&blob.data).into_owned(); if is_markdown && !plain { let link_ctx = link_context(state, repo, &ref_name, &dir); Viewer { kind: "markdown", note: String::new(), html: render_markdown(&text, Some(&link_ctx)), lines: Vec::new(), truncated: blob.truncated, } } else { Viewer { kind: "text", note: String::new(), html: String::new(), lines: text_lines(&text, &file_name), truncated: blob.truncated, } } } }; let raw = raw_url(state, repo, &ref_name, &file); let base = repo_href( &repo.owner_name, &repo.name, "blob", &format!("{ref_name}/{file}"), ); let history = repo_href( &repo.owner_name, &repo.name, "commits", &format!("{ref_name}/{file}"), ); let toggle = if !is_markdown { None } else if plain { Some((g::t(lang, K::Rendered), base.clone())) } else { Some((g::t(lang, K::SourceToggle), format!("{base}?plain=1"))) }; let crumbs: Vec = path_crumbs(repo, &ref_name, "blob", &file); let title = format!("{}/{} \u{b7} {file}", repo.owner_name, repo.name); let size = human_size(lang, blob.size); let kind = viewer.kind; let note = viewer.note; let html = viewer.html; let lines = viewer.lines; let truncated = viewer.truncated; let image_alt = file_name.clone(); let download_name = file_name.clone(); let download_href = lfs_href.clone().unwrap_or_else(|| raw.clone()); let header = ctx.head(state, "code").await?; Ok(view! { shell(title: title.as_str(), viewer: ctx.viewer.as_ref(), repo_head(head: header)
breadcrumb(lang: lang, crumbs: crumbs)
(size)
if kind == "image" {
(image_alt)
} if kind == "markdown" {
(Unescaped::new_unchecked(html))
} if kind == "text" { file_table(lines: lines) } if kind == "lfs" {

(note.clone())

} if kind == "binary" {

(note.clone())

} if kind == "symlink" {

(g::t(lang, K::SymlinkPrefix)) (note.clone())

} if kind == "submodule" {

(g::t(lang, K::SubmodulePrefix)) (note.clone()) (g::t(lang, K::SubmoduleSuffix))

} if truncated {

(g::t(lang, K::TruncatedPrefix)) (g::t(lang, K::ViewFullRaw)) "."

}
) }) }