on master help sign in
Rewrite the docs and examples for the current language

README and DESIGN.md describe the language as it is now: elm-ui layout attributes, CSS properties under their CSS names, HTML elements and attributes under their HTML names, @let for every definition, and output that is only what the source asks for. DESIGN.md is reorganized around those parts and corrects a few claims (list markers, container queries). The four examples are rewritten to use current features: tokens as custom properties, headings, @spacer, @data records, if(), named slots, scoped @style and text-body functions. docs.hl no longer loses its title to the @page line of a code sample, and the blog reads posts.json. tests/docs.rs now compiles every page in examples/ as well. The VS Code README, snippets and grammar drop removed syntax (@fn, @define, @column, @list, @stack, rounded, canonical) and point to the iko-soy/htmlang repository.

commit74fc72781e4ad4eb9fbfed0eaec9d99ebcc824cd authoriko <mail@iko.soy> dateSat 26 Sep 2026, 04:24 (2 weeks ago) parent32f2947
17 files changed+2022−976
CONTRIBUTING.md+6−3 DESIGN.md+342−211 README.md+90−65 editors/vscode/README.md+34−26 editors/vscode/snippets/htmlang.json+191−176 editors/vscode/syntaxes/htmlang.tmLanguage.json+2−2 examples/blog.hl+88−48 examples/blog.html.map+1−1 examples/demo.hl+236−299 examples/demo.html+715−3 examples/demo.html.map+1−1 examples/docs.hl+126−61 examples/docs.html.map+1−1 examples/landing.hl+120−63 examples/landing.html.map+1−1 examples/posts.json+27 tests/docs.rs+41−15
CONTRIBUTING.md+6−3
@@ -33,8 +33,10 @@ Linux, macOS, and Windows. Your change should pass all three.
3333- `editors/vscode/` — VS Code extension.
3434- `tests/snapshots.rs` — integration / snapshot tests for the compiler.
3535- `tests/regressions.rs` — one test per fixed bug.
36−- `tests/docs.rs` — compiles every example in `DESIGN.md` and `README.md`.
37−- `examples/` — sample `.hl` files used as smoke tests and documentation.
36+- `tests/docs.rs` — compiles every example in `DESIGN.md` and `README.md`,
37+ and every page in `examples/`.
38+- `examples/` — complete pages: a landing page, a blog, a docs page, and a
39+ tour of the whole language (`demo.hl`).
3840
3941## Adding a feature
4042
@@ -47,7 +49,8 @@ Linux, macOS, and Windows. Your change should pass all three.
4749 thread a feature through the parser and codegen when it needs to be, and
4850 describe it in the LSP's `docs.rs`.
49513. Document it in `DESIGN.md` (the examples there are compiled by the tests).
50− If it's user-facing, also update `README.md`.
52+ If it's user-facing, also update `README.md`, and show it in
53+ `examples/demo.hl` if the tour has a section it belongs in.
51544. If it changes the CLI surface, update the `--help` output.
5255
5356## Style
DESIGN.md+342−211
@@ -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.
README.md+90−65
@@ -1,35 +1,54 @@
11# htmlang
22
3−A minimalist layout language inspired by [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) that compiles to static HTML.
4−
5−`@` means structure. Bare lines mean content. No CSS required.
6−
7−## Example
3+A small layout language that compiles to static HTML. The layout model comes
4+from [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/):
5+rows and columns, spacing between children, and elements that fill or center
6+themselves. Styling uses CSS properties under their CSS names, and every other
7+element uses its HTML name.
88
99```
1010@page My Site
11−@let primary #3b82f6
11+@let --brand #3b82f6
1212
1313@let card $title
14− @el [padding 20, background white, border-radius 8, border 1 solid #e5e7eb, hover:border 1 solid $primary, transition all 0.15s ease]
15− @text [font-weight bold] $title
14+ @article [padding 20, spacing 8, border 1 solid #e5e7eb, border-radius 8, hover:border-color var(--brand)]
15+ @h3 $title
1616 @children
1717
18−@el [max-width 800, center-x, padding 40, spacing 20]
19− @text [font-weight bold, font-size 32] Hello
20−
18+@el [max-width 800, center-x, padding 40, spacing 24]
19+ @h1 [font-size 32] Hello
2120 @paragraph
22− Built with {@text [font-weight bold, color $primary] htmlang}.
23−
24− @row [wrap, spacing 10]
21+ Built with {@text [font-weight bold, color var(--brand)] htmlang}.
22+ @row [spacing 16, wrap]
2523 @card [title Simple]
26− Write layouts without CSS
27− @card [title Fast]
28− Compiles to a single HTML file
24+ Layout without writing a stylesheet.
25+ @card [title Static]
26+ One HTML file, with its CSS inside.
2927```
3028
31−This compiles to one self-contained `.html` file with flexbox layout and
32−generated CSS classes. No JavaScript, no external dependencies.
29+Each page compiles to one self-contained `.html` file. It has no JavaScript
30+unless you write some, and nothing is added that you didn't ask for.
31+
32+## The language in brief
33+
34+- **`@` starts structure, and any other line is content.** Indentation nests
35+ elements. Text after an element's attributes is its content, and `{...}`
36+ puts an element inside a line of text.
37+- **Layout comes from elm-ui.** `@el` lays out its children in a column and
38+ `@row` in a row, and every other container is a column too. Layout
39+ attributes (`spacing`, `width fill`, `center-x`, `align-right`, `wrap`) say
40+ how an element sits inside its parent.
41+- **Styling is CSS.** Any other attribute is a CSS property with its CSS
42+ name and value: `padding 20`, `border 1 solid #eee`,
43+ `grid-template-columns 1fr 2fr`. In lengths, a bare number means pixels.
44+ Prefixes make a style conditional: `hover:`, `md:`, `dark:`, `first:`.
45+- **HTML stays HTML.** Elements have their HTML names (`@nav`, `@ul`, `@form`,
46+ `@details`), and HTML attributes are written `key=value` (`id=main`,
47+ `type=email`) or bare (`required`).
48+- **`@let` defines everything.** It defines values, computed values,
49+ attribute bundles and functions. A layout is just a function with slots.
50+- **Data comes in as lists and records.** `@data` loads JSON, and `@each` and
51+ `@if` run at compile time.
3352
3453## Install
3554
@@ -40,15 +59,15 @@ cargo install --path .
4059## Usage
4160
4261```
43−htmlang page.hl # compile page.hl -> page.html
62+htmlang page.hl # compile page.hl to page.html
4463htmlang page.hl -o out.html # choose the output file
45−htmlang -w page.hl # recompile on change
4664htmlang serve . # dev server with live reload
65+htmlang build src -o dist # compile a whole site
4766```
4867
4968| Command | Purpose |
5069|---|---|
51−| `build <dir> [-o out] [--minify] [--strict]` | Compile every `.hl` file under a directory |
70+| `build <dir> [-o out] [--minify] [--strict]` | Compile every `.hl` file under a directory (into `out/` by default) |
5271| `serve [dir\|file] [-p PORT] [--open]` | Dev server with live reload |
5372| `watch [dir\|file] [-o out]` | Recompile on change, without a server |
5473| `check <file\|dir> [--format json]` | Report diagnostics without writing output |
@@ -56,67 +75,73 @@ htmlang serve . # dev server with live reload
5675| `fmt <file.hl>` | Format a file in place |
5776| `lsp` | Run the language server over stdio |
5877
59−Compiling directly also takes `--dev`, `--strict`, `--partial`,
60−`--format json`, `-s` / `--serve`, `-p` / `--port` and `--open`.
61−
62−## Editor support
63−
64−A VS Code extension with syntax highlighting and LSP integration is in
65−[`editors/vscode`](editors/vscode). The language server (`htmlang-lsp`) provides
66−diagnostics, completions, hover documentation, go to definition, rename and
67−formatting.
78+Compiling a file directly also takes `-w` / `--watch`, `--dev` (readable
79+output and source maps), `--strict`, `--partial` (a fragment, without the
80+document wrapper), `--format json`, `-s` / `--serve`, `-p` / `--port` and
81+`--open`.
6882
69−## Language tour
83+## A short tour
7084
71−Elements are laid out with `@el` (a column) and `@row`; `@text`, `@paragraph`,
72−`@link` and `@image` hold content, and every other HTML element is available by
73−name (`@nav`, `@section`, `@table`, `@input`, ...).
74−
75−Attributes are styles, written `key value`, or HTML attributes, written
76−`key=value`:
85+Layout attributes place an element. Everything else is CSS, and `key=value`
86+is an HTML attribute:
7787
7888```
79−-- gap between children, padding, and an HTML id
80−@row [spacing 20, padding 16, id=toolbar]
81− -- take the remaining space; center the text
82− @el [width fill, text-align center] Search
83− @input [type=search, placeholder=Find..., width 240]
89+@row [spacing 12, align-items center, padding 12 20, border-bottom 1 solid #e5e7eb, id=toolbar]
90+ @text [font-weight 700] Acme
91+ @spacer
92+ @input [type=search, placeholder=Search, aria-label=Search, width 240, padding 6 10]
93+ @button [type=submit, padding 6 14, hover:background #f3f4f6] Go
8494```
8595
86−Any standard CSS property works as a style (`opacity 0.5`, `margin-top 16`).
87−Prefixes apply a style conditionally:
96+A prefix applies a style only in a state, from a screen width up, or under a
97+media condition:
8898
8999```
90−@el [background #3b82f6, hover:background #2563eb, md:padding 32, dark:background #1e3a8a]
100+@el [padding 16, md:padding 32, background #3b82f6, hover:background #2563eb, dark:background #1e3a8a]
91101 @text [color white] Click me
92102```
93103
94−`@let` defines values, computed values, attribute bundles and functions:
104+`@let` defines values, bundles and functions. A function is called like an
105+element: its parameters are attributes, and any other attributes style its
106+root element.
95107
96108```
97−@let primary #3b82f6
109+@let --primary #3b82f6
98110@let gap = 8 * 2
99−@let card [padding 20, border-radius 8]
100−@let button $label
101− @el [$card, background $primary]
102− @text [color white, font-weight bold] $label
103− @children
111+@let rounded [border-radius 8, overflow hidden]
104112
105−@button [label Click me, padding 12]
106−```
113+@let button $label $href=#
114+ @link [$rounded, padding 10 16, background var(--primary), color white] $href
115+ $label
107116
108−Functions are used like elements: extra attributes (here `padding 12`) style
109−their root element.
117+@row [spacing $gap]
118+ @button [label Sign up]
119+ @button [label Learn more, href /about, background #64748b]
120+```
110121
111−Control flow runs at compile time:
122+Data, loops and conditions run at compile time:
112123
113124```
114−@let items Home, About, Contact
115−@row [spacing 8]
116− @each $item in $items
117− @if $item != About
118− @link /${lowercase($item)} $item
125+@data $links [
126+ {"label": "Home", "url": "/"},
127+ {"label": "Blog", "url": "/blog"},
128+ {"label": "About us", "url": "/about"}
129+]
130+@let current /blog
131+
132+@nav
133+ @row [spacing 16]
134+ @each $link in $links
135+ @link [if($link.url == $current, font-weight bold)] $link.url $link.label
119136```
120137
121−See [DESIGN.md](DESIGN.md) for the full language: files and data (`@include`,
122−layouts, `@data`), page metadata, expressions and CSS.
138+The [`examples/`](examples) directory has complete pages (a landing page, a
139+blog, a docs page, and a tour of the whole language). [DESIGN.md](DESIGN.md)
140+is the language reference.
141+
142+## Editor support
143+
144+The VS Code extension in [`editors/vscode`](editors/vscode) provides syntax
145+highlighting, snippets, and the language server (`htmlang-lsp`). The server
146+gives diagnostics, completions, hover documentation, go to definition, rename
147+and formatting.
editors/vscode/README.md+34−26
@@ -1,43 +1,51 @@
11# htmlang for VS Code
22
3−Syntax highlighting, snippets, and LSP-backed intelligence for [htmlang](https://github.com/iko/htmlang)
4−(`.hl`) files.
3+Syntax highlighting, snippets, and language server support for
4+[htmlang](https://github.com/iko-soy/htmlang) (`.hl`) files.
55
66## Features
77
8−- **Syntax highlighting** — `@` directives, `$variables`, `[attribute]` brackets,
9− `-- comments`, verbatim `@raw` / `@style` / `@script` bodies.
10−- **Diagnostics** — parse errors, missing variables/functions, unused bindings,
11− CSS / a11y warnings, and typo suggestions.
12−- **Completion** — directives, attribute keys and values, variable names, and
13− function calls. Trigger characters: `@`, `$`, `[`, `,`.
14−- **Hover** — documentation for directives, function signatures, variable
15− values, and color swatches.
16−- **Go to definition / rename** — for `$variables`, `@fn` calls, `@define`
17− bundles, and `@include` targets.
18−- **Document links** — cmd-click on `@include`.
19−- **Code lens** — per-definition reference counts for `@fn` / `@let` / `@define`.
20−- **Document symbols + workspace symbols** — outline view and `Ctrl-T` search
21− scan both open documents and every `.hl` file in the workspace.
22−- **Formatter** — `Format Document` / `Format Selection` invokes the same
8+- **Syntax highlighting**: `@` elements and directives, `$variables`,
9+ `[attribute]` lists with state and media prefixes, `key=value` HTML
10+ attributes, `{@inline}` elements, colors, numbers and `-- comments`.
11+- **Diagnostics**: parse errors, unknown elements and attributes with "did
12+ you mean" suggestions, undefined and unused definitions, and accessibility
13+ warnings.
14+- **Completion** of elements, directives, layout attributes, CSS properties,
15+ HTML attributes, prefixes, variables and functions. It triggers on `@`,
16+ `$`, `[` and `,`.
17+- **Hover** documentation for elements, directives and attributes (CSS
18+ properties link to MDN), function signatures, variable values, and color
19+ swatches.
20+- **Navigation**: go to definition, find references and rename for
21+ `$variables`, bundles and `@let` functions. `@include` paths are links.
22+- **Code actions**: fixes for common diagnostics, removing unused definitions,
23+ and extracting a selection into a `@let` function or attribute bundle.
24+- **Outline and symbols**: the document outline, and `Ctrl-T` workspace
25+ search across every `.hl` file.
26+- **Formatting**: `Format Document` and `Format Selection` use the same
2327 formatter as `htmlang fmt`.
24−- **Color picker, folding ranges, semantic tokens, inlay hints, signature help**.
28+- **Also**: reference counts as code lenses on each `@let`, variable values
29+ as inlay hints, a color picker, folding, semantic tokens, signature help for
30+ function calls, and linked editing of a variable's uses.
2531
2632## Requirements
2733
28−The extension launches the `htmlang-lsp` binary. Install it from the repo root:
34+The extension starts the `htmlang-lsp` binary. Install it from the repository
35+root:
2936
3037```
3138cargo install --path . --bin htmlang-lsp
3239```
3340
34−Make sure `htmlang-lsp` is on your `PATH`, or set `htmlang.serverPath` in
35−settings to an absolute path.
41+`htmlang-lsp` must be on your `PATH`, or you can set `htmlang.server.path`
42+to its absolute path. `htmlang.server.args` passes extra arguments to it.
3643
3744## Snippets
3845
39−A set of snippets is included for common patterns (`card`, `row`, `column`,
40−`fn`, `each`, `component`, …). Type the prefix and `Tab` to expand.
46+Snippets cover common patterns: `@page`, `@let-fn`, `@let-slots`,
47+`@let-tokens`, `@navbar`, `@hero`, `@form`, `@grid`, `@each`, `@if`,
48+`@layout` and more. Type a prefix and press `Tab` to expand it.
4149
4250## Development
4351
@@ -48,9 +56,9 @@ npm run build # compile TypeScript to ./out
4856```
4957
5058Open this folder in VS Code and run the "Extension" launch configuration to
51−try it out.
59+try it.
5260
5361## Reporting issues
5462
55−Please file issues in the main [htmlang repository](https://github.com/iko/htmlang/issues),
56−including a minimal `.hl` snippet and your `htmlang-lsp --version`.
63+File issues in the main [htmlang repository](https://github.com/iko-soy/htmlang/issues).
64+Include a minimal `.hl` snippet and the output of `htmlang --version`.
editors/vscode/snippets/htmlang.json+191−176
@@ -1,246 +1,247 @@
11{
2− "Page boilerplate": {
2+ "Page": {
33 "prefix": "@page",
44 "body": [
5− "@page ${1:My Page}",
6− "@let primary ${2:#3b82f6}",
5+ "@page [lang ${1:en}] ${2:My Page}",
6+ "@let --${3:brand} ${4:#3b82f6}",
77 "",
8− "@el [max-width ${3:800}, center-x, padding ${4:40}, spacing ${5:20}]",
8+ "@el [max-width ${5:800}, center-x, padding ${6:40}, spacing ${7:20}]",
99 " $0"
1010 ],
11− "description": "Page boilerplate with title, variables, and layout"
11+ "description": "A page with a title, tokens and a centered column"
1212 },
13− "Function component": {
13+ "Page metadata": {
14+ "prefix": "@page-meta",
15+ "body": [
16+ "@page [lang ${1:en}, favicon ${2:favicon.png}] ${3:My Page}",
17+ "@meta description ${4:Page description}",
18+ "@meta og:title $3",
19+ "@head",
20+ " <link rel=\"canonical\" href=\"${5:https://example.com/}\">",
21+ "$0"
22+ ],
23+ "description": "A page with a description, Open Graph tags and a canonical link"
24+ },
25+ "Design tokens": {
26+ "prefix": "@let-tokens",
27+ "body": [
28+ "@let --${1:brand} ${2:#3b82f6}",
29+ "@let --${3:ink} ${4:#0f172a}",
30+ "@let --${5:muted} ${6:#64748b}",
31+ "$0"
32+ ],
33+ "description": "CSS custom properties: var(--name) at run time, \\$--name at compile time"
34+ },
35+ "Attribute bundle": {
36+ "prefix": "@let-bundle",
37+ "body": [
38+ "@let ${1:card} [${2:padding 20, background white, border 1 solid #e5e7eb, border-radius 8}]",
39+ "$0"
40+ ],
41+ "description": "A named list of attributes, used as [\\$name]"
42+ },
43+ "Function": {
1444 "prefix": "@let-fn",
1545 "body": [
16− "@let ${1:component} \\$${2:title}",
17− " @el [${3:padding 20, background white, border-radius 8}]",
46+ "@let ${1:card} \\$${2:title}",
47+ " @el [${3:padding 20, spacing 8, border 1 solid #e5e7eb, border-radius 8}]",
1848 " @text [font-weight bold] \\$$2",
1949 " @children",
2050 "",
2151 "@$1 [$2 ${4:Hello}]",
2252 " $0"
2353 ],
24− "description": "Reusable function component with @children slot"
54+ "description": "A function with a parameter and @children"
2555 },
26− "Function with named slots": {
56+ "Function with slots": {
2757 "prefix": "@let-slots",
2858 "body": [
29− "@let ${1:layout} \\$${2:title}",
30− " @el [${3:max-width 800, center-x, padding 40}]",
31− " @slot ${4:header}",
32− " @text [font-weight bold, font-size 32] \\$$2",
33− " @slot ${5:content}",
34− " @slot ${6:footer}",
59+ "@let ${1:panel} \\$${2:title}",
60+ " @article [${3:padding 20, spacing 12}]",
61+ " @row [align-items center]",
62+ " @h2 \\$$2",
63+ " @spacer",
64+ " @slot ${4:actions}",
65+ " @children",
66+ " @slot ${5:footer}",
67+ " @text ${6:Default footer}",
3568 "",
36− "@$1 [$2 ${7:My Page}]",
37− " @slot $5",
38− " $0"
39− ],
40− "description": "Function with named slots for multi-region layouts"
41− },
42− "Card component": {
43− "prefix": "@card",
44− "body": [
45− "@el [padding ${1:20}, background ${2:white}, border-radius ${3:8}, border 1 solid ${4:#e5e7eb}, hover:border 1 solid ${5:\\$primary}, transition all 0.15s ease]",
46− " @text [font-weight bold, font-size ${6:18}] ${7:Title}",
47− " $0"
69+ "@$1 [$2 ${7:Title}]",
70+ " @slot $4",
71+ " $0",
72+ " Content"
4873 ],
49− "description": "Card with border, hover effect, and title"
74+ "description": "A function with named slots and default content"
5075 },
51− "Responsive row": {
52− "prefix": "@responsive-row",
76+ "Scoped style": {
77+ "prefix": "@let-style",
5378 "body": [
54− "@row [wrap, spacing ${1:20}]",
55− " @el [width fill, min-width ${2:300}]",
56− " ${3:Column 1}",
57− " @el [width fill, min-width $2]",
58− " ${4:Column 2}",
59− " $0"
79+ "@let ${1:note}",
80+ " @style",
81+ " & { ${2:border-left: 4px solid #3b82f6;} }",
82+ " @el [${3:padding 12 16}]",
83+ " @children",
84+ "$0"
6085 ],
61− "description": "Responsive wrapping row layout"
86+ "description": "A function with a stylesheet scoped to its root element"
6287 },
63− "Navigation bar": {
64− "prefix": "@navbar",
88+ "Layout": {
89+ "prefix": "@layout",
6590 "body": [
66− "@nav [padding-inline ${1:20}, padding-block ${2:12}, background ${3:#1a1a2e}]",
67− " @row [spacing ${4:20}, align-items center]",
68− " @text [font-weight bold, color ${5:white}, font-size ${6:18}] ${7:Logo}",
69− " @el [width fill]",
70− " @data \\$links [{\"label\": \"${8:Home}\", \"url\": \"/\"}, {\"label\": \"About\", \"url\": \"/about\"}]",
71− " @each \\$link in \\$links",
72− " @link \\$link.url",
73− " @text [color ${9:#a8b2d1}, hover:color white] \\$link.label",
91+ "@include ${1:layout.hl}",
92+ "@${2:layout}",
93+ " @slot ${3:header}",
7494 " $0"
7595 ],
76− "description": "Navigation bar with logo and links"
96+ "description": "Render the page inside a layout function from another file"
7797 },
78− "Each loop": {
79− "prefix": "@each",
98+ "Row": {
99+ "prefix": "@row",
80100 "body": [
81− "@each \\$${1:item} in ${2:items}",
82− " $0",
83− "@else",
84− " @text [color #888] No items found."
101+ "@row [spacing ${1:16}, align-items ${2:center}]",
102+ " $0"
85103 ],
86− "description": "Each loop with @else fallback"
104+ "description": "A row with spacing"
87105 },
88− "Conditional": {
89− "prefix": "@if",
106+ "Responsive row": {
107+ "prefix": "@responsive-row",
90108 "body": [
91− "@if ${1:condition}",
92− " $0",
93− "@else",
94− " "
109+ "@row [spacing ${1:20}, wrap]",
110+ " @el [flex 1 1 ${2:280}px]",
111+ " ${3:Column 1}",
112+ " @el [flex 1 1 $2px]",
113+ " ${4:Column 2}",
114+ "$0"
95115 ],
96− "description": "If/else conditional block"
116+ "description": "A row of columns that wraps on narrow screens"
97117 },
98− "Grid layout": {
118+ "Grid": {
99119 "prefix": "@grid",
100120 "body": [
101− "@grid [grid-cols ${1:3}, gap ${2:20}]",
102− " @el [padding ${3:20}]",
103− " ${4:Item 1}",
104− " @el [padding $3]",
105− " ${5:Item 2}",
106− " @el [padding $3]",
107− " ${6:Item 3}",
108− " $0"
121+ "@grid [grid-cols ${1:3}, spacing ${2:20}, ${3:md:grid-cols 4}]",
122+ " @el ${4:One}",
123+ " @el ${5:Two}",
124+ " @el ${6:Three}",
125+ "$0"
109126 ],
110− "description": "CSS Grid layout with columns"
127+ "description": "A grid with equal columns"
111128 },
112129 "Grid with named areas": {
113130 "prefix": "@grid-areas",
114131 "body": [
115− "@grid [grid-template-areas \"${1:header header}\" \"${2:sidebar main}\" \"${3:footer footer}\", gap ${4:20}]",
116− " @el [grid-area ${5:header}]",
117− " ${6:Header}",
118− " @el [grid-area ${7:sidebar}]",
119− " ${8:Sidebar}",
120− " @el [grid-area ${9:main}]",
132+ "@grid [grid-template-columns ${1:240px 1fr}, grid-template-areas \"${2:header header}\" \"${3:sidebar main}\", spacing ${4:20}]",
133+ " @header [grid-area header]",
134+ " ${5:Header}",
135+ " @aside [grid-area sidebar]",
136+ " ${6:Sidebar}",
137+ " @main [grid-area main]",
121138 " $0"
122139 ],
123− "description": "CSS Grid with named template areas"
124− },
125− "Form with inputs": {
126− "prefix": "@form",
127− "body": [
128− "@form [method=${1:post}, spacing ${2:16}] ${3:/submit}",
129− " @label [for=${4:email}] ${5:Email}",
130− " @input [type=${6:email}, name=$4, id=$4, required, padding 8, border 1 solid #d1d5db, border-radius 4]",
131− " @button [type=submit, padding 10, background ${7:\\$primary}, color white, border-radius 4, font-weight bold]",
132− " ${8:Submit}",
133− " $0"
134− ],
135− "description": "Form with labeled input and submit button"
140+ "description": "A grid with named template areas"
136141 },
137− "Dark mode toggle": {
138− "prefix": "@darkmode",
142+ "Navigation bar": {
143+ "prefix": "@navbar",
139144 "body": [
140− "@el [background ${1:white}, dark:background ${2:#1a1a2e}, color ${3:#333}, dark:color ${4:#eee}, transition all 0.2s ease]",
141− " $0"
145+ "@data \\$${1:links} [",
146+ " {\"label\": \"${2:Home}\", \"url\": \"/\"},",
147+ " {\"label\": \"${3:About}\", \"url\": \"/about\"}",
148+ "]",
149+ "@header [padding ${4:12 24}, border-bottom 1 solid #e5e7eb]",
150+ " @row [spacing ${5:20}, align-items center]",
151+ " @text [font-weight bold] ${6:Logo}",
152+ " @spacer",
153+ " @nav",
154+ " @row [spacing $5]",
155+ " @each \\$link in \\$$1",
156+ " @link [hover:text-decoration underline] \\$link.url \\$link.label",
157+ "$0"
142158 ],
143− "description": "Element with light/dark mode styles"
159+ "description": "A header with a logo, a spacer and links from data"
144160 },
145− "Hero section": {
161+ "Hero": {
146162 "prefix": "@hero",
147163 "body": [
148− "@el [center-x, center-y, min-height ${1:80vh}, padding ${2:40}, spacing ${3:20}]",
149− " @text [font-weight bold, font-size ${4:48}, text-align center] ${5:Hero Title}",
150− " @paragraph [max-width ${6:600}, text-align center, color ${7:#666}, line-height 1.6]",
151− " ${8:Hero description text}",
152− " @el [padding 12 24, background ${9:\\$primary}, border-radius 8] > @link ${10:#}",
153− " @text [color white, font-weight bold] ${11:Get Started}",
154− " $0"
155− ],
156− "description": "Hero section with title, description, and CTA"
157− },
158− "Theme tokens": {
159− "prefix": "@let-tokens",
160− "body": [
161− "@let --primary ${1:#3b82f6}",
162− "@let --secondary ${2:#10b981}",
163− "@let --radius ${3:8}",
164+ "@section [padding ${1:96 24}, spacing ${2:24}, text-align center]",
165+ " @h1 [font-size ${3:48}] ${4:Headline}",
166+ " @paragraph [max-width ${5:600}, center-x, color #64748b, line-height 1.6]",
167+ " ${6:A sentence about the product.}",
168+ " @link [center-x, padding 12 24, border-radius 8, background ${7:#3b82f6}, color white, font-weight bold] ${8:/signup}",
169+ " ${9:Get started}",
164170 "$0"
165171 ],
166− "description": "Design tokens as CSS custom properties (use as $--primary)"
172+ "description": "A centered hero section with a call to action"
167173 },
168− "Keyframes animation": {
169− "prefix": "@keyframes",
170− "body": [
171− "@style",
172− " @keyframes ${1:fade-in} {",
173− " from { opacity: 0; }",
174− " to { opacity: 1; }",
175− " }",
176− "",
177− "@el [animation $1 ${2:0.3s} ${3:ease}]",
178− " $0"
179− ],
180− "description": "Keyframe animation with element usage"
181− },
182− "Include/Import": {
183− "prefix": "@include",
174+ "Form": {
175+ "prefix": "@form",
184176 "body": [
185− "@include ${1:components.hl}",
177+ "@form [method=${1:post}, spacing ${2:12}] ${3:/submit}",
178+ " @label [for=${4:email}] ${5:Email}",
179+ " @input [type=${6:email}, name=$4, id=$4, required, padding 8, border 1 solid #d1d5db, border-radius 4]",
180+ " @button [type=submit, padding 10 16, border 0, border-radius 4, background ${7:#3b82f6}, color white, font-weight bold] ${8:Submit}",
186181 "$0"
187182 ],
188− "description": "Include another file (a library of @let definitions emits nothing)"
183+ "description": "A form with a labeled input and a submit button"
189184 },
190− "Layout": {
191− "prefix": "@layout",
185+ "Search": {
186+ "prefix": "@search",
192187 "body": [
193− "@include ${1:layout.hl}",
194− "@${2:layout}",
195− " @slot ${3:header}",
196− " $0"
188+ "@search",
189+ " @form [spacing 8] ${1:/search}",
190+ " @input [type=search, name=q, aria-label=Search, placeholder=${2:Search}]",
191+ " @button [type=submit] ${3:Go}",
192+ "$0"
197193 ],
198− "description": "Render the page inside a layout function"
194+ "description": "A search form"
199195 },
200− "Script (external)": {
201− "prefix": "@script-src",
196+ "Data": {
197+ "prefix": "@data",
202198 "body": [
203− "@script [src=${1:app.js}, ${2:defer}]",
199+ "@data \\$${1:items} ${2:items.json}",
204200 "$0"
205201 ],
206− "description": "External script tag with src attribute"
202+ "description": "Load JSON: records are \\$name.key, arrays are lists"
207203 },
208− "Script (inline)": {
209− "prefix": "@script",
204+ "Each": {
205+ "prefix": "@each",
210206 "body": [
211− "@script",
212− " $0"
207+ "@each \\$${1:item} in \\$${2:items}",
208+ " $0",
209+ "@else",
210+ " @text Nothing here yet."
213211 ],
214− "description": "Inline script block"
212+ "description": "Loop over a list, with a fallback for an empty one"
215213 },
216− "Noscript fallback": {
217− "prefix": "@noscript",
214+ "If": {
215+ "prefix": "@if",
218216 "body": [
219− "@noscript",
220− " @text ${1:JavaScript is required for this page.}",
221− " $0"
217+ "@if ${1:\\$condition}",
218+ " $0",
219+ "@else",
220+ " "
222221 ],
223− "description": "Fallback content when JavaScript is disabled"
222+ "description": "A conditional with an else branch"
224223 },
225− "Address block": {
226− "prefix": "@address",
224+ "Dark mode": {
225+ "prefix": "@dark",
227226 "body": [
228− "@address",
229− " @text [font-weight bold] ${1:Name}",
230− " @link mailto:${2:email@example.com} $2",
227+ "@el [background ${1:white}, color ${2:#111827}, dark:background ${3:#111827}, dark:color ${4:#f9fafb}]",
231228 " $0"
232229 ],
233− "description": "Contact address block"
230+ "description": "Light and dark colors on one element"
234231 },
235− "Search form": {
236− "prefix": "@search",
232+ "Keyframes": {
233+ "prefix": "@keyframes",
237234 "body": [
238− "@search",
239− " @input [type=search, placeholder=${1:Search...}]",
240− " @button [type=submit] ${2:Go}",
235+ "@style",
236+ " @keyframes ${1:fade-in} {",
237+ " from { opacity: 0; }",
238+ " to { opacity: 1; }",
239+ " }",
240+ "",
241+ "@el [animation $1 ${2:0.3s} ${3:ease}]",
241242 " $0"
242243 ],
243− "description": "Search form with input and submit button"
244+ "description": "A keyframe animation in @style, used by an element"
244245 },
245246 "Font face": {
246247 "prefix": "@font-face",
@@ -249,9 +250,9 @@
249250 " @font-face { font-family: '${1:Inter}'; src: url('${2:fonts/inter.woff2}') format('woff2'); font-display: swap; }",
250251 "$0"
251252 ],
252− "description": "Custom font face in an @style block"
253+ "description": "A custom font in @style"
253254 },
254− "JSON-LD structured data": {
255+ "JSON-LD": {
255256 "prefix": "@json-ld",
256257 "body": [
257258 "@head",
@@ -260,16 +261,30 @@
260261 " </script>",
261262 "$0"
262263 ],
263− "description": "JSON-LD structured data in @head"
264+ "description": "Structured data in @head"
264265 },
265− "Page metadata": {
266− "prefix": "@page-meta",
266+ "Include": {
267+ "prefix": "@include",
268+ "body": [
269+ "@include ${1:components.hl}",
270+ "$0"
271+ ],
272+ "description": "Include another file (a file of @let definitions outputs nothing)"
273+ },
274+ "Script": {
275+ "prefix": "@script",
276+ "body": [
277+ "@script",
278+ " $0"
279+ ],
280+ "description": "An inline script"
281+ },
282+ "External script": {
283+ "prefix": "@script-src",
267284 "body": [
268− "@page [lang ${1:en}, canonical ${2:https://example.com/page}, favicon ${3:/favicon.png}] ${4:My Page}",
269− "@meta description ${5:Page description}",
270− "@meta og:title $4",
285+ "@script [src=${1:app.js}, ${2:defer}]",
271286 "$0"
272287 ],
273− "description": "Page with language, canonical URL, favicon and meta tags"
288+ "description": "A script loaded from a file"
274289 }
275290}
editors/vscode/syntaxes/htmlang.tmLanguage.json+2−2
@@ -77,7 +77,7 @@
7777 },
7878 "element": {
7979 "name": "entity.name.tag.htmlang",
80− "match": "@(?:row|column|el|text|paragraph|image|link|raw|children|slot|fragment|script|nav|header|footer|main|section|article|aside|address|search|form|details|summary|dialog|figure|figcaption|blockquote|cite|fieldset|legend|noscript|datalist|list|item|dl|dt|dd|table|thead|tbody|tr|td|th|select|picture|progress|meter|output|canvas|button|label|option|textarea|time|mark|abbr|code|kbd|pre|h1|h2|h3|h4|h5|h6|input|hr|source|video|audio|iframe|grid|stack|in-front|behind)(?![a-zA-Z0-9_-])"
80+ "match": "@(?:row|el|text|paragraph|image|link|raw|children|slot|fragment|script|nav|header|footer|main|section|article|aside|address|search|form|details|summary|dialog|figure|figcaption|blockquote|cite|fieldset|legend|noscript|datalist|ul|ol|li|dl|dt|dd|table|thead|tbody|tr|td|th|select|picture|progress|meter|output|canvas|button|label|option|textarea|time|mark|abbr|code|kbd|pre|h1|h2|h3|h4|h5|h6|input|hr|source|video|audio|iframe|grid|in-front|behind)(?![a-zA-Z0-9_-])"
8181 },
8282 "function-call": {
8383 "name": "entity.name.function.htmlang",
@@ -132,7 +132,7 @@
132132 },
133133 "attribute-name": {
134134 "name": "support.type.property-name.htmlang",
135− "match": "\\b(?:spacing|padding|padding-x|padding-y|width|height|min-width|max-width|min-height|max-height|center-x|center-y|align-left|align-right|align-top|align-bottom|background|color|border|rounded|bold|italic|underline|size|font|transition|cursor|opacity|text-align|line-height|overflow|position|z-index|wrap|gap|margin|hidden)\\b"
135+ "match": "\\b(?:spacing|center-x|center-y|align-left|align-right|align-top|align-bottom|wrap|grid-cols|grid-rows|col-span|row-span|inline|padding|margin|width|height|min-width|max-width|min-height|max-height|background|color|border|border-radius|font-size|font-weight|font-family|line-height|text-align|display|gap|flex|position|overflow|opacity|cursor|transition|transform|box-shadow|z-index)\\b"
136136 },
137137 "inline-element": {
138138 "name": "meta.inline-element.htmlang",
examples/blog.hl+88−48
@@ -1,48 +1,88 @@
1−-- A minimal blog template: header + list of posts + post template.
2−@page [lang en] My Blog
3−@let primary #3b82f6
4−@let ink #1f2937
5−@let muted #6b7280
6−
7−@let card [
8− padding 24,
9− background white,
10− border-radius 10,
11− border 1 solid #e5e7eb,
12− box-shadow 0 1px 3px rgba(0,0,0,0.05),
13− transition all 0.15s ease
14−]
15−
16−@let post $title $date $excerpt
17− @article [$card, spacing 8]
18− @text [font-weight bold, font-size 20, color $ink] $title
19− @text [font-size 13, color $muted] $date
20− @paragraph [color $ink, line-height 1.6]
21− $excerpt
22− @link [color $primary, font-weight bold] /posts/$title
23− Read more
24−
25−@el [max-width 720, center-x, padding 40, spacing 32]
26− @header [spacing 8]
27− @text [font-weight bold, font-size 36, color $ink] My Blog
28− @text [color $muted] Thoughts on static HTML, layout, and nice defaults.
29−
30− @main [spacing 20]
31− @post [
32− title First post,
33− date April 16 2026,
34− excerpt "A short introduction to how I think about static site generators."
35− ]
36− @post [
37− title Why flexbox is enough,
38− date April 10 2026,
39− excerpt "Most layouts don't need CSS Grid. A walk through the cases where flexbox wins."
40− ]
41− @post [
42− title Tight feedback loops,
43− date April 2 2026,
44− excerpt "Dev-loop ergonomics beat feature counts. Here's the setup I use."
45− ]
46−
47− @footer [padding-block 24, text-align center, color $muted, font-size 13]
48− Built with htmlang.
1+-- A blog index: a layout function with named slots, posts loaded from JSON,
2+-- and loops and conditions over records.
3+@page [lang en] Notes on the Web
4+@meta description Short essays on static HTML, layout and tooling.
5+
6+@let --ink #1f2937
7+@let --muted #6b7280
8+@let --accent #3b82f6
9+@let --line #e5e7eb
10+
11+-- Posts are records: $post.title, $post.tags, ...
12+@data $posts posts.json
13+
14+-- A layout is an ordinary function. @slot blocks are filled by name; the
15+-- rest of the caller's content replaces @children.
16+@let site
17+ @el [max-width 720, center-x, padding 48 24, spacing 40, color var(--ink), line-height 1.6]
18+ @header [spacing 4]
19+ @link [font-weight 800, font-size 28, letter-spacing -0.5] / Notes on the Web
20+ @slot tagline
21+ @text [color var(--muted)] Short essays.
22+ @main [spacing 40]
23+ @children
24+ @footer [padding-block-start 24, border-top 1 solid var(--line), color var(--muted), font-size 14]
25+ @slot footer
26+ Built with htmlang.
27+
28+-- A pill for each tag
29+@let tag $name
30+ @link [
31+ padding 2 10,
32+ border-radius 999,
33+ background #eff6ff,
34+ color var(--accent),
35+ font-size 13,
36+ hover:background #dbeafe
37+ ] /tags/$name
38+ #$name
39+
40+-- A post summary. `featured` is optional: a parameter with a default
41+@let post-card $post $featured=false
42+ @article [
43+ spacing 8,
44+ padding if($featured, 24, 0),
45+ border-radius 12,
46+ if($featured, background #f8fafc),
47+ if($featured, border 1 solid var(--line))
48+ ]
49+ @if $featured
50+ @text [font-size 12, font-weight 700, text-transform uppercase, letter-spacing 1, color var(--accent)] Featured
51+ @h2 [font-size 22, line-height 1.3]
52+ @link [hover:color var(--accent)] /posts/$post.slug $post.title
53+ @row [spacing 8, align-items center, wrap, font-size 14, color var(--muted)]
54+ @time [datetime=$post.date] $post.display_date
55+ @text ·
56+ @each $name in $post.tags
57+ @tag [name $name]
58+ @paragraph [color var(--muted), line-clamp 2] $post.summary
59+
60+-- Multi-line content: a function whose body is text
61+@let about
62+ I write about building for the web with as little machinery as possible:
63+ plain HTML, a little CSS, and tools that {@text [font-style italic] stay out of the way}.
64+
65+@site
66+ @slot tagline
67+ @paragraph [color var(--muted)]
68+ @about
69+
70+ @section [spacing 32]
71+ @each $post in $posts
72+ @post-card [post $post, featured ${default($post.featured, false)}]
73+ @else
74+ @text [color var(--muted)] Nothing here yet.
75+
76+ @section [spacing 12]
77+ @h2 [font-size 18] Tooling posts
78+ @ul [spacing 4, list-style disc, padding-inline-start 20, children:display list-item]
79+ @each $post in $posts
80+ @if contains($post.tags, tooling)
81+ @li
82+ @link [color var(--accent), hover:text-decoration underline] /posts/$post.slug $post.title
83+
84+ @slot footer
85+ @row [spacing 16]
86+ @text ${length($posts)} posts
87+ @spacer
88+ @link [hover:color var(--ink)] /feed.xml RSS
examples/blog.html.map+1−1
@@ -1 +1 @@
1−{"version":3,"file":"blog.html","sourceRoot":"","sources":["blog.hl"],"names":[],"mappings":"AAyBA;AACA;AACA;AACA;AAEA;AAbA;AACA;AACA;AACA;AAEA;AALA;AACA;AACA;AACA;AAEA;AALA;AACA;AACA;AACA;AAEA;AAyBA"}
1+{"version":3,"file":"blog.html","sourceRoot":"","sources":["blog.hl"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBA;;AACA;;AACA;;;AAgDA;;AAJA;;;;AAzCA;;AAgDA;;AA5BA;;AAQA;;;AACA;;AACA;;;;AACA;;AACA;;;AACA;;;AAzBA;;;AAAA;;;;AA4BA;;;;AAhBA;;AASA;;AACA;;;;AACA;;AACA;;;AACA;;;AAzBA;;;AAAA;;;;AA4BA;;;;AAhBA;;AASA;;AACA;;;;AACA;;AACA;;;AACA;;;AAzBA;;;;AA4BA;;;;;AAkBA;;AACA;;;AACA;;AAGA;;AACA;;;;;;;AA1DA;;AA6DA;;AACA;;;AAhFA;;;AAkFA"}
examples/demo.hl+236−299
@@ -1,335 +1,272 @@
1−-- A comprehensive tour of htmlang: layout, style, components, and control flow.
2−@page [lang en] htmlang Demo
1+-- A one-page tour of htmlang. Each section shows one part of the language:
2+-- layout, text, CSS styles, functions, expressions, control flow and HTML.
3+@page [lang en] htmlang tour
34@meta description A one-page tour of htmlang.
4−@meta og:title htmlang
5−@meta og:type website
5+@meta og:title htmlang tour
66
7−-- Design tokens become both $vars and CSS custom properties
8−@let primary #3b82f6
9−@let --primary #3b82f6
10−@meta theme-color #3b82f6
11−@let ink #0f172a
7+-- Values
8+@let --brand #3b82f6
129@let --ink #0f172a
13−@let muted #64748b
1410@let --muted #64748b
15−@let subtle #f8fafc
11+@let --line #e5e7eb
1612@let --subtle #f8fafc
1713
18−-- Computed @let with arithmetic
19−@let base 8
20−@let gap = $base * 2
21−@let gutter = $gap + $base
14+-- Computed values: `=` makes the rest an expression
15+@let unit 8
16+@let gap = $unit * 2
17+@let gutter = $gap + $unit
2218
23−-- Reusable attribute bundle
19+-- Attribute bundles
2420@let card [
2521 padding $gap,
2622 background white,
27− border-radius $base,
28− border 1 solid #e5e7eb,
29− box-shadow 0 1px 3px rgba(0,0,0,0.05)
23+ border 1 solid var(--line),
24+ border-radius $unit,
25+ dark:background #111827,
26+ dark:border-color #1f2937
3027]
28+@let label [font-size 12, font-weight 700, color var(--muted), text-transform uppercase, letter-spacing 1]
3129
32−-- Composable style group (used with spread)
33−@let interactive [
34− cursor pointer,
35− hover:box-shadow 0 4px 12px rgba(0,0,0,0.08),
36− transition all 0.2s ease
37−]
30+-- A section of the tour: a function with a parameter, a default, and children
31+@let chapter $title $number=
32+ @section [spacing $gap, padding-block $gutter, border-top 1 solid var(--line)]
33+ @row [spacing $unit, align-items baseline]
34+ @if $number
35+ @text [color var(--brand), font-weight 700] $number.
36+ @h2 [font-size 26] $title
37+ @children
3838
39−-- Keyframes and other CSS go in @style
39+-- Plain CSS: keyframes, and anything else a stylesheet can hold
4040@style
41− @keyframes fade-up {
42− from { opacity: 0; transform: translateY(8px); }
43− to { opacity: 1; transform: translateY(0); }
41+ @keyframes rise {
42+ from { opacity: 0; transform: translateY(6px); }
43+ to { opacity: 1; transform: none; }
4444 }
4545
46−-- Raw HTML escape hatch
47−@raw
48− <noscript><style>.needs-js{display:none}</style></noscript>
49−
50−-- Component with default parameter + @children slot
51−@let card $title $icon=◆
52− @article [$card, $interactive, animation fade-up 0.3s ease]
53− @row [spacing 10, align-items center]
54− @text [font-size 20, color $primary] $icon
55− @text [font-weight bold, font-size 16, color $ink] $title
56− @children
57−
58−-- Component that wraps children in a titled section (function + slot)
59−@let titled $title
60− @section [spacing 12, padding-block $base]
61− @text [font-weight bold, font-size 22, color $ink] $title
62− @children
63−
64−-- Top-level layout
6546@el [
6647 max-width 960,
6748 center-x,
68− padding 32,
69− spacing $gutter,
70− background white,
71− dark:background #0b1220,
72− dark:color #e2e8f0
49+ padding $gutter,
50+ color var(--ink),
51+ line-height 1.5,
52+ dark:background #030712,
53+ dark:color #e5e7eb
7354]
55+ @header [spacing $unit, padding-block $gutter, animation rise 0.4s ease]
56+ @text [$label] A one-page tour
57+ @h1 [font-size 44, line-height 1.1, letter-spacing -1] Welcome to htmlang
58+ @paragraph [font-size 18, color var(--muted), max-width 620]
59+ A small layout language that compiles to
60+ {@text [font-weight 700, color var(--brand)] one static HTML file}.
61+ Structure starts with {@code @}, everything else is content.
7462
75− -- Header: nav + hero
76− @header [spacing $base]
77− @nav [spacing 6, color $muted, font-size 13]
78− @row [spacing 6, align-items center]
79− @link [color $muted] / Home
80− @text /
81− @link [color $muted] /demos Demos
82− @text /
83− @text [color $ink] htmlang
63+ -- 1. Layout ---------------------------------------------------------------
64+ @chapter [title Layout, number 1]
65+ @paragraph
66+ {@code @el} stacks its children in a column and {@code @row} lays them
67+ out side by side. {@code spacing} is the gap between children.
68+ @row [spacing $unit, wrap]
69+ @el [$card, width fill] width fill
70+ @el [$card, width fill] shares the row
71+ @el [$card, width 160] width 160
72+ @row [$card, spacing $unit, align-items center]
73+ @text [font-weight 700] Logo
74+ @spacer
75+ @text @spacer pushes this to the end
76+ @row [spacing $unit, height 120]
77+ @el [$card, align-top] align-top
78+ @el [$card, center-y] center-y
79+ @el [$card, align-bottom] align-bottom
80+ @grid [grid-cols 4, spacing $unit]
81+ @each $n in 1..6
82+ @el [$card, text-align center, if($n == 1, col-span 2)] Cell $n
83+ @el [$card, height 120, overflow hidden]
84+ @behind
85+ @el [background linear-gradient(135deg, #dbeafe, #fae8ff), height fill, justify-content flex-end, padding $unit] (behind)
86+ @text [font-weight 700] @in-front and @behind layer content
87+ @in-front
88+ @text [align-right, align-bottom, padding $unit, font-size 12, color var(--muted)] painted on top
8489
85− @row [spacing $gap, align-items center]
86− @el [width fill, spacing 6]
87− @text [font-weight bold, font-size 40, color $ink, dark:color white] Welcome to htmlang
88− @paragraph [line-height 1.6, color $muted]
89− A minimalist layout language that compiles to
90− {@text [font-weight bold, color $primary] static HTML} — no JS, no build step.
91− Visit the {@link [color $primary, text-decoration underline] https://github.com repo} for more.
92− @image [width 72, height 72, border-radius 36, alt="Avatar"] https://picsum.photos/72
93−
94− @hr [border-top 1 solid #e5e7eb]
95−
96− -- Feature grid using @card + @titled components
97− @titled [title Features]
98− @row [wrap, gap $base]
99− @card [title Simple, icon ✦]
100− Write layouts without CSS classes.
101− @card [title Fast, icon ⚡]
102− Compiles to a single self-contained HTML file.
103− @card [title Minimal, icon ○]
104− Zero runtime JavaScript by default.
105− @card [title Typed]
106− LSP with diagnostics and go-to-definition.
107−
108− -- Typography showcase: inline elements, blockquote, code
109− @titled [title Typography]
110− @paragraph [line-height 1.7]
111− A quick brown fox jumps over {@text [font-weight bold] the lazy} {@text [font-style italic] dog}.
112− Press {@kbd [
113− padding 2 6, background #f1f5f9, border-radius 4,
114− font-family monospace, font-size 13
115− ] Ctrl+K} to search. {@mark [background #fef08a] Highlighted} uses mark.
116− {@abbr [title=HyperText Markup Language + language] htmlang} is an abbreviation.
117−
118− @blockquote [
119− padding-inline-start $gap, border-left 3 solid $primary,
120− color $muted, font-style italic
121− ]
90+ -- 2. Text ----------------------------------------------------------------
91+ @chapter [title Text, number 2]
92+ @paragraph [max-width 640]
93+ Lines without an {@code @} are text. Braces put elements inside a line:
94+ {@text [font-weight 700] bold}, {@text [font-style italic] italic},
95+ {@mark highlighted}, {@link [color var(--brand), text-decoration underline] https://example.com a link},
96+ and {@kbd [padding 1 6, border 1 solid var(--line), border-radius 4] Ctrl+K}.
97+ @paragraph
98+ A backslash escapes: \@ at the start of a line, \$price, and \{braces}.
99+ -- `>` chains elements that have one child each
100+ @el [padding 12 20, background var(--brand), border-radius 8, align-left] > @link /start
101+ @text [color white, font-weight 700] Get started
102+ @blockquote [padding-inline-start $gap, border-left 3 solid var(--brand), color var(--muted), font-style italic]
122103 Layout is code. Style is code. Everything should compose.
123104 @cite — Anonymous
124105
125− @pre [
126− background #0f172a, color #e2e8f0,
127− padding 16, border-radius 8, font-family monospace, font-size 13,
128− overflow-x auto
129− ]
130− @raw
131− @let button $label
132− @el [padding 12, background blue]
133− @text $label
106+ -- 3. Style ---------------------------------------------------------------
107+ @chapter [title Style, number 3]
108+ @paragraph
109+ Besides the layout attributes, every attribute is a CSS property with its
110+ CSS name and value. A bare number is pixels for lengths.
111+ @row [spacing $unit, wrap]
112+ @el [$card, box-shadow 0 1px 3px rgba(0,0,0,0.1), transform rotate(-2deg)] Any CSS property
113+ @el [$card, hover:background #eff6ff, hover:border-color var(--brand), transition all 0.15s ease, cursor pointer] hover:
114+ @el [$card, md:background #ecfdf5, dark:background #064e3b] md: and dark:
115+ @el [$card, before:content "→ ", before:color var(--brand)] before:
116+ @ul [$card, spacing 4, children:padding 4 8]
117+ @each $item in first: bolds this one, odd: shades every other row, children: pads them all
118+ @li [first:font-weight 700, odd:background var(--subtle), border-radius 4] $item
119+ @el [$card, has(input:checked):border-color var(--brand), has(input:checked):background #eff6ff]
120+ @label [cursor pointer]
121+ @input [type=checkbox, margin-inline-end 8]
122+ Tick the box: has() styles the card around it
134123
135− -- Control flow: @each, @else, @match, if()
136− @titled [title Control flow]
137− @el [spacing 6]
138− @data $team [
139− {"name": "Alice", "role": "Engineer"},
140− {"name": "Bob", "role": "Designer"},
141− {"name": "Carol", "role": "PM"}
142− ]
143− @each $person in $team
144− @row [spacing 10, align-items baseline]
145− @text [font-weight bold, min-width 80] $person.name
146− @text [color $muted] $person.role
124+ -- 4. Functions -----------------------------------------------------------
125+ @let stat $value $caption $tone=var(--brand)
126+ @el [$card, spacing 4, flex 1 1 160px]
127+ @text [font-size 32, font-weight 800, color $tone] $value
128+ @text [color var(--muted)] $caption
147129
148− -- @each ... @else fallback for empty list
149− @let nothing ""
150− @each $x in $nothing
151− @text $x
152− @else
153− @text [color $muted, font-style italic, data-state=$nothing] (Nothing — @each ... @else fallback)
130+ @let panel $title
131+ @article [$card, spacing $unit]
132+ @row [align-items center]
133+ @h3 [font-size 18] $title
134+ @spacer
135+ @slot actions
136+ @children
137+ @slot footer
138+ @text [font-size 13, color var(--muted)] Default footer
154139
155− -- repeat N times
156− @row [spacing 6]
157− @each $_ in 1..6
158− @el [width 18, height 18, border-radius 9, background $primary, opacity 0.4]
140+ @let badge $text
141+ @style
142+ & { animation: rise 0.3s ease; }
143+ .dot { color: var(--brand); }
144+ @row [spacing 6, padding 2 10, border-radius 999, background var(--subtle), font-size 13]
145+ @text [class=dot] •
146+ $text
159147
160− -- numeric range
161− @row [spacing 4]
162− @each $i in 1..6
148+ @let pitch
149+ Functions are {@text [font-weight 700] @let} with a body. Their
150+ parameters are attributes, and a body can be text like this one.
151+
152+ @chapter [title Functions, number 4]
153+ @paragraph
154+ @pitch
155+ @row [spacing $unit, wrap]
156+ @stat [value 1, caption output file]
157+ @stat [value 0, caption lines of JavaScript, tone #10b981]
158+ @stat [value ∞, caption components, tone #a855f7, background var(--subtle)]
159+ @panel [title Named slots]
160+ @slot actions
161+ @link [color var(--brand), font-size 14] /edit Edit
162+ @paragraph
163+ Children fill {@code @children}; {@code @slot} blocks fill slots by name.
164+ @row [spacing $unit]
165+ @badge [text scoped style]
166+ @badge [text same function]
167+
168+ -- 5. Expressions ---------------------------------------------------------
169+ @let name htmlang
170+ @data $tags ["layout", "css", "static"]
171+ @let compact false
172+ @chapter [title Expressions, number 5]
173+ @paragraph
174+ \$name inserts a value and \${...} any expression: ${uppercase($name)}
175+ has ${length($name)} letters and ${length($tags)} tags.
176+ @row [spacing $unit, wrap]
177+ @each $shade in 0, 10, 20, 30
163178 @el [
164− width 24, height 24, border-radius 4,
165− background $primary, color white,
166− text-align center, font-size 12, font-weight bold, line-height 24px
179+ padding if($compact, 8, 16),
180+ border-radius $unit,
181+ color white,
182+ background ${darken($--brand, $shade)}
167183 ]
168− $i
184+ darken $shade%
185+ @el [padding 16, border-radius $unit, background ${alpha($--brand, 0.2)}] alpha 0.2
186+ @el [padding 16, border-radius $unit, background ${mix($--brand, #f43f5e, 50)}] mix
187+ @if contains($tags, css)
188+ @text [color var(--muted)] contains(\$tags, css) is true
169189
170− -- @if / @else if / @else
171− @let count 3
172− @row [spacing 6, align-items center]
173− @text [font-weight bold, min-width 64] Count:
174− @if $count == 0
175− @text [color $muted] Empty
176− @else if $count == 1
177− @text [color $primary] Exactly one
178− @else
179− @text [color $primary, font-weight bold] $count items
180−
181− -- @match
182− @let status ready
183− @if $status == "pending"
184− @text [color #f59e0b] Pending…
185− @else if $status == "ready"
186− @text [color #10b981, font-weight bold] ✓ Ready
187− @else if $status == "error"
188− @text [color #ef4444] Error
190+ -- 6. Control flow --------------------------------------------------------
191+ @data $team [
192+ {"name": "Ada", "role": "Engineering", "active": true},
193+ {"name": "Grace", "role": "Design, research", "active": true},
194+ {"name": "Linus", "role": "Operations", "active": false}
195+ ]
196+ @data $nobody []
197+ @chapter [title Control flow, number 6]
198+ @table [border-collapse collapse, width 100%]
199+ @thead
200+ @tr [text-align left, color var(--muted), font-size 13]
201+ @th [padding 8] #
202+ @th [padding 8] Name
203+ @th [padding 8] Role
204+ @th [padding 8] Status
205+ @tbody
206+ @each $person, $i in $team
207+ @tr [border-top 1 solid var(--line)]
208+ @td [padding 8] ${$i + 1}
209+ @td [padding 8, font-weight 600] $person.name
210+ @td [padding 8] $person.role
211+ @td [padding 8, color if($person.active, #10b981, var(--muted))]
212+ @if $person.active
213+ Active
214+ @else
215+ Away
216+ @row [spacing 4]
217+ @each $n in 10..0 step 2
218+ @text [$card, padding 4 10] $n
219+ @each $x in $nobody
220+ @text $x
189221 @else
190− @text Unknown
191−
192− -- Conditional attribute value + before: pseudo-element
193− @let emphasized true
194− @el [
195− padding 12, border-radius 6,
196− background if($emphasized, $primary, #e5e7eb),
197− color if($emphasized, white, $ink),
198− before:content "→ ", before:font-weight bold
199− ]
200− Conditional styling via {@code if(cond, a, b)}.
222+ @text [color var(--muted), font-style italic] @each … @else shows this for an empty list.
201223
202− -- Forms + pseudo-states
203− @titled [title Forms]
204− @form [spacing 10, max-width 420] /subscribe
205− @label [font-weight bold, font-size 13, for=email] Email
206− @input [
207− id=email, type=email, name=email,
208− placeholder=you@example.com, required,
209− padding 10, border 1 solid #e5e7eb, border-radius 6,
210− focus:border 1 solid $primary,
211− valid:border 1 solid #10b981,
212− invalid:border 1 solid #ef4444,
213− transition border 0.15s ease
214− ]
215− @label [font-weight bold, font-size 13, for=role] Role
216− @select [id=role, name=role, padding 10, border 1 solid #e5e7eb, border-radius 6]
217− @option [value=dev] Developer
218− @option [value=designer] Designer
219− @option [value=other] Other
220− @label [font-weight bold, font-size 13, for=msg] Message
221− @textarea [
222− id=msg, name=msg, rows=3,
223− padding 10, border 1 solid #e5e7eb, border-radius 6
224− ]
225− @row [spacing 8]
226− @button [
227− type=submit,
228− padding 10 16, background $primary, color white, font-weight bold,
229− border-radius 6, border 0,
230− hover:background ${darken($primary, 10)},
231− active:background ${darken($primary, 20)},
232− transition background 0.15s ease,
233− cursor pointer
234− ] Subscribe
235− @button [
236− type=reset,
237− padding 10 16, background white,
238− border 1 solid #e5e7eb, border-radius 6, cursor pointer
239− ] Reset
240− @progress [value=60, max=100]
241−
242− -- Lists and tables
243− @titled [title Data]
244− @row [gap $gap, wrap]
245− @el [width fill, spacing $base, min-width 240]
246− @text [font-weight bold] Unordered
247− @ul [spacing 4, list-style disc, padding-inline-start 20]
248− @li [display list-item] Composable with @each
249− @li [display list-item] Indent to nest children
250− @li [display list-item] Bold list items work too
251− @text [font-weight bold] Ordered
252− @ol [list-style decimal, padding-inline-start 20]
253− @li [display list-item] First
254− @li [display list-item] Second
255− @li [display list-item] Third
256−
257− @el [width fill, spacing $base, min-width 240]
258− @text [font-weight bold] Table
259− @table
260− @thead
261− @tr
262− @th [text-align left, padding 8] Item
263− @th [text-align right, padding 8] Qty
264− @tbody
265− @tr
266− @td [padding 8, border-top 1 solid #e5e7eb] Widgets
267− @td [padding 8, border-top 1 solid #e5e7eb, text-align right] 12
268− @tr
269− @td [padding 8, border-top 1 solid #e5e7eb] Gadgets
270− @td [padding 8, border-top 1 solid #e5e7eb, text-align right] 7
271−
272− @figure [spacing 6]
273− @image [alt="A random placeholder", border-radius 8] https://picsum.photos/640/200
274− @figcaption [color $muted, font-size 13, text-align center]
275− A placeholder image.
276−
277− -- Disclosure + named grid areas
278− @titled [title Disclosure + grid areas]
279− @details [padding 12, border 1 solid #e5e7eb, border-radius 6, background $subtle]
280− @summary [font-weight bold, cursor pointer] What is htmlang?
281− @paragraph [color $muted, line-height 1.6, padding-block-start 6]
282− htmlang is a layout-first language that compiles to a single,
283− self-contained HTML file with embedded CSS.
284−
285− @el [
286− display grid,
287− grid-template-columns 1fr 2fr,
288− grid-template-areas "h h" "s m" "f f",
289− gap $base,
290− padding $base, background $subtle, border-radius 8
291− ]
292− @el [grid-area h, padding 10, background $primary, color white, border-radius 6, text-align center]
293− Header
294− @el [grid-area s, padding 10, background white, border-radius 6, border 1 solid #e5e7eb]
295− Sidebar
296− @el [grid-area m, padding 10, background white, border-radius 6, border 1 solid #e5e7eb]
297− Main
298− @el [grid-area f, padding 10, background $ink, color white, border-radius 6, text-align center]
299− Footer
224+ -- 7. HTML ----------------------------------------------------------------
225+ @chapter [title HTML, number 7]
226+ @paragraph
227+ Every other element has its HTML name, and HTML attributes are written
228+ {@code key=value}.
229+ @row [spacing $gutter, wrap, align-items flex-start]
230+ @form [$card, spacing $unit, flex 1 1 280px, method=post] /subscribe
231+ @label [for=email, font-weight 600] Email
232+ @input [
233+ id=email,
234+ type=email,
235+ name=email,
236+ placeholder=you@example.com,
237+ required,
238+ padding 10,
239+ border 1 solid var(--line),
240+ border-radius 6,
241+ focus:outline 2 solid var(--brand),
242+ invalid:border-color #ef4444
243+ ]
244+ @label [for=topic, font-weight 600] Topic
245+ @select [id=topic, name=topic, padding 10, border-radius 6]
246+ @option [value=news] News
247+ @option [value=releases] Releases
248+ @button [type=submit, padding 10, border 0, border-radius 6, background var(--brand), color white, font-weight 700, cursor pointer, hover:background ${darken($--brand, 10)}] Subscribe
249+ @el [spacing $unit, flex 1 1 280px]
250+ @details [$card]
251+ @summary [font-weight 600, cursor pointer] What does this compile to?
252+ @paragraph [color var(--muted), padding-block-start 8]
253+ Flexbox and grid CSS in one {@code <style>}, and no JavaScript.
254+ @button [type=button, popovertarget=about, $card, cursor pointer] Open a popover
255+ @el [popover, id=about, $card, max-width 320]
256+ Native HTML popovers need no script.
257+ @row [spacing $unit, align-items center, font-size 14, color var(--muted)]
258+ @text Progress
259+ @progress [value=60, max=100]
260+ @time [datetime=2026-09-26] Sept 26
261+ @figure [spacing 4]
262+ @image [alt=A placeholder landscape, border-radius $unit, width 100%] https://picsum.photos/640/240
263+ @figcaption [font-size 13, color var(--muted)] @figure and @figcaption
300264
301− -- Misc: address, time, meter
302− @titled [title Misc]
303− @address [spacing 4, font-style normal]
304− @text [font-weight bold] htmlang HQ
305− @text 123 Flexbox Avenue
306− @link [color $primary] mailto:hi@htmlang.dev hi@htmlang.dev
307− @row [spacing 10, align-items center]
308− @text [font-size 13, color $muted] Published
309− @time [datetime=2026-04-16, font-size 13] April 16 2026
310− @row [spacing 10, align-items center]
311− @text [font-size 13, color $muted] Confidence
312− @meter [value=8, min=0, max=10, low=3, high=7] 8/10
265+ -- Raw HTML is the escape hatch
266+ @raw <hr style="border: none; border-top: 1px dashed #cbd5e1">
313267
314− -- Footer: hover/active button, visited link, print-hidden
315− @footer [
316− padding-block-start $gutter, margin-block-start $gutter,
317− border-top 1 solid #e5e7eb, spacing $base,
318− print:display none
319− ]
320− @row [spacing $base, align-items center]
321− @el [
322− padding 12 20, background $primary, color white, font-weight bold, border-radius 8,
323− hover:background ${darken($primary, 10)},
324− active:background ${darken($primary, 20)},
325− transition all 0.15s ease,
326− cursor pointer
327− ] > @link https://github.com
328− Get Started
329− @link [
330− color $primary,
331− visited:color #8b5cf6,
332− hover:text-decoration underline
333− ] https://github.com Browse source
334− @el [width fill]
335− @text [color $muted, font-size 13] © 2026 htmlang
268+ @footer [padding-block $gutter, color var(--muted), font-size 14, print:display none]
269+ @row [spacing $unit]
270+ @text © 2026 htmlang
271+ @spacer
272+ @link [hover:color var(--brand)] https://github.com/iko-soy/htmlang Source
examples/demo.html+715−3
@@ -1,3 +1,715 @@
1−<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>htmlang Demo</title><meta name="description" content="A one-page tour of htmlang."><meta name="theme-color" content="#3b82f6"><meta property="og:title" content="htmlang"><meta property="og:type" content="website"><style>@layer hl-reset,htmlang;@layer hl-reset{*,*::before,*::after{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,sans-serif}img{display:block}a{text-decoration:none;color:inherit}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid currentColor;outline-offset:2px}}:root{--primary:#3b82f6;--ink:#0f172a;--muted:#64748b;--subtle:#f8fafc;}@layer htmlang{.a{display:flex;flex-direction:column;max-width:960px;margin-left:auto;margin-right:auto;padding:32px;gap:24px;background:white;}.b{display:flex;flex-direction:column;gap:8px;}.c{display:flex;flex-direction:column;gap:6px;color:#64748b;font-size:13px;}.d{display:flex;flex-direction:row;gap:6px;align-items:center;}.e{color:#64748b;}.f{color:#0f172a;}.g{display:flex;flex-direction:row;gap:16px;align-items:center;}.h{display:flex;flex-direction:column;flex:1;min-width:0;gap:6px;}.i{font-weight:bold;font-size:40px;color:#0f172a;}.j{margin:0;line-height:1.6;color:#64748b;}.k{font-weight:bold;color:#3b82f6;}.l{color:#3b82f6;text-decoration:underline;}.m{width:72px;height:72px;border-radius:36px;}.n{border-top:1px solid #e5e7eb;}.o{display:flex;flex-direction:column;gap:12px;padding-block:8px;}.p{font-weight:bold;font-size:22px;color:#0f172a;}.q{display:flex;flex-direction:row;flex-wrap:wrap;gap:8px;}.r{display:flex;flex-direction:column;padding:16px;background:white;border-radius:8px;border:1px solid #e5e7eb;box-shadow:0 1px 3px rgba(0,0,0,0.05);cursor:pointer;transition:all 0.2s ease;animation:fade-up 0.3s ease;}.s{display:flex;flex-direction:row;gap:10px;align-items:center;}.t{font-size:20px;color:#3b82f6;}.u{font-weight:bold;font-size:16px;color:#0f172a;}.v{margin:0;line-height:1.7;}.w{font-weight:bold;}.x{font-style:italic;}.y{padding:2px 6px;background:#f1f5f9;border-radius:4px;font-family:monospace;font-size:13px;}.z{background:#fef08a;}.aa{display:flex;flex-direction:column;margin:0;padding-inline-start:16px;border-left:3px solid #3b82f6;color:#64748b;font-style:italic;}.ab{margin:0;white-space:pre;background:#0f172a;color:#e2e8f0;padding:16px;border-radius:8px;font-family:monospace;font-size:13px;overflow-x:auto;}.ac{display:flex;flex-direction:column;gap:6px;}.ad{display:flex;flex-direction:row;gap:10px;align-items:baseline;}.ae{font-weight:bold;min-width:80px;}.af{color:#64748b;font-style:italic;}.ag{display:flex;flex-direction:row;gap:6px;}.ah{display:flex;flex-direction:column;width:18px;height:18px;border-radius:9px;background:#3b82f6;opacity:0.4;}.ai{display:flex;flex-direction:row;gap:4px;}.aj{display:flex;flex-direction:column;width:24px;height:24px;border-radius:4px;background:#3b82f6;color:white;text-align:center;font-size:12px;font-weight:bold;line-height:24px;}.ak{font-weight:bold;min-width:64px;}.al{color:#3b82f6;font-weight:bold;}.am{color:#10b981;font-weight:bold;}.an{display:flex;flex-direction:column;padding:12px;border-radius:6px;background:#3b82f6;color:white;}.ao{font-family:ui-monospace,monospace;}.ap{display:flex;flex-direction:column;gap:10px;max-width:420px;}.aq{font-weight:bold;font-size:13px;}.ar{padding:10px;border:1px solid #e5e7eb;border-radius:6px;transition:border 0.15s ease;}.as{padding:10px;border:1px solid #e5e7eb;border-radius:6px;}.at{display:flex;flex-direction:row;gap:8px;}.au{padding:10px 16px;background:#3b82f6;color:white;font-weight:bold;border-radius:6px;border:0;transition:background 0.15s ease;cursor:pointer;}.av{padding:10px 16px;background:white;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;}.aw{display:flex;flex-direction:row;gap:16px;flex-wrap:wrap;}.ax{display:flex;flex-direction:column;flex:1;gap:8px;min-width:240px;}.ay{margin:0;padding-left:0;gap:4px;list-style:disc;padding-inline-start:20px;}.az{flex-direction:column;display:list-item;}.a0{margin:0;padding-left:0;list-style:decimal;padding-inline-start:20px;}.a1{text-align:left;padding:8px;}.a2{text-align:right;padding:8px;}.a3{padding:8px;border-top:1px solid #e5e7eb;}.a4{padding:8px;border-top:1px solid #e5e7eb;text-align:right;}.a5{display:flex;flex-direction:column;margin:0;gap:6px;}.a6{border-radius:8px;}.a7{color:#64748b;font-size:13px;text-align:center;}.a8{display:flex;flex-direction:column;padding:12px;border:1px solid #e5e7eb;border-radius:6px;background:#f8fafc;}.a9{font-weight:bold;cursor:pointer;}.ba{margin:0;color:#64748b;line-height:1.6;padding-block-start:6px;}.bb{flex-direction:column;display:grid;grid-template-columns:1fr 2fr;grid-template-areas:"h h" "s m" "f f";gap:8px;padding:8px;background:#f8fafc;border-radius:8px;}.bc{display:flex;flex-direction:column;grid-area:h;padding:10px;background:#3b82f6;color:white;border-radius:6px;text-align:center;}.bd{display:flex;flex-direction:column;grid-area:s;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.be{display:flex;flex-direction:column;grid-area:m;padding:10px;background:white;border-radius:6px;border:1px solid #e5e7eb;}.bf{display:flex;flex-direction:column;grid-area:f;padding:10px;background:#0f172a;color:white;border-radius:6px;text-align:center;}.bg{gap:4px;font-style:normal;}.bh,.bn{color:#3b82f6;}.bi{font-size:13px;color:#64748b;}.bj{font-size:13px;}.bk{display:flex;flex-direction:column;padding-block-start:24px;margin-block-start:24px;border-top:1px solid #e5e7eb;gap:8px;}.bl{display:flex;flex-direction:row;gap:8px;align-items:center;}.bm{display:flex;flex-direction:column;padding:12px 20px;background:#3b82f6;color:white;font-weight:bold;border-radius:8px;transition:all 0.15s ease;cursor:pointer;}.bo{display:flex;flex-direction:column;flex:1;min-width:0;}.bp{color:#64748b;font-size:13px;}.r:hover{box-shadow:0 4px 12px rgba(0,0,0,0.08);}.au:hover,.bm:hover{background:#3575dd;}.bn:hover{text-decoration:underline;}.an::before{content:"→ ";font-weight:bold;}.ar:focus{border:1px solid #3b82f6;}.ar:valid{border:1px solid #10b981;}.ar:invalid{border:1px solid #ef4444;}.au:active,.bm:active{background:#2f68c5;}.bn:visited{color:#8b5cf6;}@media(prefers-color-scheme:dark){.a{background:#0b1220;color:#e2e8f0;}.i{color:white;}}@media print{.bk{display:none;}}}@keyframes fade-up {from { opacity: 0; transform: translateY(8px); }to { opacity: 1; transform: translateY(0); }}</style></head><body><noscript><style>.needs-js{display:none}</style></noscript><div class="a"><header class="b"><nav class="c"><div class="d"><a href="/" class="e">Home</a><span>/</span><a href="/demos" class="e">Demos</a><span>/</span><span class="f">htmlang</span></div></nav><div class="g"><div class="h"><span class="i">Welcome to htmlang</span><p class="j">A minimalist layout language that compiles to <span class="k">static HTML</span> — no JS, no build step. Visit the <a href="https://github.com" class="l">repo</a> for more.</p></div><img src="https://picsum.photos/72" class="m" alt="Avatar"></div></header><hr class="n"><section class="o"><span class="p">Features</span><div class="q"><article class="r"><div class="s"><span class="t">✦</span><span class="u">Simple</span></div><span>Write layouts without CSS classes.</span></article><article class="r"><div class="s"><span class="t">⚡</span><span class="u">Fast</span></div><span>Compiles to a single self-contained HTML file.</span></article><article class="r"><div class="s"><span class="t">○</span><span class="u">Minimal</span></div><span>Zero runtime JavaScript by default.</span></article><article class="r"><div class="s"><span class="t">◆</span><span class="u">Typed</span></div><span>LSP with diagnostics and go-to-definition.</span></article></div></section><section class="o"><span class="p">Typography</span><p class="v">A quick brown fox jumps over <span class="w">the lazy</span> <span class="x">dog</span>. Press <kbd class="y">Ctrl+K</kbd> to search. <mark class="z">Highlighted</mark> uses mark. <abbr title="HyperText Markup Language + language">htmlang</abbr> is an abbreviation.</p><blockquote class="aa"><span>Layout is code. Style is code. Everything should compose.</span><cite>— Anonymous</cite></blockquote><pre class="ab">@let button $label
2− @el [padding 12, background blue]
3− @text $label</pre></section><section class="o"><span class="p">Control flow</span><div class="ac"><div class="ad"><span class="ae">Alice</span><span class="e">Engineer</span></div><div class="ad"><span class="ae">Bob</span><span class="e">Designer</span></div><div class="ad"><span class="ae">Carol</span><span class="e">PM</span></div></div><span class="af" data-state="">(Nothing — @each ... @else fallback)</span><div class="ag"><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div><div class="ah"></div></div><div class="ai"><div class="aj"><span>1</span></div><div class="aj"><span>2</span></div><div class="aj"><span>3</span></div><div class="aj"><span>4</span></div><div class="aj"><span>5</span></div><div class="aj"><span>6</span></div></div><div class="d"><span class="ak">Count:</span><span class="al">3 items</span></div><span class="am">✓ Ready</span><div class="an"><span>Conditional styling via <code class="ao">if(cond, a, b)</code>.</span></div></section><section class="o"><span class="p">Forms</span><form action="/subscribe" class="ap"><label class="aq" for="email">Email</label><input class="ar" id="email" type="email" name="email" placeholder="you@example.com" required><label class="aq" for="role">Role</label><select class="as" id="role" name="role"><option value="dev">Developer</option><option value="designer">Designer</option><option value="other">Other</option></select><label class="aq" for="msg">Message</label><textarea class="as" id="msg" name="msg" rows="3"></textarea><div class="at"><button class="au" type="submit">Subscribe</button><button class="av" type="reset">Reset</button></div><progress value="60" max="100"></progress></form></section><section class="o"><span class="p">Data</span><div class="aw"><div class="ax"><span class="w">Unordered</span><ul class="ay"><li class="az">Composable with @each</li><li class="az">Indent to nest children</li><li class="az">Bold list items work too</li></ul><span class="w">Ordered</span><ol class="a0"><li class="az">First</li><li class="az">Second</li><li class="az">Third</li></ol></div><div class="ax"><span class="w">Table</span><table><thead><tr><th class="a1">Item</th><th class="a2">Qty</th></tr></thead><tbody><tr><td class="a3">Widgets</td><td class="a4">12</td></tr><tr><td class="a3">Gadgets</td><td class="a4">7</td></tr></tbody></table></div></div><figure class="a5"><img src="https://picsum.photos/640/200" class="a6" alt="A random placeholder"><figcaption class="a7">A placeholder image.</figcaption></figure></section><section class="o"><span class="p">Disclosure + grid areas</span><details class="a8"><summary class="a9">What is htmlang?</summary><p class="ba">htmlang is a layout-first language that compiles to a single, self-contained HTML file with embedded CSS.</p></details><div class="bb"><div class="bc"><span>Header</span></div><div class="bd"><span>Sidebar</span></div><div class="be"><span>Main</span></div><div class="bf"><span>Footer</span></div></div></section><section class="o"><span class="p">Misc</span><address class="bg"><span class="w">htmlang HQ</span><span>123 Flexbox Avenue</span><a href="mailto:hi@htmlang.dev" class="bh">hi@htmlang.dev</a></address><div class="s"><span class="bi">Published</span><time class="bj" datetime="2026-04-16">April 16 2026</time></div><div class="s"><span class="bi">Confidence</span><meter value="8" min="0" max="10" low="3" high="7">8/10</meter></div></section><footer class="bk"><div class="bl"><div class="bm"><a href="https://github.com">Get Started</a></div><a href="https://github.com" class="bn">Browse source</a><div class="bo"></div><span class="bp">© 2026 htmlang</span></div></footer></div></body></html>
1+<!DOCTYPE html>
2+<html lang="en">
3+<head>
4+<meta charset="utf-8">
5+<meta name="viewport" content="width=device-width, initial-scale=1">
6+<title>htmlang tour</title>
7+<meta name="description" content="A one-page tour of htmlang.">
8+<meta property="og:title" content="htmlang tour">
9+<style>
10+@layer hl-reset, htmlang;
11+@layer hl-reset {
12+*, *::before, *::after { box-sizing: border-box; }
13+body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }
14+img { display: block; }
15+a { text-decoration: none; color: inherit; }
16+a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
17+}
18+:root {
19+ --brand: #3b82f6;
20+ --ink: #0f172a;
21+ --muted: #64748b;
22+ --line: #e5e7eb;
23+ --subtle: #f8fafc;
24+}
25+@layer htmlang {
26+ .a {display:flex;flex-direction:column;max-width:960px;margin-left:auto;margin-right:auto;padding:24px;color:var(--ink);line-height:1.5;}
27+ .b {display:flex;flex-direction:column;gap:8px;padding-block:24px;animation:rise 0.4s ease;}
28+ .c {font-size:12px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;}
29+ .d {margin:0;font-size:44px;line-height:1.1;letter-spacing:-1px;}
30+ .e {margin:0;font-size:18px;color:var(--muted);max-width:620px;}
31+ .f {font-weight:700;color:var(--brand);}
32+ .g {font-family:ui-monospace,monospace;}
33+ .h {display:flex;flex-direction:column;gap:16px;padding-block:24px;border-top:1px solid var(--line);}
34+ .i {display:flex;flex-direction:row;gap:8px;align-items:baseline;}
35+ .j {color:var(--brand);font-weight:700;}
36+ .k {margin:0;font-size:26px;}
37+ .l {margin:0;}
38+ .m {display:flex;flex-direction:row;gap:8px;flex-wrap:wrap;}
39+ .n {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;flex:1;min-width:0;}
40+ .o {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;width:160px;}
41+ .p {display:flex;flex-direction:row;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;gap:8px;align-items:center;}
42+ .q {font-weight:700;}
43+ .r {display:flex;flex-direction:column;flex:1;}
44+ .s {display:flex;flex-direction:row;gap:8px;height:120px;}
45+ .t {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;align-self:flex-start;}
46+ .u {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;align-self:center;}
47+ .v {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;align-self:flex-end;}
48+ .w {display:grid;grid-template-columns:repeat(4,1fr);gap:8px;}
49+ .x {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;text-align:center;grid-column:span 2;}
50+ .y {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;text-align:center;}
51+ .z {position:relative;isolation:isolate;display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;height:120px;overflow:hidden;}
52+ .aa {display:flex;flex-direction:column;position:absolute;inset:0;z-index:-1;}
53+ .ab {display:flex;flex-direction:column;background:linear-gradient(135deg, #dbeafe, #fae8ff);flex:1;min-height:0;justify-content:flex-end;padding:8px;}
54+ .ac {display:flex;flex-direction:column;position:absolute;inset:0;}
55+ .ad {align-self:flex-end;margin-top:auto;padding:8px;font-size:12px;color:var(--muted);}
56+ .ae {margin:0;max-width:640px;}
57+ .af {font-style:italic;}
58+ .ag {color:var(--brand);text-decoration:underline;}
59+ .ah {font-family:ui-monospace,monospace;padding:1px 6px;border:1px solid var(--line);border-radius:4px;}
60+ .ai {display:flex;flex-direction:column;padding:12px 20px;background:var(--brand);border-radius:8px;align-self:flex-start;}
61+ .aj {color:white;font-weight:700;}
62+ .ak {display:flex;flex-direction:column;margin:0;padding-inline-start:16px;border-left:3px solid var(--brand);color:var(--muted);font-style:italic;}
63+ .al {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.1);transform:rotate(-2deg);}
64+ .am {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;transition:all 0.15s ease;cursor:pointer;}
65+ .an,.ao,.ar,.bu {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;}
66+ .ap {margin:0;padding-left:0;list-style:none;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;gap:4px;}
67+ .aq {display:flex;flex-direction:column;border-radius:4px;}
68+ .as {cursor:pointer;}
69+ .at {margin-inline-end:8px;}
70+ .au {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;gap:4px;flex:1 1 160px;}
71+ .av {font-size:32px;font-weight:800;color:var(--brand);}
72+ .aw {color:var(--muted);}
73+ .ax {font-size:32px;font-weight:800;color:#10b981;}
74+ .ay {display:flex;flex-direction:column;padding:16px;border:1px solid var(--line);border-radius:8px;gap:4px;flex:1 1 160px;background:var(--subtle);}
75+ .az {font-size:32px;font-weight:800;color:#a855f7;}
76+ .a0 {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;gap:8px;}
77+ .a1 {display:flex;flex-direction:row;align-items:center;}
78+ .a2 {margin:0;font-size:18px;}
79+ .a3 {color:var(--brand);font-size:14px;}
80+ .a4 {font-size:13px;color:var(--muted);}
81+ .a5 {display:flex;flex-direction:row;gap:8px;}
82+ .a6 {display:flex;flex-direction:row;gap:6px;padding:2px 10px;border-radius:999px;background:var(--subtle);font-size:13px;}
83+ .a7 {display:flex;flex-direction:column;padding:16px;border-radius:8px;color:white;background:#3b82f6;}
84+ .a8 {display:flex;flex-direction:column;padding:16px;border-radius:8px;color:white;background:#3575dd;}
85+ .a9 {display:flex;flex-direction:column;padding:16px;border-radius:8px;color:white;background:#2f68c5;}
86+ .ba {display:flex;flex-direction:column;padding:16px;border-radius:8px;color:white;background:#295bac;}
87+ .bb {display:flex;flex-direction:column;padding:16px;border-radius:8px;background:#3b82f633;}
88+ .bc {display:flex;flex-direction:column;padding:16px;border-radius:8px;background:#9861aa;}
89+ .bd {border-collapse:collapse;width:100%;}
90+ .be {text-align:left;color:var(--muted);font-size:13px;}
91+ .bf {padding:8px;}
92+ .bg {border-top:1px solid var(--line);}
93+ .bh {padding:8px;font-weight:600;}
94+ .bi {padding:8px;color:#10b981;}
95+ .bj {padding:8px;color:var(--muted);}
96+ .bk {display:flex;flex-direction:row;gap:4px;}
97+ .bl {background:white;border:1px solid var(--line);border-radius:8px;padding:4px 10px;}
98+ .bm {color:var(--muted);font-style:italic;}
99+ .bn {display:flex;flex-direction:row;gap:24px;flex-wrap:wrap;align-items:flex-start;}
100+ .bo {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;gap:8px;flex:1 1 280px;}
101+ .bp {font-weight:600;}
102+ .bq {padding:10px;border:1px solid var(--line);border-radius:6px;}
103+ .br {padding:10px;border-radius:6px;}
104+ .bs {padding:10px;border:0;border-radius:6px;background:var(--brand);color:white;font-weight:700;cursor:pointer;}
105+ .bt {display:flex;flex-direction:column;gap:8px;flex:1 1 280px;}
106+ .bv {font-weight:600;cursor:pointer;}
107+ .bw {margin:0;color:var(--muted);padding-block-start:8px;}
108+ .bx {padding:16px;background:white;border:1px solid var(--line);border-radius:8px;cursor:pointer;}
109+ .by {display:flex;flex-direction:column;padding:16px;background:white;border:1px solid var(--line);border-radius:8px;max-width:320px;}
110+ .bz {display:flex;flex-direction:row;gap:8px;align-items:center;font-size:14px;color:var(--muted);}
111+ .b0 {display:flex;flex-direction:column;margin:0;gap:4px;}
112+ .b1 {border-radius:8px;width:100%;}
113+ .b2 {display:flex;flex-direction:column;padding-block:24px;color:var(--muted);font-size:14px;}
114+ .am:hover {background:#eff6ff;border-color:var(--brand);}
115+ .bs:hover {background:#3575dd;}
116+ .b3:hover {color:var(--brand);}
117+ .ao::before {content:"→ ";color:var(--brand);}
118+ .ap > * {padding:4px 8px;}
119+ .aq:first-child {font-weight:700;}
120+ .aq:nth-child(odd) {background:var(--subtle);}
121+ .ar:has(input:checked) {border-color:var(--brand);background:#eff6ff;}
122+ .bq:focus {outline:2px solid solid var(--brand);}
123+ .bq:invalid {border-color:#ef4444;}
124+@media (min-width: 768px) {
125+ .an {background:#ecfdf5;}
126+}
127+@media (prefers-color-scheme: dark) {
128+ .a {background:#030712;color:#e5e7eb;}
129+ .n,.o,.p,.t,.u,.v,.x,.y,.z,.al,.am,.ao,.ap,.ar,.au,.ay,.a0,.bl,.bo,.bu,.bx,.by {background:#111827;border-color:#1f2937;}
130+ .an {border-color:#1f2937;background:#064e3b;}
131+}
132+@media print {
133+ .b2 {display:none;}
134+}
135+}
136+@keyframes rise {
137+ from { opacity: 0; transform: translateY(6px); }
138+ to { opacity: 1; transform: none; }
139+}
140+.hl-badge {
141+& { animation: rise 0.3s ease; }
142+.dot { color: var(--brand); }
143+}
144+</style>
145+</head>
146+<body>
147+<!-- @el line 46 -->
148+<div class="a" data-hl-line="46" data-hl-el="el">
149+ <!-- @header line 55 -->
150+ <header class="b" data-hl-line="55" data-hl-el="header">
151+ <!-- @text line 56 -->
152+ <span class="c" data-hl-line="56" data-hl-el="text">
153+A one-page tour </span>
154+ <!-- @h1 line 57 -->
155+ <h1 class="d" data-hl-line="57" data-hl-el="h1">
156+Welcome to htmlang </h1>
157+ <!-- @paragraph line 58 -->
158+ <p class="e" data-hl-line="58" data-hl-el="paragraph">
159+A small layout language that compiles to <!-- @text line 60 -->
160+ <span class="f" data-hl-line="60" data-hl-el="text">
161+one static HTML file </span>. Structure starts with <!-- @code line 61 -->
162+ <code class="g" data-hl-line="61" data-hl-el="code">
163+@ </code>, everything else is content. </p>
164+ </header>
165+ <!-- @section line 32 -->
166+ <section class="h" data-hl-line="32" data-hl-el="section">
167+ <!-- @row line 33 -->
168+ <div class="i" data-hl-line="33" data-hl-el="row">
169+ <!-- @text line 35 -->
170+ <span class="j" data-hl-line="35" data-hl-el="text">
171+1. </span>
172+ <!-- @h2 line 36 -->
173+ <h2 class="k" data-hl-line="36" data-hl-el="h2">
174+Layout </h2>
175+ </div>
176+ <!-- @paragraph line 65 -->
177+ <p class="l" data-hl-line="65" data-hl-el="paragraph">
178+ <!-- @code line 66 -->
179+ <code class="g" data-hl-line="66" data-hl-el="code">
180+@el </code> stacks its children in a column and <!-- @code line 66 -->
181+ <code class="g" data-hl-line="66" data-hl-el="code">
182+@row </code> lays them out side by side. <!-- @code line 67 -->
183+ <code class="g" data-hl-line="67" data-hl-el="code">
184+spacing </code> is the gap between children. </p>
185+ <!-- @row line 68 -->
186+ <div class="m" data-hl-line="68" data-hl-el="row">
187+ <!-- @el line 69 -->
188+ <div class="n" data-hl-line="69" data-hl-el="el">
189+ <span>width fill</span>
190+ </div>
191+ <!-- @el line 70 -->
192+ <div class="n" data-hl-line="70" data-hl-el="el">
193+ <span>shares the row</span>
194+ </div>
195+ <!-- @el line 71 -->
196+ <div class="o" data-hl-line="71" data-hl-el="el">
197+ <span>width 160</span>
198+ </div>
199+ </div>
200+ <!-- @row line 72 -->
201+ <div class="p" data-hl-line="72" data-hl-el="row">
202+ <!-- @text line 73 -->
203+ <span class="q" data-hl-line="73" data-hl-el="text">
204+Logo </span>
205+ <!-- @el line 6 -->
206+ <div class="r" data-hl-line="6" data-hl-el="el">
207+ </div>
208+ <!-- @text line 75 -->
209+ <span data-hl-line="75" data-hl-el="text">
210+@spacer pushes this to the end </span>
211+ </div>
212+ <!-- @row line 76 -->
213+ <div class="s" data-hl-line="76" data-hl-el="row">
214+ <!-- @el line 77 -->
215+ <div class="t" data-hl-line="77" data-hl-el="el">
216+ <span>align-top</span>
217+ </div>
218+ <!-- @el line 78 -->
219+ <div class="u" data-hl-line="78" data-hl-el="el">
220+ <span>center-y</span>
221+ </div>
222+ <!-- @el line 79 -->
223+ <div class="v" data-hl-line="79" data-hl-el="el">
224+ <span>align-bottom</span>
225+ </div>
226+ </div>
227+ <!-- @grid line 80 -->
228+ <div class="w" data-hl-line="80" data-hl-el="grid">
229+ <!-- @el line 82 -->
230+ <div class="x" data-hl-line="82" data-hl-el="el">
231+ <span>Cell 1</span>
232+ </div>
233+ <!-- @el line 82 -->
234+ <div class="y" data-hl-line="82" data-hl-el="el">
235+ <span>Cell 2</span>
236+ </div>
237+ <!-- @el line 82 -->
238+ <div class="y" data-hl-line="82" data-hl-el="el">
239+ <span>Cell 3</span>
240+ </div>
241+ <!-- @el line 82 -->
242+ <div class="y" data-hl-line="82" data-hl-el="el">
243+ <span>Cell 4</span>
244+ </div>
245+ <!-- @el line 82 -->
246+ <div class="y" data-hl-line="82" data-hl-el="el">
247+ <span>Cell 5</span>
248+ </div>
249+ <!-- @el line 82 -->
250+ <div class="y" data-hl-line="82" data-hl-el="el">
251+ <span>Cell 6</span>
252+ </div>
253+ </div>
254+ <!-- @el line 83 -->
255+ <div class="z" data-hl-line="83" data-hl-el="el">
256+ <!-- @behind line 84 -->
257+ <div class="aa" data-hl-line="84" data-hl-el="behind">
258+ <!-- @el line 85 -->
259+ <div class="ab" data-hl-line="85" data-hl-el="el">
260+ <span>(behind)</span>
261+ </div>
262+ </div>
263+ <!-- @text line 86 -->
264+ <span class="q" data-hl-line="86" data-hl-el="text">
265+@in-front and @behind layer content </span>
266+ <!-- @in-front line 87 -->
267+ <div class="ac" data-hl-line="87" data-hl-el="in-front">
268+ <!-- @text line 88 -->
269+ <span class="ad" data-hl-line="88" data-hl-el="text">
270+painted on top </span>
271+ </div>
272+ </div>
273+ </section>
274+ <!-- @section line 32 -->
275+ <section class="h" data-hl-line="32" data-hl-el="section">
276+ <!-- @row line 33 -->
277+ <div class="i" data-hl-line="33" data-hl-el="row">
278+ <!-- @text line 35 -->
279+ <span class="j" data-hl-line="35" data-hl-el="text">
280+2. </span>
281+ <!-- @h2 line 36 -->
282+ <h2 class="k" data-hl-line="36" data-hl-el="h2">
283+Text </h2>
284+ </div>
285+ <!-- @paragraph line 92 -->
286+ <p class="ae" data-hl-line="92" data-hl-el="paragraph">
287+Lines without an <!-- @code line 93 -->
288+ <code class="g" data-hl-line="93" data-hl-el="code">
289+@ </code> are text. Braces put elements inside a line: <!-- @text line 94 -->
290+ <span class="q" data-hl-line="94" data-hl-el="text">
291+bold </span>, <!-- @text line 94 -->
292+ <span class="af" data-hl-line="94" data-hl-el="text">
293+italic </span>, <!-- @mark line 95 -->
294+ <mark data-hl-line="95" data-hl-el="mark">
295+highlighted </mark>, <!-- @link line 95 -->
296+ <a href="https://example.com" class="ag" data-hl-line="95" data-hl-el="link">
297+a link </a>, and <!-- @kbd line 96 -->
298+ <kbd class="ah" data-hl-line="96" data-hl-el="kbd">
299+Ctrl+K </kbd>. </p>
300+ <!-- @paragraph line 97 -->
301+ <p class="l" data-hl-line="97" data-hl-el="paragraph">
302+A backslash escapes: @ at the start of a line, $price, and {braces}. </p>
303+ <!-- @el line 100 -->
304+ <div class="ai" data-hl-line="100" data-hl-el="el">
305+ <!-- @link line 100 -->
306+ <a href="/start" data-hl-line="100" data-hl-el="link">
307+ <!-- @text line 101 -->
308+ <span class="aj" data-hl-line="101" data-hl-el="text">
309+Get started </span>
310+ </a>
311+ </div>
312+ <!-- @blockquote line 102 -->
313+ <blockquote class="ak" data-hl-line="102" data-hl-el="blockquote">
314+ <span>Layout is code. Style is code. Everything should compose.</span>
315+ <!-- @cite line 104 -->
316+ <cite data-hl-line="104" data-hl-el="cite">
317+— Anonymous </cite>
318+ </blockquote>
319+ </section>
320+ <!-- @section line 32 -->
321+ <section class="h" data-hl-line="32" data-hl-el="section">
322+ <!-- @row line 33 -->
323+ <div class="i" data-hl-line="33" data-hl-el="row">
324+ <!-- @text line 35 -->
325+ <span class="j" data-hl-line="35" data-hl-el="text">
326+3. </span>
327+ <!-- @h2 line 36 -->
328+ <h2 class="k" data-hl-line="36" data-hl-el="h2">
329+Style </h2>
330+ </div>
331+ <!-- @paragraph line 108 -->
332+ <p class="l" data-hl-line="108" data-hl-el="paragraph">
333+Besides the layout attributes, every attribute is a CSS property with its CSS name and value. A bare number is pixels for lengths. </p>
334+ <!-- @row line 111 -->
335+ <div class="m" data-hl-line="111" data-hl-el="row">
336+ <!-- @el line 112 -->
337+ <div class="al" data-hl-line="112" data-hl-el="el">
338+ <span>Any CSS property</span>
339+ </div>
340+ <!-- @el line 113 -->
341+ <div class="am" data-hl-line="113" data-hl-el="el">
342+ <span>hover:</span>
343+ </div>
344+ <!-- @el line 114 -->
345+ <div class="an" data-hl-line="114" data-hl-el="el">
346+ <span>md: and dark:</span>
347+ </div>
348+ <!-- @el line 115 -->
349+ <div class="ao" data-hl-line="115" data-hl-el="el">
350+ <span>before:</span>
351+ </div>
352+ </div>
353+ <!-- @ul line 116 -->
354+ <ul class="ap" data-hl-line="116" data-hl-el="ul">
355+ <!-- @li line 118 -->
356+ <li class="aq" data-hl-line="118" data-hl-el="li">
357+first: bolds this one </li>
358+ <!-- @li line 118 -->
359+ <li class="aq" data-hl-line="118" data-hl-el="li">
360+odd: shades every other row </li>
361+ <!-- @li line 118 -->
362+ <li class="aq" data-hl-line="118" data-hl-el="li">
363+children: pads them all </li>
364+ </ul>
365+ <!-- @el line 119 -->
366+ <div class="ar" data-hl-line="119" data-hl-el="el">
367+ <!-- @label line 120 -->
368+ <label class="as" data-hl-line="120" data-hl-el="label">
369+ <!-- @input line 121 -->
370+ <input class="at" type="checkbox" data-hl-line="121" data-hl-el="input">
371+Tick the box: has() styles the card around it </label>
372+ </div>
373+ </section>
374+ <!-- @section line 32 -->
375+ <section class="h" data-hl-line="32" data-hl-el="section">
376+ <!-- @row line 33 -->
377+ <div class="i" data-hl-line="33" data-hl-el="row">
378+ <!-- @text line 35 -->
379+ <span class="j" data-hl-line="35" data-hl-el="text">
380+4. </span>
381+ <!-- @h2 line 36 -->
382+ <h2 class="k" data-hl-line="36" data-hl-el="h2">
383+Functions </h2>
384+ </div>
385+ <!-- @paragraph line 153 -->
386+ <p class="l" data-hl-line="153" data-hl-el="paragraph">
387+Functions are <!-- @text line 149 -->
388+ <span class="q" data-hl-line="149" data-hl-el="text">
389+@let </span> with a body. Their parameters are attributes, and a body can be text like this one. </p>
390+ <!-- @row line 155 -->
391+ <div class="m" data-hl-line="155" data-hl-el="row">
392+ <!-- @el line 126 -->
393+ <div class="au" data-hl-line="126" data-hl-el="el">
394+ <!-- @text line 127 -->
395+ <span class="av" data-hl-line="127" data-hl-el="text">
396+1 </span>
397+ <!-- @text line 128 -->
398+ <span class="aw" data-hl-line="128" data-hl-el="text">
399+output file </span>
400+ </div>
401+ <!-- @el line 126 -->
402+ <div class="au" data-hl-line="126" data-hl-el="el">
403+ <!-- @text line 127 -->
404+ <span class="ax" data-hl-line="127" data-hl-el="text">
405+0 </span>
406+ <!-- @text line 128 -->
407+ <span class="aw" data-hl-line="128" data-hl-el="text">
408+lines of JavaScript </span>
409+ </div>
410+ <!-- @el line 126 -->
411+ <div class="ay" data-hl-line="126" data-hl-el="el">
412+ <!-- @text line 127 -->
413+ <span class="az" data-hl-line="127" data-hl-el="text">
414+∞ </span>
415+ <!-- @text line 128 -->
416+ <span class="aw" data-hl-line="128" data-hl-el="text">
417+components </span>
418+ </div>
419+ </div>
420+ <!-- @article line 131 -->
421+ <article class="a0" data-hl-line="131" data-hl-el="article">
422+ <!-- @row line 132 -->
423+ <div class="a1" data-hl-line="132" data-hl-el="row">
424+ <!-- @h3 line 133 -->
425+ <h3 class="a2" data-hl-line="133" data-hl-el="h3">
426+Named slots </h3>
427+ <!-- @el line 6 -->
428+ <div class="r" data-hl-line="6" data-hl-el="el">
429+ </div>
430+ <!-- @link line 161 -->
431+ <a href="/edit" class="a3" data-hl-line="161" data-hl-el="link">
432+Edit </a>
433+ </div>
434+ <!-- @paragraph line 162 -->
435+ <p class="l" data-hl-line="162" data-hl-el="paragraph">
436+Children fill <!-- @code line 163 -->
437+ <code class="g" data-hl-line="163" data-hl-el="code">
438+@children </code>; <!-- @code line 163 -->
439+ <code class="g" data-hl-line="163" data-hl-el="code">
440+@slot </code> blocks fill slots by name. </p>
441+ <!-- @text line 138 -->
442+ <span class="a4" data-hl-line="138" data-hl-el="text">
443+Default footer </span>
444+ </article>
445+ <!-- @row line 164 -->
446+ <div class="a5" data-hl-line="164" data-hl-el="row">
447+ <!-- @row line 144 -->
448+ <div class="a6 hl-badge" data-hl-line="144" data-hl-el="row">
449+ <!-- @text line 145 -->
450+ <span class="dot" data-hl-line="145" data-hl-el="text">
451+• </span>
452+ <span>scoped style</span>
453+ </div>
454+ <!-- @row line 144 -->
455+ <div class="a6 hl-badge" data-hl-line="144" data-hl-el="row">
456+ <!-- @text line 145 -->
457+ <span class="dot" data-hl-line="145" data-hl-el="text">
458+• </span>
459+ <span>same function</span>
460+ </div>
461+ </div>
462+ </section>
463+ <!-- @section line 32 -->
464+ <section class="h" data-hl-line="32" data-hl-el="section">
465+ <!-- @row line 33 -->
466+ <div class="i" data-hl-line="33" data-hl-el="row">
467+ <!-- @text line 35 -->
468+ <span class="j" data-hl-line="35" data-hl-el="text">
469+5. </span>
470+ <!-- @h2 line 36 -->
471+ <h2 class="k" data-hl-line="36" data-hl-el="h2">
472+Expressions </h2>
473+ </div>
474+ <!-- @paragraph line 173 -->
475+ <p class="l" data-hl-line="173" data-hl-el="paragraph">
476+$name inserts a value and ${...} any expression: HTMLANG has 7 letters and 3 tags. </p>
477+ <!-- @row line 176 -->
478+ <div class="m" data-hl-line="176" data-hl-el="row">
479+ <!-- @el line 178 -->
480+ <div class="a7" data-hl-line="178" data-hl-el="el">
481+ <span>darken 0%</span>
482+ </div>
483+ <!-- @el line 178 -->
484+ <div class="a8" data-hl-line="178" data-hl-el="el">
485+ <span>darken 10%</span>
486+ </div>
487+ <!-- @el line 178 -->
488+ <div class="a9" data-hl-line="178" data-hl-el="el">
489+ <span>darken 20%</span>
490+ </div>
491+ <!-- @el line 178 -->
492+ <div class="ba" data-hl-line="178" data-hl-el="el">
493+ <span>darken 30%</span>
494+ </div>
495+ <!-- @el line 185 -->
496+ <div class="bb" data-hl-line="185" data-hl-el="el">
497+ <span>alpha 0.2</span>
498+ </div>
499+ <!-- @el line 186 -->
500+ <div class="bc" data-hl-line="186" data-hl-el="el">
501+ <span>mix</span>
502+ </div>
503+ </div>
504+ <!-- @text line 188 -->
505+ <span class="aw" data-hl-line="188" data-hl-el="text">
506+contains($tags, css) is true </span>
507+ </section>
508+ <!-- @section line 32 -->
509+ <section class="h" data-hl-line="32" data-hl-el="section">
510+ <!-- @row line 33 -->
511+ <div class="i" data-hl-line="33" data-hl-el="row">
512+ <!-- @text line 35 -->
513+ <span class="j" data-hl-line="35" data-hl-el="text">
514+6. </span>
515+ <!-- @h2 line 36 -->
516+ <h2 class="k" data-hl-line="36" data-hl-el="h2">
517+Control flow </h2>
518+ </div>
519+ <!-- @table line 198 -->
520+ <table class="bd" data-hl-line="198" data-hl-el="table">
521+ <!-- @thead line 199 -->
522+ <thead data-hl-line="199" data-hl-el="thead">
523+ <!-- @tr line 200 -->
524+ <tr class="be" data-hl-line="200" data-hl-el="tr">
525+ <!-- @th line 201 -->
526+ <th class="bf" data-hl-line="201" data-hl-el="th">
527+# </th>
528+ <!-- @th line 202 -->
529+ <th class="bf" data-hl-line="202" data-hl-el="th">
530+Name </th>
531+ <!-- @th line 203 -->
532+ <th class="bf" data-hl-line="203" data-hl-el="th">
533+Role </th>
534+ <!-- @th line 204 -->
535+ <th class="bf" data-hl-line="204" data-hl-el="th">
536+Status </th>
537+ </tr>
538+ </thead>
539+ <!-- @tbody line 205 -->
540+ <tbody data-hl-line="205" data-hl-el="tbody">
541+ <!-- @tr line 207 -->
542+ <tr class="bg" data-hl-line="207" data-hl-el="tr">
543+ <!-- @td line 208 -->
544+ <td class="bf" data-hl-line="208" data-hl-el="td">
545+1 </td>
546+ <!-- @td line 209 -->
547+ <td class="bh" data-hl-line="209" data-hl-el="td">
548+Ada </td>
549+ <!-- @td line 210 -->
550+ <td class="bf" data-hl-line="210" data-hl-el="td">
551+Engineering </td>
552+ <!-- @td line 211 -->
553+ <td class="bi" data-hl-line="211" data-hl-el="td">
554+Active </td>
555+ </tr>
556+ <!-- @tr line 207 -->
557+ <tr class="bg" data-hl-line="207" data-hl-el="tr">
558+ <!-- @td line 208 -->
559+ <td class="bf" data-hl-line="208" data-hl-el="td">
560+2 </td>
561+ <!-- @td line 209 -->
562+ <td class="bh" data-hl-line="209" data-hl-el="td">
563+Grace </td>
564+ <!-- @td line 210 -->
565+ <td class="bf" data-hl-line="210" data-hl-el="td">
566+Design, research </td>
567+ <!-- @td line 211 -->
568+ <td class="bi" data-hl-line="211" data-hl-el="td">
569+Active </td>
570+ </tr>
571+ <!-- @tr line 207 -->
572+ <tr class="bg" data-hl-line="207" data-hl-el="tr">
573+ <!-- @td line 208 -->
574+ <td class="bf" data-hl-line="208" data-hl-el="td">
575+3 </td>
576+ <!-- @td line 209 -->
577+ <td class="bh" data-hl-line="209" data-hl-el="td">
578+Linus </td>
579+ <!-- @td line 210 -->
580+ <td class="bf" data-hl-line="210" data-hl-el="td">
581+Operations </td>
582+ <!-- @td line 211 -->
583+ <td class="bj" data-hl-line="211" data-hl-el="td">
584+Away </td>
585+ </tr>
586+ </tbody>
587+ </table>
588+ <!-- @row line 216 -->
589+ <div class="bk" data-hl-line="216" data-hl-el="row">
590+ <!-- @text line 218 -->
591+ <span class="bl" data-hl-line="218" data-hl-el="text">
592+10 </span>
593+ <!-- @text line 218 -->
594+ <span class="bl" data-hl-line="218" data-hl-el="text">
595+8 </span>
596+ <!-- @text line 218 -->
597+ <span class="bl" data-hl-line="218" data-hl-el="text">
598+6 </span>
599+ <!-- @text line 218 -->
600+ <span class="bl" data-hl-line="218" data-hl-el="text">
601+4 </span>
602+ <!-- @text line 218 -->
603+ <span class="bl" data-hl-line="218" data-hl-el="text">
604+2 </span>
605+ <!-- @text line 218 -->
606+ <span class="bl" data-hl-line="218" data-hl-el="text">
607+0 </span>
608+ </div>
609+ <!-- @text line 222 -->
610+ <span class="bm" data-hl-line="222" data-hl-el="text">
611+@each … @else shows this for an empty list. </span>
612+ </section>
613+ <!-- @section line 32 -->
614+ <section class="h" data-hl-line="32" data-hl-el="section">
615+ <!-- @row line 33 -->
616+ <div class="i" data-hl-line="33" data-hl-el="row">
617+ <!-- @text line 35 -->
618+ <span class="j" data-hl-line="35" data-hl-el="text">
619+7. </span>
620+ <!-- @h2 line 36 -->
621+ <h2 class="k" data-hl-line="36" data-hl-el="h2">
622+HTML </h2>
623+ </div>
624+ <!-- @paragraph line 226 -->
625+ <p class="l" data-hl-line="226" data-hl-el="paragraph">
626+Every other element has its HTML name, and HTML attributes are written <!-- @code line 228 -->
627+ <code class="g" data-hl-line="228" data-hl-el="code">
628+key=value </code>. </p>
629+ <!-- @row line 229 -->
630+ <div class="bn" data-hl-line="229" data-hl-el="row">
631+ <!-- @form line 230 -->
632+ <form action="/subscribe" class="bo" method="post" data-hl-line="230" data-hl-el="form">
633+ <!-- @label line 231 -->
634+ <label class="bp" for="email" data-hl-line="231" data-hl-el="label">
635+Email </label>
636+ <!-- @input line 232 -->
637+ <input class="bq" id="email" type="email" name="email" placeholder="you@example.com" required data-hl-line="232" data-hl-el="input">
638+ <!-- @label line 244 -->
639+ <label class="bp" for="topic" data-hl-line="244" data-hl-el="label">
640+Topic </label>
641+ <!-- @select line 245 -->
642+ <select class="br" id="topic" name="topic" data-hl-line="245" data-hl-el="select">
643+ <!-- @option line 246 -->
644+ <option value="news" data-hl-line="246" data-hl-el="option">
645+News </option>
646+ <!-- @option line 247 -->
647+ <option value="releases" data-hl-line="247" data-hl-el="option">
648+Releases </option>
649+ </select>
650+ <!-- @button line 248 -->
651+ <button class="bs" type="submit" data-hl-line="248" data-hl-el="button">
652+Subscribe </button>
653+ </form>
654+ <!-- @el line 249 -->
655+ <div class="bt" data-hl-line="249" data-hl-el="el">
656+ <!-- @details line 250 -->
657+ <details class="bu" data-hl-line="250" data-hl-el="details">
658+ <!-- @summary line 251 -->
659+ <summary class="bv" data-hl-line="251" data-hl-el="summary">
660+What does this compile to? </summary>
661+ <!-- @paragraph line 252 -->
662+ <p class="bw" data-hl-line="252" data-hl-el="paragraph">
663+Flexbox and grid CSS in one <!-- @code line 253 -->
664+ <code class="g" data-hl-line="253" data-hl-el="code">
665+&lt;style&gt; </code>, and no JavaScript. </p>
666+ </details>
667+ <!-- @button line 254 -->
668+ <button class="bx" type="button" popovertarget="about" data-hl-line="254" data-hl-el="button">
669+Open a popover </button>
670+ <!-- @el line 255 -->
671+ <div class="by" id="about" popover data-hl-line="255" data-hl-el="el">
672+ <span>Native HTML popovers need no script.</span>
673+ </div>
674+ <!-- @row line 257 -->
675+ <div class="bz" data-hl-line="257" data-hl-el="row">
676+ <!-- @text line 258 -->
677+ <span data-hl-line="258" data-hl-el="text">
678+Progress </span>
679+ <!-- @progress line 259 -->
680+ <progress value="60" max="100" data-hl-line="259" data-hl-el="progress">
681+ </progress>
682+ <!-- @time line 260 -->
683+ <time datetime="2026-09-26" data-hl-line="260" data-hl-el="time">
684+Sept 26 </time>
685+ </div>
686+ <!-- @figure line 261 -->
687+ <figure class="b0" data-hl-line="261" data-hl-el="figure">
688+ <!-- @image line 262 -->
689+ <img src="https://picsum.photos/640/240" class="b1" alt="A placeholder landscape" data-hl-line="262" data-hl-el="image">
690+ <!-- @figcaption line 263 -->
691+ <figcaption class="a4" data-hl-line="263" data-hl-el="figcaption">
692+@figure and @figcaption </figcaption>
693+ </figure>
694+ </div>
695+ </div>
696+ </section>
697+ <hr style="border: none; border-top: 1px dashed #cbd5e1">
698+ <!-- @footer line 268 -->
699+ <footer class="b2" data-hl-line="268" data-hl-el="footer">
700+ <!-- @row line 269 -->
701+ <div class="a5" data-hl-line="269" data-hl-el="row">
702+ <!-- @text line 270 -->
703+ <span data-hl-line="270" data-hl-el="text">
704+© 2026 htmlang </span>
705+ <!-- @el line 6 -->
706+ <div class="r" data-hl-line="6" data-hl-el="el">
707+ </div>
708+ <!-- @link line 272 -->
709+ <a href="https://github.com/iko-soy/htmlang" class="b3" data-hl-line="272" data-hl-el="link">
710+Source </a>
711+ </div>
712+ </footer>
713+</div>
714+</body>
715+</html>
examples/demo.html.map+1−1
@@ -1 +1 @@
1−{"version":3,"file":"demo.html","sourceRoot":"","sources":["demo.hl"],"names":[],"mappings":"AA4DA;AAWA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAIA;AAEA;AAlCA;AACA;AAqCA;AA9CA;AACA;AACA;AACA;AAHA;AACA;AACA;AACA;AAHA;AACA;AACA;AACA;AAHA;AACA;AACA;AACA;AAKA;AACA;AAiDA;AAQA;AAKA;AAEA;AAjEA;AACA;AA6EA;AAEA;AACA;AACA;AAFA;AACA;AACA;AAFA;AACA;AACA;AAOA;AAGA;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA;AACA;AAMA;AAQA;AAQA;AApIA;AACA;AA6IA;AACA;AACA;AASA;AACA;AACA;AACA;AACA;AACA;AACA;AAIA;AACA;AASA;AAKA;AAlLA;AACA;AAqLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AApNA;AACA;AAwNA;AACA;AACA;AAKA;AAOA;AAEA;AAEA;AAEA;AA7OA;AACA;AAiPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAGA;AAKA;AACA;AAAA;AAQA;AAKA;AACA"}
1+{"version":3,"file":"demo.html","sourceRoot":"","sources":["demo.hl"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA;;AASA;;AACA;;;AACA;;;AACA;;AAEA;;AACA;;;;AA7BA;;AACA;;AAEA;;;AACA;;;;AA6BA;;AACA;;AAAA;;AACA;;;AACA;;AACA;;;;AACA;;;;AACA;;;;;AACA;;AACA;;;AAnEA;;;AAqEA;;;;AACA;;AACA;;;;AACA;;;;AACA;;;;;AACA;;AAEA;;;;AAAA;;;;AAAA;;;;AAAA;;;;AAAA;;;;AAAA;;;;;AACA;;AACA;;AACA;;;;;AACA;;;AACA;;AACA;;;;;;AAxDA;;AACA;;AAEA;;;AACA;;;;AAwDA;;AACA;;AACA;;AAAA;;AACA;;AAAA;;AACA;;;AACA;;;AAGA;;AAAA;;AACA;;;;;AACA;;;AAEA;;;;;AAxEA;;AACA;;AAEA;;;AACA;;;;AAwEA;;;AAGA;;AACA;;;;AACA;;;;AACA;;;;AACA;;;;;AACA;;AAEA;;;AAAA;;;AAAA;;;;AACA;;AACA;;AACA;;;;;AAzFA;;AACA;;AAEA;;;AACA;;;;AAqHA;;AAJA;;;AAMA;;AA7BA;;AACA;;;AACA;;;;AAFA;;AACA;;;AACA;;;;AAFA;;AACA;;;AACA;;;;;AAGA;;AACA;;AACA;;;AA/HA;;;AA2JA;;;;AACA;;AACA;;AAAA;;;AAzBA;;;;AA0BA;;AApBA;;AACA;;;;;AADA;;AACA;;;;;;;AAjHA;;AACA;;AAEA;;;AACA;;;;AAyIA;;;AAGA;;AAEA;;;;AAAA;;;;AAAA;;;;AAAA;;;;AAOA;;;;AACA;;;;;AAEA;;;;AA5JA;;AACA;;AAEA;;;AACA;;;;AAkKA;;AACA;;AACA;;AACA;;;AACA;;;AACA;;;AACA;;;;;AACA;;AAEA;;AACA;;;AACA;;;AACA;;;AACA;;;;AAJA;;AACA;;;AACA;;;AACA;;;AACA;;;;AAJA;;AACA;;;AACA;;;AACA;;;AACA;;;;;;AAKA;;AAEA;;;AAAA;;;AAAA;;;AAAA;;;AAAA;;;AAAA;;;;AAIA;;;;AA9LA;;AACA;;AAEA;;;AACA;;;;AA8LA;;AAEA;;;AACA;;AACA;;AACA;;;AACA;;AAYA;;;AACA;;AACA;;;AACA;;;;AACA;;;;AACA;;AACA;;AACA;;;AACA;;AACA;;;;AACA;;;AACA;;;;AAEA;;AACA;;;AACA;;;AACA;;;;AACA;;AACA;;AACA;;;;;;;;AAKA;;AACA;;AACA;;;AAxQA;;;AA0QA"}
examples/docs.hl+126−61
@@ -1,75 +1,140 @@
1−-- A documentation page: sidebar nav + prose content + code samples.
2−@page [lang en] Docs · Getting Started
3−@let ink #0f172a
4−@let muted #64748b
5−@let primary #3b82f6
6−@let code-bg #f1f5f9
1+-- A documentation page: a sidebar built from data, prose in htmlang and in
2+-- Markdown, code samples, a table, and a component with scoped styles.
3+@page [lang en] Getting started · htmlang docs
74
8−@let nav-link $label $href
9− @link [
10− padding 8, padding-inline 12, border-radius 6, color $ink,
11− hover:background #f1f5f9, transition all 0.1s ease
12− ] $href
13− $label
5+@let --ink #0f172a
6+@let --muted #64748b
7+@let --accent #3b82f6
8+@let --line #e5e7eb
9+@let --code-bg #f1f5f9
1410
15−@row [min-height 100vh]
16− -- Sidebar
17− @aside [
18− width 240, padding 32, padding-inline 20,
19− background #fafafa, border-right 1 solid #e5e7eb, spacing 4
20− ]
21− @text [font-weight bold, font-size 13, color $muted, text-transform uppercase, letter-spacing 1] Introduction
22− @nav-link [label Overview, href #overview]
23− @nav-link [label Installation, href #install]
24− @nav-link [label First page, href #first-page]
25− @text [
26− font-weight bold, font-size 13, color $muted, text-transform uppercase, letter-spacing 1,
27− margin-block-start 20
28− ] Guides
29− @nav-link [label Layout, href #layout]
30− @nav-link [label Styling, href #styling]
31− @nav-link [label Functions, href #functions]
11+-- The page this file renders, used to highlight it in the sidebar
12+@let current install
13+
14+@data $sections [
15+ {"title": "Introduction", "pages": [
16+ {"id": "overview", "label": "Overview"},
17+ {"id": "install", "label": "Getting started"},
18+ {"id": "first-page", "label": "Your first page"}
19+ ]},
20+ {"title": "Guides", "pages": [
21+ {"id": "layout", "label": "Layout"},
22+ {"id": "styling", "label": "Styling"},
23+ {"id": "functions", "label": "Functions"},
24+ {"id": "data", "label": "Data and loops"}
25+ ]}
26+]
3227
33− -- Prose
34− @main [padding 48, padding-inline 56, max-width 800, spacing 24]
35− @text [font-weight bold, font-size 36, color $ink] Getting started
28+-- Code samples: @raw keeps the lines exactly as written, @ and all
29+@let code-block
30+ @pre [padding 16, background var(--code-bg), border-radius 8, font-size 14, line-height 1.5, overflow-x auto]
31+ @children
3632
37− @paragraph [color $ink, line-height 1.7, font-size 17]
38− htmlang is a minimalist layout language that compiles to a single static
39− HTML file. This guide walks through the essentials in about five minutes.
33+-- A callout with its own stylesheet. The @style rules are scoped to the
34+-- function's root element; & is the root itself.
35+@let callout $kind=Note
36+ @style
37+ & { border-inline-start: 4px solid var(--accent); }
38+ .label { font-weight: 700; color: var(--accent); }
39+ @aside [padding 12 16, spacing 4, background #eff6ff, border-radius 6]
40+ @text [class=label] $kind
41+ @children
4042
41− @text [font-weight bold, font-size 24, color $ink] Installation
43+@row [min-height 100vh, color var(--ink)]
44+ -- Sidebar
45+ @aside [
46+ width 260,
47+ flex-shrink 0,
48+ padding 32 20,
49+ spacing 24,
50+ background #fafafa,
51+ border-right 1 solid var(--line),
52+ display none,
53+ md:display flex
54+ ]
55+ @link [font-weight 800, font-size 18] / htmlang
56+ @nav [spacing 20]
57+ @each $section in $sections
58+ @el [spacing 4]
59+ @text [font-size 12, font-weight 700, color var(--muted), text-transform uppercase, letter-spacing 1] $section.title
60+ @each $item in $section.pages
61+ @link [
62+ padding 6 12,
63+ border-radius 6,
64+ hover:background var(--code-bg),
65+ if($item.id == $current, background var(--code-bg)),
66+ if($item.id == $current, font-weight 600),
67+ if($item.id == $current, aria-current=page)
68+ ] /docs/$item.id $item.label
4269
43− @pre [background $code-bg, padding 16, border-radius 8, overflow-x auto]
44− cargo install --path .
70+ -- Content
71+ @main [width fill, padding 48 24, md:padding 48 56]
72+ @article [max-width 760, spacing 20, line-height 1.7]
73+ @el [spacing 8]
74+ @text [font-size 14, color var(--muted)] Introduction
75+ @h1 [font-size 40, line-height 1.2] Getting started
4576
46− @text [font-weight bold, font-size 24, color $ink] Your first page
77+ @paragraph [font-size 18]
78+ htmlang is a small layout language that compiles to static HTML. This
79+ guide takes about five minutes: install the compiler, write a page,
80+ and see it in the browser.
4781
48− @paragraph [color $ink, line-height 1.7]
49− Create a file called {@code [background $code-bg, padding 2 6, border-radius 4] hello.hl} with:
82+ @h2 [id=install, font-size 24, margin-block-start 16] Install
83+ @code-block
84+ @raw
85+ cargo install --path .
5086
51− @pre [background $code-bg, padding 16, border-radius 8, overflow-x auto]
52− @page Hello
53− @el [padding 40, spacing 16]
54− @text [font-weight bold, font-size 32] Hi there
87+ @h2 [id=first-page, font-size 24, margin-block-start 16] Your first page
88+ @paragraph
89+ Create a file called {@code hello.hl}:
90+ @code-block
91+ @raw
92+ @page Hello
93+ @el [padding 40, spacing 16]
94+ @h1 Hi there
95+ @paragraph
96+ This is {@text [font-weight bold] htmlang}.
97+ @paragraph
98+ Then start the dev server and open the page. It recompiles and reloads
99+ whenever you save; press {@kbd [padding 1 6, border 1 solid var(--line), border-radius 4, font-size 14] Ctrl+C} to stop it.
100+ @code-block
101+ @raw
102+ htmlang serve .
55103
56− @paragraph [color $ink, line-height 1.7]
57− Compile it:
104+ @callout [kind Tip]
105+ @paragraph
106+ The output is one {@code .html} file with its CSS inside and no
107+ JavaScript, so you can open it straight from disk.
58108
59− @pre [background $code-bg, padding 16, border-radius 8, overflow-x auto]
60− htmlang hello.hl
109+ @h2 [font-size 24, margin-block-start 16] Commands
110+ @table [border-collapse collapse, width 100%, font-size 15]
111+ @thead
112+ @tr [border-bottom 2 solid var(--line), text-align left]
113+ @th [padding 8] Command
114+ @th [padding 8] What it does
115+ @tbody
116+ @tr [border-bottom 1 solid var(--line)]
117+ @td [padding 8] htmlang page.hl
118+ @td [padding 8] Compile one file to page.html
119+ @tr [border-bottom 1 solid var(--line)]
120+ @td [padding 8] htmlang serve .
121+ @td [padding 8] Serve a folder with live reload
122+ @tr [border-bottom 1 solid var(--line)]
123+ @td [padding 8] htmlang build src -o dist
124+ @td [padding 8] Compile a whole site
125+ @tr
126+ @td [padding 8] htmlang check page.hl
127+ @td [padding 8] Report problems without writing output
61128
62− @paragraph [color $ink, line-height 1.7]
63− You now have {@code [background $code-bg, padding 2 6, border-radius 4] hello.html},
64− a single self-contained file with embedded CSS. No dependencies, no
65− JavaScript, no build step beyond the one command you just ran.
129+ -- Longer prose can be Markdown
130+ @markdown
131+ ## Next steps
66132
67− @text [font-weight bold, font-size 24, color $ink] Next steps
133+ - Read about **layout**: `@row`, `@el`, `spacing` and `width fill`.
134+ - Style anything with CSS properties: `[padding 20, color navy]`.
135+ - Turn repeated markup into functions with `@let`.
68136
69− @ul [spacing 8]
70− @li
71− @link [color $primary] /docs/layout Learn the layout model
72− @li
73− @link [color $primary] /docs/styling Customize styling
74− @li
75− @link [color $primary] /docs/functions Extract reusable components
137+ @row [spacing 12, padding-block-start 24, border-top 1 solid var(--line), font-weight 600]
138+ @link [color var(--accent)] /docs/overview ← Overview
139+ @spacer
140+ @link [color var(--accent)] /docs/first-page Your first page →
examples/docs.html.map+1−1
@@ -1 +1 @@
1−{"version":3,"file":"docs.html","sourceRoot":"","sources":["docs.hl"],"names":[],"mappings":"AAeA;AAEA;AAIA;AAZA;AAAA;AAAA;AAgBA;AAhBA;AAAA;AAAA;AAyBA;AACA;AAEA;AAIA;AAEA;AAGA;AAEA;AAGA;AAEA;AACA;AAEA;AAGA;AAGA;AAKA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA"}
1+{"version":3,"file":"docs.html","sourceRoot":"","sources":["docs.hl"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA;;AAEA;;AAUA;;;AACA;;AAEA;;AACA;;;AAEA;;;AAAA;;;AAAA;;;;AAHA;;AACA;;;AAEA;;;AAAA;;;AAAA;;;AAAA;;;;;;AAUA;;AACA;;AACA;;AACA;;;AACA;;;;AAEA;;;AAKA;;;AApDA;;;;AAyDA;;;AACA;;AACA;;;AA3DA;;;;;;;;AAmEA;;AAEA;;;AArEA;;;;AASA;;AACA;;;AAiEA;;AACA;;;;AAGA;;;AACA;;AACA;;AACA;;AACA;;;AACA;;;;;AACA;;AACA;;AACA;;;AACA;;;;AACA;;AACA;;;AACA;;;;AACA;;AACA;;;AACA;;;;AACA;;AACA;;;AACA;;;;;;;;;;;;;AAUA;;AACA;;;AApIA;;;AAsIA"}
examples/landing.hl+120−63
@@ -1,70 +1,127 @@
1−-- A marketing landing page: hero, feature grid, CTA.
1+-- A product landing page: navigation, hero, features, pricing and a call to action.
2+--
3+-- Shows the layout attributes (spacing, center-x, wrap, @spacer), CSS
4+-- properties with their own names, design tokens as custom properties, and
5+-- functions that take parameters, attributes and children.
26@page [lang en] Launchpad
3−@let primary #2563eb
4−@let ink #0f172a
5−@let muted #64748b
6−@let bg-subtle #f8fafc
7+@meta description Launchpad turns static pages into a product site in minutes.
8+@meta og:title Launchpad
79
8−@let feature $title $body
9− @el [padding 24, background white, border-radius 12, border 1 solid #e2e8f0, spacing 8]
10− @text [font-weight bold, font-size 18, color $ink] $title
11− @paragraph [color $muted, line-height 1.6]
12− $body
10+-- Design tokens: CSS custom properties, usable as var(--name) and $--name
11+@let --brand #2563eb
12+@let --ink #0f172a
13+@let --muted #64748b
14+@let --line #e2e8f0
15+@let --subtle #f8fafc
16+
17+-- An attribute bundle shared by both button styles
18+@let button [
19+ padding 12 22,
20+ border-radius 8,
21+ font-weight 600,
22+ transition all 0.15s ease
23+]
24+
25+-- Functions: parameters are attributes, extra attributes style the root
26+@let primary-link $href
27+ @link [$button, background var(--brand), color white, hover:background ${darken($--brand, 8)}] $href
28+ @children
29+
30+@let secondary-link $href
31+ @link [$button, border 1 solid var(--line), color var(--ink), hover:border-color var(--muted)] $href
32+ @children
33+
34+@let section-heading $title $lede
35+ @el [spacing 8, center-x, max-width 640, text-align center]
36+ @h2 [font-size 32, color var(--ink)] $title
37+ @paragraph [color var(--muted), line-height 1.6] $lede
38+
39+@data $features [
40+ {"icon": "⚡", "title": "Zero-config dev server", "body": "Live reload on every save. Point it at a folder and start writing."},
41+ {"icon": "◎", "title": "Editor support", "body": "Diagnostics, completions, hover docs, go to definition and rename."},
42+ {"icon": "▢", "title": "One file per page", "body": "Each page compiles to a single HTML file with its CSS inside. Deploy anywhere."}
43+]
44+
45+@data $plans [
46+ {"name": "Hobby", "price": "0", "note": "For personal sites", "perks": ["1 site", "Community support"]},
47+ {"name": "Team", "price": "24", "note": "For small teams", "perks": ["10 sites", "Shared components", "Email support"], "featured": true},
48+ {"name": "Company", "price": "99", "note": "For many teams", "perks": ["Unlimited sites", "SSO", "Priority support"]}
49+]
50+
51+@el [color var(--ink), dark:background #0b1220, dark:color #e2e8f0]
52+ -- Navigation
53+ @header [padding 16 24, border-bottom 1 solid var(--line)]
54+ @row [max-width 1100, center-x, spacing 24, align-items center, width 100%]
55+ @text [font-weight 800, font-size 18] Launchpad
56+ @spacer
57+ @nav
58+ @row [spacing 20, color var(--muted)]
59+ @link [hover:color var(--ink)] #features Features
60+ @link [hover:color var(--ink)] #pricing Pricing
61+ @link [hover:color var(--ink)] /docs Docs
62+ @primary-link [href /signup, padding 8 16] Sign up
1363
14−@el [spacing 0]
1564 -- Hero
16− @section [
17− padding 80,
18− padding-inline 24,
19− background $bg-subtle,
20− text-align center,
21− spacing 24
22− ]
23− @text [font-weight bold, font-size 48, color $ink] Build faster with less CSS.
24− @paragraph [max-width 560, center-x, color $muted, line-height 1.6, font-size 18]
25− Launchpad turns your static pages into a polished product in minutes.
26− Ship faster, iterate smaller, sleep at night.
27− @row [center-x, spacing 12]
28− @link [
29− padding 14, padding-inline 24, border-radius 8,
30− background $primary, color white, font-weight bold,
31− transition all 0.15s ease
32− ] /signup
33− Start free
34− @link [
35− padding 14, padding-inline 24, border-radius 8,
36− border 1 solid #e2e8f0, color $ink, font-weight bold,
37− transition all 0.15s ease
38− ] /demo
39− Watch demo
65+ @section [padding 96 24, background var(--subtle), dark:background #111a2e]
66+ @el [max-width 720, center-x, spacing 24, text-align center]
67+ @h1 [font-size 52, line-height 1.1, letter-spacing -1, md:font-size 64] Build faster with less CSS.
68+ @paragraph [font-size 19, color var(--muted), line-height 1.6]
69+ Launchpad turns your static pages into a polished product site in
70+ minutes. Write the layout, {@text [font-weight 600, color var(--ink)] ship one file}, and sleep at night.
71+ @row [center-x, spacing 12, wrap]
72+ @primary-link [href /signup] Start free
73+ @secondary-link [href /demo] Watch the demo
4074
41− -- Features
42− @section [padding 80, padding-inline 24, max-width 1100, center-x, spacing 40]
43− @el [spacing 8, text-align center]
44− @text [font-weight bold, font-size 32, color $ink] Everything you need
45− @text [color $muted] Opinionated where it matters, flexible where it counts.
46− @row [wrap, gap 20]
47− @feature [
48− title Zero-config dev server,
49− body "Live reload, asset pipeline, HTTPS, and auto-routing out of the box."
50− ]
51− @feature [
52− title First-class LSP,
53− body "Goto-definition, rename, diagnostics, hover docs, and code lens — all built in."
54− ]
55− @feature [
56− title Static by default,
57− body "Compiles to a single HTML file. Deploy anywhere, no JS runtime needed."
58− ]
75+ -- Features, from a list of records
76+ @section [id=features, padding 80 24, spacing 40]
77+ @section-heading [title Everything you need, lede Opinionated where it matters and flexible where it counts.]
78+ @row [max-width 1100, center-x, spacing 20, wrap]
79+ @each $feature in $features
80+ @article [flex 1 1 280px, padding 24, spacing 8, border 1 solid var(--line), border-radius 12]
81+ @text [font-size 24, color var(--brand)] $feature.icon
82+ @h3 [font-size 18] $feature.title
83+ @paragraph [color var(--muted), line-height 1.6] $feature.body
5984
60− -- CTA
61− @section [padding 60, padding-inline 24, background $primary, color white, text-align center, spacing 16]
62− @text [font-weight bold, font-size 28] Ready to launch?
63− @paragraph [max-width 520, center-x, line-height 1.6]
64− Join thousands of teams building their marketing sites with htmlang.
65− @link [
66− padding 14, padding-inline 28, border-radius 8,
67− background white, color $primary, font-weight bold,
68− transition all 0.15s ease
69− ] /signup
85+ -- Pricing: one plan is highlighted with if()
86+ @section [id=pricing, padding 80 24, spacing 40, background var(--subtle), dark:background #111a2e]
87+ @section-heading [title Simple pricing, lede Start free and upgrade when your team grows.]
88+ @row [max-width 1000, center-x, spacing 20, wrap, align-items stretch]
89+ @each $plan in $plans
90+ @article [
91+ flex 1 1 260px,
92+ padding 28,
93+ spacing 16,
94+ background white,
95+ dark:background #0b1220,
96+ border-radius 12,
97+ border if($plan.featured, 2 solid var(--brand), 1 solid var(--line)),
98+ if($plan.featured, box-shadow 0 12px 32px rgba(37, 99, 235, 0.15))
99+ ]
100+ @el [spacing 4]
101+ @h3 [font-size 18] $plan.name
102+ @text [color var(--muted), font-size 14] $plan.note
103+ @row [spacing 4, align-items baseline]
104+ @text [font-size 40, font-weight 800] \$$plan.price
105+ @text [color var(--muted)] / month
106+ @ul [spacing 8, flex 1]
107+ @each $perk in $plan.perks
108+ @li [before:content "✓ ", before:color var(--brand)] $perk
109+ @if $plan.featured
110+ @primary-link [href /signup?plan=$plan.name, text-align center] Choose $plan.name
111+ @else
112+ @secondary-link [href /signup?plan=$plan.name, text-align center] Choose $plan.name
113+
114+ -- Call to action
115+ @section [padding 72 24, spacing 16, background var(--brand), color white, text-align center]
116+ @h2 [font-size 30] Ready to launch?
117+ @paragraph [max-width 520, center-x, line-height 1.6, opacity 0.9]
118+ Join the teams building their marketing sites with htmlang.
119+ @link [$button, center-x, background white, color var(--brand), hover:background #eff6ff] /signup
70120 Start for free
121+
122+ @footer [padding 32 24, color var(--muted), font-size 14]
123+ @row [max-width 1100, center-x, width 100%, wrap, spacing 16]
124+ @text © 2026 Launchpad
125+ @spacer
126+ @link [hover:color var(--ink)] /privacy Privacy
127+ @link [hover:color var(--ink)] /terms Terms
examples/landing.html.map+1−1
@@ -1 +1 @@
1−{"version":3,"file":"landing.html","sourceRoot":"","sources":["landing.hl"],"names":[],"mappings":"AAcA;AAEA;AAOA;AACA;AAGA;AACA;AAMA;AAQA;AACA;AACA;AACA;AACA;AArCA;AACA;AACA;AAFA;AACA;AACA;AAFA;AACA;AACA;AAkDA;AACA;AACA;AAEA"}
1+{"version":3,"file":"landing.html","sourceRoot":"","sources":["landing.hl"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA;;AAEA;;AACA;;AACA;;;AAjDA;;;AAmDA;;AACA;;AACA;;;AACA;;;AACA;;;;;AAlCA;;;;;AAsCA;;AACA;;AACA;;;AACA;;AAEA;;;AACA;;AA5CA;;;AAIA;;;;;;AA6CA;;AAzCA;;AACA;;;AACA;;;;AAyCA;;AAEA;;AACA;;;AACA;;;AACA;;;;AAHA;;AACA;;;AACA;;;AACA;;;;AAHA;;AACA;;;AACA;;;AACA;;;;;;AAGA;;AAnDA;;AACA;;;AACA;;;;AAmDA;;AAEA;;AAUA;;AACA;;;AACA;;;;AACA;;AACA;;;AACA;;;;AACA;;AAEA;;;AAAA;;;;AA7EA;;;;AA2DA;;AAUA;;AACA;;;AACA;;;;AACA;;AACA;;;AACA;;;;AACA;;AAEA;;;AAAA;;;AAAA;;;;AAjFA;;;;AA+DA;;AAUA;;AACA;;;AACA;;;;AACA;;AACA;;;AACA;;;;AACA;;AAEA;;;AAAA;;;AAAA;;;;AA7EA;;;;;;AAoFA;;AACA;;;AACA;;;AAEA;;;;AAGA;;AACA;;AACA;;;AAtHA;;;AAwHA;;;AACA"}
examples/posts.jsonnew file+27
@@ -0,0 +1,27 @@
1+[
2+ {
3+ "slug": "tight-feedback-loops",
4+ "title": "Tight feedback loops",
5+ "date": "2026-04-16",
6+ "display_date": "April 16, 2026",
7+ "tags": ["tooling", "workflow"],
8+ "summary": "Dev-loop ergonomics beat feature counts. Here's the setup I use: a watcher, a browser, and nothing else.",
9+ "featured": true
10+ },
11+ {
12+ "slug": "why-flexbox-is-enough",
13+ "title": "Why flexbox is enough",
14+ "date": "2026-04-10",
15+ "display_date": "April 10, 2026",
16+ "tags": ["css", "layout"],
17+ "summary": "Most page layouts are rows and columns. A walk through the cases where flexbox wins, and the few where grid does."
18+ },
19+ {
20+ "slug": "first-post",
21+ "title": "First post",
22+ "date": "2026-04-02",
23+ "display_date": "April 2, 2026",
24+ "tags": ["meta"],
25+ "summary": "A short introduction: what this blog is about, why it's a handful of static files, and what comes next."
26+ }
27+]
tests/docs.rs+41−15
@@ -1,5 +1,5 @@
1−//! Compile every htmlang example in the docs, so they can't drift from the
2−//! language. A fenced block without a language tag is htmlang unless it is a
1+//! Compile every htmlang example in the docs and in `examples/`, so they
2+//! can't drift from the language. A fenced block without a language tag is htmlang unless it is a
33//! shell session (lines starting with `htmlang ` / `cargo `) or the syntax
44//! template in DESIGN.md. Blocks whose first line is `-- name.hl` are written
55//! to disk first so `@include` between them resolves.
@@ -41,6 +41,22 @@ fn is_htmlang(block: &str) -> bool {
4141 || first.starts_with("@element "))
4242}
4343
44+/// The diagnostics that make an example wrong rather than just imperfect.
45+fn problems(result: &parser::ParseResult) -> Vec<String> {
46+ result
47+ .diagnostics
48+ .iter()
49+ .filter(|d| {
50+ d.severity == Severity::Error
51+ || d.message.contains("unknown")
52+ || d.message.contains("undefined variable")
53+ || d.message.contains("is an HTML attribute")
54+ || d.message.contains("no single root")
55+ })
56+ .map(|d| format!("line {}: {}", d.line, d.message))
57+ .collect()
58+}
59+
4460fn check_doc(file: &str) {
4561 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
4662 let markdown = std::fs::read_to_string(root.join(file)).unwrap();
@@ -63,19 +79,7 @@ fn check_doc(file: &str) {
6379
6480 let mut failures = Vec::new();
6581 for block in &blocks {
66− let result = parser::parse_with_base(block, Some(&dir));
67− let problems: Vec<String> = result
68− .diagnostics
69− .iter()
70− .filter(|d| {
71− d.severity == Severity::Error
72− || d.message.contains("unknown")
73− || d.message.contains("undefined variable")
74− || d.message.contains("is an HTML attribute")
75− || d.message.contains("no single root")
76− })
77− .map(|d| format!("line {}: {}", d.line, d.message))
78− .collect();
82+ let problems = problems(&parser::parse_with_base(block, Some(&dir)));
7983 if !problems.is_empty() {
8084 failures.push(format!("{}\n=> {}", block, problems.join("\n=> ")));
8185 }
@@ -98,3 +102,25 @@ fn design_md_examples_compile() {
98102fn readme_examples_compile() {
99103 check_doc("README.md");
100104}
105+
106+#[test]
107+fn examples_compile() {
108+ let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
109+ let mut failures = Vec::new();
110+ for entry in std::fs::read_dir(&dir).unwrap() {
111+ let path = entry.unwrap().path();
112+ if path.extension().is_none_or(|e| e != "hl") {
113+ continue;
114+ }
115+ let source = std::fs::read_to_string(&path).unwrap();
116+ let problems = problems(&parser::parse_with_base(&source, Some(&dir)));
117+ if !problems.is_empty() {
118+ failures.push(format!("{}\n=> {}", path.display(), problems.join("\n=> ")));
119+ }
120+ }
121+ assert!(
122+ failures.is_empty(),
123+ "examples don't compile cleanly:\n\n{}",
124+ failures.join("\n\n")
125+ );
126+}