steid

@jamesgill /

feat: the languages this repository actually uses

syntect's default set is Sublime's, and it has no TOML and no Dockerfile — two
file types sitting in this repository's root, rendering as flat text on the one
page whose whole job is to make code legible. TypeScript was missing too.

`two_face::syntax::extra_newlines()` is that set plus the grammars `bat` curates,
so it is a drop-in for `load_defaults_newlines` and the change is one line in the
`OnceLock`. The `hl-` class output and the CSS mapping are untouched: the extra
grammars emit the same TextMate scopes the mapping was already written against,
which is the payoff for having mapped scopes rather than a theme.

`default-features = false` is load-bearing rather than tidy — two-face defaults to
`syntect-onig`, and `syntect-fancy` keeps it on the pure-Rust engine so no C
dependency reaches the Debian release build. The cost is 0.59 MiB of embedded
grammar dump: 13.65 MB to 14.27 MB.

A test now asserts the sixteen languages that highlight rather than describing
them, so a future syntax-set swap cannot quietly reopen the hole. `.jsx` is the
one extension still plain, and the handover says why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnmRPpETeB9y6RSowpigye
JamesPatrickGill authored 15 hours agoparent30018a8Browse files8331d0cc1f2dcc2daf2f7d69345e50c730285035

4 files changed+76 −24

