---
name: html-conformance
description: >
Incrementally make Panache's CST shape for HTML-block /
raw-HTML conform to pandoc's AST shape under `Flavor::Pandoc`, so
downstream consumers (linter, salsa anchor index, LSP, formatter)
see the same structural decisions pandoc would have made. The
`pandoc -f markdown -t native` projector at
`crates/panache-parser/src/pandoc_ast.rs` is a test-only
diagnostic - divergence from pandoc-native points at a wrong CST,
not a fix-it-here problem. Lift HTML structure into the CST by
tokenizing existing source bytes at finer granularity (e.g.
`HTML_ATTRS` inside `HTML_BLOCK_TAG`), retag wrappers (e.g.
`HTML_BLOCK` → `HTML_BLOCK_DIV`), and emit inner block content as
real CST children — so the projector becomes a trivial structural
walk rather than a second-stage parser.
---
Use this skill when asked to advance Panache's HTML conformance,
unblock a regression that involves raw HTML attributes (issue #263 and
its descendants), or pick "the next best phase" of the HTML lift.
## What this skill is NOT
- **Not a chase for the conformance pass-rate.** The pass-rate is a
metric, not a goal. A passing case can still hide a wrong CST if
the projector compensates (re-parses bytes, walks text instead of
children, makes context-dependent decisions at projection time).
When that happens, the projector silently absorbs structural bugs
the CST should have surfaced.
- **Not a place to add projector logic that papers over CST gaps.**
The projector at `crates/panache-parser/src/pandoc_ast.rs` is a
test-only diagnostic — its job is to reveal CST shape problems by
diffing against pandoc-native. Putting logic there to make a test
pass while the CST stays wrong destroys the diagnostic value. The
consumers of structural HTML decisions (linter, salsa, LSP,
formatter) read the CST, not the projector output.
- **Not "make it look like pandoc's output text."** The objective is
for our CST to encode the same structural decisions pandoc encodes
in its AST — Plain vs Para, Div vs RawBlock, RawBlock vs RawInline,
matched-pair vs single emit, etc. — so reading the CST gives you
the same answers reading pandoc's AST would.
If a session's diff is mostly in `pandoc_ast.rs` (other than removing
existing compensation), that's a smell. The fix probably belongs in
the parser.
## Scope boundaries
- Target is HTML-block + raw-HTML parsing under `Flavor::Pandoc`.
Block-level: `crates/panache-parser/src/parser/blocks/html_blocks.rs`
+ `block_dispatcher.rs`. Inline-level:
`crates/panache-parser/src/parser/inlines/inline_html.rs`. Projection:
`crates/panache-parser/src/pandoc_ast.rs`. Salsa indexer:
`src/salsa.rs`.
- `Flavor::Pandoc` only. CommonMark dialect must stay byte-identical
in CST and pandoc-native projection. `Dialect::CommonMark` keeps
the opaque `HTML_BLOCK` shape; lifts are gated on
`Dialect::Pandoc`.
- Pandoc-native (`pandoc -f markdown -t native`) is the **behavioral
reference**. Existing parser fixtures and projector output are not
the reference — when they disagree with pandoc-native, fix toward
pandoc-native.
- This is a **long-horizon effort** (5 phases — see "Phased plan"
below). Each session moves at most one phase forward; no sweeping
rewrites in a single go.
- Reuses the existing **pandoc-conformance harness** verbatim. New
cases live in
`crates/panache-parser/tests/fixtures/pandoc-conformance/corpus/--/`
with section prefix `html-block` (block-level) or `html-inline`
(inline-level). The pandoc allowlist
(`crates/panache-parser/tests/pandoc/allowlist.txt`) gets new
section header comments `# html-block` / `# html-inline`.
- Out-of-scope and deferred to
`crates/panache-parser/tests/pandoc/blocked.txt`:
`markdown="1"` / `markdown="0"` (Ext_markdown_attribute, default
off in pandoc-flavored markdown); `` / `` legacy
anchor lift (pandoc does NOT lift these in default `markdown` —
treat as opaque RawInline); malformed/unbalanced tags (fall back
to opaque `HTML_BLOCK` / `INLINE_HTML`).
Follow the Pandoc conformance, parser, and formatter invariants in the
repository's root `AGENTS.md`.
## Phased plan
The HTML lift is bounded by 5 phases. Pick one (or part of one) per
session. Latest phase status lives in `RECAP.md`.
**Phase 1** — Block-level `