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