diff --git a/Cargo.lock b/Cargo.lock
index f0b1daa..d7caff6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2566,8 +2566,10 @@ dependencies = [
"tree-sitter-cmake",
"tree-sitter-containerfile",
"tree-sitter-cpp",
+ "tree-sitter-css",
"tree-sitter-cuda",
"tree-sitter-go",
+ "tree-sitter-html",
"tree-sitter-javascript",
"tree-sitter-json",
"tree-sitter-lua",
@@ -3794,6 +3796,16 @@ dependencies = [
"tree-sitter-language",
]
+[[package]]
+name = "tree-sitter-css"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5cbc5e18f29a2c6d6435891f42569525cf95435a3e01c2f1947abcde178686f"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
[[package]]
name = "tree-sitter-cuda"
version = "0.21.1"
@@ -3814,6 +3826,16 @@ dependencies = [
"tree-sitter-language",
]
+[[package]]
+name = "tree-sitter-html"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
[[package]]
name = "tree-sitter-javascript"
version = "0.25.0"
diff --git a/Cargo.toml b/Cargo.toml
index e49da58..589ccaf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -233,6 +233,14 @@ tree-sitter-md = "0.5"
# uses the in-repo overlay `builtin/queries/latex/highlights.scm` (the first
# such overlay; `include_str!`'d in `crate::syntax::BUILTIN_LANGUAGES`).
codebook-tree-sitter-latex = "0.6"
+# Web grammars: HTML + CSS (`.html`/`.htm`/`.xhtml`, `.css`). The official
+# tree-sitter-org grammars; both export `LANGUAGE` + query constants over
+# `tree-sitter-language 0.1` (shared ABI crate, `tree-sitter` dev-only), so no
+# overlay is needed. HTML's `INJECTIONS_QUERY` lights up `` parses as JavaScript and
+ `` parses as CSS, via HTML's crate-exported injections query
+ riding the #122 injection engine. CSS is registered here so the `` and `` produces a
+ `ParseTreeBundle` whose child layers resolve to `css` and `javascript`, and
+ a grid-paint asserts an injected CSS property and JS keyword paint **inside**
+ the embedded regions (mirroring `grid_paints_injected_child_keyword`).
+6. **Paint (non-vacuous highlighting)**: parse `` and assert **both**
+ the `` tag (`@tag`) **and** the `href` attribute name (`@attribute`) paint
+ the two new non-default styles — the attribute assertion is load-bearing, since
+ a tag-only test could pass with `@attribute` unverified. Also assert a CSS
+ selector (`@tag`) paints the new `tag` style and an ordinary CSS property
+ (`color` → `@property`) paints non-default (an ordinary property has a single
+ unconditional capture, so no precedence subtlety — Q#WEB4).
+7. **Full gate suite** per `CLAUDE.md`.
+
+## 7. Prior art in pmacs
+
+- **LaTeX lane #144** (`docs/latex-grammar-math-substrate-framing.md`) — the
+ grammar-add mechanics, the table-guard/smoke/paint test conventions, and the
+ compile-gate-plus-paint-test discipline. (Its overlay convention is not
+ needed here.)
+- **Multi-language injections #122**
+ (`docs/multi-language-injections-framing.md`) — the `ParseTreeBundle` +
+ `Layer` engine HTML's injections ride.
+- **markdown → rust fenced code** (`src/syntax.rs:848`, tested by
+ `src/highlight.rs`'s `grid_paints_injected_child_keyword`) — the working
+ injection precedent this lane's acceptance 5 mirrors.
diff --git a/src/highlight.rs b/src/highlight.rs
index e7e5a98..2c7369f 100644
--- a/src/highlight.rs
+++ b/src/highlight.rs
@@ -169,6 +169,11 @@ impl Theme {
("decorator", fg(13)),
("regexp", fg(2)),
("typeParameter", fg_italic(11)),
+ // Web grammars (HTML/CSS, framing Q#WEB4): the only captures their
+ // crate-exported queries use that the set above lacks. `@tag.error`
+ // prefix-walks to `tag`.
+ ("tag", fg(5)),
+ ("attribute", fg(3)),
];
let by_capture = entries
.iter()
@@ -1422,4 +1427,182 @@ mod tests {
"the \\section title text is painted @keyword.control (bold)"
);
}
+
+ #[test]
+ fn rust_attribute_repaints_via_shared_attribute_capture() {
+ // Intended side effect, named in the framing (Q#WEB4): the
+ // `("attribute", fg(3))` entry added for HTML/CSS also colours the
+ // `@attribute` capture that tree-sitter-rust (attribute_item), -lua
+ // (``), and -yaml (directives) already emit. A Rust
+ // `#[derive(Debug)]` — previously unpainted, since `@attribute` was
+ // unrecognized — now paints the attribute style throughout. Pinned so
+ // the retro-paint on this repo's primary language is a chosen effect,
+ // not an incidental one.
+ use crate::buffer::{Buffer, BufferId, EditOp};
+ use crate::cell::{Cell, CellSize};
+ use crate::syntax::{ParseView, SyntaxRegistry};
+
+ let reg = SyntaxRegistry::new();
+ let language = reg.language("rust").expect("rust grammar");
+ let src = b"#[derive(Debug)]\nstruct S;\n";
+ let mut buf = Buffer::new(BufferId::next(), "a.rs");
+ buf.apply_edit(EditOp::Insert { pos: 0, bytes: src })
+ .unwrap();
+ let view = ParseView::new(&buf, language, "rust".to_owned());
+ let handle = view.handle();
+ let _vid = buf.attach_view(Box::new(view));
+ let mut req = handle.make_request();
+ req.injection_aliases = reg.injection_alias_snapshot();
+ let bundle = crate::syntax::run_parse(req).expect("rust parse");
+ handle.install(reg.resolve_layer_queries(&bundle));
+
+ let mut hv = SyntaxHighlightView::new(handle, reg.theme());
+ let (rows, cols) = (1usize, 20usize);
+ let mut backing: Vec = vec![Cell::default(); rows * cols];
+ let mut grid = CellGrid {
+ cells: &mut backing,
+ stride: cols as u32,
+ size: CellSize::new(rows as u32, cols as u32),
+ };
+ let viewport = Viewport {
+ buffer_start: 0,
+ buffer_end: u64::MAX,
+ cell_origin: CellCoord::new(0, 0),
+ cell_size: CellSize::new(rows as u32, cols as u32),
+ gutter_w: 0,
+ folds: None,
+ };
+ let registry = buf;
+ hv.render(®istry, viewport, &mut grid);
+
+ // `derive` (col 2) sits inside the `attribute_item` and paints the
+ // shared @attribute style (fg 3) — the intended retro-paint.
+ assert_eq!(
+ grid.get(CellCoord::new(0, 2)).style.fg,
+ pmacs_protocol::cell::Color::Indexed(3),
+ "a Rust #[derive] attribute paints the shared @attribute style (fg 3)"
+ );
+ }
+
+ #[test]
+ fn web_grid_paints_html_tag_and_attribute() {
+ // Q#WEB4 acceptance: the two capture entries this lane adds (`tag`,
+ // `attribute`) actually reach painted cells. The attribute assertion is
+ // load-bearing — a tag-only test could pass with `@attribute` unverified.
+ use crate::buffer::{Buffer, BufferId, EditOp};
+ use crate::cell::{Cell, CellSize};
+ use crate::syntax::{ParseView, SyntaxRegistry};
+
+ let reg = SyntaxRegistry::new();
+ let language = reg.language("html").expect("html grammar");
+ // Line 0: Hi — `a` (tag_name) at col 1, `href`
+ // (attribute_name) at col 3.
+ let src = b"Hi\n";
+ let mut buf = Buffer::new(BufferId::next(), "index.html");
+ buf.apply_edit(EditOp::Insert { pos: 0, bytes: src })
+ .unwrap();
+ let view = ParseView::new(&buf, language, "html".to_owned());
+ let handle = view.handle();
+ let _vid = buf.attach_view(Box::new(view));
+ let mut req = handle.make_request();
+ req.injection_aliases = reg.injection_alias_snapshot();
+ let bundle = crate::syntax::run_parse(req).expect("html parse");
+ handle.install(reg.resolve_layer_queries(&bundle));
+
+ let mut hv = SyntaxHighlightView::new(handle, reg.theme());
+ let (rows, cols) = (1usize, 40usize);
+ let mut backing: Vec = vec![Cell::default(); rows * cols];
+ let mut grid = CellGrid {
+ cells: &mut backing,
+ stride: cols as u32,
+ size: CellSize::new(rows as u32, cols as u32),
+ };
+ let viewport = Viewport {
+ buffer_start: 0,
+ buffer_end: u64::MAX,
+ cell_origin: CellCoord::new(0, 0),
+ cell_size: CellSize::new(rows as u32, cols as u32),
+ gutter_w: 0,
+ folds: None,
+ };
+ let registry = buf;
+ hv.render(®istry, viewport, &mut grid);
+
+ let tag_cell = grid.get(CellCoord::new(0, 1));
+ let attr_cell = grid.get(CellCoord::new(0, 3));
+ assert_ne!(
+ tag_cell.style,
+ Cell::default().style,
+ "the tag_name paints @tag (non-default)"
+ );
+ assert_ne!(
+ attr_cell.style,
+ Cell::default().style,
+ "the href attribute_name paints @attribute (non-default)"
+ );
+ assert_ne!(
+ tag_cell.style, attr_cell.style,
+ "@tag and @attribute use the two distinct new styles"
+ );
+ }
+
+ #[test]
+ fn html_injects_css_and_js() {
+ // The payoff (Q#WEB acceptance 5): HTML's INJECTIONS_QUERY parses
+ // — `color` (CSS property) at col 9.
+ // Line 1: — `let` (JS keyword) at col 8.
+ let src = b"\n\n";
+ let mut buf = Buffer::new(BufferId::next(), "page.html");
+ buf.apply_edit(EditOp::Insert { pos: 0, bytes: src })
+ .unwrap();
+ let view = ParseView::new(&buf, language, "html".to_owned());
+ let handle = view.handle();
+ let _vid = buf.attach_view(Box::new(view));
+ let mut req = handle.make_request();
+ req.injection_aliases = reg.injection_alias_snapshot();
+ let bundle = crate::syntax::run_parse(req).expect("html parse");
+ handle.install(reg.resolve_layer_queries(&bundle));
+
+ let mut hv = SyntaxHighlightView::new(handle, reg.theme());
+ let (rows, cols) = (2usize, 60usize);
+ let mut backing: Vec| = vec![Cell::default(); rows * cols];
+ let mut grid = CellGrid {
+ cells: &mut backing,
+ stride: cols as u32,
+ size: CellSize::new(rows as u32, cols as u32),
+ };
+ let viewport = Viewport {
+ buffer_start: 0,
+ buffer_end: u64::MAX,
+ cell_origin: CellCoord::new(0, 0),
+ cell_size: CellSize::new(rows as u32, cols as u32),
+ gutter_w: 0,
+ folds: None,
+ };
+ let registry = buf;
+ hv.render(®istry, viewport, &mut grid);
+
+ // Inside | | |