@@ -1,29 +1,53 @@
1−# htmlang (.hl)
1+# htmlang language reference
22
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:
45
5−## Principles
6−
7−- `@` means structure, bare lines mean content.
8−- Layout is explicit and compositional: every element declares its own layout
9− role, and there is no CSS cascade to reason about.
10−- Output is one self-contained HTML file with embedded CSS (flexbox), and no
11− JavaScript unless you write some.
6+```
7+@page Hello
8+@el [max-width 640, center-x, padding 40, spacing 16]
9+ @h1 Hello
10+ @paragraph
11+ This page was written in {@text [font-weight bold] htmlang}.
12+```
1213
1314Every example block in this file is compiled by the test suite
1415(`tests/docs.rs`), so the examples stay correct.
1516
17+## Principles
18+
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
25+ adds.
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
29+ abbreviated.
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.
39+
1640## Syntax
1741
18−### Structure
42+### Elements
1943
2044```
2145@element [attributes] argument
2246 children
2347```
2448
25−Children are indented under their parent. Attributes are comma-separated inside
26−`[...]` and may span several lines:
49+Children are indented under their parent. Attributes go inside `[...]`,
50+separated by commas, and the list may span several lines:
2751
2852```
2953@el [
@@ -35,27 +59,37 @@ Children are indented under their parent. Attributes are comma-separated inside
3559 Content
3660```
3761
38−An attribute is either a **style**, written `key value` (`padding 20`,
39−`color red`), or an **HTML attribute**, written `key=value` (`id=main`,
40−`type=email`, `aria-label=Close`). Boolean HTML attributes are written bare
41−(`required`, `disabled`, `open`). A comma inside `(...)` or `"..."` does not
42−split attributes, so a font stack is written `font-family "Inter, sans-serif"`.
62+An attribute can take one of three forms:
63+
64+- `key value` is a **style**: a layout attribute or a CSS property
65+ (`spacing 20`, `color red`).
66+- `key=value` is an **HTML attribute** (`id=main`, `type=email`,
67+ `aria-label=Close`).
68+- A bare word is a **flag**: a layout attribute such as `center-x`, or a
69+ boolean HTML attribute such as `required`, `disabled` or `open`.
70+
71+A comma inside `(...)` or `"..."` doesn't split attributes, so a font stack
72+is written `font-family "Inter, sans-serif"`.
4373
4474### Text
4575
46−Any 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:
76+A line that doesn't start with `@` is text. Text after an element's
77+attributes is the element's content. Inside a line of text, `{...}` holds an
78+inline element:
4879
4980```
5081@paragraph
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}.
5283@text [font-weight bold, font-size 24, color #333] Hello world
5384@section [padding 8] Text after the attributes is content too.
5485```
5586
56−A backslash makes the next character literal: `\@` and `\--` let a text line
57−start with `@` or `--`, `\$` shows a `$` that isn't a variable, `\{` a brace
58−that isn't an inline element, and `\\` a backslash.
87+A backslash makes the next character literal:
88+
89+- `\@` and `\--` let a line of text start with `@` or `--`.
90+- `\$` writes a `$` that isn't a variable.
91+- `\{` writes a brace that doesn't start an inline element.
92+- `\\` writes a backslash.
5993
6094```
6195\@htmlang on social media
@@ -65,30 +99,32 @@ that isn't an inline element, and `\\` a backslash.
6599
66100### Comments
67101
68−`--` at the start of a line begins a comment. Comments must be on their own
69−line: `--` later in a line is ordinary text.
102+A line starting with `--` is a comment. A comment must be on its own line,
103+because `--` later in a line is ordinary text.
70104
71105```
72106-- this is a comment
73107@row [spacing 10]
74− -- todo: add nav items
108+ -- todo: more nav items
109+ @link / Home
75110```
76111
77−### Shorthands
112+### Chains
78113
79−`>` chains single-child elements on one line; the last element in the chain
80−gets the indented children.
114+`>` puts elements that have one child each on one line. The last element in
115+the chain gets the indented children.
81116
82117```
83118@el [padding 16, background blue, border-radius 8] > @link https://example.com
84119 @text [color white] Get Started
85120```
86121
87−### Raw content
122+### Verbatim bodies
88123
89−`@raw` pastes HTML into the output verbatim: the rest of its line, or its
90−indented block. The bodies of `@style`, `@head`, `@script` and `@markdown` are
91−verbatim too (a `--` line in them is not a comment).
124+`@raw` writes HTML into the output exactly as given, either the rest of its
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
127+htmlang, and a `--` line in them is not a comment.
92128
93129```
94130@raw <hr class="fancy">
@@ -98,72 +134,68 @@ verbatim too (a `--` line in them is not a comment).
98134 </div>
99135```
100136
101−## Elements
102−
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`.
137+## Layout
105138
106−| Element | Output | Purpose |
107−|---|---|---|
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 |
139+### Rows and columns
117140
118−Semantic containers, all laid out as columns: `@nav`, `@header`, `@footer`,
119−`@main`, `@section`, `@article`, `@aside`, `@address`, `@search`, `@form`,
120−`@details` / `@summary`, `@dialog`, `@figure` / `@figcaption`, `@blockquote` /
121−`@cite`, `@fieldset` / `@legend`, `@noscript`.
141+`@el` is the container, and it lays out its children in a column. `@row`
142+lays them out side by side. Every other container (`@section`, `@nav`,
143+`@article`, `@form`, and so on) is a column too, so `@row` is the only
144+element that changes direction. `@grid` is a CSS grid.
122145
123−Content: `@ul` / `@ol` / `@li`, `@dl` / `@dt` /
124−`@dd`, `@table` / `@thead` / `@tbody` / `@tr` / `@th` / `@td`, `@code`, `@pre`,
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).
146+These attributes are htmlang's own. They describe how an element lays out
147+its children, and how it sits in its parent:
128148
129−Form controls: `@input`, `@button`, `@select` / `@option`, `@textarea`,
130−`@label`, `@datalist`. `@form URL` sets the form's `action`.
149+| Attribute | Effect |
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 |
131159
132160```
133−@ol
134− @li First
135− @li Second
136−@form [method=post] /subscribe
137− @label [for=email] Email
138− @input [type=email, name=email, id=email, required]
139− @button [type=submit] Send
140−@details [open]
141− @summary Question
142− @text The answer.
161+@row [spacing 8]
162+ @el [width fill, padding 12, background #f3f4f6] Fills the row
163+ @el [width 120, padding 12, background #e5e7eb] 120px
164+@el [height 200, spacing 8, background #f9fafb]
165+ @text [center-x] Centered
166+ @el [height fill, background #dbeafe] Fills the rest of the column
167+ @text [align-right] Right-aligned
168+@grid [grid-cols 3, spacing 8, md:grid-cols 4]
169+ @el [col-span 2, background #fef3c7] Two columns wide
170+ @el [background #fde68a] One
171+ @el [background #fcd34d] One
143172```
144173
145−### `@in-front` / `@behind`
174+Everything else about layout is plain CSS: `justify-content`, `align-items`,
175+`flex 1 1 240px`, `grid-template-areas`, `position`.
176+
177+### Overlays: `@in-front` and `@behind`
146178
147−Overlay layers, like elm-ui's `inFront` and `behind`. Their children fill the
148−parent's bounds; the parent automatically becomes a positioning context
149−(`position: relative; isolation: isolate`). An explicit `position` on the parent
150−wins.
179+These layers work like elm-ui's `inFront` and `behind`. Their children fill
180+the parent's bounds, and the parent becomes a positioning context
181+(`position: relative; isolation: isolate`). An explicit `position` on the
182+parent takes precedence.
151183
152184```
153185@el [width 200, height 200, background blue]
154186 @text [color white] Main content
155187 @in-front
156− @text [color yellow] Painted on top
188+ @text [color yellow, align-right, align-bottom] Painted on top
157189 @behind
158− @el [background red, width fill, height fill]
190+ @el [background red, height fill]
159191```
160192
161193### Standard library
162194
163−Two definitions are written in htmlang (`std.hl`) and available in every
164−file; your own `@let` with the same name takes precedence. `@spacer` takes up
165−the remaining space in a row or column, and the `$truncate` bundle cuts text
166−off at one line with an ellipsis. Components beyond that are yours to define
195+Two definitions are written in htmlang (`std.hl`) and are available in every
196+file. A `@let` of your own with the same name replaces them. `@spacer` takes
197+up the remaining space in a row or column. The `$truncate` bundle cuts text
198+off at one line with an ellipsis. You define any other components yourself
167199with `@let`.
168200
169201```
@@ -173,81 +205,56 @@ with `@let`.
173205 @text [$truncate, max-width 200] A long title that gets cut off
174206```
175207
176−## Attributes
208+## Style
177209
178−### Layout
179−
180−htmlang's own attributes are about layout, in the spirit of elm-ui: how an
181−element sits in its row or column.
182−
183−| Attribute | Effect |
184−|---|---|
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 |
210+### CSS properties
192211
193−### Style
194−
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`,
212+Any attribute that isn't a layout attribute is a **CSS property**, with the
213+same name and value as in CSS: `padding 20`, `font-weight bold`,
197214`border 1 solid #e5e7eb`, `border-radius 8`, `background red`,
198−`display none`, `grid-template-areas "a b"`, and so on. `line-clamp N` also
199−adds the `-webkit-box` declarations browsers still need to cut text off after
200−N lines.
201−
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)`.
215+`grid-template-areas "a b"`, `display none`.
206216
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`).
208223
209−Write HTML attributes as `key=value`: `id=main`, `class=note`, `href=/about`,
210−`type=email`, `alt=Logo`, `target=_blank`, `aria-label=Close menu`,
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:
214−
215−```
216−@image [width=800, width 200, alt=A photo] photo.jpg
217−@select [size=4, font-size 18]
218− @option One
219−```
224+`line-clamp N` also adds the `-webkit-box` declarations that browsers still
225+need to cut text off after N lines.
220226
221−### State and media prefixes
227+### Prefixes
222228
223−Prefix a style attribute to apply it conditionally:
229+A prefix applies a style only in some condition:
224230
225231| Prefix | Applies |
226232|---|---|
227233| `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 |
229235| `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`) |
231237| `has(SELECTOR):` | When the element contains a match |
232−| `sm:`, `md:`, `lg:`, `xl:`, `2xl:` | From that viewport width up (640–1536px) |
233−| `cq-sm:` … `cq-2xl:` | Container queries |
234−| `dark:`, `print:`, `motion-safe:`, `motion-reduce:`, `landscape:`, `portrait:` | Media conditions |
238+| `sm:`, `md:`, `lg:`, `xl:`, `2xl:` | From that viewport width up (640, 768, 1024, 1280, 1536px) |
239+| `cq-sm:` … `cq-2xl:` | From that container width up (the same widths, for an ancestor with `container-type inline-size`) |
240+| `dark:`, `print:`, `motion-safe:`, `motion-reduce:`, `landscape:`, `portrait:` | Under that media condition |
235241
236242```
237243@el [padding 16, background #3b82f6, hover:background #2563eb, md:padding 32, dark:background #1e3a8a]
238244 @text [color white] Click me
239−@row [spacing 4, children:flex 1, nth:2n:background #f3f4f6]
240− @el A
241− @el B
245+@row [spacing 4, children:flex 1]
246+ @el [odd:background #f3f4f6] A
247+ @el [odd:background #f3f4f6] B
248+ @el [odd:background #f3f4f6] C
242249@el [before:content "→ ", before:color red]
243250 Item with an arrow
244251```
245252
246253### Conditional attributes
247254
248−`if(CONDITION, A, B)` picks `A` or `B` by the condition. It works as a value or
249−as a whole attribute, and an empty choice (the `B` can be left out) leaves the
250−attribute out.
255+`if(CONDITION, A, B)` chooses `A` or `B` depending on the condition. It works
256+as a value or as a whole attribute. If the chosen side is empty, the attribute
257+is left out, and `B` itself can be omitted.
251258
252259```
253260@let active true
@@ -255,47 +262,146 @@ attribute out.
255262 Conditionally styled
256263```
257264
258−## Variables and functions
265+## HTML
266+
267+### Elements
259268
260−`@let` defines everything reusable. What it defines depends on its shape:
269+The layout and text elements have elm-ui's names. Every other element has
270+its HTML name.
271+
272+| Element | Output | Purpose |
273+|---|---|---|
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 |
283+
284+The HTML elements, by kind:
285+
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`,
290+ `@noscript`.
291+- **Text**: `@h1` … `@h6`, `@code`, `@pre`, `@mark`, `@kbd`, `@abbr`,
292+ `@time`.
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`.
300+
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
305+the list.
306+
307+```
308+@ol [spacing 4]
309+ @li First
310+ @li Second
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
315+@details [open]
316+ @summary Question
317+ @text The answer.
318+```
319+
320+### HTML attributes
321+
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:
327+
328+```
329+@image [width=800, width 200, alt=A photo] photo.jpg
330+@label
331+ Size
332+ @select [size=4, font-size 18]
333+ @option One
334+```
335+
336+## Definitions
337+
338+`@let` defines everything reusable. What it defines depends on its form:
261339
262340```
263341-- A value, used as $primary
264342@let primary #3b82f6
265−-- A computed value: `=` makes it an expression
343+-- A computed value: `=` makes the rest an expression
266344@let gap = 8 * 2
267−-- A quoted string, with $variables interpolated
268−@let greeting "Hello $primary"
345+-- A string, with $variables interpolated
346+@let greeting "Hello from $primary"
269347-- An attribute bundle, used as [$card]
270348@let card [padding 20, background white, border-radius 8]
271349-- A function: a @let with an indented body
272−@let panel $title $tone=neutral
273− @el [$card]
274− @text [font-weight bold] $title
350+@let panel $title
351+ @el [$card, spacing $gap]
352+ @text [font-weight bold, color $primary] $title
275353 @children
276354
277355@panel [title Welcome]
278− Body text goes into @children.
356+ $greeting
279357@el [$card, background #f9fafb]
280358 Attributes after a bundle override it.
281359```
282360
361+A definition applies from its own line onward.
362+
363+### Functions
364+
283365A function is called like an element:
284366
285−- Its parameters are passed as attributes; parameters with `=default` may be
286− omitted.
287−- Other attributes style its root element, so `@panel [title Hi, padding 40]`
288− works like styling a built-in element.
289−- Text after the attributes and indented children replace `@children`, and a
290− caller's `@slot name` block replaces `@slot name` (the slot's own children are
291− the default).
292−- An `@style` block at the top of the body is scoped to the function: its rules
293− apply inside the function's root element (`&` is the root itself). The
294− body needs a single root element.
367+- Its parameters are passed as attributes. A parameter with a default
368+ (`$tone=info`) can be left out.
369+- Any other attributes style the function's root element, so
370+ `@panel [title Hi, padding 40]` works the same as styling a built-in
371+ element.
372+- Text after the attributes and the indented children replace `@children`.
373+ A caller's `@slot NAME` block replaces the function's `@slot NAME`, and
374+ the slot's own children are the default content.
375+
376+```
377+@let card $title $tone=#f9fafb
378+ @article [padding 16, spacing 8, background $tone, border-radius 8]
379+ @row [align-items center]
380+ @h3 $title
381+ @spacer
382+ @slot actions
383+ @children
384+ @slot footer
385+ @text [font-size 12, color gray] No footer
295386
387+@card [title Plain]
388+ Just children.
389+@card [title Full, tone #eff6ff, padding 24]
390+ @slot actions
391+ @link /edit Edit
392+ The children go here.
393+ @slot footer
394+ @text Updated today
296395```
297−@let note $kind=info
396+
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.
400+
401+```
402+@let note $kind=Note
298403 @style
404+ & { border-left: 4px solid #3b82f6; }
299405 .title { font-weight: bold; }
300406 @el [padding 12, border-radius 6, background #eff6ff]
301407 @text [class=title] $kind
@@ -304,7 +410,7 @@ A function is called like an element:
304410@note [kind Tip, padding 20] Scoped styles and forwarded attributes.
305411```
306412
307−Multi-line content is a function whose body is text:
413+A function whose body is text holds multi-line content:
308414
309415```
310416@let intro
@@ -315,9 +421,11 @@ Multi-line content is a function whose body is text:
315421 @intro
316422```
317423
318−`@let --name value` also declares the CSS custom property `--name` on
319−`:root`. `$--name` is its value at compile time; `var(--name)` refers to it at
320−run time, so a stylesheet can override it:
424+### CSS custom properties
425+
426+`@let --name value` declares the CSS custom property `--name` on `:root`.
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:
321429
322430```
323431@let --brand #3b82f6
@@ -330,20 +438,20 @@ Conditions and computed values (`@let x = ...`) are expressions:
330438
331439| | |
332440|---|---|
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 |
334442| Arithmetic | `+ - * / %` with the usual precedence, unary `-`, `( )` |
335443| 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 |
337445| 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)` |
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)` |
340−| Color functions | `lighten(c, pct)`, `darken(c, pct)`, `alpha(c, a)`, `mix(c1, c2, pct)` |
446+| Tests | `contains(s, x)` (in a text, or as an item of a list), `starts-with(s, x)`, `ends-with(s, x)` |
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)` |
448+| Color | `lighten(c, pct)`, `darken(c, pct)`, `alpha(c, a)`, `mix(c1, c2, pct)` |
341449
342−Variables are looked up while evaluating, so a value containing `==` or spaces
343−is still one value. An invalid expression is a compile error.
450+Variables are looked up during evaluation, so a value that contains `==` or
451+spaces is still one value. An invalid expression is a compile error.
344452
345−In text and attribute values, `$name` inserts a variable and `${EXPR}` inserts
346−the value of any expression:
453+In text and attribute values, `$name` inserts a variable, and `${EXPR}`
454+inserts the value of any expression:
347455
348456```
349457@let name htmlang
@@ -356,8 +464,8 @@ the value of any expression:
356464All control flow runs at compile time.
357465
358466```
359−@let items apple, banana, cherry
360467@let count 3
468+@let hidden false
361469
362470@if $count > 2 and not $hidden
363471 @text Many
@@ -365,56 +473,70 @@ All control flow runs at compile time.
365473 @text None
366474@else
367475 @text Few
476+```
368477
369−@each $item, $i in $items
370− @text $i: $item
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.
483+
484+```
485+@let fruits apple, banana, cherry
486+@each $fruit, $i in $fruits
487+ @text ${$i + 1}. $fruit
371488@else
372− @text The list is empty.
489+ @text No fruit.
373490
374−@each $n in 1..10 step 3
375− @text $n
491+@row [spacing 8]
492+ @each $n in 10..0 step 5
493+ @text $n
376494```
377495
378−`@each $item in LIST` loops over a list: a list loaded with `@data`, text
379−split on commas, or a range (which counts down when its start is greater than
380−its end). An optional second variable is the index, from 0. An item loaded from
381−JSON can be a record, whose fields are `$item.key`:
496+## Data and files
497+
498+| Directive | Effect |
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 |
507+
508+Records keep their values whole, even when a value contains spaces or
509+commas:
382510
383511```
384−@data $links [
385− {"label": "About us", "url": "/about"},
386− {"label": "Blog", "url": "/blog"}
512+@data $team [
513+ {"name": "Ada", "role": "Engineering, research"},
514+ {"name": "Grace", "role": "Design"}
387515]
388−@row [spacing 12]
389− @each $link in $links
390− @link $link.url $link.label
391−@text ${length($links)} links
516+@ul [spacing 4]
517+ @each $person in $team
518+ @li
519+ @text [font-weight bold] $person.name
520+ @text $person.role
521+@text ${length($team)} people
392522```
393523
394−## Files and data
395−
396−| Directive | Effect |
397−|---|---|
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) |
524+### Layouts
405525
406−A layout is an ordinary function in its own file. It marks where content goes
407−with `@slot name` (named blocks, with default content) and `@children`
408−(everything in the call outside `@slot` blocks):
526+A layout is an ordinary function in a file of its own. It marks where content
527+goes with `@slot NAME` (named blocks, with default content) and `@children`
528+(everything in the call that is outside a `@slot` block).
409529
410530```
411531-- layout.hl
412532@let layout
413533 @page My Site
414− @el [max-width 800, center-x]
415− @slot header
416− @text Default header
417− @children
534+ @el [max-width 800, center-x, spacing 24]
535+ @header
536+ @slot header
537+ @text Default header
538+ @main
539+ @children
418540```
419541
420542```
@@ -422,30 +544,32 @@ with `@slot name` (named blocks, with default content) and `@children`
422544@include layout.hl
423545@layout
424546 @slot header
425− @text About us
547+ @h1 About us
426548 @paragraph This fills @children.
427549```
428550
429551## Page and head
430552
431−`@page TITLE` produces a full HTML document; without it the output is a fragment.
432−Its attributes set `lang` and `favicon` (inlined into the page). `@meta NAME VALUE`
433−adds a meta tag (`og:` names become Open Graph `property` tags), and
434−`@head` holds any other raw HTML for the `<head>` (fonts, JSON-LD, a manifest
435−link). Translations are a JSON file per locale: `@data $t locales/$lang.json`.
553+`@page TITLE` makes the output a full HTML document. Without it, the output
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
556+start with `og:` become Open Graph `property` tags. `@head` holds any other
557+raw HTML for the `<head>`, such as a font link, a canonical URL or JSON-LD.
436558
437559```
438−@page [lang en, favicon /favicon.png] My Site
560+@page [lang en, favicon favicon.png] My Site
439561@meta description A small site
440562@meta og:title My Site
563+@head
564+ <link rel="canonical" href="https://example.com/">
441565```
442566
567+Translations are a JSON file per locale: `@data $t locales/$lang.json`.
568+
443569## CSS
444570
445−- `@style` holds raw CSS, including at-rules such as `@keyframes`, `@scope`,
446− `@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.
571+`@style` holds raw CSS, including at-rules such as `@keyframes`,
572+`@font-face`, `@property` and `@starting-style`:
449573
450574```
451575@style
@@ -456,3 +580,10 @@ link). Translations are a JSON file per locale: `@data $t locales/$lang.json`.
456580 .note { color: gray; }
457581@el [animation fade-in 0.3s ease, class=note] Fades in
458582```
583+
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
589+generated ones.