Cargo.lock+12 −0View file
@@ -2015,6 +2015,7 @@ dependencies = [
20152015 "tokio",
20162016 "tokio-util",
20172017 "topcoat",
2018+ "two-face",
20182019 "uuid",
20192020 ]
20202021
@@ -2764,6 +2765,17 @@ dependencies = [
27642765 "thiserror",
27652766 ]
27662767
2768+[[package]]
2769+name = "two-face"
2770+version = "0.5.2+bat-0.26.1"
2771+source = "registry+https://github.com/rust-lang/crates.io-index"
2772+checksum = "915be7adc2ff6f4338acbf71f3eda0a146f46003b992a1669c674308610efdac"
2773+dependencies = [
2774+ "serde",
2775+ "serde_derive",
2776+ "syntect",
2777+]
2778+
27672779 [[package]]
27682780 name = "typenum"
27692781 version = "1.20.1"
Cargo.toml+1 −0View file
@@ -21,6 +21,7 @@ syntect = { version = "5.3.0", default-features = false, features = ["default-fa
2121 tokio = { version = "1.53.1", features = ["rt-multi-thread", "macros", "process", "fs", "io-util", "time"] }
2222 tokio-util = { version = "0.7", features = ["io"] }
2323 topcoat = { version = "0.5.0", features = ["icon-iconify", "tailwind", "ui"] }
24+two-face = { version = "0.5.2", default-features = false, features = ["syntect-fancy"] }
2425 uuid = { version = "1.24.0", features = ["v4"] }
2526
2627 [build-dependencies]
plans/handover-highlight.md+30 −10View file
@@ -20,6 +20,10 @@ highlighting. Nothing else changed: no new route, no new `GitQuery` method, **no
2020 `hl-` classes at the bottom of the file.
2121 - `syntect 5.3` with `default-features = false, features = ["default-fancy"]`: the pure
2222 Rust regex engine, so the Debian release build gains no C dependency.
23+- `two-face 0.5` with `default-features = false, features = ["syntect-fancy"]`: the
24+ grammars syntect's defaults lack. Its default feature is `syntect-onig`, so
25+ `default-features = false` is not tidiness — it is what keeps `onig`, and therefore a
26+ C dependency, out of the release build.
2327
2428 ## progress.md material — decisions worth not rediscovering
2529
@@ -62,17 +66,15 @@ highlighting. Nothing else changed: no new route, no new `GitQuery` method, **no
6266
6367 ## current.md material — holes opened and shortcuts taken
6468
65- **The default syntax set is missing four languages this project itself uses.** Checked
66 by test (`which_common_languages_the_default_set_knows`, prints its findings):
69+- **`.jsx` is the one extension still unhighlighted.** `two_face` closed the gap that
70+ mattered — TOML, Dockerfile and TypeScript all highlight now, and the first two are in
71+ this repository — but its JavaScript grammar claims only `js` and `htc`, while React
72+ is covered on the TypeScript side by `TypeScriptReact` for `tsx`. So a `.jsx` file
73+ renders plain. Nothing here uses one; it is recorded rather than fixed, and the fix if
74+ it is ever wanted is to alias `jsx` onto the JavaScript syntax in `syntax_for`, not
75+ another crate. `the_languages_this_repository_uses_are_all_highlighted` asserts the
76+ sixteen that do work, so a future syntax-set swap cannot quietly reopen the hole.
6777
68 | Highlighted | **Plain** |
69 |---|---|
70 | Rust, shell, Markdown, Python, JavaScript, Go, YAML, JSON, CSS, SQL, Makefile, C | **TOML, Dockerfile, TypeScript, JSX/TSX** |
71
72 `Cargo.toml` and `Dockerfile` render unhighlighted in this repository today — the
73 `highlight-toml-dark.png` screenshot is exactly that. `two-face` bundles the missing
74 grammars and is the fix, **deliberately not added here**: it is another dependency and
75 a decision for the orchestrator. As briefed, it was not attempted.
7678 - **Highlighting costs ~87 ms per 1,255-line file in a release build** — measured on
7779 `browse.rs` with `cargo test --release`, and it is paid on every view because nothing
7880 caches. In the `topcoat dev` (debug) build the same page takes ~0.7 s to serve versus
@@ -91,6 +93,24 @@ highlighting. Nothing else changed: no new route, no new `GitQuery` method, **no
9193 - **The blob view's own header has no "this is Rust" indicator.** The language is
9294 detected and then only ever visible as colour.
9395
96+## The `two-face` decision, and what it cost
97+
98+Taken by the user after the first pass reported the gap.
99+
100+- **The syntax set is now `two_face::syntax::extra_newlines()`**, which is syntect's
101+ defaults plus the grammars `bat` curates. It is a drop-in for
102+ `SyntaxSet::load_defaults_newlines` — one line in the `OnceLock`. Nothing else moved:
103+ the `hl-` class output and every rule in `styles.css` are unchanged, because the extra
104+ grammars emit the same TextMate scopes the mapping was already written against.
105+- **The release binary grew 0.59 MiB, from 13.65 MB to 14.27 MB** (`cargo build
106+ --release`, measured on this branch with and without the dependency). That is the
107+ embedded grammar dump; it is data, not code. Worth knowing because the artifact is
108+ downloaded by `install.sh` over whatever connection an operator has, and because
109+ 5b's release notes quote a size.
110+- **No new C dependency.** `syntect-fancy` resolves to `syntect/regex-fancy`, the same
111+ pure-Rust engine already in use, so the musl/glibc reasoning recorded in `current.md`
112+ is untouched.
113+
94114 ## ui.md material
95115
96116 - The blob's code cell is the only place colour carries information rather than
src/infrastructure/highlight.rs+33 −14View file
@@ -104,14 +104,21 @@ pub fn source_lines(file_name: &str, text: &str) -> Source {
104104 Source { lines, too_large }
105105 }
106106
107/// The default syntax set, loaded once.
107+/// The syntax set, loaded once.
108+///
109+/// `two_face` rather than `SyntaxSet::load_defaults_newlines`: syntect's own defaults
110+/// are Sublime's, which have no TOML, no Dockerfile and no TypeScript — the first two
111+/// of which are in this repository. `two_face` is the same set plus the grammars `bat`
112+/// curates, so the gap closes without teaching this module anything about individual
113+/// languages. Its `syntect-fancy` feature pins it to the same pure-Rust regex engine,
114+/// so no `onig` — and therefore no C — comes in behind it.
108115 ///
109116 /// Loading is a few megabytes of deserialization, so it happens on the first blob
110117 /// viewed and never again. `_newlines` is the variant whose rules expect a trailing
111118 /// newline on each line, which is what [`LinesWithEndings`] hands it.
112119 fn syntaxes() -> &'static SyntaxSet {
113120 static SYNTAXES: OnceLock<SyntaxSet> = OnceLock::new();
114 SYNTAXES.get_or_init(SyntaxSet::load_defaults_newlines)
121+ SYNTAXES.get_or_init(two_face::syntax::extra_newlines)
115122 }
116123
117124 /// The language, from the file name and then the first line.
@@ -301,30 +308,42 @@ mod tests {
301308 assert_eq!(source.lines, vec![SourceLine::Plain("hello".to_owned())]);
302309 }
303310
304 /// The default syntax set is Sublime's, not a forge's, so the languages it lacks
305 /// are worth knowing rather than discovering on a page.
311+ /// The gap `two_face` was added to close, asserted rather than described: TOML,
312+ /// Dockerfile and TypeScript rendered plain until it landed, and the first two are
313+ /// in this repository. `.jsx` is still absent — see the handover.
306314 #[test]
307 fn which_common_languages_the_default_set_knows() {
315+ fn the_languages_this_repository_uses_are_all_highlighted() {
308316 for name in [
309317 "main.rs",
318+ "install.sh",
310319 "Cargo.toml",
311320 "Dockerfile",
312 "install.sh",
321+ "app.ts",
322+ "App.tsx",
313323 "README.md",
324+ "styles.css",
325+ "config.yaml",
326+ "data.json",
327+ "Makefile",
314328 "app.py",
315329 "index.js",
316 "app.ts",
317330 "main.go",
318 "config.yaml",
319 "data.json",
320 "styles.css",
321331 "query.sql",
322 "Makefile",
323332 "main.c",
324 "App.jsx",
325333 ] {
326 let known = syntax_for(syntaxes(), name, "").is_some();
327 println!("{name}: {}", if known { "highlighted" } else { "PLAIN" });
334+ assert!(
335+ syntax_for(syntaxes(), name, "").is_some(),
336+ "{name} has no syntax"
337+ );
328338 }
329339 }
340+
341+ #[test]
342+ fn toml_is_highlighted_and_not_merely_recognised() {
343+ let source = source_lines("Cargo.toml", "[package]\nname = \"steid\"\n");
344+
345+ assert!(matches!(source.lines[1], SourceLine::Classed(_)));
346+ let html = classes(&source.lines[1]);
347+ assert!(html.contains("hl-string"), "{html}");
348+ }
330349 }