on master help sign in

A static site generator without HTML and CSS

git clone ssh://git.iko.soy/htmlang

htmlang

A small layout language that compiles to static HTML. The layout model comes from elm-ui: rows and columns, spacing between children, and elements that fill or center themselves. Styling uses CSS properties under their CSS names, and every other element uses its HTML name.

@page [lang=en, background #f8fafc] My Site
@let --brand #3b82f6

@let @card [title]
  @article [padding 20, spacing 8, border 1 solid #e5e7eb, border-radius 8, hover:border-color var(--brand)]
    @h3 $title
    @children

@el [width fill, max-width 800, center-x, padding 40, spacing 24]
  @h1 [font-size 32] Hello
  @paragraph
    Built with {@text [font-weight bold, color var(--brand)] htmlang}.
  @row [spacing 16, wrap]
    @card [title Simple]
      Layout without writing a stylesheet.
    @card [title Static]
      One HTML file, with its CSS inside.

Each page compiles to one self-contained .html file. It has no JavaScript unless you write some, and nothing is added that you didn't ask for.

The language in brief

Install

cargo install --path .

Usage

htmlang page.hl              # compile page.hl to page.html
htmlang page.hl -o out.html  # choose the output file
htmlang serve .              # dev server with live reload
htmlang build src -o dist    # compile a whole site
CommandPurpose
build <dir> [-o out] [--minify] [--strict]Compile every .hl file under a directory (into out/ by default), except libraries: files that hold only @lets. Two files whose different styles got one class name are an error (class-collision)
serve [dir|file] [-p PORT] [--open]Dev server with live reload. It serves files, so it answers GET and HEAD (other methods get 405), and reloads pages, not fragments
watch [dir|file] [-o out]Recompile on change, without a server
check <file|dir> [--format json]Report diagnostics without writing output
lint <file|dir> [--format json]Stricter checks (accessibility, nesting)
fmt <file.hl>...Format files in place
lspRun the language server over stdio

Compiling a file directly also takes -w / --watch, --dev (readable output and source maps), --strict, --partial (a fragment, without the document wrapper), --format json, -s / --serve, -p / --port and --open.

A short tour

Layout attributes place an element. Everything else is CSS, and key=value is an HTML attribute:

@row [spacing 12, align-items center, padding 12 20, border-bottom 1 solid #e5e7eb, id=toolbar]
  @text [font-weight 700] Acme
  @spacer
  @input [type=search, placeholder=Search, aria-label=Search, width 240, padding 6 10]
  @button [type=submit, padding 6 14, hover:background #f3f4f6] Go

A prefix applies a style only in a state, from a screen width up, or under a media condition. Prefixes stack, and a prefix before a [group] applies to each style in it:

@el [padding 16, md:[padding 32, font-size 20], background #3b82f6, hover:background #2563eb, dark:[background #1e3a8a, hover:background #1e40af]]
  @text [color white] Click me

An element prefix, @td:, styles every such element inside, so rows a loop writes and HTML from @markdown need no attributes of their own:

@table [border-collapse collapse, @th:[padding 8, text-align left], @td:padding 8, @tr:nth-child(even):background #f8fafc]
  @tr
    @th Name
  @tr
    @td Ada

@let defines values, bundles and functions, in one namespace. A function has @ before its name and its parameters in brackets (label, or href # with a default). It is called like an element, on its own line, in a chain or inline in text: its parameters are attributes, name value (a name alone is true), a parameter without a default must be passed, and any other attributes style its root element and are checked like attributes written there.

@let --primary #3b82f6
@let gap = 8 * 2
@let rounded [border-radius 8, overflow hidden]

@let @cta [label, href #]
  @link [$rounded, padding 10 16, background var(--primary), color white] $href
    $label
@let @key
  @kbd [padding 1 6, border 1 solid #d1d5db, border-radius 4]
    @children

@row [spacing $gap]
  @cta [label Sign up]
  @cta [label Learn more, href /about, background #64748b]
@paragraph
  Press {@key Ctrl+K} to search.

Data, loops and conditions run at compile time:

@data $links [
  {"label": "Home", "url": "/"},
  {"label": "Blog", "url": "/blog"},
  {"label": "About us", "url": "/about"}
]
@let current /blog

@nav
  @row [spacing 16]
    @each $link in $links
      @link [if($link.url == $current, [font-weight bold, aria-current=page])] $link.url $link.label

The examples/ directory has complete pages (a landing page, a blog, a docs page, and a tour of the whole language). DESIGN.md is the language reference.

Editor support

The VS Code extension in editors/vscode provides syntax highlighting, snippets, and the language server (htmlang-lsp). The server gives diagnostics, completions, hover documentation, go to definition, rename and formatting.

The compiler, the formatter and the language server read a file with the same parser, so the editor sees exactly what the compiler sees, relative @include and @data paths included. Every diagnostic has a stable code (error[unknown-element] on the command line, a code field in --format json), and the whole file is checked, including branches and functions that don't run (a $name there is checked only where the code runs, since it may come from data).