feat(web): bundle HTML + CSS grammars + HTML injections
Register `html` (.html/.htm/.xhtml) and `css` (.css) entries in BUILTIN_LANGUAGES, backed by the official tree-sitter-html 0.23 and tree-sitter-css 0.25 grammars over the tree-sitter-language shim (ABI-fine, no overlay — both export their query constants). The single `extensions` field wires detection ahead of the LSP filetype map. HTML's crate-exported INJECTIONS_QUERY lights up <script> -> javascript (already registered) and <style> -> css (registered here) via the #122 injection engine — the north-star injection consumer. The only capture reconciliation (Q#WEB4): the two web captures both grammars' queries use that pmacs did not recognize — ("tag", fg(5)) and ("attribute", fg(3)) — added to highlight.rs's table; @tag.error prefix-walks to tag, and everything else already maps. Tests: table guards; load-and-parse smokes (roots document/stylesheet); highlights-resolve (node-name compat gate, asserts @tag present); extension resolution; a tag+attribute paint test (the attribute assertion is load-bearing); and the injection payoff — an HTML buffer with embedded <style>/<script> paints a CSS property and a JS keyword INSIDE the injected regions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
05eab9e4e5
commit
394d39942c
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 `<script>` ->
|
||||
# javascript (already registered) and `<style>` -> css via the #122 injection
|
||||
# engine (see `crate::syntax::BUILTIN_LANGUAGES`).
|
||||
tree-sitter-html = "0.23"
|
||||
tree-sitter-css = "0.25"
|
||||
# T M4.4 process supervisor: signal sending without `unsafe`. Keep
|
||||
# the feature surface tight to keep build time low. `poll` feeds the
|
||||
# compile-mode group readers (cancellable poll-based reads, Q#CM3).
|
||||
|
|
|
|||
125
src/highlight.rs
125
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()
|
||||
|
|
@ -1410,4 +1415,124 @@ mod tests {
|
|||
"the \\section title text is painted @keyword.control (bold)"
|
||||
);
|
||||
}
|
||||
|
||||
#[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: <a href="x">Hi</a> — `a` (tag_name) at col 1, `href`
|
||||
// (attribute_name) at col 3.
|
||||
let src = b"<a href=\"x\">Hi</a>\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<Cell> = 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,
|
||||
};
|
||||
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 <a> 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
|
||||
// <style> as CSS and <script> as JavaScript, and the child layers paint
|
||||
// INSIDE the embedded regions — styling only the injected grammars can
|
||||
// produce. `css` is resolvable only because this lane registered it.
|
||||
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: <style>a{color:red}</style> — `color` (CSS property) at col 9.
|
||||
// Line 1: <script>let x=1</script> — `let` (JS keyword) at col 8.
|
||||
let src = b"<style>a{color:red}</style>\n<script>let x=1</script>\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<Cell> = 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,
|
||||
};
|
||||
let registry = buf;
|
||||
hv.render(®istry, viewport, &mut grid);
|
||||
|
||||
// Inside <style>: the CSS `color` property paints (non-default) —
|
||||
// proves the <style> -> css injection resolved and parsed.
|
||||
assert_ne!(
|
||||
grid.get(CellCoord::new(0, 9)).style,
|
||||
Cell::default().style,
|
||||
"the CSS `color` property paints inside the <style> injection"
|
||||
);
|
||||
// Inside <script>: the JS `let` keyword paints bold — proves the
|
||||
// <script> -> javascript injection resolved and parsed.
|
||||
assert!(
|
||||
grid.get(CellCoord::new(1, 8)).style.bold,
|
||||
"the JS `let` keyword paints (bold) inside the <script> injection"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
143
src/syntax.rs
143
src/syntax.rs
|
|
@ -1108,6 +1108,28 @@ pub const BUILTIN_LANGUAGES: &[LanguageEntry] = &[
|
|||
locals_query: &[],
|
||||
injections_query: &[],
|
||||
},
|
||||
// HTML + CSS (framing `docs/web-grammars-html-css-framing.md`). Both crates
|
||||
// export their query constants (no overlay). HTML's `INJECTIONS_QUERY`
|
||||
// wires `<script>` -> javascript (already registered) and `<style>` -> css
|
||||
// (below), riding the #122 injection engine; `css` must be registered here
|
||||
// for that injection to resolve. The `tag`/`attribute` captures these
|
||||
// queries use are taught to the highlighter in `crate::highlight` (Q#WEB4).
|
||||
LanguageEntry {
|
||||
name: "html",
|
||||
extensions: &["html", "htm", "xhtml"],
|
||||
loader: || tree_sitter_html::LANGUAGE.into(),
|
||||
highlights_query: &[tree_sitter_html::HIGHLIGHTS_QUERY],
|
||||
locals_query: &[],
|
||||
injections_query: &[tree_sitter_html::INJECTIONS_QUERY],
|
||||
},
|
||||
LanguageEntry {
|
||||
name: "css",
|
||||
extensions: &["css"],
|
||||
loader: || tree_sitter_css::LANGUAGE.into(),
|
||||
highlights_query: &[tree_sitter_css::HIGHLIGHTS_QUERY],
|
||||
locals_query: &[],
|
||||
injections_query: &[],
|
||||
},
|
||||
];
|
||||
|
||||
/// LaTeX highlights overlay (framing Q#LX2). The chosen grammar crate
|
||||
|
|
@ -2372,6 +2394,127 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_languages_include_html_and_css() {
|
||||
// Both crate grammars export their query constants (no overlay). HTML
|
||||
// additionally carries an injections query (script/style); CSS does not.
|
||||
let html = BUILTIN_LANGUAGES
|
||||
.iter()
|
||||
.find(|l| l.name == "html")
|
||||
.expect("`html` language entry must be present");
|
||||
for ext in ["html", "htm", "xhtml"] {
|
||||
assert!(html.extensions.contains(&ext), "`html` claims `.{ext}`");
|
||||
}
|
||||
assert!(
|
||||
!html.highlights_query.is_empty(),
|
||||
"`html` ships a highlights query"
|
||||
);
|
||||
assert!(
|
||||
!html.injections_query.is_empty(),
|
||||
"`html` ships an injections query (script/style)"
|
||||
);
|
||||
let css = BUILTIN_LANGUAGES
|
||||
.iter()
|
||||
.find(|l| l.name == "css")
|
||||
.expect("`css` language entry must be present");
|
||||
assert!(css.extensions.contains(&"css"), "`css` claims `.css`");
|
||||
assert!(
|
||||
!css.highlights_query.is_empty(),
|
||||
"`css` ships a highlights query"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_grammar_loads_and_parses() {
|
||||
// ABI acceptance: `tree-sitter-html` (LanguageFn over
|
||||
// `tree-sitter-language 0.1`) is accepted by our `tree-sitter` 0.26 core.
|
||||
let reg = SyntaxRegistry::new();
|
||||
let language = reg
|
||||
.language("html")
|
||||
.expect("`html` language loads from BUILTIN_LANGUAGES");
|
||||
let mut buf = fresh_buffer("index.html");
|
||||
buf.apply_edit(EditOp::Insert {
|
||||
pos: 0,
|
||||
bytes: b"<!DOCTYPE html>\n<html><body><a href=\"x\">Hi</a></body></html>\n",
|
||||
})
|
||||
.unwrap();
|
||||
let view = ParseView::new(&buf, language, "html".to_owned());
|
||||
let handle = view.handle();
|
||||
let _vid = buf.attach_view(Box::new(view));
|
||||
let bundle = parse_synchronously(&handle);
|
||||
assert_eq!(
|
||||
bundle.root_tree().root_node().kind(),
|
||||
"document",
|
||||
"HTML grammar roots at document"
|
||||
);
|
||||
assert!(
|
||||
!bundle.root_tree().root_node().has_error(),
|
||||
"HTML grammar parses a document without error"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn css_grammar_loads_and_parses() {
|
||||
let reg = SyntaxRegistry::new();
|
||||
let language = reg
|
||||
.language("css")
|
||||
.expect("`css` language loads from BUILTIN_LANGUAGES");
|
||||
let mut buf = fresh_buffer("style.css");
|
||||
buf.apply_edit(EditOp::Insert {
|
||||
pos: 0,
|
||||
bytes: b"a { color: red; }\n",
|
||||
})
|
||||
.unwrap();
|
||||
let view = ParseView::new(&buf, language, "css".to_owned());
|
||||
let handle = view.handle();
|
||||
let _vid = buf.attach_view(Box::new(view));
|
||||
let bundle = parse_synchronously(&handle);
|
||||
assert_eq!(
|
||||
bundle.root_tree().root_node().kind(),
|
||||
"stylesheet",
|
||||
"CSS grammar roots at stylesheet"
|
||||
);
|
||||
assert!(
|
||||
!bundle.root_tree().root_node().has_error(),
|
||||
"CSS grammar parses a rule without error"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_and_css_highlights_resolve() {
|
||||
// The crate-exported queries compile against their grammars (node-name
|
||||
// compatibility gate), and both use the `@tag` capture this lane teaches
|
||||
// the highlighter (Q#WEB4).
|
||||
let reg = SyntaxRegistry::new();
|
||||
for lang in ["html", "css"] {
|
||||
let query = reg
|
||||
.highlights_query(lang)
|
||||
.unwrap_or_else(|| panic!("{lang} highlights compile against the grammar"));
|
||||
let names = query.capture_names();
|
||||
assert!(
|
||||
names.contains(&"tag"),
|
||||
"{lang} highlights use the @tag capture; got {names:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_for_path_resolves_web_extensions() {
|
||||
let reg = SyntaxRegistry::new();
|
||||
for (path, lang) in [
|
||||
("index.html", "html"),
|
||||
("page.htm", "html"),
|
||||
("doc.xhtml", "html"),
|
||||
("style.css", "css"),
|
||||
] {
|
||||
assert_eq!(
|
||||
reg.language_name_for_path(path).as_deref(),
|
||||
Some(lang),
|
||||
"{path} resolves to {lang}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_languages_include_bash() {
|
||||
// Regression guard: the bash entry claims the wider shell family
|
||||
|
|
|
|||
Loading…
Reference in New Issue