git-ents.gitmain
⌘K
foforge
commit 72d8860
roots: highlight blob source with arborium

The line-numbered source view escaped every blob into plain <code>; the pre-redo forge highlighted known languages via arborium. Ports that path: detect_language maps a filename to a grammar, Highlighter renders it as class-named spans matched by the ported --s-*/.code token CSS, and a detection miss falls back to today’s escaped plain text exactly as before. The highlighter is built and used synchronously inside the handler (never held across an .await), since its grammar store isn’t Send.

roots: add arborium as a workspace-declared dependency of ents-web roots: highlight recognized blob languages in the files browser roots: port the pre-redo syntax-token and diff CSS variables and rules 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 @@ -116,6 +116,258 @@ "vte", ] +[[package]] +name = "arborium" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb7a99c146e42da48d5783335814d9bd0f088143aa51fb5ea5b59855df5ca70" +dependencies = [ + "arborium-asciidoc", + "arborium-bash", + "arborium-c", + "arborium-cpp", + "arborium-css", + "arborium-go", + "arborium-highlight", + "arborium-html", + "arborium-javascript", + "arborium-json", + "arborium-markdown", + "arborium-python", + "arborium-rust", + "arborium-theme", + "arborium-toml", + "arborium-tree-sitter", + "arborium-typescript", + "arborium-yaml", + "dlmalloc", +] + +[[package]] +name = "arborium-asciidoc" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58270e81ade15dc1f1397c2acb827bdaf9de4a3adb94a776a0b7d0d5ce57cfc7" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-bash" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa5ef1fa9c298d071d0817b068880a22c86f11202508215bdfddc3386aae643" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-c" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44b533ac7c14da8a26dfb8f7b4bfa29da969a7f01015d63a3ed4097af14c9d" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-cpp" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f307d299c2089784e11c6a93374b80c0d5f42c46bdc9f10bcd02163a1fd30bb" +dependencies = [ + "arborium-c", + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-css" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "040085bd355e439915eaeca9ed9db316a8dc3795f36c23948f216a808192376f" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-go" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9a45c7deab55b1dfed13074fe9cbc0bfa8d4a40e4bc509f4295291e53efdec" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-highlight" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65635c663883cb887d52abce06650f81e4abeb7bfd8e79a555f6a0d8e8f0a31b" +dependencies = [ + "arborium-theme", + "arborium-tree-sitter", + "streaming-iterator", +] + +[[package]] +name = "arborium-html" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b109985d29a842c1fe328b567ac48f8d2211bce778dfe12da6297dd58adac2a" +dependencies = [ + "arborium-css", + "arborium-javascript", + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-javascript" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424c648bf3075e137a5620318a7f865cf320c5f43163d3094764d4cc050e7cc0" +dependencies = [ + "arborium-jsdoc", + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-jsdoc" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f06a77e65f79ded95575bbc9b48a3f213e82ba1a23e5bc64ee5df77f14166cf4" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-json" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbd847461cd81d50cd34454870359bc73ac1b96512eb8a33943fe54cba82d9f" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-markdown" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd7c263aeb25a87a0b9eb0d90d2f559eb4e7ec4bce2a305271893902201a6e1" +dependencies = [ + "arborium-html", + "arborium-sysroot", + "arborium-toml", + "arborium-yaml", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-python" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ade74ad0c312807b2f4c439c7e416c6de41f5a6a638cdea1d5e93470d87ba91" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-rust" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a291f443c124244ff7dea2248a1f2bf60a3b4d6ab0585784ea680dc33fd7cd47" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-sysroot" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d99d80550b726f9dec7ee6d07118c31e08b10e729ac488eabd4c10603dc841" +dependencies = [ + "cc", + "dlmalloc", +] + +[[package]] +name = "arborium-theme" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00a8f02b994f454d9703dd6a8b6075ba778320550d699198faff1882594c0f7" + +[[package]] +name = "arborium-toml" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102a80466c8afd64c0c05c57cae30e5ff5ddbf3f943065fdd8fbe64331b49952" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-tree-sitter" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b27f3bd6bb7192e4a19dc177748532b305e9052839b34e55705f53e5eea2d5ca" +dependencies = [ + "arborium-sysroot", + "cc", + "regex", + "regex-syntax", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "arborium-typescript" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a226449c2081a153662af88ac5db08d6ee2654ac9022bdc1bacb8a2769053d" +dependencies = [ + "arborium-javascript", + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + +[[package]] +name = "arborium-yaml" +version = "2.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83a224e870912490905c2cb21fa1d0b055ae9f88a47329f5849662cb9fe656f" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + [[package]] name = "arc-swap" version = "1.9.2" @@ -740,6 +992,17 @@ "syn", ] +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "document-features" version = "0.2.12" @@ -1002,6 +1265,7 @@ "acdc-converters-core", "acdc-converters-html", "acdc-parser", + "arborium", "axum", "ents-anchor", "ents-effect", @@ -3984,6 +4248,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + [[package]] name = "strip-ansi-escapes" version = "0.2.1" @@ -4310,6 +4580,12 @@ "once_cell", ] +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + [[package]] name = "typenum" version = "1.20.1"
crates/cli/ents-web/Cargo.toml @@ -9,6 +9,7 @@ acdc-converters-core = { workspace = true } acdc-converters-html = { workspace = true } acdc-parser = { workspace = true } +arborium = { workspace = true } ents-anchor = { workspace = true } ents-effect = { workspace = true } ents-forge = { workspace = true }
crates/cli/ents-web/tests/router.rs @@ -604,11 +604,47 @@ assert!(body.contains("src")); } -/// `GET /files/<path>` on a plain-text blob renders a line-numbered, -/// escaped `pre.blob-code` source view -- no syntax highlighting, and no -/// unescaped source. +/// `GET /files/<path>` on a plain-text blob with no recognized grammar +/// renders a line-numbered, escaped `pre.blob-code` source view -- no +/// syntax highlighting, and no unescaped source. #[tokio::test] async fn files_blob_view_renders_a_plain_text_file() { + let dir = seed_repo(&[("notes.txt", "true and 1 < 2\n")]); + let state = build_state_at( + FixtureIdentity { + name: "local-user", + key: Keypair::from_seed(1), + }, + dir.path().to_owned(), + ); + let router = ents_web::router(state); + + let response = router + .oneshot( + Request::get("/files/notes.txt") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("in-process call"); + assert_eq!(response.status(), StatusCode::OK); + let body = response + .into_body() + .collect() + .await + .expect("body") + .to_bytes(); + let body = String::from_utf8(body.to_vec()).expect("utf8 html"); + assert!(body.contains("blob-nums")); + assert!(body.contains("<pre class=\"blob-code\"><code>")); + assert!(body.contains("1 &lt; 2")); +} + +/// `GET /files/<path>` on a `.rs` blob renders syntax-highlighted source: +/// `arborium`'s `HtmlFormat::ClassNames` spans, matched by +/// `crate::assets::OVERRIDES`'s `.code .keyword`-family rules. +#[tokio::test] +async fn files_blob_view_syntax_highlights_a_rust_file() { let dir = seed_repo(&[("src/main.rs", "fn main() {\n let ok = 1 < 2;\n}\n")]); let state = build_state_at( FixtureIdentity { @@ -636,8 +672,8 @@ .to_bytes(); let body = String::from_utf8(body.to_vec()).expect("utf8 html"); assert!(body.contains("blob-nums")); - assert!(body.contains("<pre class=\"blob-code\"><code>")); - assert!(body.contains("1 &lt; 2")); + assert!(body.contains("class=\"code\"")); + assert!(body.contains("class=\"keyword\"")); } /// The `meta` tab restructure (`crate::pages::mod`'s own doc): `GET /meta`
crates/cli/ents-web/src/assets/ents.css @@ -1,15 +1,15 @@ /* Hand-rolled stylesheet, ported from the pre-redo web UI * (`pre-redo:crates/git-ents-server/src/web/style.css`) and reverified * against the markup `crate::pages::layout` and `crate::render` actually - * emit. Dropped from the pre-redo sheet: syntax-highlight token consumers - * (`.code .keyword` and friends -- no highlighter is ported, see - * `crate::pages::files`), diff views (`.diff`, `--diff-add`/`--diff-del`), - * the clone-URL copy button (`.copy-btn`, `.clone`), live-check output + * emit. The `--s-*` syntax-token variables and their `.code .keyword`-family + * consumers, and the `--diff-add`/`--diff-del` variables and `.diff` + * rules, are ported alongside `crate::pages::files`'s `arborium` highlighter + * and `crate::pages::commits`'s unified-diff view. Still dropped: the + * clone-URL copy button (`.copy-btn`, `.clone`), live-check output * (`.checks-grid`, `.status-*`, `.terminal-*`), and every chrome class for a * page family this crate does not have (releases, issues, settings, - * account-strip sign-in). The `--s-*` syntax tokens are dropped with their - * only consumers. The pre-redo brand type stack (DM Sans, IBM Plex Mono, - * Lora, loaded from Google Fonts) is dropped in favor of system font + * account-strip sign-in). The pre-redo brand type stack (DM Sans, IBM Plex + * Mono, Lora, loaded from Google Fonts) is dropped in favor of system font * stacks, so this sheet never depends on a network fetch. Added beyond the * pre-redo sheet: `.meta-layout`/`.meta-rail` for the `meta` tab's * page-family rail (`crate::pages::META_SECTIONS`) and `.id-chip` for the @@ -35,6 +35,16 @@ --shadow-md: 0 4px 16px #0000000f; --radius-sm: 10px; --radius-pill: 100px; + --s-comment: #9c8f74; + --s-keyword: #9d0006; + --s-func: #427b58; + --s-type: #b57614; + --s-string: #79740e; + --s-const: #8f3f71; + --s-op: #7c6f57; + --s-prop: #076678; + --diff-add: #4e9a0622; + --diff-del: #cc241d22; } @media (prefers-color-scheme: dark) { :root { @@ -50,6 +60,16 @@ --color-accent-subtle: #d4a03012; --shadow-sm: 0 1px 3px #00000040; --shadow-md: 0 4px 16px #0000004d; + --s-comment: #928374; + --s-keyword: #fb4934; + --s-func: #8ec07c; + --s-type: #fabd2f; + --s-string: #b8bb26; + --s-const: #d3869b; + --s-op: #a89984; + --s-prop: #83a598; + --diff-add: #b8bb2620; + --diff-del: #fb493420; } } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } @@ -220,6 +240,30 @@ .blob-code code { display: block; font-family: inherit; padding: 0 1.25rem; white-space: pre; color: var(--color-text); } .binary { padding: 2.5rem; text-align: center; font-family: var(--font-mono); font-size: .85rem; color: var(--color-text-muted); } +/* Syntax-highlight token classes (`crate::pages::files::highlight`, `arborium`'s `HtmlFormat::ClassNames`). */ +.code .keyword, .code .macro, .code .tag { color: var(--s-keyword); } +.code .function, .code .constructor { color: var(--s-func); } +.code .type { color: var(--s-type); } +.code .string { color: var(--s-string); } +.code .number, .code .constant, .code .label { color: var(--s-const); } +.code .comment { color: var(--s-comment); font-style: italic; } +.code .operator, .code .punctuation { color: var(--s-op); } +.code .property, .code .attribute { color: var(--s-prop); } +.code .title { color: var(--s-keyword); font-weight: 700; } +.code .strong { font-weight: 700; } +.code .emphasis { font-style: italic; } +.code .link, .code .url, .code .reference { color: var(--s-prop); text-decoration: underline; } +.code .markup { color: var(--s-func); } + +/* Unified diffs (`crate::pages::commits::diff_view`). */ +.diff { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-sm); box-shadow: var(--shadow-sm); overflow-x: auto; margin-bottom: 1.5rem; font-family: var(--font-mono); font-size: .82rem; line-height: 1.55; padding: .6rem 0; } +.diff .ln { display: block; padding: 0 1rem; white-space: pre; } +.diff .add { background: var(--diff-add); } +.diff .del { background: var(--diff-del); } +.diff .hunk { color: var(--s-prop); background: var(--color-code-bg); } +.diff .meta { color: var(--color-text-muted); } +.diff .file { color: var(--color-text); font-weight: 600; background: var(--color-code-bg); padding-top: .3rem; padding-bottom: .3rem; } + /* Rendered Markdown/AsciiDoc documents (`crate::markdown`, `crate::asciidoc`). */ .doc-body { padding: 40px 48px 52px; max-width: 44rem; overflow-wrap: break-word; } .doc-body > :first-child { margin-top: 0; }
crates/cli/ents-web/src/pages/files.rs @@ -2,11 +2,10 @@ //! blob viewer over the `HEAD` tree of the repository `git ents serve` is //! serving. A `.md` blob renders via [`crate::markdown`], a //! `.adoc`/`.asciidoc`/`.asc`/`.adc` blob via [`crate::asciidoc`], and -//! everything else as an escaped `<pre><code>` block -- no syntax -//! highlighting is ported (`pre-redo:crates/git-ents-server/src/web/pages.rs`'s -//! `arborium`-based `highlight` has no equivalent here; see -//! `crate::assets::OVERRIDES`'s own doc for the rest of what pre-redo -//! carried that this crate does not). +//! everything else as a line-numbered source view, syntax-highlighted via +//! [`arborium`] when its filename maps to a known grammar (ported from +//! `pre-redo:crates/git-ents-server/src/web/pages.rs`'s own `highlight`; +//! see [`highlight`]'s own doc), escaped plain text otherwise. //! //! Tree/blob reads go through `gix`'s high-level `Repository`/`Tree`/`Blob` //! types (`repo.head_tree()`, `Tree::lookup_entry_by_path`, @@ -20,10 +19,11 @@ use std::sync::Arc; +use arborium::{Config, Highlighter, HtmlFormat}; use axum::extract::{Path, State}; use gix::bstr::ByteSlice as _; use gix_object::{Find, Write}; -use maud::{Markup, html}; +use maud::{Markup, PreEscaped, html}; use crate::assets; use crate::error::{Error, Result}; @@ -253,13 +253,13 @@ /// A single blob's contents: a Markdown/AsciiDoc document rendered as such /// via [`crate::markdown`]/[`crate::asciidoc`], a binary-content -/// placeholder, or a line-numbered, escaped source view of the raw text. +/// placeholder, or a line-numbered source view of the raw text. /// /// The source view mirrors `pre-redo:crates/git-ents-server/src/web/pages.rs`'s /// `blob_body`: a `.blob` grid pairing a `pre.blob-nums` gutter of -/// per-line `#L{n}` anchors with the escaped `pre.blob-code` code column -/// (no syntax highlighting is ported, so the code stays a plain escaped -/// `<code>` rather than pre-redo's highlighted spans). +/// per-line `#L{n}` anchors with a `pre.blob-code` code column, highlighted +/// via [`highlight`] when `name`'s grammar is known and falling back to a +/// plain escaped `<code>` otherwise. /// /// # Errors /// @@ -278,6 +278,7 @@ return Ok(html! { div.card { div.doc-body { (crate::asciidoc::to_html(text)?) } } }); } let lines = text.lines().count().max(1); + let highlighted = highlight(name, text); Ok(html! { div.blob { pre.blob-nums { @@ -286,12 +287,37 @@ } } pre.blob-code { - code { (text) } + @match highlighted { + Some(html) => code.code { (PreEscaped(html)) }, + None => code { (text) }, + } } } }) } +/// Highlighted HTML for `source`, or `None` when `name`'s extension names +/// no grammar [`arborium::detect_language`] recognizes -- [`blob_view`] +/// then falls back to escaped plain text. Ported from +/// `pre-redo:crates/git-ents-server/src/web/pages.rs`'s own `highlight`, +/// its `HtmlFormat::ClassNames` output matched by +/// `crate::assets::OVERRIDES`'s `.code .keyword`-family rules. +/// +/// The [`Highlighter`] is built and used entirely within this synchronous +/// call -- its grammar store is not `Send`, so it must never be held +/// across an `.await` (this function itself is never `async`, and neither +/// is any caller between it and the request handler). +fn highlight(name: &str, source: &str) -> Option<String> { + let language = arborium::detect_language(name)?; + let config = Config { + html_format: HtmlFormat::ClassNames, + ..Default::default() + }; + Highlighter::with_config(config) + .highlight(language, source) + .ok() +} + #[cfg(test)] mod tests { #![allow(clippy::expect_used, reason = "unit test")] @@ -352,7 +378,7 @@ #[test] fn blob_view_escapes_plain_text_into_a_line_numbered_code_block() { - let rendered = blob_view("main.rs", b"fn main() { let x = 1 < 2; }") + let rendered = blob_view("notes.txt", b"1 < 2 and true") .expect("plain text renders") .into_string(); assert!(rendered.contains("blob-nums")); @@ -360,6 +386,16 @@ assert!(rendered.contains("1 &lt; 2")); } + #[test] + fn blob_view_highlights_a_recognized_language_with_syntax_token_classes() { + let rendered = blob_view("main.rs", b"fn main() { let x = 1; }") + .expect("rust renders") + .into_string(); + assert!(rendered.contains("blob-nums")); + assert!(rendered.contains("class=\"code\"")); + assert!(rendered.contains("class=\"keyword\"")); + } + #[test] fn blob_view_shows_a_placeholder_for_binary_content() { let rendered = blob_view("data.bin", b"\0\x01\x02binary")