# 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` → `

`. 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` → `

`. - **`#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 & Co."` (the `&` would be escaped again, emitting `&`). 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"` → `
  • Docs
  • `. - **A line starting with `<` is raw HTML passthrough**, e.g. an inline ``: 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` → ``. `//` 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 `` icons factor well: put the raw `` 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/`.