# Canonical HAML

`haml fmt` rewrites a `.haml` source file into its **canonical form**: the
single representative chosen from every set of HAML sources that parse to
the same AST. This page declares the rules. The formatter aims to make
`fmt(fmt(x)) == fmt(x)` (idempotent) and `parse(fmt(x)) == parse(x)`
(semantics-preserving) for every input the parser accepts.

If a rule below is silent on some construct, the formatter leaves the
existing source unchanged for that construct.

## File-level rules

- Encoding is UTF-8, no BOM.
- Line endings are LF (`\n`). CR and CRLF are normalized to LF.
- The file ends with exactly one trailing newline.
- No leading blank lines.
- No trailing whitespace on any line.
- At most one consecutive blank line between siblings; runs of blank
  lines collapse to a single blank line.

## Indentation

- Two spaces per level. Tabs and odd-width indents are rejected by the
  parser (`X::IllegalIndent`); the formatter never produces them.
- A child line is indented exactly two more spaces than its parent.
- Continuation lines inside a multi-line attribute hash, HTML-style
  attribute list, or multi-line expression are indented two spaces
  beyond the line that opens the construct.

## Tag head

The canonical order of a tag head, left to right:

```
%name  .class…  #id  [obj-ref]  {hash-attrs}  (html-attrs)  trim  self-close  content
```

- `%name` is omitted when at least one shorthand (`.class` or `#id`) is
  present *and* the implicit element is `div`. `%div.foo` canonicalizes
  to `.foo`; `%span.foo` is preserved.
- Shorthand order is *all* classes first (in source order), then the id.
  Mixed shorthand like `%li#row.item.lead` is reordered to
  `%li.item.lead#row`. At most one `#id` shorthand is allowed by the
  parser; the formatter never invents a second.
- The object-reference bracket `[expr]` (and the optional prefix
  `[expr, 'prefix']`) sits between shorthand and any attribute hash.
- Hash-style `{ … }` precedes HTML-style `( … )` when both are present.
- Whitespace-trim modifiers `<`, `>`, `<>` appear immediately before the
  self-close slash; the canonical spelling is `<>` (not `><`).
- The self-close `/` appears last in the head. Void elements (`img`,
  `br`, `hr`, `input`, `meta`, `link`, `area`, `base`, `col`, `embed`,
  `param`, `source`, `track`, `wbr`) never carry an explicit `/`.

## Attributes

### Form

- Hash form `{ key: value, … }` is canonical. HTML-style `( … )`
  attribute lists are rewritten to hash form: once parsed, the two
  forms are semantically identical for the values HTML-style admits
  (string-typed only), and the parser does not preserve the source
  spelling.
- Inside `{ … }`, keys use the bareword form (`key: value`) when the
  key is a valid Raku identifier (matches `<[A..Za..z_]> \w*`). Keys
  that require quoting (containing `-`, starting with a digit, etc.)
  use the `'key' => value` rocket form.

### Spacing

- One space after the opening `{`, one space before the closing `}`
  on single-line hashes: `{ href: '/', title: 'home' }`.
- No space between key and colon: `href:` not `href :`.
- One space after every comma; no space before a comma.

### Layout

- A single-line tag head plus content fits on one source line if the
  result is at most **100 columns**. Past that, the hash spills:

  ```haml
  %a{
    href: '/about',
    title: 'About us',
  } About
  ```

- In multi-line form: each entry on its own line, indented two spaces
  beyond the opening line, with a trailing comma after every entry
  including the last.

### Order

Entry order is **preserved**, never reordered. Order is observable:

- `class:` and `id:` accumulate with shorthand and earlier splats.
- Plain keys collide last-wins; splat vs. literal order changes which
  value wins.

The formatter must not move splats (`|$expr`) past literal entries or
each other.

### Quoting

- String values use single quotes by default: `title: 'home'`.
- Switch to double quotes when the value contains a single quote, a
  newline/tab/CR escape, or uses `#{…}` / `!{…}` interpolation.
- Symbol form (`:value`) is normalized to a quoted string. The
  attribute renderer cannot distinguish the two after parsing, so
  there is no canonical spelling to preserve.

### Splats

- `|$expr` keeps its position relative to surrounding entries.
- No space between `|` and the expression.

## Embedded code

- One space follows the operator: `= expr`, `- stmt`, `!= raw`,
  `&= forced`, `~ preserve`.
- Multi-line expressions (trailing comma or unbalanced brackets) keep
  continuation lines aligned to the column of the first non-operator
  character on the opening line.

## Plain text

- A line that is purely literal text and does not begin with a HAML
  sigil (`%`, `.`, `#`, `=`, `-`, `!`, `&`, `~`, `/`, `:`, `\`, `!!!`)
  is emitted verbatim.
- A leading `\` escape is kept only when needed to disambiguate the
  first character. The formatter strips a redundant `\` (one whose
  removal still parses as plain text).

## Comments

- HTML comment lead: `/ text` with exactly one space after the slash
  on the single-line form; the block form is `/` on its own line with
  the body indented.
- Conditional comment: `/[expr]`. The bracketed expression has no
  surrounding whitespace.
- Revealed conditional: `/![expr]`.
- Silent comment: `-# text` with one space after `-#` for the inline
  form. Block silent comments place `-#` alone on a line with the
  opaque body indented.

## Doctype

- The doctype line (if present) is the first non-blank line of the
  file.
- Capitalization of named variants is preserved (`!!! Strict`,
  `!!! 1.1`).
- Exactly one space between `!!!` and any argument.

## Filters

- Filter head is `:name` flush at the parent indent.
- Body is indented two spaces beyond the filter head.
- The smallest indent across the body is **not** further normalized —
  relative indentation inside the body is part of the filter's
  contract.
- A blank line is preserved between the filter and its next sibling
  when present in source; the formatter does not insert one.

## What is *not* canonicalized

The formatter preserves all of the following exactly as the user wrote
them, because rewriting would either change semantics or destroy
information the parser cannot recover:

- Splat order relative to literal entries.
- The text of any filter body (filters are opaque to the formatter).
- The text of `-#` silent-comment bodies.
- Conditional-comment expressions inside `[ … ]`.
- Whitespace inside double-quoted strings and `#{…}` interpolations.

These constructs *are* canonicalized — the parser collapses them to a
single representation, so the formatter cannot recover the original
spelling:

- HTML-style attribute lists `( … )` collapse to hash-style `{ … }`.
- Symbol attribute values (`:value`) collapse to quoted strings.

## Idempotency

For every source `x` that parses successfully, `fmt(x)` parses to the
same AST as `x`, and `fmt(fmt(x)) == fmt(x)` byte-for-byte. The
emitter sub-tasks (`haml fmt` for tags, attributes, filters, etc.)
each carry their own idempotency tests, and the CLI integration
includes an end-to-end check that the full file round-trips.
