git-ents.gitmain
⌘K
foforge
commit a34948c
feat: add MIME-keyed document rendering shared by CLI and web UI

A lookup table (mirroring registry::RECIPES’s style, not a trait hierarchy) dispatching a document’s MIME type to an HTML or plain-text renderer, replacing the extension-only `is_asciidoc/is_markdown checks with one shared registry. git-ents-server already sat in `git-ents’s dependency graph, so the registry lives there as a public module rather than a new crate.

feat: add git-ents-server::render (mime_for_name/to_html/to_text) feat: add asciidoc::to_text via acdc’s terminal converter feat: render comment bodies as AsciiDoc in the file-comments card feat: render comment show’s body as AsciiDoc text in the CLI adds: `acdc-converters-terminal as a direct dependency docs: save the document-rendering/toolchain-view implementation plan Assisted-by: Claude:claude-sonnet-5

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

Cargo.lock @@ -1498,6 +1498,7 @@ dependencies = [ "acdc-converters-core", "acdc-converters-html", + "acdc-converters-terminal", "acdc-parser", "arborium", "askama",
Cargo.toml @@ -42,6 +42,7 @@ acdc-converters-html = { git = "https://github.com/nlopes/acdc", rev = "6ae19bc2e6f0fa4254a3e9ebd1c3d2f6c7caafde", features = [ "terminal", ] } +acdc-converters-terminal = { git = "https://github.com/nlopes/acdc", rev = "6ae19bc2e6f0fa4254a3e9ebd1c3d2f6c7caafde" } askama = "0.16" axum = "0.8" figue = "5.0.0-rc.5"
crates/git-ents-server/Cargo.toml @@ -11,6 +11,7 @@ askama = { workspace = true } acdc-converters-core = { workspace = true } acdc-converters-html = { workspace = true } +acdc-converters-terminal = { workspace = true } arborium = { workspace = true } axum = { workspace = true, features = ["ws"] } facet = { workspace = true }
crates/git-ents-server/src/asciidoc.rs @@ -60,6 +60,27 @@ }) } +/// Render AsciiDoc `source` to plain text via acdc's `cat`-like terminal +/// converter — the same parser [`to_html`] uses, feeding a converter meant +/// for TTY output (a shell, `git ents comment show`) instead of a browser. +pub(crate) fn to_text(source: &str) -> Option<String> { + let parsed = acdc_parser::parse(source, &ParseOptions::default()).ok()?; + let doc = parsed.document(); + let processor = + acdc_converters_terminal::Processor::new(ConvertOptions::default(), doc.attributes.clone()); + let mut output = Vec::new(); + let source = WarningSource::new("terminal"); + let mut warnings = Vec::new(); + let mut diagnostics = Diagnostics::new(&source, &mut warnings); + processor + .write_to(doc, &mut output, None, None, &mut diagnostics) + .ok()?; + for warning in &warnings { + eprintln!("asciidoc text render: {warning}"); + } + String::from_utf8(output).ok() +} + /// CSS for the `.terminal-view` player acdc's HTML converter emits, vendored /// here because embedded-fragment output carries no `<head>` to link or inline /// it from (see [`to_html`]'s doctitle note for the same embedded-mode gap).
crates/git-ents-server/src/lib.rs @@ -7,6 +7,9 @@ mod checks; mod http; mod markdown; +/// MIME-keyed document rendering (HTML and plain-text), shared by the web +/// UI and the `git-ents` CLI, which embeds this crate as a library. +pub mod render; mod verify; mod web;
crates/git-ents/src/main.rs @@ -817,7 +817,11 @@ } } println!(); - for line in comment.body.lines() { + let rendered = git_ents_server::render::to_text( + git_ents_server::render::DEFAULT_PROSE_MIME, + &comment.body, + ); + for line in rendered.lines() { println!(" {line}"); } Ok(())
crates/git-ents-server/src/web/pages.rs @@ -165,7 +165,7 @@ let entry = tree.iter().find(|e| { let name = e.filename.to_str_lossy(); !e.mode.is_tree() - && (crate::asciidoc::is_asciidoc(&name) || crate::markdown::is_markdown(&name)) + && is_doc(&name) && name .rsplit_once('.') .is_some_and(|(stem, _)| stem.eq_ignore_ascii_case("readme")) @@ -181,10 +181,11 @@ /// renders (AsciiDoc via acdc, Markdown via pulldown-cmark), or `None` when it /// is not one or fails to render. fn doc_html(name: &str, text: &str) -> Option<String> { - if crate::asciidoc::is_asciidoc(name) { - return crate::asciidoc::to_html(text); + match crate::render::mime_for_name(name) { + "text/asciidoc" => crate::asciidoc::to_html(text), + "text/markdown" => Some(crate::markdown::to_html(text)), + _ => None, } - crate::markdown::is_markdown(name).then(|| crate::markdown::to_html(text)) } /// The clone URL for `rel`, using the request host when known. @@ -588,7 +589,7 @@ /// Whether `name` is a prose format the forge renders as a document, and so /// gets the rendered/source toggle. fn is_doc(name: &str) -> bool { - crate::asciidoc::is_asciidoc(name) || crate::markdown::is_markdown(name) + crate::render::mime_for_name(name) != "text/plain" } /// Render text file `source` with a line-number gutter — each number a @@ -636,13 +637,15 @@ } /// A comment as a file view shows it: who wrote it and when, where its anchor -/// lands on `HEAD`, and its body. +/// lands on `HEAD`, and its body, rendered as AsciiDoc (a comment carries no +/// filename to infer a MIME type from, so it gets the forge's default prose +/// treatment — see [`crate::render::DEFAULT_PROSE_MIME`]). struct FileComment { author: String, seconds: i64, lines: Option<LineRange>, outdated: bool, - body: String, + body_html: String, } /// The comments whose anchors project onto `path` at `HEAD`, read off the @@ -679,7 +682,7 @@ .map_or(0, |p| i64::try_from(p.created.seconds).unwrap_or(i64::MAX)), lines, outdated, - body: comment.body, + body_html: crate::render::to_html(crate::render::DEFAULT_PROSE_MIME, &comment.body), }); } out @@ -712,7 +715,7 @@ } @if comment.outdated { span.chip { "outdated" } } } - p.comment-body { (comment.body) } + div.comment-body { (PreEscaped(&comment.body_html)) } } } @if let Some(form) = form { (form) }
crates/git-ents-server/src/render.rs @@ -1,0 +1,50 @@ +//! MIME-keyed document rendering: the one place that decides, from a +//! document's declared or inferred MIME type, which of [`asciidoc`] or +//! [`markdown`]'s converters turns it into HTML (for the web UI) or plain +//! text (for the CLI). A lookup table rather than a trait hierarchy, the +//! same style as `registry::RECIPES` in the `git-ents` CLI — MIME is an open +//! namespace, so unrecognized types fall through to a passthrough instead of +//! refusing to render at all. + +use crate::{asciidoc, markdown}; + +/// The MIME type this crate treats prose documents as when nothing else +/// declares one — e.g. a comment or issue body, which carries no filename to +/// infer an extension from. +pub const DEFAULT_PROSE_MIME: &str = "text/asciidoc"; + +/// Guess a document's MIME type from its filename's extension. Replaces +/// separate `is_asciidoc`/`is_markdown` extension checks with one lookup +/// that both HTML and text rendering key off of. +pub fn mime_for_name(name: &str) -> &'static str { + if asciidoc::is_asciidoc(name) { + "text/asciidoc" + } else if markdown::is_markdown(name) { + "text/markdown" + } else { + "text/plain" + } +} + +/// Render `source` (declared or inferred as `mime`) to an embedded HTML +/// fragment. Unrecognized MIME types fall through to an escaped `<pre>` +/// block rather than an error. +pub fn to_html(mime: &str, source: &str) -> String { + match mime { + "text/asciidoc" => asciidoc::to_html(source), + "text/markdown" => Some(markdown::to_html(source)), + _ => None, + } + .unwrap_or_else(|| maud::html! { pre { (source) } }.into_string()) +} + +/// Render `source` (declared or inferred as `mime`) to plain text, for +/// terminal output. Unrecognized MIME types fall through to `source` +/// verbatim. +pub fn to_text(mime: &str, source: &str) -> String { + match mime { + "text/asciidoc" => asciidoc::to_text(source), + _ => None, + } + .unwrap_or_else(|| source.to_owned()) +}
docs/document-rendering-plan.adoc @@ -1,0 +1,114 @@ += Document rendering plan + +Two independent threads, both scoped from the "documents are a database, give +me nicer views" conversation. + +== 1. MIME-keyed document rendering + +Today, AsciiDoc/Markdown rendering is HTML-only and dispatched by filename +extension (`asciidoc::is_asciidoc`, `markdown::is_markdown` in +`git-ents-server`), and only reachable from the web UI's blob viewer +(`web/pages.rs`). `Issue.body`/`Comment.body` are prose documents but render +as raw unstyled text (`web/pages.rs:715`), and the CLI has no rendering at +all (`comment show` prints raw lines). + +`git-ents` (the CLI binary) already depends on `git-ents-server` as a +library (see its `Cargo.toml` and `lib.rs`'s doc comment: "so `git ents` can +embed this server as its own `server` subcommand"). So the registry doesn't +need a new shared crate — it lives in `git-ents-server` and both the server +and the CLI consume it as a library dependency. + +=== New module: `git-ents-server/src/render.rs` (`pub mod render`) + +Two entry points, one per output target: + +[source,rust] +---- +pub fn mime_for_name(name: &str) -> &'static str; // extension -> MIME, replaces is_asciidoc/is_markdown +pub fn to_html(mime: &str, source: &str) -> String; // HTML output, for the web UI +pub fn to_text(mime: &str, source: &str) -> String; // plain-text output, for the CLI +---- + +Internally, each is a small const dispatch table (same style as +`registry::RECIPES`), not a trait hierarchy: + +[source,rust] +---- +const HTML: &[(&str, fn(&str) -> Option<String>)] = &[ + ("text/asciidoc", asciidoc::to_html), + ("text/markdown", markdown::to_html), +]; +const TEXT: &[(&str, fn(&str) -> String)] = &[ + ("text/asciidoc", asciidoc::to_text), +]; +---- + +Unrecognized MIME types fall through to a passthrough (HTML-escaped for +`to_html`, verbatim for `to_text`) rather than an error — MIME is an open +namespace, unlike an exhaustive enum match. + +=== `asciidoc.rs` changes + +Add `pub(crate) fn to_text(source: &str) -> String` using +`acdc-converters-terminal` — a plain-text AsciiDoc converter from the same +upstream `acdc` project (same git rev) already used for HTML. It's already +resolvable via `Cargo.lock` transitively, but needs to become a **direct +dependency** of `git-ents-server` (new `Cargo.toml` line + workspace +`[workspace.dependencies]` entry) — flagging per house rule since this is a +new dependency to sign off on. `to_html`/`is_asciidoc` are otherwise +unchanged. + +`markdown.rs` has no terminal converter available upstream; `to_text` for +`text/markdown` isn't added to the dispatch table, so it falls through to +the generic passthrough (acceptable — still better than nothing). + +=== Call sites + +* `web/pages.rs` (~168, ~591): replace + `is_asciidoc(&name) || is_markdown(&name)` + hand-rolled branch with + `render::mime_for_name(&name)` + `render::to_html(mime, &source)`. +* `web/pages.rs` (~715): render `Issue.body`/`Comment.body` through + `render::to_html("text/asciidoc", &body)` instead of raw text — closes the + "comments render as raw unstyled text" gap. +* `git-ents/src/main.rs`'s `comment_show` (~753): replace the raw + `for line in comment.body.lines()` loop with + `render::to_text("text/asciidoc", &comment.body)`. + +`lib.rs` gains `pub mod render;`; `asciidoc`/`markdown` stay private +(`mod`), since `render.rs` is a sibling module in the same crate — no +visibility escalation needed beyond `render`'s own public wrapper functions. + +== 2. `git ents toolchain view <name>` + +Separate from rendering — a gh-CLI-style single-entity view command, not a +`toolchain list` column. + +* `git_toolchain::disk_usage(repo, name) -> Result<Usage, Error>` + (`git-toolchain/src/lib.rs`): walks `Toolchain.bin` + (`Bin::Embedded(RawTree)` recursively via the tree's git backend, summing + blob sizes; `Bin::Downloaded(components)` reports per-component + size-unknown since there's no local tree to walk) and `Toolchain.src` if + present. No existing code does this tree-walk/size-sum today — new logic, + not a reuse. +* `Usage` is a `#[derive(Facet)]` struct (`total_bytes`, `bin_bytes`, + `src_bytes: Option<u64>`), rendered via `facet_pretty`, consistent with + `toolchain recipes`. +* New `ToolchainAction::View { name: String }` in `git-ents/src/main.rs` + alongside `Export`/`Log`/`Recipes`: prints recipe/version/provenance + (reusing `git_toolchain::history`'s latest entry) plus the `Usage` + breakdown. + +== Files touched + +* `crates/git-ents-server/src/render.rs` (new) +* `crates/git-ents-server/src/asciidoc.rs` (+`to_text`) +* `crates/git-ents-server/src/lib.rs` (`pub mod render;`) +* `crates/git-ents-server/src/web/pages.rs` (swap dispatch; render comment/issue bodies) +* `crates/git-ents-server/Cargo.toml` (+ `acdc-converters-terminal`) +* `Cargo.toml` workspace deps (+ `acdc-converters-terminal` entry) +* `crates/git-ents/src/main.rs` (`comment_show` uses `render::to_text`; new `ToolchainAction::View`) +* `crates/git-toolchain/src/lib.rs` (`disk_usage` + `Usage`) + +New dependency requiring sign-off: `acdc-converters-terminal` (git dep, +already resolvable in `Cargo.lock` via the existing `acdc` git rev, but not +yet a direct dependency of any crate).