| 257 | let (kind, rest) = view.split_once('/').unwrap_or((view, "")); | 257 | let (kind, rest) = view.split_once('/').unwrap_or((view, "")); |
| 258 | match kind { | 258 | match kind { |
| 259 | "tree" => tree_view(state, viewer, jar, uri, ctx, rest).await, | 259 | "tree" => tree_view(state, viewer, jar, uri, ctx, rest).await, |
| 260 | "blob" => blob_view(state, viewer, jar, uri, ctx, rest, false).await, | 260 | "blob" => blob_view(state, viewer, jar, uri, ctx, rest).await, |
| 261 | "raw" => raw_view(state, ctx, rest).await, | 261 | "raw" => raw_view(state, ctx, rest).await, |
| 262 | "commits" => commits_view(state, viewer, jar, uri, ctx, rest).await, | 262 | "commits" => commits_view(state, viewer, jar, uri, ctx, rest).await, |
| 263 | "commit" => match rest.split_once('/') { | 263 | "commit" => match rest.split_once('/') { |
| 322 | }) | 322 | }) |
| 323 | .await?; | 323 | .await?; |
| 324 | let readme = load_readme(state, &ctx.repo.id, &rev, &entries).await; | 324 | let readme = load_readme(state, &ctx.repo.id, &rev, &entries).await; |
| | 325 | let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path); |
| 325 | html! { | 326 | html! { |
| 326 | (repo_header(&ctx, Tab::Code, &rev_name)) | 327 | (repo_header(&ctx, Tab::Code, &rev_name)) |
| 327 | (clone_widget(state, &ctx)) | 328 | (clone_widget(state, &ctx)) |
| | 329 | form class="search-row repo-codesearch" method="get" action=(format!("{base}/-/search")) { |
| | 330 | input type="search" name="q" placeholder="Search this repository…" |
| | 331 | aria-label="Search this repository"; |
| | 332 | button class="btn" type="submit" { (icon(Icon::Search)) span { "Search" } } |
| | 333 | } |
| 328 | (tree_table(&ctx.owner.username, &ctx.repo.path, &rev_name, "", &entries)) | 334 | (tree_table(&ctx.owner.username, &ctx.repo.path, &rev_name, "", &entries)) |
| 329 | @if let Some(rendered) = readme { | 335 | @if let Some(rendered) = readme { |
| 330 | div class="card markdown readme" { (rendered) } | 336 | div class="card markdown readme" { (rendered) } |
| 422 | uri: &Uri, | 428 | uri: &Uri, |
| 423 | ctx: RepoCtx, | 429 | ctx: RepoCtx, |
| 424 | rest: &str, | 430 | rest: &str, |
| 425 | _raw: bool, | | |
| 426 | ) -> AppResult<Response> { | 431 | ) -> AppResult<Response> { |
| | 432 | |
| | 433 | let source_view = uri.query().is_some_and(|q| q.contains("display=source")); |
| 427 | let (rev_name, path) = split_ref_path(rest); | 434 | let (rev_name, path) = split_ref_path(rest); |
| 428 | let rev_name = ref_or_default(&ctx.repo, &rev_name); | 435 | let rev_name = ref_or_default(&ctx.repo, &rev_name); |
| 429 | if path.is_empty() { | 436 | if path.is_empty() { |
| 448 | }) | 455 | }) |
| 449 | .await?; | 456 | .await?; |
| 450 | | 457 | |
| 451 | let content_body = render_blob(&ctx, &rev_name, &path, &blob, attr_lang.as_deref()); | 458 | let content_body = render_blob( |
| | 459 | &ctx, |
| | 460 | &rev_name, |
| | 461 | &path, |
| | 462 | &blob, |
| | 463 | attr_lang.as_deref(), |
| | 464 | source_view, |
| | 465 | ); |
| 452 | let body = html! { | 466 | let body = html! { |
| 453 | (repo_header(&ctx, Tab::Code, &rev_name)) | 467 | (repo_header(&ctx, Tab::Code, &rev_name)) |
| 454 | (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path)) | 468 | (breadcrumb(&ctx.owner.username, &ctx.repo.path, &rev_name, &path)) |
| 459 | Ok((jar, page(&chrome, &title, body)).into_response()) | 473 | Ok((jar, page(&chrome, &title, body)).into_response()) |
| 460 | } | 474 | } |
| 461 | | 475 | |
| 462 | | 476 | |
| 463 | | 477 | |
| 464 | fn render_blob( | 478 | fn render_blob( |
| 465 | ctx: &RepoCtx, | 479 | ctx: &RepoCtx, |
| 466 | rev_name: &str, | 480 | rev_name: &str, |
| 467 | path: &str, | 481 | path: &str, |
| 468 | blob: &git::Blob, | 482 | blob: &git::Blob, |
| 469 | attr_lang: Option<&str>, | 483 | attr_lang: Option<&str>, |
| | 484 | source_view: bool, |
| 470 | ) -> Markup { | 485 | ) -> Markup { |
| 471 | let raw_url = format!( | 486 | let base = format!("/{}/{}", ctx.owner.username, ctx.repo.path); |
| 472 | "/{}/{}/-/raw/{rev_name}/{path}", | 487 | let raw_url = format!("{base}/-/raw/{rev_name}/{path}"); |
| 473 | ctx.owner.username, ctx.repo.path | 488 | let blob_url = format!("{base}/-/blob/{rev_name}/{path}"); |
| 474 | ); | | |
| 475 | let filename = path.rsplit('/').next().unwrap_or(path); | 489 | let filename = path.rsplit('/').next().unwrap_or(path); |
| | 490 | let previewable = is_markdown(filename) && !blob.is_binary; |
| | 491 | let show_preview = previewable && !source_view; |
| 476 | | 492 | |
| 477 | html! { | 493 | html! { |
| 478 | div class="blob-header" { | 494 | div class="blob-header" { |
| 479 | span class="muted" { (blob.size) " bytes" } | 495 | span class="muted" { (blob.size) " bytes" } |
| 480 | span { a class="btn" href=(raw_url) { "Raw" } | 496 | div class="blob-actions" { |
| 481 | button class="btn" type="button" data-clipboard=(raw_url) { | 497 | @if previewable { |
| 482 | (icon(Icon::Copy)) span { "Copy path" } | 498 | a class=(toggle_class(show_preview)) href=(blob_url.clone()) { "Preview" } |
| 483 | } } | 499 | a class=(toggle_class(!show_preview)) href=(format!("{blob_url}?display=source")) { "Code" } |
| | 500 | } |
| | 501 | a class="btn" href=(raw_url) { "Raw" } |
| | 502 | button class="btn" type="button" data-clipboard=(raw_url) { |
| | 503 | (icon(Icon::Copy)) span { "Copy path" } |
| | 504 | } |
| | 505 | } |
| 484 | } | 506 | } |
| 485 | @if is_image(filename) { | 507 | @if is_image(filename) { |
| 486 | p { img src=(raw_url) alt=(filename) style="max-width:100%"; } | 508 | p { img src=(raw_url) alt=(filename) style="max-width:100%"; } |
| 489 | p class="muted" { "Binary file not shown." } | 511 | p class="muted" { "Binary file not shown." } |
| 490 | p { a class="btn" href=(raw_url) { "Download" } } | 512 | p { a class="btn" href=(raw_url) { "Download" } } |
| 491 | } | 513 | } |
| | 514 | } @else if show_preview { |
| | 515 | div class="card markdown" { |
| | 516 | (markdown::render(&String::from_utf8_lossy(&blob.content))) |
| | 517 | } |
| 492 | } @else { | 518 | } @else { |
| 493 | (highlighted_code(FsPath::new(path), &blob.content, attr_lang)) | 519 | (highlighted_code(FsPath::new(path), &blob.content, attr_lang)) |
| 494 | } | 520 | } |
| 495 | } | 521 | } |
| 496 | } | 522 | } |
| 497 | | 523 | |
| | 524 | |
| | 525 | fn is_markdown(filename: &str) -> bool { |
| | 526 | matches!( |
| | 527 | filename |
| | 528 | .rsplit('.') |
| | 529 | .next() |
| | 530 | .map(str::to_ascii_lowercase) |
| | 531 | .as_deref(), |
| | 532 | Some("md" | "markdown" | "mdown" | "mkd") |
| | 533 | ) |
| | 534 | } |
| | 535 | |
| 498 | | 536 | |
| 499 | fn highlighted_code(path: &FsPath, content: &[u8], attr_lang: Option<&str>) -> Markup { | 537 | fn highlighted_code(path: &FsPath, content: &[u8], attr_lang: Option<&str>) -> Markup { |
| 500 | let highlighted = highlight::highlight(path, content, attr_lang); | 538 | let highlighted = highlight::highlight(path, content, attr_lang); |
| 1193 | html! { | 1231 | html! { |
| 1194 | div class="repo-head" { | 1232 | div class="repo-head" { |
| 1195 | h1 class="repo-slug" { | 1233 | h1 class="repo-slug" { |
| 1196 | a href={ "/" (ctx.owner.username) } { (ctx.owner.username) } "/" | 1234 | a href={ "/" (ctx.owner.username) } { (ctx.owner.username) } |
| | 1235 | span class="slug-sep" { "/" } |
| 1197 | a href=(base) { (ctx.repo.name) } | 1236 | a href=(base) { (ctx.repo.name) } |
| 1198 | @if ctx.repo.is_private { " " span class="badge badge-private" { "private" } } | 1237 | @if ctx.repo.is_private { span class="badge badge-private" { "private" } } |
| 1199 | } | 1238 | } |
| 1200 | @if let Some(desc) = &ctx.repo.description { p class="muted" { (desc) } } | 1239 | @if let Some(desc) = &ctx.repo.description { p class="muted" { (desc) } } |
| 1201 | nav class="tabs repo-tabs" { | 1240 | nav class="tabs repo-tabs" { |
| 1202 | (tab(Tab::Code, "Code", Icon::File, base.clone())) | 1241 | (tab(Tab::Code, "Code", Icon::File, base.clone())) |
| 1203 | (tab(Tab::Branches, "Branches", Icon::GitBranch, format!("{base}/-/branches"))) | 1242 | (tab(Tab::Branches, "Branches", Icon::GitBranch, format!("{base}/-/branches"))) |
| 1204 | (tab(Tab::Tags, "Tags", Icon::Box, format!("{base}/-/tags"))) | 1243 | (tab(Tab::Tags, "Tags", Icon::Box, format!("{base}/-/tags"))) |
| 1205 | (tab(Tab::Search, "Search", Icon::Search, format!("{base}/-/search"))) | | |
| 1206 | } | 1244 | } |
| 1207 | div class="ref-row" { | 1245 | div class="repo-toolbar" { |
| 1208 | | 1246 | a class="btn" href=(format!("{base}/-/branches")) { |
| 1209 | a class="badge" href=(format!("{base}/-/branches")) { (rev) } | 1247 | (icon(Icon::GitBranch)) span { (rev) } |
| 1210 | a href=(format!("{base}/-/commits/{rev}")) { "Commits" } | 1248 | } |
| | 1249 | a class="btn" href=(format!("{base}/-/commits/{rev}")) { "Commits" } |
| 1211 | } | 1250 | } |
| 1212 | } | 1251 | } |
| 1213 | } | 1252 | } |