@@ -1,29 +1,53 @@
1−# htmlang (.hl)
1+# htmlang language reference
3−A minimalist layout language inspired by elm-ui that compiles to static HTML.
3+htmlang is a small layout language that compiles to static HTML. A page is a
4+tree of elements written one per line, with indentation for nesting:
7−- `@` means structure, bare lines mean content.
8+@el [max-width 640, center-x, padding 40, spacing 16]
8−- Layout is explicit and compositional: every element declares its own layout
9+ @h1 Hello
9− role, and there is no CSS cascade to reason about.
10+ @paragraph
10−- Output is one self-contained HTML file with embedded CSS (flexbox), and no
11+ This page was written in {@text [font-weight bold] htmlang}.
11− JavaScript unless you write some.
12+```
13Every example block in this file is compiled by the test suite
14Every example block in this file is compiled by the test suite
14(`tests/docs.rs`), so the examples stay correct.
15(`tests/docs.rs`), so the examples stay correct.
19+- **`@` starts structure, and any other line is content.** An element is `@name`,
20+ and a line without `@` is text.
21+- **htmlang's own vocabulary is about layout.** It comes from elm-ui. An
22+ element is a row or a column, `spacing` sets the gap between its children,
23+ and each child says how it sits in its parent (`width fill`, `center-x`,
24+ `align-right`). The layout attributes are the only styling words htmlang
26+- **Everything else is CSS or HTML, under its own name.** A style is any CSS
27+ property with a CSS value. An element that isn't about layout has its HTML
28+ name, and an HTML attribute is written `key=value`. Nothing is renamed or
30+- **Each thing is done one way.** `@let` defines every reusable piece, `@data`
31+ loads data, `@include` brings in a file, and `if()` makes an attribute
32+ conditional. A layout is an ordinary function.
33+- **The output is what you wrote.** Each page compiles to one self-contained
34+ HTML file. Its CSS is inside, and it has no JavaScript unless you write
35+ some. The compiler doesn't add attributes, tags or rewritten values that
36+ the source doesn't ask for.
37+- **Everything runs at compile time.** Variables, expressions, loops and
38+ conditions are resolved when the page is built.
18−### Structure
42+### Elements
21@element [attributes] argument
45@element [attributes] argument
25−Children are indented under their parent. Attributes are comma-separated inside
49+Children are indented under their parent. Attributes go inside `[...]`,
26−`[...]` and may span several lines:
50+separated by commas, and the list may span several lines:
@@ -35,27 +59,37 @@ Children are indented under their parent. Attributes are comma-separated inside
38−An attribute is either a **style**, written `key value` (`padding 20`,
62+An attribute can take one of three forms:
64+- `key value` is a **style**: a layout attribute or a CSS property
65+ (`spacing 20`, `color red`).
39−`color red`), or an **HTML attribute**, written `key=value` (`id=main`,
66+- `key=value` is an **HTML attribute** (`id=main`, `type=email`,
40−`type=email`, `aria-label=Close`). Boolean HTML attributes are written bare
67+ `aria-label=Close`).
68+- A bare word is a **flag**: a layout attribute such as `center-x`, or a
41−(`required`, `disabled`, `open`). A comma inside `(...)` or `"..."` does not
69+ boolean HTML attribute such as `required`, `disabled` or `open`.
71+A comma inside `(...)` or `"..."` doesn't split attributes, so a font stack
42−split attributes, so a font stack is written `font-family "Inter, sans-serif"`.
72+is written `font-family "Inter, sans-serif"`.
46−Any line that doesn't start with `@` is text. Text after an element's
76+A line that doesn't start with `@` is text. Text after an element's
47−attributes is its content, and `{...}` puts elements inside a line of text:
77+attributes is the element's content. Inside a line of text, `{...}` holds an
51− This is {@text [font-weight bold] important} and this is a {@link https://example.com link}.
82+ This is {@text [font-weight bold] important}, and this is {@link https://example.com a link}.
52@text [font-weight bold, font-size 24, color #333] Hello world
83@text [font-weight bold, font-size 24, color #333] Hello world
53@section [padding 8] Text after the attributes is content too.
84@section [padding 8] Text after the attributes is content too.
56−A backslash makes the next character literal: `\@` and `\--` let a text line
87+A backslash makes the next character literal:
89+- `\@` and `\--` let a line of text start with `@` or `--`.
57−start with `@` or `--`, `\$` shows a `$` that isn't a variable, `\{` a brace
90+- `\$` writes a `$` that isn't a variable.
58−that isn't an inline element, and `\\` a backslash.
91+- `\{` writes a brace that doesn't start an inline element.
92+- `\\` writes a backslash.
61\@htmlang on social media
95\@htmlang on social media
@@ -65,30 +99,32 @@ that isn't an inline element, and `\\` a backslash.
66### Comments
100### Comments
68−`--` at the start of a line begins a comment. Comments must be on their own
102+A line starting with `--` is a comment. A comment must be on its own line,
69−line: `--` later in a line is ordinary text.
103+because `--` later in a line is ordinary text.
72-- this is a comment
106-- this is a comment
73@row [spacing 10]
107@row [spacing 10]
74− -- todo: add nav items
108+ -- todo: more nav items
77−### Shorthands
112+### Chains
79−`>` chains single-child elements on one line; the last element in the chain
114+`>` puts elements that have one child each on one line. The last element in
80−gets the indented children.
115+the chain gets the indented children.
83@el [padding 16, background blue, border-radius 8] > @link https://example.com
118@el [padding 16, background blue, border-radius 8] > @link https://example.com
84 @text [color white] Get Started
119 @text [color white] Get Started
87−### Raw content
122+### Verbatim bodies
89−`@raw` pastes HTML into the output verbatim: the rest of its line, or its
124+`@raw` writes HTML into the output exactly as given, either the rest of its
90−indented block. The bodies of `@style`, `@head`, `@script` and `@markdown` are
125+line or its indented block. The bodies of `@style`, `@head`, `@script` and
126+`@markdown` are also kept exactly as written: nothing in them is parsed as
91−verbatim too (a `--` line in them is not a comment).
127+htmlang, and a `--` line in them is not a comment.
94@raw <hr class="fancy">
130@raw <hr class="fancy">
@@ -98,72 +134,68 @@ verbatim too (a `--` line in them is not a comment).
101−## Elements
137+## Layout
103−The layout and text elements come from elm-ui; every other element has its
104−HTML name. Every container lays its children out in a column, except `@row`.
106−| Element | Output | Purpose |
139+### Rows and columns
108−| `@row` | div, flex row | Horizontal layout |
109−| `@el` | div, flex column | The container: children laid out top to bottom |
110−| `@grid` | div, grid | Grid container (use `grid-cols`) |
111−| `@in-front` / `@behind` | div, absolute | Overlay layers filling the parent (see below) |
112−| `@text` | span | Styled inline text |
113−| `@paragraph` | p | Flowing text with inline elements |
114−| `@link url` | a | Link; text after the URL becomes its content |
115−| `@image src` | img | Image |
116−| `@h1` … `@h6` | h1 … h6 | Headings |
118−Semantic containers, all laid out as columns: `@nav`, `@header`, `@footer`,
141+`@el` is the container, and it lays out its children in a column. `@row`
119−`@main`, `@section`, `@article`, `@aside`, `@address`, `@search`, `@form`,
142+lays them out side by side. Every other container (`@section`, `@nav`,
120−`@details` / `@summary`, `@dialog`, `@figure` / `@figcaption`, `@blockquote` /
143+`@article`, `@form`, and so on) is a column too, so `@row` is the only
121−`@cite`, `@fieldset` / `@legend`, `@noscript`.
144+element that changes direction. `@grid` is a CSS grid.
123−Content: `@ul` / `@ol` / `@li`, `@dl` / `@dt` /
146+These attributes are htmlang's own. They describe how an element lays out
124−`@dd`, `@table` / `@thead` / `@tbody` / `@tr` / `@th` / `@td`, `@code`, `@pre`,
147+its children, and how it sits in its parent:
125−`@hr`, `@mark`, `@kbd`, `@abbr`, `@time`, `@progress`, `@meter`, `@output`,
126−`@canvas`, `@iframe src`, `@video src`, `@audio src`, `@picture` / `@source`,
127−`@script`, `@fragment` (children without a wrapper).
129−Form controls: `@input`, `@button`, `@select` / `@option`, `@textarea`,
149+| Attribute | Effect |
130−`@label`, `@datalist`. `@form URL` sets the form's `action`.
150+|---|---|
151+| `spacing N` | Gap between children |
152+| `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 |
153+| `height fill` / `height shrink` / `height N` | Take the remaining space in a column, fit the content, or an exact size |
154+| `center-x`, `center-y` | Center the element in its parent |
155+| `align-left`, `align-right`, `align-top`, `align-bottom` | Align the element in its parent |
156+| `wrap` | Let a row wrap onto more lines |
157+| `grid-cols N`, `grid-rows N` | Equal grid columns or rows (on `@grid`) |
158+| `col-span N`, `row-span N` | Cells a grid child spans |
133−@ol
161+@row [spacing 8]
134− @li First
162+ @el [width fill, padding 12, background #f3f4f6] Fills the row
135− @li Second
163+ @el [width 120, padding 12, background #e5e7eb] 120px
136−@form [method=post] /subscribe
164+@el [height 200, spacing 8, background #f9fafb]
137− @label [for=email] Email
165+ @text [center-x] Centered
138− @input [type=email, name=email, id=email, required]
166+ @el [height fill, background #dbeafe] Fills the rest of the column
139− @button [type=submit] Send
167+ @text [align-right] Right-aligned
140−@details [open]
168+@grid [grid-cols 3, spacing 8, md:grid-cols 4]
141− @summary Question
169+ @el [col-span 2, background #fef3c7] Two columns wide
142− @text The answer.
170+ @el [background #fde68a] One
171+ @el [background #fcd34d] One
174+Everything else about layout is plain CSS: `justify-content`, `align-items`,
175+`flex 1 1 240px`, `grid-template-areas`, `position`.
145−### `@in-front` / `@behind`
177+### Overlays: `@in-front` and `@behind`
147−Overlay layers, like elm-ui's `inFront` and `behind`. Their children fill the
179+These layers work like elm-ui's `inFront` and `behind`. Their children fill
148−parent's bounds; the parent automatically becomes a positioning context
180+the parent's bounds, and the parent becomes a positioning context
149−(`position: relative; isolation: isolate`). An explicit `position` on the parent
181+(`position: relative; isolation: isolate`). An explicit `position` on the
150−wins.
182+parent takes precedence.
153@el [width 200, height 200, background blue]
185@el [width 200, height 200, background blue]
154 @text [color white] Main content
186 @text [color white] Main content
155 @in-front
187 @in-front
156− @text [color yellow] Painted on top
188+ @text [color yellow, align-right, align-bottom] Painted on top
158− @el [background red, width fill, height fill]
190+ @el [background red, height fill]
161### Standard library
193### Standard library
163−Two definitions are written in htmlang (`std.hl`) and available in every
195+Two definitions are written in htmlang (`std.hl`) and are available in every
164−file; your own `@let` with the same name takes precedence. `@spacer` takes up
196+file. A `@let` of your own with the same name replaces them. `@spacer` takes
165−the remaining space in a row or column, and the `$truncate` bundle cuts text
197+up the remaining space in a row or column. The `$truncate` bundle cuts text
166−off at one line with an ellipsis. Components beyond that are yours to define
198+off at one line with an ellipsis. You define any other components yourself
167with `@let`.
199with `@let`.
@@ -173,81 +205,56 @@ with `@let`.
173 @text [$truncate, max-width 200] A long title that gets cut off
205 @text [$truncate, max-width 200] A long title that gets cut off
176−## Attributes
208+## Style
178−### Layout
210+### CSS properties
180−htmlang's own attributes are about layout, in the spirit of elm-ui: how an
181−element sits in its row or column.
183−| Attribute | Effect |
185−| `spacing N` | Gap between children |
186−| `width fill` / `width N` / `width shrink` | Take remaining space, exact size, or fit content |
187−| `height fill` / `height N` / `height shrink` | Same for height |
188−| `center-x`, `center-y` | Center within the parent |
189−| `align-left`, `align-right`, `align-top`, `align-bottom` | Align within the parent |
190−| `wrap` | Let a row wrap |
191−| `grid-cols N`, `grid-rows N`, `col-span N`, `row-span N` | Grid layout |
193−### Style
212+Any attribute that isn't a layout attribute is a **CSS property**, with the
195−Everything else is CSS: **any standard CSS property** is an attribute, with
196−the same name and value: `padding 20`, `font-weight bold`, `font-size 18`,
213+same name and value as in CSS: `padding 20`, `font-weight bold`,
197`border 1 solid #e5e7eb`, `border-radius 8`, `background red`,
214`border 1 solid #e5e7eb`, `border-radius 8`, `background red`,
198−`display none`, `grid-template-areas "a b"`, and so on. `line-clamp N` also
215+`grid-template-areas "a b"`, `display none`.
199−adds the `-webkit-box` declarations browsers still need to cut text off after
202−**Units.** For lengths, a bare number is pixels (`padding 20` is `20px`,
203−`border 1 solid red` is `1px solid red`). Values with a unit, keywords and CSS
204−functions are passed through: `width 50%`, `margin 0 auto`,
205−`max-width min(100%, 800px)`.
207−### HTML attributes
217+Values mean what they mean in CSS, with one addition: **a bare number in a
218+length is pixels**. So `padding 20` is `20px`, `border 1 solid red` is
219+`1px solid red`, and `margin 0 auto` is `0 auto`. Values that have a unit,
220+keywords and CSS functions pass through as written (`width 50%`,
221+`max-width min(100%, 800px)`), and so do numbers that aren't lengths
222+(`opacity 0.5`, `z-index 2`, `line-height 1.5`, `flex 1`).
209−Write HTML attributes as `key=value`: `id=main`, `class=note`, `href=/about`,
224+`line-clamp N` also adds the `-webkit-box` declarations that browsers still
210−`type=email`, `alt=Logo`, `target=_blank`, `aria-label=Close menu`,
225+need to cut text off after N lines.
211−`data-id=42`. Any name works. Booleans are written bare: `required`,
212−`disabled`, `checked`, `open`, `sandbox`. A style and an HTML attribute may
213−share a name without clashing:
216−@image [width=800, width 200, alt=A photo] photo.jpg
217−@select [size=4, font-size 18]
221−### State and media prefixes
227+### Prefixes
223−Prefix a style attribute to apply it conditionally:
229+A prefix applies a style only in some condition:
225| Prefix | Applies |
231| Prefix | Applies |
227| `hover:`, `active:`, `focus:`, `focus-visible:`, `focus-within:`, `disabled:`, `checked:`, `visited:`, `target:`, `valid:`, `invalid:`, `empty:`, `placeholder:`, `selection:` | In that state |
233| `hover:`, `active:`, `focus:`, `focus-visible:`, `focus-within:`, `disabled:`, `checked:`, `visited:`, `target:`, `valid:`, `invalid:`, `empty:`, `placeholder:`, `selection:` | In that state |
228−| `first:`, `last:`, `odd:`, `even:`, `nth:EXPR:` | By position among siblings |
234+| `first:`, `last:`, `odd:`, `even:`, `nth:EXPR:` | By the element's position among its siblings |
229| `children:` | To each direct child |
235| `children:` | To each direct child |
230−| `before:`, `after:` | On the `::before` / `::after` pseudo-element (with `content`) |
236+| `before:`, `after:` | To the `::before` / `::after` pseudo-element (together with `content`) |
231| `has(SELECTOR):` | When the element contains a match |
237| `has(SELECTOR):` | When the element contains a match |
232−| `sm:`, `md:`, `lg:`, `xl:`, `2xl:` | From that viewport width up (640–1536px) |
238+| `sm:`, `md:`, `lg:`, `xl:`, `2xl:` | From that viewport width up (640, 768, 1024, 1280, 1536px) |
233−| `cq-sm:` … `cq-2xl:` | Container queries |
239+| `cq-sm:` … `cq-2xl:` | From that container width up (the same widths, for an ancestor with `container-type inline-size`) |
234−| `dark:`, `print:`, `motion-safe:`, `motion-reduce:`, `landscape:`, `portrait:` | Media conditions |
240+| `dark:`, `print:`, `motion-safe:`, `motion-reduce:`, `landscape:`, `portrait:` | Under that media condition |
237@el [padding 16, background #3b82f6, hover:background #2563eb, md:padding 32, dark:background #1e3a8a]
243@el [padding 16, background #3b82f6, hover:background #2563eb, md:padding 32, dark:background #1e3a8a]
238 @text [color white] Click me
244 @text [color white] Click me
239−@row [spacing 4, children:flex 1, nth:2n:background #f3f4f6]
245+@row [spacing 4, children:flex 1]
240− @el A
246+ @el [odd:background #f3f4f6] A
241− @el B
247+ @el [odd:background #f3f4f6] B
248+ @el [odd:background #f3f4f6] C
242@el [before:content "→ ", before:color red]
249@el [before:content "→ ", before:color red]
243 Item with an arrow
250 Item with an arrow
246### Conditional attributes
253### Conditional attributes
248−`if(CONDITION, A, B)` picks `A` or `B` by the condition. It works as a value or
255+`if(CONDITION, A, B)` chooses `A` or `B` depending on the condition. It works
249−as a whole attribute, and an empty choice (the `B` can be left out) leaves the
256+as a value or as a whole attribute. If the chosen side is empty, the attribute
250−attribute out.
257+is left out, and `B` itself can be omitted.
253@let active true
260@let active true
@@ -255,47 +262,146 @@ attribute out.
255 Conditionally styled
262 Conditionally styled
258−## Variables and functions
265+## HTML
269+The layout and text elements have elm-ui's names. Every other element has
272+| Element | Output | Purpose |
274+| `@el` | div, flex column | The container: children laid out top to bottom |
275+| `@row` | div, flex row | Children laid out side by side |
276+| `@grid` | div, grid | Grid container (with `grid-cols` or `grid-template-*`) |
277+| `@in-front` / `@behind` | div, absolute | Overlay layers that fill the parent |
278+| `@text` | span | Styled inline text |
279+| `@paragraph` | p | Flowing text with inline elements |
280+| `@link URL` | a | Link, whose content is the text after the URL |
281+| `@image SRC` | img | Image |
282+| `@fragment` | (none) | Its children, without a wrapper element |
284+The HTML elements, by kind:
286+- **Semantic containers**, all laid out as columns: `@nav`, `@header`,
287+ `@footer`, `@main`, `@section`, `@article`, `@aside`, `@address`,
288+ `@search`, `@form`, `@details` / `@summary`, `@dialog`, `@figure` /
289+ `@figcaption`, `@blockquote` / `@cite`, `@fieldset` / `@legend`,
291+- **Text**: `@h1` … `@h6`, `@code`, `@pre`, `@mark`, `@kbd`, `@abbr`,
293+- **Lists and tables**: `@ul` / `@ol` / `@li`, `@dl` / `@dt` / `@dd`,
294+ `@table` / `@thead` / `@tbody` / `@tr` / `@th` / `@td`.
295+- **Forms**: `@input`, `@button`, `@select` / `@option`, `@textarea`,
296+ `@label`, `@datalist`, `@progress`, `@meter`, `@output`. For `@form URL`,
297+ the URL is the form's `action`.
298+- **Media and embeds**: `@picture` / `@source`, `@video SRC`, `@audio SRC`,
299+ `@iframe SRC`, `@canvas`, `@script`, `@hr`.
301+Browser default margins on headings, paragraphs, lists and figures are reset
302+to 0, so `spacing` controls the gaps. List items are columns like other
303+containers, so a list shows no markers. To bring them back, write
304+`[list-style disc, padding-inline-start 20, children:display list-item]` on
311+@form [method=post, spacing 8] /subscribe
312+ @label [for=email] Email
313+ @input [type=email, name=email, id=email, required]
314+ @button [type=submit] Send
322+HTML attributes are written `key=value`: `id=main`, `class=note`,
323+`href=/about`, `type=email`, `alt=Logo`, `target=_blank`,
324+`aria-label=Close menu`, `data-id=42`. Any name works. Boolean attributes are
325+written bare: `required`, `disabled`, `checked`, `open`, `popover`. A style
326+and an HTML attribute can share a name, because the `=` tells them apart:
329+@image [width=800, width 200, alt=A photo] photo.jpg
332+ @select [size=4, font-size 18]
260−`@let` defines everything reusable. What it defines depends on its shape:
338+`@let` defines everything reusable. What it defines depends on its form:
263-- A value, used as $primary
341-- A value, used as $primary
264@let primary #3b82f6
342@let primary #3b82f6
265−-- A computed value: `=` makes it an expression
343+-- A computed value: `=` makes the rest an expression
266@let gap = 8 * 2
344@let gap = 8 * 2
267−-- A quoted string, with $variables interpolated
345+-- A string, with $variables interpolated
268−@let greeting "Hello $primary"
346+@let greeting "Hello from $primary"
269-- An attribute bundle, used as [$card]
347-- An attribute bundle, used as [$card]
270@let card [padding 20, background white, border-radius 8]
348@let card [padding 20, background white, border-radius 8]
271-- A function: a @let with an indented body
349-- A function: a @let with an indented body
272−@let panel $title $tone=neutral
350+@let panel $title
273− @el [$card]
351+ @el [$card, spacing $gap]
274− @text [font-weight bold] $title
352+ @text [font-weight bold, color $primary] $title
275 @children
353 @children
277@panel [title Welcome]
355@panel [title Welcome]
278− Body text goes into @children.
356+ $greeting
279@el [$card, background #f9fafb]
357@el [$card, background #f9fafb]
280 Attributes after a bundle override it.
358 Attributes after a bundle override it.
361+A definition applies from its own line onward.
283A function is called like an element:
365A function is called like an element:
285−- Its parameters are passed as attributes; parameters with `=default` may be
367+- Its parameters are passed as attributes. A parameter with a default
286− omitted.
368+ (`$tone=info`) can be left out.
287−- Other attributes style its root element, so `@panel [title Hi, padding 40]`
369+- Any other attributes style the function's root element, so
288− works like styling a built-in element.
370+ `@panel [title Hi, padding 40]` works the same as styling a built-in
289−- Text after the attributes and indented children replace `@children`, and a
372+- Text after the attributes and the indented children replace `@children`.
290− caller's `@slot name` block replaces `@slot name` (the slot's own children are
373+ A caller's `@slot NAME` block replaces the function's `@slot NAME`, and
291− the default).
374+ the slot's own children are the default content.
292−- An `@style` block at the top of the body is scoped to the function: its rules
375+
293− apply inside the function's root element (`&` is the root itself). The
376+```
294− body needs a single root element.
377+@let card $title $tone=#f9fafb
378+ @article [padding 16, spacing 8, background $tone, border-radius 8]
379+ @row [align-items center]
385+ @text [font-size 12, color gray] No footer
389+@card [title Full, tone #eff6ff, padding 24]
392+ The children go here.
397+An `@style` block at the top of a function body is **scoped** to the
398+function. Its rules apply inside the function's root element, and `&` is the
399+root itself. This requires the body to have a single root element.
297−@let note $kind=info
402+@let note $kind=Note
404+ & { border-left: 4px solid #3b82f6; }
299 .title { font-weight: bold; }
405 .title { font-weight: bold; }
300 @el [padding 12, border-radius 6, background #eff6ff]
406 @el [padding 12, border-radius 6, background #eff6ff]
301 @text [class=title] $kind
407 @text [class=title] $kind
@@ -304,7 +410,7 @@ A function is called like an element:
304@note [kind Tip, padding 20] Scoped styles and forwarded attributes.
410@note [kind Tip, padding 20] Scoped styles and forwarded attributes.
307−Multi-line content is a function whose body is text:
413+A function whose body is text holds multi-line content:
310@let intro
416@let intro
@@ -315,9 +421,11 @@ Multi-line content is a function whose body is text:
424+### CSS custom properties
318−`@let --name value` also declares the CSS custom property `--name` on
426+`@let --name value` declares the CSS custom property `--name` on `:root`.
319−`:root`. `$--name` is its value at compile time; `var(--name)` refers to it at
320−run time, so a stylesheet can override it:
427+`var(--name)` refers to it at run time, so a stylesheet can still override
428+it. `$--name` is its value at compile time, for use in expressions:
323@let --brand #3b82f6
431@let --brand #3b82f6
@@ -330,20 +438,20 @@ Conditions and computed values (`@let x = ...`) are expressions:
333−| Values | numbers, `"strings"` (with `$var` interpolation), `$variables`, `true`, `false`, and bare words (`dark`, `#fff`) as strings |
441+| Values | numbers, `"strings"` (with `$var` interpolation), `$variables`, `$record.field`, `true`, `false`, and bare words (`dark`, `#fff`) as strings |
334| Arithmetic | `+ - * / %` with the usual precedence, unary `-`, `( )` |
442| Arithmetic | `+ - * / %` with the usual precedence, unary `-`, `( )` |
335| Comparison | `== != < > <= >=` (numeric when both sides are numbers) |
443| Comparison | `== != < > <= >=` (numeric when both sides are numbers) |
336−| Logic | `and`, `or`, `not`; empty, `false` and `0` are false |
444+| Logic | `and`, `or`, `not`. An empty value, `false` and `0` are false |
337| Choice | `if(CONDITION, A, B)` |
445| Choice | `if(CONDITION, A, B)` |
338−| Tests | `contains(s, x)` (in text, or as an item of a list), `starts-with(s, x)`, `ends-with(s, x)` |
446+| Tests | `contains(s, x)` (in a text, or as an item of a list), `starts-with(s, x)`, `ends-with(s, x)` |
339−| Text functions | `uppercase(s)`, `lowercase(s)`, `capitalize(s)`, `trim(s)`, `length(s)` (a list's items, or a text's characters), `reverse(s)`, `truncate(s, n)`, `replace(s, old, new)`, `default(s, fallback)` |
447+| Text | `uppercase(s)`, `lowercase(s)`, `capitalize(s)`, `trim(s)`, `length(s)` (the items of a list, or the characters of a text), `reverse(s)`, `truncate(s, n)`, `replace(s, old, new)`, `default(s, fallback)` |
340−| Color functions | `lighten(c, pct)`, `darken(c, pct)`, `alpha(c, a)`, `mix(c1, c2, pct)` |
448+| Color | `lighten(c, pct)`, `darken(c, pct)`, `alpha(c, a)`, `mix(c1, c2, pct)` |
342−Variables are looked up while evaluating, so a value containing `==` or spaces
450+Variables are looked up during evaluation, so a value that contains `==` or
343−is still one value. An invalid expression is a compile error.
451+spaces is still one value. An invalid expression is a compile error.
345−In text and attribute values, `$name` inserts a variable and `${EXPR}` inserts
453+In text and attribute values, `$name` inserts a variable, and `${EXPR}`
346−the value of any expression:
454+inserts the value of any expression:
349@let name htmlang
457@let name htmlang
@@ -356,8 +464,8 @@ the value of any expression:
356All control flow runs at compile time.
464All control flow runs at compile time.
359−@let items apple, banana, cherry
360@let count 3
467@let count 3
362@if $count > 2 and not $hidden
470@if $count > 2 and not $hidden
363 @text Many
471 @text Many
@@ -365,56 +473,70 @@ All control flow runs at compile time.
365 @text None
473 @text None
367 @text Few
475 @text Few
369−@each $item, $i in $items
478+`@each $item in LIST` repeats its body for each item. A list can be a list
479+loaded with `@data`, a text split on commas, or a range. A range counts down
480+when its start is greater than its end, and `step` sets the increment. An
481+optional second variable is the index, starting from 0. `@else` gives the
482+content to show when the list is empty.
485+@let fruits apple, banana, cherry
486+@each $fruit, $i in $fruits
370− @text $i: $item
487+ @text ${$i + 1}. $fruit
372− @text The list is empty.
489+ @text No fruit.
374−@each $n in 1..10 step 3
492+ @each $n in 10..0 step 5
375− @text $n
493+ @text $n
378−`@each $item in LIST` loops over a list: a list loaded with `@data`, text
496+## Data and files
379−split on commas, or a range (which counts down when its start is greater than
497+
380−its end). An optional second variable is the index, from 0. An item loaded from
498+| Directive | Effect |
381−JSON can be a record, whose fields are `$item.key`:
499+|---|---|
500+| `@data $name file.json` | Load a JSON file. Objects are records (`$name.key`) and arrays are lists |
501+| `@data $name [...]` / `@data $name {...}` | Inline JSON, which may span several lines |
502+| `@data $name dir/*.json` | A list with one record per file, in name order. `$item.file` is the file's name |
503+| `@data $name env:NAME [default]` | An environment variable |
504+| `@include file.hl` | Insert another file: its content and its definitions. A file that holds only `@let`s outputs nothing |
505+| `@markdown` / `@markdown file.md` | Markdown (an indented body or a file), converted to HTML |
506+| `@image [inline] file.svg` | Put the file inside the page: SVG as markup (`width`, `height`, `color`, `class=` and `id=` apply to it), other images as base64 |
508+Records keep their values whole, even when a value contains spaces or
384−@data $links [
512+@data $team [
385− {"label": "About us", "url": "/about"},
513+ {"name": "Ada", "role": "Engineering, research"},
386− {"label": "Blog", "url": "/blog"}
514+ {"name": "Grace", "role": "Design"}
388−@row [spacing 12]
516+@ul [spacing 4]
389− @each $link in $links
517+ @each $person in $team
390− @link $link.url $link.label
518+ @li
519+ @text [font-weight bold] $person.name
391−@text ${length($links)} links
521+@text ${length($team)} people
394−## Files and data
524+### Layouts
396−| Directive | Effect |
398−| `@include file.hl` | Insert another file: its content and definitions (a library of `@let`s emits nothing) |
399−| `@data $name file.json` | Load JSON values as variables (`$name.key`; arrays are lists) |
400−| `@data $name [...]` / `@data $name {...}` | Inline JSON (an array may span lines) |
401−| `@data $name dir/*.json` | A list with one record per file, in name order; `$item.file` is the file's name |
402−| `@data $name env:NAME [default]` | Read an environment variable |
403−| `@markdown` / `@markdown file.md` | Markdown, converted to HTML |
404−| `@image [inline] file.svg` | Inline an SVG file (`width`, `height`, `color`, `class=`, `id=` apply to it) |
406−A layout is an ordinary function in its own file. It marks where content goes
526+A layout is an ordinary function in a file of its own. It marks where content
407−with `@slot name` (named blocks, with default content) and `@children`
527+goes with `@slot NAME` (named blocks, with default content) and `@children`
408−(everything in the call outside `@slot` blocks):
528+(everything in the call that is outside a `@slot` block).
411-- layout.hl
531-- layout.hl
412@let layout
532@let layout
413 @page My Site
533 @page My Site
414− @el [max-width 800, center-x]
534+ @el [max-width 800, center-x, spacing 24]
415− @slot header
536+ @slot header
416− @text Default header
537+ @text Default header
417− @children
539+ @children
@@ -422,30 +544,32 @@ with `@slot name` (named blocks, with default content) and `@children`
422@include layout.hl
544@include layout.hl
424 @slot header
546 @slot header
425− @text About us
547+ @h1 About us
426 @paragraph This fills @children.
548 @paragraph This fills @children.
429## Page and head
551## Page and head
431−`@page TITLE` produces a full HTML document; without it the output is a fragment.
553+`@page TITLE` makes the output a full HTML document. Without it, the output
432−Its attributes set `lang` and `favicon` (inlined into the page). `@meta NAME VALUE`
554+is a fragment. The attributes of `@page` set `lang`, and `favicon` (the file
555+is embedded in the page). `@meta NAME VALUE` adds a meta tag, and names that
433−adds a meta tag (`og:` names become Open Graph `property` tags), and
556+start with `og:` become Open Graph `property` tags. `@head` holds any other
434−`@head` holds any other raw HTML for the `<head>` (fonts, JSON-LD, a manifest
557+raw HTML for the `<head>`, such as a font link, a canonical URL or JSON-LD.
435−link). Translations are a JSON file per locale: `@data $t locales/$lang.json`.
438−@page [lang en, favicon /favicon.png] My Site
560+@page [lang en, favicon favicon.png] My Site
439@meta description A small site
561@meta description A small site
440@meta og:title My Site
562@meta og:title My Site
564+ <link rel="canonical" href="https://example.com/">
567+Translations are a JSON file per locale: `@data $t locales/$lang.json`.
445−- `@style` holds raw CSS, including at-rules such as `@keyframes`, `@scope`,
571+`@style` holds raw CSS, including at-rules such as `@keyframes`,
446− `@property` and `@starting-style`.
572+`@font-face`, `@property` and `@starting-style`:
447−- Generated rules live in `@layer htmlang` (after a `hl-reset` layer), so any
448− CSS in `@style` or `@raw` overrides them.
@@ -456,3 +580,10 @@ link). Translations are a JSON file per locale: `@data $t locales/$lang.json`.
456 .note { color: gray; }
580 .note { color: gray; }
457@el [animation fade-in 0.3s ease, class=note] Fades in
581@el [animation fade-in 0.3s ease, class=note] Fades in
584+Each element gets a short generated class for its styles, and elements with
585+the same styles share one. The generated rules live in `@layer htmlang`. A
586+small reset (`box-sizing`, body margin, block images, unstyled links, a
587+focus outline) lives in `@layer hl-reset` before it. CSS outside any layer
588+takes precedence over both, so rules in `@style` or `@raw` override the