# fhtml — agent skill (single-file form)

Generated by skills/build.sh from skills/fhtml/SKILL.md + llms.md — do not edit.
Practice files mentioned below (references/language.md) are inlined at the end
of this file; references/examples.md ships with the full skill directory.

# FHTML

fhtml is a whitespace-based markup language that compiles 1:1 to HTML: no
closing tags, no angle brackets, bare tokens after a tag are the class list.
Files end in `.fhtml`.

**Before writing any fhtml, read `references/language.md`** — the complete
syntax reference (line shape, attributes, text, templates, components). This
file covers what that one doesn't: how to structure a project, when to factor
components, and the workflow that keeps output correct and readable.

The golden loop: **write → `fhtml fmt` → build**. Never hand-finish a file
without compiling it.

## Workflow

```sh
fhtml fmt page.fhtml                  # canonical formatting, in place
fhtml page.fhtml                      # compile to stdout (minified) — the correctness check
fhtml page.fhtml --pretty -o out.html # readable output to a file
fhtml build src/ -o dist/             # compile a directory tree
fhtml page.fhtml --data data.json     # render the template layer
fhtml page.fhtml --deny-warnings      # CI: any warning fails the build
```

- Compile errors carry `line:col` — fix and re-run until clean.
- Run `fhtml fmt` after every edit; the formatter never changes compiled
  output, so there is no reason to skip it.
- Treat warnings as errors while developing (`--deny-warnings`) — a warning
  today is a corrupted class list tomorrow.
- Template data goes in JSON files (`--data`, plus read-only `--ctx`), never
  hardcoded into the markup when the task calls for templating.

## Project structure

Keep pages and shared markup in separate files:

```
src/
  index.fhtml          # one page = one .fhtml file
  about.fhtml
  partials/
    layout.fhtml       # shared head/nav/footer defs
    card.fhtml         # one component family per file
static/
  js/menu.js           # behavior lives in .js files, not in markup
```

- **One page per `.fhtml` file.** A page that grows past ~150 lines should be
  split: move self-contained sections into partials and `include` them.
- **Shared `def`s live in partial files** under `partials/` (or `_inc/`),
  spliced with `include ./partials/card` — an include's `def`s join the
  namespace and its markup (if any) emits at the include site. Keep def-only
  partials (no top-level markup) for component libraries.
- **One component family per partial file** — `card.fhtml` holds `card` and
  its variants, not every component in the project.
- Include paths are relative to the including file; cycles and duplicate
  `def` names are errors.

## Components: when and how

- **Factor with `def` only when markup repeats.** Two or more instances of
  the same shape with only text/attribute values changing → one `def`,
  instantiated with `+name(…)`. Single-use markup stays plain — wrapping it
  in a def adds noise and tokens for nothing.
- **Parameterize every difference between the repeats** — ids, `aria-label`s,
  a `checked` flag, the highlighted tier's extra classes. Compare instances
  token by token; a difference you flatten away corrupts the output. If the
  blocks differ in *structure* (not just values), leave them plain.
- **Longest varying content goes in the `children` block**, not a parameter:
  sentences and paragraphs read better as an indented `|` block under the
  `+call` than as a giant string argument.
- **Names use underscores, never hyphens** — `def blog_post(img_src)`, not
  `def blog-post(img-src)` (`-` is minus).
- **The one quoting trap:** in a `+call`, an unquoted value is an
  *expression* — `n=3` is a number, `wide=false` a boolean, but `href=/fast`
  is an ERROR. **Every string argument must be double-quoted, including
  URLs**: `+card(href="/fast")`, even though `a(href=/fast)` is fine on a
  plain tag. Same rule for `def` parameter defaults.
- A def body sees only its parameters — it closes over nothing.
- Don't leave dead defs: every `def` you write should be called at least once.

## Classes and Tailwind

- Emit **plain Tailwind classes only**. Never emit `#!shorthand` codes — the
  shorthand codebook is write-time storage compression (`fhtml fmt
  --contract`), not an output format.
- **Never build class names from expressions** — Tailwind's scanner is
  static. `bg-{color}-100` is a compile ERROR; `{"bg-" + color}` compiles but
  warns and produces classes Tailwind will never see. Interpolate whole class
  names: `button {active ? "bg-blue-600 text-white" : "bg-gray-100"}`.
