feat: add MIME-keyed document rendering shared by CLI and web UI
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
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).