pmacs/docs/web-grammars-html-css-frami...

320 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Web grammars (HTML + CSS) + HTML injections — framing
**Revision 4 — implemented on branch `web-grammars` (PR #146). Ground truth:
canonical `main` @ `4daa1b8` (after LaTeX #144 and inline-math-docs #145),
2026-07-23.** Rev 2 settled the capture set, injection scope, and LSP claim
(round 1); rev 3 corrected the `#match?` predicate analysis (round 2); rev 4
names the intended `@attribute` retro-paint on the already-bundled rust/lua/yaml
grammars — verified on a Rust buffer and pinned by a test (round 3). See §0.1.
## 0.1 Revision history
### Round 1 (rev 1 → rev 2)
- **F1 (high).** Q#WEB4 deferred the capture mapping. The upstream queries
settle it: HTML and CSS need exactly **`tag`** and **`attribute`**; `@tag.error`
prefix-walks to `tag`; there is **no** `@tag.delimiter` (rev-1 speculation).
Q#WEB4 now names both mappings and their styles, and acceptance 6 paints an
HTML **attribute** explicitly (a tag/property-only test could pass with
`attribute` unverified since `property` is already recognized).
- **F2 (medium).** The HTML injections query has **exactly two** patterns —
`script_element`→javascript, `style_element`→css. Q#WEB5 no longer claims
event-handler / `style=` coverage; inline-attribute injection is now a named
deferral.
- **F3 (medium).** No HTML/CSS language server is bundled: `lsp.lua`'s default
config list ends at YAML (`:247`) and `:521` returns `nil` without a
configured command. §2 and §5 corrected — the grammars give stable language
IDs so a *user-supplied* config attaches automatically, but nothing ships or
starts by default.
### Round 2 (rev 2 → rev 3)
- **R2-1 (medium).** The rev-2 claim that pmacs *ignores* `#match?` was wrong.
`src/syntax.rs:1701` passes the source to `QueryCursor::captures`, so
tree-sitter evaluates the standard `#match?`/`#eq?`/`#any-of?` text predicates
natively (pmacs special-cases only `#is? local`, `:1703`). Corrected in §2 and
Q#WEB4: an ordinary CSS property has a single `@property` capture; only a
`--custom-property` also receives `@variable` (a benign double-capture).
Acceptance 6 no longer claims an ordinary property pins custom-property
precedence — it just verifies `color``@property` paints.
- **R2-2 (low).** The folding footprint in §0 was stale. At
`githubsucks/folding` @ `036a994` the branch touches `src/fold.rs`,
`editor.rs`, `editor_core.rs`, `lib.rs`, `lua_bindings/{fold.rs,mod.rs}`,
`semantic_render.rs`, `fold.lua`, and tests — not
`overlay.rs`/`daemon.rs`/`syntax.rs`. §0 now lists the real set; the
zero-overlap conclusion is unchanged (stronger, if anything).
### Round 3 (rev 3 → rev 4)
- **R3-1 (medium).** Q#WEB4 verified which captures pmacs *lacks* but not the
reverse: which already-bundled grammars *use* `@attribute`. Three do — rust
(`attribute_item`), lua (`<const>`), yaml (directives) — so the new
`("attribute", fg(3))` entry retro-paints their previously-unpainted spans
yellow. Named in Q#WEB4 as intended (verified on a Rust buffer: uniform
yellow, an improvement over unpainted), and pinned by
`rust_attribute_repaints_via_shared_attribute_capture`. `tag` is unaffected
(HTML/CSS only).
Add tree-sitter **HTML** and **CSS** grammars so `.html`/`.htm` and `.css`
buffers get lexical highlighting, and — the north-star payoff — light up
HTML's embedded-language **injections**: `<script>` → JavaScript (already
registered) and `<style>` → CSS (added here). This is the side-quest
backlog's "HTML/CSS grammars that light up more injection consumers."
## 0. Why this lane, why now (parallel-safety)
Board at this snapshot: LaTeX #144 and inline-math-docs #145 merged;
**folding Stage 1 (#142) is the only open PR** — a headless fold engine. Its
current diff (`main...githubsucks/folding` @ `036a994`) touches `src/fold.rs`
(new), `src/editor.rs`, `src/editor_core.rs`, `src/lib.rs`,
`src/lua_bindings/{fold.rs,mod.rs}`, `src/semantic_render.rs`,
`builtin/runtime/fold.lua`, and tests — **none of which this lane touches**.
gpu-invocation has landed. The GPU render path (`pmacs-gpu/src/main.rs`) remains
contended by folding's later Stage 3 — this lane never touches it.
This lane's footprint: two `Cargo.toml` grammar deps, two `BUILTIN_LANGUAGES`
entries, and — per Q#WEB4 — a small extension of `src/highlight.rs`'s recognized
capture set. **Zero file overlap with folding**; the three files it edits:
- `src/syntax.rs` — a localized append to the language table (after the `latex`
entry, currently ~`:1111`). Folding does not touch this file.
- `Cargo.toml` — two dependency lines in the grammar block (`:142``:220`).
- `src/highlight.rs` — appends to the capture-style `entries` table
(`:143-172`). Folding does not touch this file.
All are localized, low-conflict edits — the same pattern #144 and the
config-registry ∥ Vterm lanes landed conflict-free.
Unlike LaTeX (#144), **no in-repo query overlay is needed** — both crates
export their queries as constants. The overlay convention #144 established
stays available for future crates that don't (e.g. some of ruby/php).
## 1. What ships
- **HTML grammar** (`.html`/`.htm`/`.xhtml`) — tags, attributes, text,
comments, doctype.
- **CSS grammar** (`.css`) — selectors, properties, values, at-rules,
comments.
- **HTML injections** — `<script>…</script>` parses as JavaScript and
`<style>…</style>` parses as CSS, via HTML's crate-exported injections query
riding the #122 injection engine. CSS is registered here so the `<style>`
injection resolves.
## 2. Ground truth (scouted 2026-07-23, `main` @ `4daa1b8`)
### Crate facts (verified)
- **`tree-sitter-html` 0.23.2** (2026-06). Exports `LANGUAGE: LanguageFn`,
**`HIGHLIGHTS_QUERY`**, **`INJECTIONS_QUERY`**, `NODE_TYPES`. Deps:
`tree-sitter-language ^0.1` (the shim every pmacs grammar uses),
`tree-sitter ^0.24` **dev-only** → ABI-compatible with our `tree-sitter 0.26`
via `.into()`. Its `INJECTIONS_QUERY` is the load-bearing piece: it names
`javascript` and `css` via `#set! injection.language`.
- **`tree-sitter-css` 0.25.0** (2026-05). Exports `LANGUAGE: LanguageFn`,
**`HIGHLIGHTS_QUERY`**, `NODE_TYPES`. Deps: `tree-sitter-language ^0.1`,
`tree-sitter ^0.25` **dev-only** → shim-ABI-fine. No injections query (CSS
injects nothing).
Both export their highlights query as a constant, so **no overlay/vendoring**
(the LaTeX complication) applies.
### Codebase
- **`BUILTIN_LANGUAGES`** (`src/syntax.rs:816`…`];`) currently ends at the
`latex` entry (~`:1104-1111`, added by #144). No `html`/`css` entry. Append
the two new entries before `];`.
- **`javascript` is registered** (`src/syntax.rs:1010`, `extensions: js/mjs/cjs`,
`injections_query: &[]`), so HTML's `<script>``javascript` resolves today.
`css` does not exist → HTML's `<style>``css` resolves only once this lane
adds it.
- **Injection engine** — `collect_injection_matches` (`src/syntax.rs:375-427`)
reads `@injection.content` (`:381`) + `injection.language` (dynamic node text
or `#set!`, `:387-402`) + `injection.include-children` (`:402`).
`resolve_injected_language` (`:304`) resolves the name against
`BUILTIN_LANGUAGES` + `default_injection_aliases` (`:234`). Working
precedents: rust `INJECTIONS_QUERY` (`:823`), markdown block/inline
(`:848`/`:862`). HTML's `#set! injection.language "javascript"|"css"` is the
same shape.
- **Recognized capture set** — `src/highlight.rs:143-172`, resolved by a
dotted-prefix walk (`:189`). Confirmed against the two upstream queries
(HTML v0.23.2 `highlights.scm`, CSS v0.25.0 `highlights.scm`): the **only**
captures not already recognized are **`@tag`** and **`@attribute`** — used by
BOTH grammars (`@tag.error` prefix-walks to `tag`; there is no `@tag.delimiter`).
Everything else maps: HTML's `@constant` (doctype), `@string` (attribute
value), `@comment`, `@punctuation.bracket`; CSS's `@operator`/`@property`/
`@function`/`@keyword`/`@number`/`@type`/`@string.special`/`@punctuation.*`.
CSS also has two `#match?`-gated `@variable` patterns for `--custom-props`.
pmacs passes the buffer text to `QueryCursor::captures` (`src/syntax.rs:1701`),
so tree-sitter **evaluates** the standard `#match?`/`#eq?`/`#any-of?` text
predicates natively; pmacs adds handling only for the `#is? local` property
predicate (the `property_predicates … "local"` filter at `:1703`). So an
ordinary property (`color`) has a single `@property` capture, and only a custom
property (`--brand`) additionally receives `@variable` — see Q#WEB4.
- **Detection** — the `extensions` field drives `language_name_for_path`
(`:1223`) ahead of the LSP filetype map in the Lua chain
(`builtin/runtime/syntax.lua:452-466`); no Lua edit. **No HTML/CSS language
server is bundled**: `builtin/runtime/lsp.lua`'s default config list ends at
YAML (`:247`), and `:521` returns `nil` without a configured command. The
grammars provide stable language IDs (`html`/`css`), so a **user-supplied**
`pmacs.lsp.config.html`/`.css` attaches automatically, but nothing ships or
starts by default.
## 3. Decisions
### Q#WEB1 — Bundle `tree-sitter-html` 0.23 + `tree-sitter-css` 0.25
Add both to the `Cargo.toml` grammar block; loaders
`|| tree_sitter_html::LANGUAGE.into()` and `|| tree_sitter_css::LANGUAGE.into()`.
ABI is fine via the shared `tree-sitter-language` shim, as every current
grammar. No provenance saga (these are the official tree-sitter-org grammars,
not squatted republishes).
### Q#WEB2 — Register both; CSS before HTML's injection can resolve
- `html`: `highlights_query: &[tree_sitter_html::HIGHLIGHTS_QUERY]`,
`injections_query: &[tree_sitter_html::INJECTIONS_QUERY]`, `locals_query: &[]`.
- `css`: `highlights_query: &[tree_sitter_css::HIGHLIGHTS_QUERY]`, injections and
locals empty.
Both live in the same `BUILTIN_LANGUAGES`, so `resolve_injected_language`
finds `css` (and the existing `javascript`) when HTML's injections query fires.
### Q#WEB3 — Extensions
`html`: `["html", "htm", "xhtml"]`. `css`: `["css"]`. SCSS/LESS/Sass are
distinct grammars (`scss`/`less` node sets) and are deferred (§5).
### Q#WEB4 — Add exactly two capture entries: `tag` and `attribute`
The LaTeX lane could rename captures because it owned an editable overlay.
Here the highlights queries are **crate constants** — not editable — so the
reconciliation is to **extend `src/highlight.rs`'s `entries` table**
(`:143-172`). The upstream queries settle the exact set: the **only** captures
neither grammar's query already resolves are `tag` and `attribute`. Add exactly
two entries:
- `("tag", fg(5))` — HTML `(tag_name)` and CSS element/nesting/universal
selectors, in the keyword hue (magenta) but non-bold to stay light in dense
markup. `@tag.error` (HTML erroneous end tags) prefix-walks to this entry, so
it needs no separate mapping.
- `("attribute", fg(3))` — HTML `(attribute_name)` and CSS
pseudo-/attribute-selector names, in the type hue (yellow) — distinct from
`tag`, from `property`/`operator` (cyan), and from `string` values (green).
There is **no** `tag.delimiter` (rev-1 speculation, removed); HTML's `<`/`>`/
`</`/`/>` are `@punctuation.bracket`, already handled. This extension is:
- **low-conflict** — folding does not touch `highlight.rs`;
- **general** — `tag`/`attribute` are standard tree-sitter web captures, so it
also serves future html-ish grammars (vue/svelte/astro).
**Retro-paint on already-bundled languages (intended, added rev 4).** The
capture table is global, so adding `attribute` also colours the `@attribute`
capture that three bundled grammars already emit but which was previously
unrecognized (and so unpainted): **rust** (`attribute_item`/`inner_attribute_item`
— every `#[derive(…)]` / `#![…]`), **lua** (the `<const>`/`<close>` variable
attribute), and **yaml** (`%YAML`/`%TAG` directives). On merge these begin
painting `attribute` yellow (`fg 3`). Verified on a Rust buffer: `#[derive(Debug)]`
paints uniformly yellow (the `attribute_item` span carries no narrower overriding
captures) — a distinct-attribute convention most editors follow, and an
improvement over unpainted. This is **chosen, not incidental**: it is pinned by
`rust_attribute_repaints_via_shared_attribute_capture` (`src/highlight.rs`).
`tag` is clean — only HTML/CSS use it, so it has no retro-effect.
**On CSS custom properties (corrected in rev 3):** pmacs passes the buffer text
to `QueryCursor::captures` (`src/syntax.rs:1701`), so tree-sitter evaluates the
standard `#match?`/`#eq?`/`#any-of?` predicates natively — pmacs special-cases
only `#is? local`. So an ordinary property (`color`) matches only the
unconditional `(property_name) @property` and paints cleanly; a custom property
(`--brand`) additionally satisfies `#match? "^--"` and also receives `@variable`
— a benign double-capture whose winner is a within-layer precedence detail, out
of scope for v0 and not relied upon. Rejected: a shadow overlay re-capturing the
same nodes with recognized names (fragile, precedence-dependent, duplicative).
### Q#WEB5 — Injection scope: script + style only
HTML's `INJECTIONS_QUERY` (v0.23.2) has **exactly two** patterns:
`(script_element (raw_text) @injection.content) (#set! injection.language "javascript")`
and the same for `(style_element …)``"css"`. There is **no** event-handler
(`onclick=…`) or inline `style=` attribute injection — those are a named
deferral (§5). Each element is its own subtree, so `injection.combined` (many
matches → one shared parse, the PHP-in-HTML case) is not needed and stays
deferred. Acceptance pins script + style.
### Q#WEB6 — No protocol, frontend, or GPU change
Pure instance-side: two grammar entries, one injections query, a capture-table
extension. No wire type, no TUI/GPU edit.
## 4. Categorical bets
1. **The shim makes ABI a non-issue.** Both crates ride `tree-sitter-language
0.1`, exactly like every bundled grammar.
2. **HTML's value is the injection, not the tags.** Highlighting a web page
*with* its embedded JS and CSS is the north-star injection consumer;
registering CSS is the prerequisite, which is why the two grammars ship
together as one lane.
3. **Extending the core capture table is the correct reconciliation for
crate-exported queries.** You cannot rename a `const` query's captures;
teaching the highlighter the standard web captures is the general fix and
costs one localized, uncontended edit.
## 5. Deferred (named)
- **SCSS / LESS / Sass** grammars (distinct node sets; own extensions).
- **`injection.combined`** (PHP-in-HTML and other many→one-parse schemes) — the
existing side-quest deferral; not needed for script/style.
- **CSS-in-JS / HTML-in-JS** via `tree-sitter-javascript`'s `INJECTIONS_QUERY`
(the `javascript` entry is `injections_query: &[]` today) — a clean
follow-up: add the JS injections query so tagged template literals
(`` css`…` ``, `` html`…` ``) parse. Separate consumer, separate PR.
- **Inline HTML attribute injection** — event handlers (`onclick=…`) and
`style=` attributes are not in the upstream injections query (Q#WEB5); adding
them would need a pmacs-side injections overlay.
- **Vue / Svelte / Astro** single-file-component grammars.
- **Bundled HTML/CSS LSP configs** — no server ships today (§2). Shipping
default `pmacs.lsp.config.html`/`.css` entries (vscode-langservers-extracted)
is a separate follow-up; until then the grammar's stable language ID lets a
user config attach automatically.
## 6. Acceptance
Mirrors the #144 / CUDA conventions (`src/syntax.rs` table guards + smokes;
`src/highlight.rs` paint template `grid_paints_injected_child_keyword`).
1. **Table guards** `builtin_languages_include_html` / `_css`: entries exist,
claim their extensions, carry non-empty highlights; `html` also carries
`INJECTIONS_QUERY`.
2. **Load-and-parse smokes**: a minimal HTML document and a CSS rule parse with
the expected root node kind and `!has_error()` (exact kinds — likely
`document` / `stylesheet` — confirmed at implementation).
3. **Highlights resolve**: `reg.highlights_query("html")` / `("css")` compile
against their grammars (the node-name compatibility gate).
4. **Extension resolution**: `.html`/`.htm`/`.xhtml` → `html`; `.css``css`.
5. **Injection (the payoff)**: an HTML buffer with
`<style>a{color:red}</style>` and `<script>let x=1</script>` 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 `<a href="x">` and assert **both**
the `<a>` 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.