- Conditional classes need no helper — in class position, falsy results emit
  nothing (the clsx rule): `{active && 'bg-indigo-600 text-white'}` adds the
  classes or nothing; `{size || 'text-sm'}` supplies a default.
- Negation needs a space after the brace — `{ !done && 'opacity-50'}` —
  because `{!` means raw interpolation.

## Scripts and styles

`script` and `style` bodies are raw text: every line indented under the tag
emits verbatim (no `|` prefix), with no escaping and **no interpolation**.
Blank lines and relative indentation inside the body are preserved. (The
pre-0.4 `|`-line form still parses; `fhtml fmt` migrates it.)

- **Do not write inline JavaScript blocks.** Put behavior in a separate `.js`
  file and reference it: `script(src=/js/menu.js defer)`. Inline `script`
  bodies can't use template data, bloat every page they're pasted into, and
  are where markup files rot.
- Same for CSS: nontrivial styles belong in a stylesheet loaded with
  `link(rel=stylesheet href=/site.css)` (usually Tailwind's output), not in
  `style` blocks.
- The only acceptable inline block is a 1–3 line bootstrap that must run
  before paint (a theme-class toggle, an analytics snippet). Anything longer
  gets a file.
- Never wire behavior through `on*=` attributes — that's inline JS too.

## Text and content

- Short text goes in quotes at the end of the element's line: `span "Sign
  in"`. Multi-line text and text containing quotes use `|` lines.
- Text is HTML-escaped automatically — **write characters literally, never
  HTML entities**: `"Fenwick & Co."`, not `"Fenwick &amp; Co."` (the `&`
  would be double-escaped).
- Mixed inline content (text with elements mid-sentence) is written as
  sibling lines: text as `|` lines, elements as normal lines; an empty `|`
  preserves a meaningful space.
- Repeated **byte-identical** inline `<svg>` icons factor well into a def's
  body as raw `<` lines. Identical only — interpolation does not run inside
  raw lines.

## Readability

- 2-space indentation, always — `fhtml fmt` enforces it.
- Collapse single-child chains with `>`: `li > a(href=/docs) "Docs"`. Don't
  chain past one level of real content.
- Use `.` for a classless-tag div: `. flex gap-4`.
- Comment with `//` only where the *why* is non-obvious; `//!` when the
  comment should ship as an HTML comment.
- Past ~150 lines, split the file (see Project structure) before adding more.

## Reference files

- `references/language.md` — the complete syntax and template reference
  (canonical; same content the compiler's test suite pins).
- `references/examples.md` — annotated idiomatic examples: flat vs
  component-factored markup, and a multi-file partial layout.

## Quick checklist

Before calling any fhtml work done:

1. `fhtml fmt` run on every touched file?
2. `fhtml <file>` (or `fhtml build`) exits clean — with `--deny-warnings`?
3. Repeated markup factored into a `def`; single-use markup left plain?
4. Every string argument in every `+call` double-quoted (including URLs)?
5. No dead defs (each `def` called at least once)?
6. No inline `script`/`style` bodies beyond a tiny bootstrap; no `on*=`
   attributes?
7. Text written as literal characters, not HTML entities?
8. No class names built from expressions; conditionals interpolate whole
   class names?
9. Pages split into partials + `include` once they grow large?
10. Template tasks render from `--data` JSON, not hardcoded values?

---

# fhtml — reference for agents

fhtml is a whitespace-based markup language that compiles 1:1 to HTML,
designed for token-cheap generation: no closing tags, no angle brackets,
no `class="…"` wrappers. Bare tokens after a tag are the class list,
byte-for-byte — every Tailwind class works unquoted. Files end in
`.fhtml`.

Paste this file (or the sections you need) into a project's `CLAUDE.md`,
`AGENTS.md`, or `.cursorrules` to have an agent write correct fhtml.
The syntax and components sections are the exact prompts that were
benchmark-validated across multiple models (see `bench/RESULTS.md`);
`SPEC.md` is the normative definition.

## Syntax (complete)

Line shape: `tag(attrs) #id classes… "text"` — everything after the tag
is optional.

- **Indentation nests** (2 spaces per level, Python's rules). No closing tags.
- **Bare tokens are CSS classes**, copied verbatim: `p text-lg hover:bg-blue-500 w-1/2`
  → `<p class="text-lg hover:bg-blue-500 w-1/2">`. Any Tailwind class works unquoted,
  including `data-[state=open]:rotate-180` and `bg-[#0f172a]`.
- **Attributes go in parens butted against the tag** (no space): `a(href=/about target=_blank)`.
  Quote a value only if it contains spaces or parens: `img(alt="Team photo")`.
  Boolean attributes are bare: `input(type=checkbox checked)`.
- **`.` alone means `div`**: `. flex gap-4` → `<div class="flex gap-4">`.
- **`#id` as a bare token sets the id**: `nav #main-nav flex`.
- **Text is double-quoted at the end of the line**: `span "Sign in"`. It is
  HTML-escaped automatically — write characters literally, never HTML entities:
  `span "Fenwick & Co."`, not `span "Fenwick &amp; Co."` (the `&` would be
  escaped again, emitting `&amp;amp;`). Same in `|` text blocks.
- **`|` lines are text blocks** for multi-line text or text containing quotes:
  ```
  p text-sm
    | Multi-line text goes here,
    | one line per source line.
  ```
- **`>` chains a single child inline**: `li > a(href=/docs) "Docs"` →
  `<li><a href="/docs">Docs</a></li>`.
- **A line starting with `<` is raw HTML passthrough**, e.g. an inline `<svg>`:
  its whole indented subtree is emitted verbatim. Continuation lines of one raw
  element are indented 2 extra spaces.
- **`script`/`style` bodies are raw text**: every line indented under the tag
  emits verbatim — no `|` prefix, no escaping, no interpolation:
  ```
  script
    if (a < b) {
      go();
    }
  ```
  (The pre-0.4 `|`-line form still parses; `fhtml fmt` migrates it.)
- **Mixed inline content** (text with inline elements inside a sentence) is
  written as sibling lines: text as `|` lines, elements as normal lines. An
  empty `|` line preserves a meaningful space between a text line and the
  element that follows it.
- Void elements (`img`, `br`, `input`, `meta`, …) need no closing.
  `doctype` → `<!DOCTYPE html>`. `//` starts a comment (not emitted);
  `//!` is a comment that IS emitted as an HTML comment.

Example:

```
div flex items-center gap-4 rounded-xl bg-white p-6 shadow-md
  img(src=/img/ava.jpg alt="Erin's avatar") size-12 rounded-full
  .
    p text-lg font-semibold text-gray-900 "Erin Lindford"
    p text-gray-500 "Product Engineer"
  button ml-auto rounded-full px-4 py-1 text-sm hover:bg-purple-600 hover:text-white "Message"
```

## Templates

`{expr}` interpolation and statements render with JSON data (`--data`);
`ctx` is a second read-only root (`--ctx`). Missing names render as
null (empty text). Expressions are a small language: literals, `. []`
access, arithmetic, comparisons, `&& || !`, ternary — not JavaScript.

```
ul divide-y
  for item, i in items
    li py-2 {i % 2 == 0 ? 'bg-gray-50' : ''} "{i + 1}. {item.title}"
  empty
    li text-gray-400 "Nothing here yet."
if user.admin
  a(href=/admin) "Admin"
elif user.name
  span "{user.name}"
else
  a(href=/login) "Sign in"
```

- **Never build class names from expressions** — Tailwind's scanner is
  static. An interpolation glued to class text (`bg-{color}-100`) is a
  compile ERROR; string concatenation (`{"bg-" + color}`) compiles but
  warns. Interpolate whole class names instead:
  `button {active ? "bg-blue-600 text-white" : "bg-gray-100"}`.
- Conditional classes need no helper: in class position, booleans and
  falsy results emit no classes (the clsx rule), so
  `{active && 'bg-indigo-600 text-white'}` adds the classes or nothing,
  and `{size || 'text-sm'}` supplies a default. Negation needs a space —
  `{ !done && 'opacity-50'}` — because `{!` means raw interpolation.
- Inside `{…}`, string literals take single or double quotes:
  `{done ? 'Yes' : "No"}`.
- A literal `{` in text is escaped `\{`.

## Components

When the same markup shape repeats with only the text or attribute values
changing, factor it once with `def` and instantiate it with `+name(…)`:

```
def feature_card(title href badge_text=null)
  li rounded-xl bg-white p-6 shadow
    h3 text-lg font-semibold > a(href={href}) "{title}"
    if badge_text
      span rounded-full bg-indigo-50 px-2 text-xs "{badge_text}"
    p mt-2 text-sm text-gray-600
      children

ul grid grid-cols-3 gap-6
  +feature_card(title="Fast" href="/fast" badge_text="New")
    | Ships in milliseconds.
  +feature_card(title="Safe" href="/safe")
    | Every change is previewed.
```

- **Names use underscores, never hyphens.** Component and parameter names are
  expression identifiers (letters, digits, `_`); `-` is minus. `def
  blog-post(img-src)` is an ERROR — write `def blog_post(img_src)`.
- **`def name(param param=default)`** — top level only. The body sees ONLY its
  parameters (interpolate them: `{title}`); it cannot see other variables.
  Defaults follow call-argument quoting: `variant="emerald"`, not
  `variant=emerald` (that's a variable reference, which is null).
- **`+name(args)` instantiates.** Arguments are named-only. **Quoting differs
  from tag attributes — this is the one trap:** in a call, an unquoted value
  is an *expression*, not a string. `n=3` is the number 3, `wide=false` is a
  boolean, but `title=Fast` and `href=/fast` are ERRORS. **Every string
  argument must be double-quoted**, including URLs: `href="/fast"` (even
  though `a(href=/fast)` on a plain tag is fine).
  - A bare argument name means `true`: `+card(compact)`.
- **`children`** in the body marks where the caller's indented block goes.
  Put the longest varying content (a sentence, a paragraph) in the block
  instead of a parameter. A block is only allowed if the def uses `children`.
- A parameter without a default is required at every call.
- **Parameterize EVERY difference between the repeats** — ids, `aria-label`s,
  a `checked` flag, the selected item's extra classes. Compare the instances
  token by token; a difference you flatten away corrupts the output. If the
  blocks differ in structure, leave them plain.
- Byte-identical repeated `<svg>` icons factor well: put the raw `<svg …>`
  lines inside a def's body. Identical ONLY — interpolation does not run
  inside raw `<` lines, so a `{path_d}` there stays literal text.
- A text-only child line starts with `|`. A bare quoted line is an ERROR —
  quotes only attach text to an element's own line.
- Markup that does **not** repeat stays plain — never wrap single-use markup
  in a def; that costs tokens instead of saving them.
- `include ./partials/head` splices another file: its `def`s join the
  namespace, its markup emits at the include site. Paths are relative to
  the including file; cycles and `def` collisions are errors.

## Toolchain

The intended loop is **write → `fhtml fmt` → build**. Compile errors
carry `line:col`; fix and re-run.

```sh
fhtml page.fhtml                     # compile to stdout (minified)
fhtml page.fhtml --data data.json    # render the template layer
fhtml build src/ -o dist/            # compile a directory tree
fhtml fmt src/                       # canonical formatting, in place
fhtml page.fhtml --deny-warnings     # CI: any warning fails the build
fhtml build src/ -o dist --target=js # ES modules: (data, ctx={}) => string
```

- Always generate plain Tailwind classes. Never emit `#!shorthand`
  codes — the shorthand codebook is a write-time storage compression
  (`fhtml fmt --contract`), not an output format.
- From JavaScript: `npm install @fhtml/core` (WebAssembly, runs on
  Node/Bun/Deno/Workers/browsers) — `render`, `compileToJs`, `format`,
  `analyze`; on Node, `@fhtml/core/node` exports `renderFile` and
  friends, plus `compileFilesToDir` (batch-compile views into a
  directory of ES modules + an index registry, with atomic writes —
  use it instead of hand-writing a wipe-then-write build script);
  `@fhtml/core/express` is an Express view engine, `@fhtml/core/hono`
  a Hono renderer middleware. Vite: `vite-plugin-fhtml` imports
  `.fhtml` files as render functions.
- Editor support: `fhtml lsp` (diagnostics, formatting, go-to-def,
  completion) with a VS Code extension under `editors/vscode/`.
