Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
e5cf070
One grammar, shared by every tool
claude Sep 26, 2026
10713ff
One grammar, shared by every tool: review fixes
claude Sep 26, 2026
e57a996
Parse first, then substitute
claude Sep 26, 2026
8f5f58d
Parse first, then substitute: review fixes
claude Sep 26, 2026
c563149
Commas, quotes and backslashes: one rule each
claude Sep 26, 2026
c0f76fe
Commas, quotes and backslashes: one rule each: review fixes
claude Sep 26, 2026
df75a0a
Functions are marked where they are defined
claude Sep 26, 2026
caa179a
Functions are marked where they are defined: review fixes
claude Sep 26, 2026
71ed520
Parameters are passed the way they are declared
claude Sep 26, 2026
e195561
Parameters are passed the way they are declared: review fixes
claude Sep 26, 2026
0ddbe68
Functions really are elements
claude Sep 26, 2026
76c92c0
Functions really are elements: review fixes
claude Sep 26, 2026
23be553
Slot mistakes are errors
claude Sep 26, 2026
eb91a77
Slot mistakes are errors: review fixes
claude Sep 26, 2026
87f32bd
One if(): expressions in values, groups as whole attributes
claude Sep 26, 2026
6d75355
One if(): expressions in values, groups as whole attributes: review f…
claude Sep 26, 2026
62656a6
Values are typed, lists are real, names are scoped
claude Sep 26, 2026
6487641
Values are typed, lists are real, names are scoped: review fixes
claude Sep 26, 2026
9b68ce9
Check names, not values, and never drop input silently
claude Sep 26, 2026
7af43a3
Check names, not values, and never drop input silently: review fixes
claude Sep 26, 2026
c053967
One layout per element: column, row, grid, text, native or void
claude Sep 26, 2026
6de35ee
One layout per element: column, row, grid, text, native or void: revi…
claude Sep 26, 2026
84e5043
One text rule: arguments are read like text everywhere
claude Sep 26, 2026
d68954a
One text rule: arguments are read like text everywhere: review fixes
claude Sep 26, 2026
19a1192
Every HTML element under its HTML name
claude Sep 26, 2026
169472e
Every HTML element under its HTML name: review fixes
claude Sep 26, 2026
24640ee
Layout words follow the direction CSS actually sets
claude Sep 26, 2026
b585296
Layout words follow the direction CSS actually sets: review fixes
claude Sep 26, 2026
b05e86c
One rule for the leading argument
claude Sep 26, 2026
7a15528
One rule for the leading argument: review fixes
claude Sep 26, 2026
fb1f74b
One verbatim rule, and code samples that are escaped text
claude Sep 26, 2026
3bff74e
One verbatim rule, and code samples that are escaped text: review fixes
claude Sep 26, 2026
9206abc
The page is the root element
claude Sep 26, 2026
80232fb
The page is the root element: review fixes
claude Sep 26, 2026
e875ec8
One CSS path, and element defaults that stay on htmlang's elements
claude Sep 26, 2026
1267161
One CSS path, and element defaults that stay on htmlang's elements: r…
claude Sep 26, 2026
af4428d
The px rule in one sentence
claude Sep 26, 2026
e8b62ab
The px rule in one sentence: review fixes
claude Sep 26, 2026
149fcb9
Custom properties are ordinary CSS properties
claude Sep 26, 2026
bc35b42
Custom properties are ordinary CSS properties: review fixes
claude Sep 26, 2026
2693aad
Prefixes compose, and apply to a group or a bundle
claude Sep 26, 2026
22a5b92
Prefixes compose, and apply to a group or a bundle: review fixes
claude Sep 26, 2026
801c075
Selector prefixes under their CSS names
claude Sep 26, 2026
b0177c4
Selector prefixes under their CSS names: review fixes
claude Sep 26, 2026
47b2d6c
Style every element of a kind inside: @td:padding 8
claude Sep 26, 2026
4727428
Style every element of a kind inside: @td:padding 8: review fixes
claude Sep 27, 2026
2054535
Colour maths in CSS, and one way to read a token
claude Sep 27, 2026
cc0fa3d
Colour maths in CSS, and one way to read a token: review fixes
claude Sep 27, 2026
4157d45
Fix what the final review found in the compiler
claude Sep 27, 2026
6623071
Bring the docs and examples in line with the language
claude Sep 27, 2026
2635547
Bring the editor tools in line with the language
claude Sep 27, 2026
95a7eed
Say that @let --name inside @if is an error too
claude Sep 27, 2026
0c4217d
Final review fixes
claude Sep 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 90 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,92 @@ Linux, macOS, and Windows. Your change should pass all three.
## Repository layout

