diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77c89ce..48d60ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,14 +19,82 @@ 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 `
` 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`. @@ -34,7 +102,9 @@ Linux, macOS, and Windows. Your change should pass all three. - `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`). @@ -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). @@ -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. diff --git a/DESIGN.md b/DESIGN.md index 621e134..7f5e76c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -5,10 +5,10 @@ tree of elements written one per line, with indentation for nesting: ``` @page Hello -@el [max-width 640, center-x, padding 40, spacing 16] +@el [width fill, max-width 640, center-x, padding 40, spacing 16] @h1 Hello @paragraph - This page was written in {@text [font-weight bold] htmlang}. + This page was written in {@b htmlang}. ``` Every example block in this file is compiled by the test suite @@ -19,23 +19,28 @@ Every example block in this file is compiled by the test suite - **`@` starts structure, and any other line is content.** An element is `@name`, and a line without `@` is text. - **htmlang's own vocabulary is about layout.** It comes from elm-ui. An - element is a row or a column, `spacing` sets the gap between its children, + element is a row, a column, a grid or text (or keeps HTML's own layout), + `spacing` sets the gap between its children, and each child says how it sits in its parent (`width fill`, `center-x`, `align-right`). The layout attributes are the only styling words htmlang adds. - **Everything else is CSS or HTML, under its own name.** A style is any CSS - property with a CSS value. An element that isn't about layout has its HTML - name, and an HTML attribute is written `key=value`. Nothing is renamed or - abbreviated. + property with a CSS value. An element that isn't about layout or text has + its HTML name, and an HTML attribute is written `key=value`. Nothing is + renamed or abbreviated. - **Each thing is done one way.** `@let` defines every reusable piece, `@data` loads data, `@include` brings in a file, and `if()` makes an attribute conditional. A layout is an ordinary function. - **The output is what you wrote.** Each page compiles to one self-contained HTML file. Its CSS is inside, and it has no JavaScript unless you write some. The compiler doesn't add attributes, tags or rewritten values that - the source doesn't ask for. + the source doesn't ask for, and it doesn't leave out anything you wrote: + what can't go into the page is an error. The one exception is a style + whose value comes out empty, such as an optional field a record doesn't + have: it is simply not written. - **Everything runs at compile time.** Variables, expressions, loops and - conditions are resolved when the page is built. + conditions are resolved when the page is built. What CSS computes in the + browser (`var()`, `calc()`, `color-mix()`) stays CSS, written as it is. ## Syntax @@ -46,7 +51,9 @@ Every example block in this file is compiled by the test suite children ``` -Children are indented under their parent. Attributes go inside `[...]`, +Children are indented under their parent, with spaces or with tabs (a +tab counts as one space, so a file that mixes them is a warning). +Attributes go inside `[...]`, separated by commas, and the list may span several lines: ``` @@ -54,7 +61,7 @@ separated by commas, and the list may span several lines: padding 20, background white, border-radius 8, - box-shadow 0 2px 4px rgba(0,0,0,0.1) + box-shadow 0 2 4 rgba(0,0,0,0.1) ] Content ``` @@ -66,53 +73,137 @@ An attribute can take one of three forms: - `key=value` is an **HTML attribute** (`id=main`, `type=email`, `aria-label=Close`). - A bare word is a **flag**: a layout attribute such as `center-x`, or a - boolean HTML attribute such as `required`, `disabled` or `open`. + boolean HTML attribute such as `required`, `disabled`, `hidden` or `open`. -A comma inside `(...)` or `"..."` doesn't split attributes, so a font stack -is written `font-family "Inter, sans-serif"`. +A comma inside `(...)`, `[...]` or `"..."` doesn't split attributes. Anywhere else, +`\,` keeps a comma in the value: + +``` +@el [transition opacity 0.3s\, transform 0.3s, font-family "Open Sans"\, sans-serif] +``` + +A variable fills an attribute's value (`padding $gap`, `alt=$title`), never +its name or a whole attribute: attributes come from a +[bundle](#definitions), written `[$card]`, and a condition chooses whole +attributes with [`if()`](#conditional-attributes). + +The attribute list belongs to the element name right before it. Anywhere +else, `[` is an ordinary character, so a line of text can contain one: + +``` +@text Use [ to open a list +@paragraph Arrays look like [1, 2, 3]. +``` ### Text A line that doesn't start with `@` is text. Text after an element's -attributes is the element's content. Inside a line of text, `{...}` holds an -inline element: +attributes is the element's content: its first line of text, read like any +other. Inside a line of text, `{...}` holds an inline element, and +[escapes](#escapes) and [`$names`](#variables) work: ``` @paragraph - This is {@text [font-weight bold] important}, and this is {@link https://example.com a link}. + This is {@strong important}, and this is {@link https://example.com a link}. @text [font-weight bold, font-size 24, color #333] Hello world +@h2 Welcome to {@text [color #3b82f6] htmlang} +@ul + @li Read {@link /docs the docs} @section [padding 8] Text after the attributes is content too. ``` -A backslash makes the next character literal: +How an element's lines combine is its [layout](#rows-columns-and-text)'s +business: in a row or column each line is a child; in a text element lines +flow. Text that `@if`, `@each`, `@fragment` or a function writes counts as +lines written in its place, so in a text element it flows with the rest, +joined with a space, and in a native element it is on lines of its own. + +The text of `@code` and `@textarea` is shown as written. On its line and +inline, a `{@...}` in it is text, while escapes and `$names` still work. +The lines indented under it are a [verbatim](#verbatim-bodies) sample: +nothing in them is htmlang (`@`, `$`, `{`, `\` and `--` are text), and +they are HTML-escaped with their line breaks and indentation kept. This +is how a page shows code, htmlang included. `@pre` is an ordinary element, +so `@pre > @code` is HTML's ``, and a function can wrap `@pre`
+around a sample. A sample is text on the line or lines in the block, not
+both.
+
+```
+@paragraph
+ Write {@code {@link /docs docs}} for a link.
+@pre [padding 16] > @code
+ @el [padding 40]
+ shown as text
+@textarea [aria-label=Notes]
+ First line
+ Second line, indented
+```
+
+A sample in the block has no `$names`, so a version number in it can't
+come from a variable; a one-line sample on `@code`'s own line can
+(`@code cargo install htmlang@$version`).
+
+### Escapes
-- `\@` and `\--` let a line of text start with `@` or `--`.
-- `\$` writes a `$` that isn't a variable.
-- `\{` writes a brace that doesn't start an inline element.
-- `\\` writes a backslash.
+A backslash before one of these characters stands for the character itself.
+The table is the same in every htmlang string: text, arguments, attribute
+values, `@let`, `@page` and `@meta`.
+
+| Escape | Writes | For |
+|---|---|---|
+| `\@`, `\--` | `@`, `--` | a line of text that starts with `@` or `--` |
+| `\$` | `$` | a `$` before a name, which would start a variable |
+| `\{`, `\}` | `{`, `}` | braces that don't start or end an inline element |
+| `\[`, `\]` | `[`, `]` | brackets that don't open or close an attribute list |
+| `\,` | `,` | a comma in an attribute's value |
+| `\"` | `"` | a quote that doesn't start or end quoted text |
+| `\\` | `\` | a backslash before one of these characters |
+
+A backslash before any other character is kept as written, so CSS's own
+escapes and patterns pass through: `before:content "\201C"`,
+`pattern=\d{3}`. A `$` that isn't followed by a letter or `_` is text
+anyway: `$5`, `$$`.
```
\@htmlang on social media
@let price 5
-@text \$price is $price dollars
+@paragraph \$price is $price dollars: {@mark \{braces\}}, \[brackets\] and a comma\, too
+@input [type=text, pattern=\d{3}-\d{4}, title=Costs \$5]
```
### Comments
-A line starting with `--` is a comment. A comment must be on its own line,
-because `--` later in a line is ordinary text.
+A line whose first word is `--` is a comment: `--` followed by a space, or
+alone on its line. A comment must be on its own line, because `--` later in
+a line is ordinary text. A comment may sit between the lines of an attribute
+list, and `--name` with no space is not a comment but a
+[custom property](#custom-properties), so it can start a line of a list
+(anywhere else, a line starting `--name` or `---` is text).
```
-- this is a comment
@row [spacing 10]
-- todo: more nav items
@link / Home
+@el [
+ -- the gap, wider on larger screens
+ --gap 12px,
+ md:--gap 24px,
+ spacing var(--gap)
+]
+ @text One
+ @text Two
```
### Chains
`>` puts elements that have one child each on one line. The last element in
-the chain gets the indented children.
+the chain gets the indented children. A `>` is a chain only between two
+elements (`@name [attributes]`); in text it is just a character. A
+function call can be a link of a chain, like any element. After an
+element's leading argument (`@link /about`) the rest of the line is text,
+so a link around an image is `@link [href=/] > @image logo.png` (the
+compiler warns when `> @name` follows a leading argument).
```
@el [padding 16, background blue, border-radius 8] > @link https://example.com
@@ -121,10 +212,20 @@ the chain gets the indented children.
### Verbatim bodies
-`@raw` writes HTML into the output exactly as given, either the rest of its
-line or its indented block. The bodies of `@style`, `@head`, `@script` and
-`@markdown` are also kept exactly as written: nothing in them is parsed as
-htmlang, and a `--` line in them is not a comment.
+The bodies of `@raw`, `@style`, `@head`, `@markdown`, `@script`, `@code`
+and `@textarea` are foreign text, kept exactly as written: nothing in them
+is parsed as htmlang, and a `--` line in them is not a comment. `@raw`
+writes its HTML into the page, `@style` its CSS and `@head` its HTML into
+the ``; `@code` and `@textarea` show theirs as text. Which lines are
+verbatim is decided by the element or directive on the line, also at the
+end of a chain (`@pre > @code`, `@el > @script`).
+
+The header's line follows one rule. For `@raw`, `@style` and `@head`, text
+on the line is a one-line body; for `@markdown` it is the file and for
+`@script` the `src`. Text on the line and an indented body together are an
+error. Only `@script` takes attributes: an attribute list on `@raw`,
+`@style`, `@head` or `@markdown` is an error, so content that starts with
+`[` (a CSS attribute selector) goes in the indented block.
```
@raw
@@ -132,16 +233,99 @@ htmlang, and a `--` line in them is not a comment.
+@style .note { color: gray; }
```
+### Directives
+
+A directive is a built-in word that isn't an element. Each one takes a
+fixed kind of argument and a fixed kind of body. `@page` is the one
+directive that is also an element: it stands for the page's root, ``,
+whose content is the rest of the file (see [Page and head](#page-and-head)).
+
+| Directive | Argument | Body |
+|---|---|---|
+| `@page` | attributes (styles for ``, `key=value` for ``) and a title | none |
+| `@let` | a name and a value or bundle, or `@name` and parameters | a function's body |
+| `@include` | a file | none |
+| `@data` | a variable and a source | none |
+| `@meta` | a name and a value | none |
+| `@if`, `@else`, `@each` | a condition or a loop | htmlang |
+| `@raw`, `@style`, `@head` | the rest of the line: a one-line body | verbatim, instead of the line |
+| `@markdown` | a file | verbatim Markdown, instead of the file |
+
+An indented line under a directive that takes no body is an error, because
+it would otherwise silently become a sibling.
+
+### Diagnostics
+
+The compiler checks the whole file, including code that doesn't run: the
+branch of an `@if` or an `if()` that isn't taken, the body of a function that is never
+called and the body of a loop over an empty list. Unknown elements,
+functions, attributes and prefixes are reported there too. In code that
+doesn't run, a call is checked against every function in the file and the
+files it includes; where the code runs, a name is looked up where it is
+written, so a function's body sees only what is defined above the function
+(a call to a function defined further down is reported when the body
+runs, and two functions can't call each other). A `$name` is checked only
+where the code runs, since any of them may come from data: an undefined
+variable in a branch that isn't taken, or in a function that is never
+called, isn't reported.
+
+**Names are checked, and values are CSS's.** A misspelled element, prefix,
+parameter or slot is an error, and so is an undefined variable. A CSS
+property htmlang doesn't know, but whose name CSS could have, is written to
+the CSS as it is, with a warning that suggests the closest known name
+(`text-grow`, or `colr`, which asks "did you mean `color`?"). Values go to
+the CSS as written, so the browser decides what `z-index auto` or `color
+rebeccapurple` means; only what is wrong in any CSS is reported (see
+[CSS properties](#css-properties)).
+
+**Nothing you write is left out silently.** An error means something
+couldn't go into the page: a flag htmlang doesn't know, an attribute
+`@fragment` has no element for, content for a function without
+`@children`. A warning means it went into the page as written, but may not
+be what you meant. A warning is reported once, even from a line that runs
+many times in a loop or a function.
+
+Every diagnostic has a stable code, such as `unknown-element` or
+`unused-variable`. The command line prints it as `error[unknown-element]`,
+`--format json` has it in a `code` field, and the editor's quick fixes are
+keyed on it.
+
## Layout
-### Rows and columns
+### Rows, columns and text
+
+Every element has one layout, which decides what the lines written inside
+it are. **In a row or column each line is a child; in a text element lines
+flow.**
-`@el` is the container, and it lays out its children in a column. `@row`
-lays them out side by side. Every other container (`@section`, `@nav`,
-`@article`, `@form`, and so on) is a column too, so `@row` is the only
-element that changes direction. `@grid` is a CSS grid.
+| Layout | Elements | What is inside it |
+|---|---|---|
+| column | `@el`, and the containers: `@section`, `@nav`, `@form`, `@ul`, `@li`, ... | Each line of text is a child of its own, and the children are laid out top to bottom (side by side with `flex-direction row`) |
+| row | `@row` | The same, side by side |
+| grid | `@grid` | Each line of text is a cell |
+| text | `@paragraph`, `@text`, `@link`, `@h1` … `@h6`, `@button`, `@label`, `@td`, `@strong`, `@em`, ... | The argument, the lines and the children flow as one run of text, joined with spaces |
+| native | `@table`, `@caption`, `@select`, `@pre`, `@textarea`, `@video`, ... | HTML's own layout, which htmlang leaves alone |
+| void | `@input`, `@hr`, `@br`, `@image`, `@source` | Nothing: it takes no content |
+
+The table under [HTML elements](#html-elements) gives every element's layout. The
+page itself is a column too: `@page` is the [root element](#page-and-head),
+``, so what is written at the top of a page stacks like the children
+of an `@el`, `height fill` there takes the rest of the window and
+`center-y` centres in it.
+
+```
+@el [spacing 4]
+ First line
+ Second line
+@button [type=button]
+ Save
+ changes
+```
+
+The column shows two lines 4px apart; the button says "Save changes".
These attributes are htmlang's own. They describe how an element lays out
its children, and how it sits in its parent:
@@ -149,14 +333,31 @@ its children, and how it sits in its parent:
| Attribute | Effect |
|---|---|
| `spacing N` | Gap between children |
-| `width fill` / `width shrink` / `width N` | Take the remaining space in a row (the full width in a column), fit the content, or an exact size |
-| `height fill` / `height shrink` / `height N` | Take the remaining space in a column, fit the content, or an exact size |
-| `center-x`, `center-y` | Center the element in its parent |
-| `align-left`, `align-right`, `align-top`, `align-bottom` | Align the element in its parent |
+| `width fill` / `width shrink` / `width N` | In a row, take the remaining width or keep the content's width; in a column or anywhere else, take the full width or fit the content; or an exact size |
+| `height fill` / `height shrink` / `height N` | In a column, take the remaining height or keep the content's height; in a row or anywhere else, take the full height or fit the content; or an exact size |
+| `center-x`, `center-y` | Center the element in its parent (auto margins) |
+| `align-left`, `align-right`, `align-top`, `align-bottom` | Align the element in its parent (an auto margin on the other side) |
| `wrap` | Let a row wrap onto more lines |
-| `grid-cols N`, `grid-rows N` | Equal grid columns or rows (on `@grid`) |
+| `grid-cols N`, `grid-rows N` | Equal grid columns or rows (on `@grid`); any other value is a track list, as in `grid-template-columns` (`grid-cols 200 1fr`) |
| `col-span N`, `row-span N` | Cells a grid child spans |
+`spacing`, `wrap`, `grid-cols` and `grid-rows` lay out an element's
+children, so they go on a row, column or grid. On a text, native or void
+element they are an error: text has no gap between its lines. For a flex
+layout of your own on such an element, write the CSS (`@label [display
+flex, gap 8]`).
+
+A child's `fill` and `shrink` follow the direction its parent's CSS
+actually sets: a row for `@row`, a column for `@el` and the other columns,
+or whatever the parent's own `flex-direction` (or `flex-flow`) says. Along
+that direction, `fill` takes the remaining space and `shrink` keeps the
+content's size; across it, `fill` is the full size and `shrink` fits the
+content. `center-x`, `center-y` and `align-*` are auto margins, which work
+in either direction; a `margin` on the same element keeps its other sides
+(`[center-x, margin 20]`). A layout word never overrides a CSS property
+the element writes itself: `[width fill, min-width 200]` keeps its 200px,
+and `[flex-shrink 0, width fill]` still grows but never shrinks.
+
```
@row [spacing 8]
@el [width fill, padding 12, background #f3f4f6] Fills the row
@@ -165,15 +366,77 @@ its children, and how it sits in its parent:
@text [center-x] Centered
@el [height fill, background #dbeafe] Fills the rest of the column
@text [align-right] Right-aligned
+@nav [flex-direction row, spacing 20]
+ @link /features Features
+ @el [width fill]
+ @link /signup Sign up
@grid [grid-cols 3, spacing 8, md:grid-cols 4]
@el [col-span 2, background #fef3c7] Two columns wide
@el [background #fde68a] One
@el [background #fcd34d] One
```
+A direction set under media or container prefixes (`md:flex-direction
+row`, `print:`, `cq-md:`, or a stack of them such as `md:dark:`) switches
+the children's `fill` and `shrink` in that same condition. As in elm-ui, whose child rules are keyed on the
+parent's class (`.r > .wf`), each child gets a rule keyed on its parent's
+class in the parent's `@media` or `@container` block. A width prefix holds
+from its width up, so `sm:flex-direction column` still applies at `lg:`
+unless `lg:` sets a direction of its own. A direction under a state prefix
+(`hover:`, `first-child:`, `children:`, or a stack with one in it) leaves the
+children's layout words as they are.
+
+```
+@header [spacing 16, md:flex-direction row, md:align-items center]
+ @text [font-weight 800] Launchpad
+ @el [width fill]
+ @nav [flex-direction row, spacing 20]
+ @link /features Features
+ @link /pricing Pricing
+@section [padding 24]
+ @el [width fill, max-width 800, center-x, spacing 16]
+ The full width up to 800px, in the middle
+```
+
+On a phone the header is a column and its `@el [width fill]` is the full
+width; from md up it is a row and the `@el` takes the free space, pushing
+the navigation to the right. The section's `@el` is the usual centred
+column: the full width, at most 800px, with auto margins on both sides.
+
Everything else about layout is plain CSS: `justify-content`, `align-items`,
`flex 1 1 240px`, `grid-template-areas`, `position`.
+### Layout inside text
+
+A row, column or grid written inside text, inline in a line (`{@el ...}`) or
+as a child of a text element, is laid out inline (`display: inline-flex`, or
+`inline-grid`), and keeps its attributes. Anywhere inside text, `@el`, `@row`
+and `@grid` (and `@in-front` and `@behind`) are written as a `` rather
+than a ``, since text can't hold a ``.
+
+```
+@paragraph
+ Price: {@el [padding 2 6, background #fef3c7, border-radius 4] $9} a month.
+@h2
+ Plans
+ @row [spacing 4, font-size 14] > @text New
+```
+
+Any other element keeps its HTML element. One whose HTML ends a paragraph,
+such as `@section`, `@ul` or `@h2`, is a warning inside `@paragraph`
+(`block-in-paragraph`): the browser would move it, and what follows it, out
+of the ``.
+
+`@link` is text too, so a card written inside a link is an inline ``
+that shrinks to its content. A clickable card is the link itself, laid out
+as a column with CSS:
+
+```
+@link [href=/post, display flex, flex-direction column, gap 8, padding 16, border 1 solid #e5e7eb]
+ @strong A post
+ @text Its summary.
+```
+
### Overlays: `@in-front` and `@behind`
These layers work like elm-ui's `inFront` and `behind`. Their children fill
@@ -214,15 +477,114 @@ same name and value as in CSS: `padding 20`, `font-weight bold`,
`border 1 solid #e5e7eb`, `border-radius 8`, `background red`,
`grid-template-areas "a b"`, `display none`.
-Values mean what they mean in CSS, with one addition: **a bare number in a
-length is pixels**. So `padding 20` is `20px`, `border 1 solid red` is
-`1px solid red`, and `margin 0 auto` is `0 auto`. Values that have a unit,
-keywords and CSS functions pass through as written (`width 50%`,
-`max-width min(100%, 800px)`), and so do numbers that aren't lengths
-(`opacity 0.5`, `z-index 2`, `line-height 1.5`, `flex 1`).
+A quoted string in a value is CSS's own, so it keeps its quotes and its
+commas: `font-family "Inter, sans-serif"` names one family called
+"Inter, sans-serif", and the compiler warns about it. A font stack is
+several values with commas between them, written `\,`:
+`font-family Inter\, sans-serif`.
+
+Values mean what they mean in CSS, with one addition: **in a length
+property, every bare number outside parentheses and quotes is pixels**. So
+`padding 20` is `20px`, `border 1 solid red` is `1px solid red`,
+`box-shadow 0 2 4 rgba(0,0,0,0.1)` is `0 2px 4px rgba(0,0,0,0.1)`, and
+`margin 0 auto` is `0 auto` (a zero stays `0`). Numbers inside a function
+belong to it (`rgb(255 128 0)`, `calc(100% - 20px)`), and values that have
+a unit or are keywords pass through as written (`width 50%`, `max-width
+min(100%, 800px)`). Every other property takes numbers as CSS does, so
+`opacity 0.5`, `z-index 2`, `line-height 1.5`, `flex 1 1 240px`,
+`grid-column 1 / 3`, `initial-letter 3` and `border-image-width 2` are
+written as they are, and so are custom properties (`--cols 3`), which have
+no type to go by.
+
+The length properties are these, and only these:
+
+| Group | Properties |
+|---|---|
+| Size | `width`, `height`, `min-*` and `max-*` of both, `block-size`, `inline-size` and their `min-`/`max-`, `flex-basis`, `column-width`, `contain-intrinsic-*` |
+| Space | `margin`, `padding`, `scroll-margin`, `scroll-padding` and every side of them, `gap`, `row-gap`, `column-gap`, `border-spacing` |
+| Position | `top`, `right`, `bottom`, `left`, `inset` and `inset-*`, `translate`, `transform-origin`, `perspective`, `perspective-origin`, `offset-position`, `offset-anchor`, `offset-distance` |
+| Border | `border` and every `border-*` shorthand, width and radius (not `border-image-*`), `outline`, `outline-width`, `outline-offset`, `column-rule`, `column-rule-width` |
+| Shadow | `box-shadow`, `text-shadow` |
+| Grid | `grid-template-columns`, `grid-template-rows`, `grid-auto-columns`, `grid-auto-rows` |
+| Background | `background-position` (and `-x`, `-y`), `background-size`, `object-position`, `mask-position`, `mask-size` |
+| Text | `font-size`, `letter-spacing`, `word-spacing`, `text-indent`, `text-decoration-thickness`, `text-underline-offset`, `vertical-align`, `baseline-shift` |
+| Other | `shape-margin`, `overflow-clip-margin`, `view-timeline-inset` |
+
+Shorthands that mix lengths with numbers of other kinds (`font`,
+`background`, `mask`, `grid`, `grid-template`, `offset`, `flex`) are not in
+the table, so the lengths in them are written with their unit:
+`font 700 16px/1.5 Inter`.
+
+Nothing else is added to a value, and a shorthand means what it means in
+CSS: `outline 2 solid red` is `2px solid red`, while `outline 2 red` names
+no line style, so CSS draws no outline. `container card / inline-size`
+and `contain content` are CSS's own shorthands too, and a bare `contain` is
+an error, like any style without a value. `line-clamp N` alone also adds
+the `-webkit-box` declarations that browsers still need to cut text off
+after N lines.
+
+A property htmlang doesn't know is written to the CSS as it is, so new CSS
+(`text-grow per-line`) works before htmlang lists it; the
+compiler warns and suggests the closest known name, in case it is a typo.
+Vendor-prefixed properties (`-webkit-tap-highlight-color`) pass without a
+warning. The value of a property htmlang doesn't know is written exactly as
+it is, without pixels added. A style needs a value: `[padding]` is an error.
+
+```
+@el [-webkit-tap-highlight-color transparent, -webkit-text-stroke 1px black]
+ Passed through as written
+```
-`line-clamp N` also adds the `-webkit-box` declarations that browsers still
-need to cut text off after N lines.
+A value is checked only for what is wrong in any CSS. A `;`, `{` or `}`
+(outside a quoted string, and for `;` outside parentheses, as in CSS's own
+`if()`) or an unclosed quote or parenthesis would break out of the rule, and
+a ` img):`, `is(...):`, ... | When that pseudo-class, with its argument, matches |
+| `before:`, `after:`, `marker:`, `placeholder:`, `backdrop:`, ... | To that pseudo-element (`::before`, `::marker`, ...) |
| `children:` | To each direct child |
-| `before:`, `after:` | To the `::before` / `::after` pseudo-element (together with `content`) |
-| `has(SELECTOR):` | When the element contains a match |
+| `@td:`, `@h2:`, `@link:`, ... | To every such element inside it, at any depth (see [Styling the elements inside](#styling-the-elements-inside)) |
| `sm:`, `md:`, `lg:`, `xl:`, `2xl:` | From that viewport width up (640, 768, 1024, 1280, 1536px) |
| `cq-sm:` … `cq-2xl:` | From that container width up (the same widths, for an ancestor with `container-type inline-size`) |
| `dark:`, `print:`, `motion-safe:`, `motion-reduce:`, `landscape:`, `portrait:` | Under that media condition |
+The selector prefixes are CSS's own pseudo-classes and pseudo-elements,
+under their CSS names, without the colons: `link`, `visited`,
+`first-child`, `last-child`, `only-child`, `first-of-type`,
+`last-of-type`, `only-of-type`, `empty`, `target`, `open`,
+`popover-open`, `default`, `checked`, `indeterminate`,
+`placeholder-shown`, `autofill`, `required`, `optional`, `valid`,
+`invalid`, `user-valid`, `user-invalid`, `read-only`, `read-write`,
+`focus-within`, `hover`, `focus`, `focus-visible`, `active`, `enabled`
+and `disabled`; with an argument, `nth-child()`, `nth-last-child()`,
+`nth-of-type()`, `nth-last-of-type()`, `not()`, `is()`, `where()` and
+`has()`; and the pseudo-elements `before`, `after`, `marker`,
+`placeholder`, `selection`, `backdrop`, `first-line`, `first-letter` and
+`file-selector-button`, which htmlang writes with CSS's `::`. The argument
+is CSS, written as it is between balanced parentheses, and it may hold
+spaces and colons: the key ends at the first space outside parentheses, so
+`has(> img):padding 0` and `is(:hover, :focus-visible):color red` are one
+key each. `children:` is htmlang's one combinator word, and an element's
+name with `@` (`@td:`) picks out the elements of that kind inside. A name
+that isn't one of these (`hvoer:`, or `first:` for `first-child:`) is an
+unknown prefix, an error, not a selector that silently never matches.
+
+A prefix goes on a style or a layout flag, and prefixes stack: the key
+is a chain of prefixes, then the property, and the style applies where
+all of them hold. The selector prefixes (every row above the widths)
+read left to right: `hover:children:` styles the children of a hovered
+element, `children:hover:` a hovered child. A pseudo-element is not an
+element with states of its own, so it comes last among them:
+`hover:before:color` is the `::before` of a hovered element, and
+`before:hover:color` is an error that says so. The width, media and
+container prefixes go anywhere in the chain, in any order, and each is
+an `@media` or `@container` rule around the ones inside it:
+`md:dark:padding 32` and `dark:md:padding 32` are the same style, from
+768px up in dark mode.
+
+A prefix written before a `[group]` applies to each style in it, and one
+written before a `$bundle` to each style the bundle holds, so styles that
+share a condition are written under it once. A prefix before an `if()`
+applies to the branch it picks. The group or bundle holds styles only:
+an HTML attribute (`md:[id=x]`) or a flag that is not a style has no
+states, and it is an error there, as it is with a prefix of its own
+(`md:id=x`).
+
+`children:` styles each direct child, as the parent sees it. They take
+effect only where the child sets nothing itself, since what an element
+says about itself wins over what its parent says about it. They do win
+over the defaults of the child's kind, so `children:display list-item`
+brings back a list's markers and `children:margin-bottom 8` spaces
+headings. The words
+that place an element in its parent (`width fill`, `width shrink`,
+`center-x`, `align-*`) are errors under `children:`, because there the
+parent they measure against is this element: they go on the children
+themselves, or the CSS they stand for does (`children:flex 1`).
+
+#### Styling the elements inside
+
+An element prefix, `@NAME:`, styles every element of that kind inside
+this one, at any depth. `@table [@td:padding 8]` pads every cell of the
+table, and `@article [@h2:font-size 20]` sizes every heading in the
+article. It is how content that has no attributes of its own is styled:
+the HTML `@markdown` writes, and the rows a loop writes.
+
+```
+@let team Ada, Grace, Linus
+@table [border-collapse collapse, width 100%, @th:[padding 8, text-align left], @td:padding 8, @tr:border-bottom 1 solid #e5e7eb]
+ @tr
+ @th Name
+ @th Team
+ @each $name in $team
+ @tr
+ @td $name
+ @td [padding 2] Platform
+@article [spacing 12, @h2:font-size 20, @code:[padding 1 4, background #f1f5f9], @link:hover:color #2563eb]
+ @markdown
+ ## Notes
+
+ Run `htmlang check` before you [publish](/publish).
```
-@el [padding 16, background #3b82f6, hover:background #2563eb, md:padding 32, dark:background #1e3a8a]
+
+- **The element has a tag of its own.** `@td:` is every `
`,
+ `@paragraph:` every `