- `crates/htmlang-core/` — parser, AST, code generator. No I/O lives here.
- `ast.rs` — the element kinds; every plain HTML element is one row in `TAGS`.
- `syntax.rs` — the syntax tree: every line as written, with spans. The
compiler evaluates it, the formatter prints it back, and the language
server takes definitions, directives and verbatim bodies from it. It
also holds `ESCAPES`, the one escape table of every htmlang string,
and decides where `{@...}` is an inline element: in every line of text
except the text of a `literal` element.
- `parser.rs` — evaluates the tree: definitions, data, loops and
conditions, and the checks over code that doesn't run. Names live in
`Env`, one frame per block, so a definition is visible to the end of
its block and a function keeps the frames of its definition.
- `diagnostic.rs` — `Diagnostic` and the stable diagnostic codes.
- `codegen.rs` — writes the HTML and the CSS. A parent's `Flow` (the
direction its CSS sets, with the changes under media and container
prefixes) is what its children's `fill` and `shrink` compile against;
the rules for a change are keyed on the parent's class in that block.
A page's `<body>` is written like an `@el` whose styles are `@page`'s
(the reset makes it the column), so the top level of a page is laid
out like any column; a fragment's top level has no layout of its own.
Every CSS property goes through one generic path (its name, its value
as written, with `vocab::with_px`'s pixels); only htmlang's layout
words and `line-clamp` have code of their own. Generated classes are
`hl-` + a short name; an element's defaults (its layout,
`ElementKind::css`) go with its class, as `:where(.hl-a)`, never a
rule on a global element selector, so `@markdown`/`@raw` HTML and a
page embedding a fragment are untouched. The rules are in cascade
layers (`LAYERS`): `hl-reset`, `hl-kind` (the defaults), `hl-inside`
(element prefixes, `@td:`, each an `@scope` on the class, and
functions' scoped `@style`, written when a function is called) and
`htmlang` (the elements' own styles and `children:`), so which one
wins is the layer's order, not specificity.
A style's prefixes are a `Condition`: its at-rule prefixes (sorted, so
`dark:md:` is `md:dark:`, and written as nested `@media`/`@container`
blocks) and its selector chain (read left to right; an element prefix
splits it into the scope's root and the elements it styles). Each
class keeps one list of (condition, declarations), and every block and
chain is written in one fixed order, never the order of the source.
- `ast.rs` — the element kinds; every plain HTML element is one row in
`TAGS` with its one `Layout` (column, row, grid, text, native or void,
which decides its text lines, `spacing` and its children's layout
words), whether its text is shown as written (`literal`: `@code`,
`@textarea`, whose indented body is verbatim text, HTML-escaped), the
one HTML attribute its first word fills (`arg`: `src`, `action`, ...;
`ElementKind::arg` gives `@link`'s and `@image`'s too) and whether its
body is foreign text written into the page as it is (`verbatim`:
`@script`), and every directive one row in `DIRECTIVES` (its argument
and its kind of body; a verbatim directive's line is its one-line body
or file, and it takes no attributes). The list of
elements is fixed; `HTML_NAMES_WRITTEN_OTHERWISE` maps HTML's `a`, `img`,
`span`, `p` and `div` to htmlang's own names for the unknown-element
suggestion only.
- `vocab.rs` — the attribute vocabulary (htmlang attributes, CSS properties,
HTML attributes) and the state/media prefixes.
- `expr.rs` — the expression language for conditions and computed values.
HTML attributes, `@page`'s own `favicon`), the pixel rule
(`LENGTH_PROPERTIES`, the one table of properties whose bare numbers
are px, and `with_px`, the one function that applies it) and the
prefixes: `PSEUDOS`, CSS's pseudo-classes and pseudo-elements under
their CSS names, each marked `:`, `::` or taking an argument, in the
order their rules are written; `children:`; element prefixes (`@td:`,
any name; the parser checks it is an element with a tag of its own,
`ElementKind::own_tag`); and the width, media and container prefixes,
with the rank that orders the at-rule blocks. A new pseudo-class is one
row in `PSEUDOS` and one word in DESIGN.md's list.
- `expr.rs` — the expression language for conditions, computed values and
`${...}`. It evaluates only what decides the result (the branch `if()`
takes, the side of `and`/`or` that decides) and only reads the rest.
- `value.rs` — the value types (text, number, true/false, list,
record) and the rules defined once on them: truthiness, comparison,
how numbers and lists print, ranges.
- `interp.rs` — `$name` and `${...}`: where a name ends and how one slot
of text (a text run, an attribute's value, an argument, a path) is
filled in, including what quoted text inserts in CSS and in text. The
one place variables are interpolated.
- `std.hl` — the standard library, written in htmlang and loaded before
every file.
- `crates/htmlang-wasm/` — thin wrapper exposing `compile` to the web playground.
- `src/` — CLI, dev server and formatter.
- `src/` — CLI, dev server and formatter (`fmt.rs`, which prints the syntax
tree back with its comments and blank lines).
- `src/bin/htmlang_lsp/` — language server binary (`htmlang-lsp`). Its
completions come from the compiler's tables; hover and completion text live
in `docs.rs`.
- `editors/vscode/` — VS Code extension.
- `tests/snapshots.rs` — integration / snapshot tests for the compiler.
- `tests/regressions.rs` — one test per fixed bug.
- `tests/docs.rs` — compiles every example in `DESIGN.md` and `README.md`,
and every page in `examples/`.
and every page in `examples/`: any diagnostic fails it.
- `tests/fmt.rs` — formats every example and snapshot input: the result is
stable, compiles to the same HTML and keeps every comment.
- `examples/` — complete pages: a landing page, a blog, a docs page, and a
tour of the whole language (`demo.hl`).

Expand All @@ -45,7 +115,9 @@ Linux, macOS, and Windows. Your change should pass all three.
parser-to-HTML pipeline. Pure parser / codegen helpers can live as unit
tests alongside the code.
2. Prefer the smallest mechanism: a component belongs in `std.hl`, an HTML
element is a row in `TAGS`, and a CSS property needs no code at all. Only
element is a row in `TAGS` (and a line in DESIGN.md's table of layouts
and a name in the VS Code grammar's element list, which tests check), and
a CSS property needs no code at all. Only
thread a feature through the parser and codegen when it needs to be, and
describe it in the LSP's `docs.rs`.
3. Document it in `DESIGN.md` (the examples there are compiled by the tests).
Expand All @@ -57,9 +129,20 @@ Linux, macOS, and Windows. Your change should pass all three.

- Prefer enum-based ASTs and pattern matching over stringly-typed dispatch.
- Keep `@` prefixes and bracket-attribute syntax consistent with existing
directives. Add `KNOWN_DIRECTIVES` entries when you add a new `@foo`.
directives. A new `@foo` is a row in `ast::DIRECTIVES`; the parser, the
formatter and the language server read that table, and `tests/editor.rs`
fails when the VS Code grammar's word lists drift from it.
- Every diagnostic has a code from `diagnostic::code`; add one there when no
existing code fits, and key tool behavior (quick fixes, tests) on the code
rather than on the message. Put the name the diagnostic is about in
`subject` and a replacement in `suggestion`.
- Diagnostics should include `line` and, when practical, `column` and a
`source_line` excerpt. Use `Severity::Help` for suggestions, not `Warning`.
- Check names, not values, and never drop input silently. When something
the author wrote can't go into the page, report an error; a warning means
it went into the page as written. Check a CSS value only for what is
wrong in any CSS (see `css_breakout` in `parser.rs`), not against a list
of values the browser may know better.
- No unwrap() on parsed user input. Use `Result<_, ParseError>` and record a
diagnostic so the compiler keeps going.

Expand Down
Loading
Loading