# CoffeeHaml Grammar

## Notation

```
Rule         := Alternative1 | Alternative2
X?            := zero or one X
X*            := zero or more X
X+            := one or more X
X (sep Y)*    := X optionally followed by repeated (Y X)
'literal'      := literal token
[class]        := character class
[^class]       := negated character class
```

Whitespace is **significant**: indentation determines nesting. The lexer
emits `INDENT`/`DEDENT` tokens analogous to Python/CoffeeScript.

---

## Top-Level Production

```
Document       := (Node | BlankLine)* EOF

BlankLine      := Whitespace* Newline

Node           := Doctype
               | Comment
               | Filter
               | ControlFlow
               | Output
               | Element
               | ImplicitDiv
               | RawText
```

---

## Doctype

```
Doctype        := '!!!' Whitespace? DoctypeValue? Newline
DoctypeValue   := [^\n]+
```

Examples: `!!!`, `!!! 5`, `!!! html`

Emits nothing in React output (React renders into a DOM container, not a full
document). Retained for Haml compatibility; may emit an HTML comment in
non-React backends.

---

## Comments

```
Comment        := HamlComment | HTMLComment

HamlComment    := '-#' Whitespace? CommentText? Newline
                  (Indent Node* Dedent)?
                // Removed entirely from output

HTMLComment    := '/' Whitespace? CommentText? Newline
                  (Indent Node* Dedent)?
                // Emitted as <!-- ... --> in development
```

Haml comments (`-#`) are stripped. HTML comments (`/`) are preserved in the
AST and emitted as `<!-- ... -->` (wrapped in a raw HTML insertion at
runtime).

---

## Elements

```
Element        := '%' TagName TagModifiers? AttributeBlock? InlineContent? SelfClose? Newline
                  (Indent Node* Dedent)?

SelfClose      := '/' Whitespace?    // preceding newline
                // %br/  →  <br />

TagName        := Identifier (':' Identifier)*
                // Allows SVG: %svg:circle, %math:mi

TagModifiers   := (ClassModifier | IdModifier)+
ClassModifier  := '.' ClassName
IdModifier     := '#' IdName

ClassName      := Identifier
IdName         := Identifier

Identifier     := [a-zA-Z_$] [a-zA-Z0-9_$]*
```

### Implicit Div

When a line begins with `.class` or `#id` without a `%tag`, a `<div>` is
implied — exactly as in Haml:

```
ImplicitDiv    := TagModifiers AttributeBlock? InlineContent? Newline
                  (Indent Node* Dedent)?
```

```
.container
  %h1 Hello
```

Is equivalent to:

```
%div.container
  %h1 Hello
```

---

## Attributes

```
AttributeBlock := AttrBraces | AttrParens

AttrBraces     := '{' AttrContent? '}'
AttrParens     := '(' AttrContent? ')'

AttrContent    := Arbitrary CoffeeScript until matching delimiter.
                // Parsed as a CoffeeScript object literal.
                // Comma-optional, supports splats, nested objects,
                // function expressions, destructuring.
```

The content between `{...}` or `(...)` is extracted verbatim and parsed
by the CoffeeScript compiler as an object literal. This means:

```haml
%div{class: "container", id: if active then "on" else "off"}
%Button{onClick: (e) -> handleClick(e), disabled: !ready}
%Panel{
  style: {color: "red", margin: 10}
  "aria-label": "Close"
}
```

All CoffeeScript expression forms are valid inside attribute values:
strings, numbers, bools, functions (`->`), conditionals, existential
operator (`?.`), destructuring, splats (`...`).

---

## Inline Content

```
InlineContent  := Whitespace InlineText? OutputSuffix?

InlineText     := ( [^\n%=] [^\n%]* )?
                // Text up to newline, '%', or '='

OutputSuffix   := '=' Whitespace? Expression
                // Mixed text + output on same line:
                // %p Hello, = user.name
```

Inline content follows the tag and attributes on the same line:

```haml
%h1 Welcome, = user.name
%p This is a paragraph with inline content.
%span.status{class: status} = statusText
```

---

## Output

```
Output         := '='  Whitespace? Expression Newline   // escaped
               | '==' Whitespace? Expression Newline   // unescaped (raw)
```

`=` inserts the result of a CoffeeScript expression as a child node.
`==` inserts without HTML escaping (for raw HTML — use cautiously).

```haml
%h1
  = pageTitle
%div
  = formatMarkdown(content)
  == user.bioHTML
```

---

## Control Flow

```
ControlFlow    := '-' Whitespace? ControlExpression

ControlExpression := IfChain
                  | UnlessBlock
                  | ForBlock
                  | WhileBlock
                  | CoffeeStatement

IfChain        := IfBlock (ElseIfBlock | ElseBlock)*

IfBlock        := 'if' Whitespace Expression Newline
                  (Indent Node* Dedent)?
UnlessBlock    := 'unless' Whitespace Expression Newline
                  (Indent Node* Dedent)?
ElseIfBlock    := 'else' Whitespace 'if' Whitespace Expression Newline
                  (Indent Node* Dedent)?
ElseBlock      := 'else' Newline
                  (Indent Node* Dedent)?

ForBlock       := 'for' Whitespace ForClause Newline
                  (Indent Node* Dedent)?
ForClause      := ForIn | ForOf
ForIn          := Pattern 'in' Expression
ForOf          := Pattern 'of' Expression
Pattern        := Identifier (',' Identifier)*
               | '[' Pattern (',' Pattern)* ']'
               | '{' Pattern (',' Pattern)* '}'

WhileBlock     := 'while' Whitespace Expression Newline
                  (Indent Node* Dedent)?

CoffeeStatement := Any valid CoffeeScript statement.
                 // Arbitrary CoffeeScript code inserted verbatim.
```

Control flow bodies contain CoffeeHaml nodes (elements, output, nested
control flow) and compile to JavaScript that produces child arrays.

```haml
- if user
  %WelcomeBanner{user: user}
- else
  %LoginPrompt

- for item, index in items
  %ItemCard{item: item, key: index}

- unless loading
  %Content{data: data}
```

### Semantics

Control flow bodies compile to expressions that evaluate to arrays of
React elements:

```
- for x in xs          →  ...xs.map((x) => jsx(...))
- if cond              →  ...(cond ? [jsx(...)] : [])
```

---

## Filters

```
Filter         := ':' FilterName Newline
                  (Indent FilterContent Dedent)?

FilterName     := Identifier
FilterContent  := Lines of text at increased indentation.
```

Haml-style filters for embedded content. Common filters:

| Filter | Purpose |
|--------|---------|
| `:css` | Embedded CSS block |
| `:javascript` | Embedded JS block |
| `:coffeescript` | Embedded CoffeeScript block |
| `:markdown` | Markdown content → compiled at build time |
| `:plain` | Pass-through text (no processing) |

```haml
%head
  :css
    body { margin: 0; }
  :javascript
    console.log("loaded");
```

Filters are compiled at build time. `:css` may be extracted or inlined
depending on configuration.

---

## CoffeeScript Blocks (`---`)

```
CoffeeBlock    := '---' Newline
                  CoffeeBody
                  '---' Newline

CoffeeBody     := Raw CoffeeScript lines, dedented to column 0.
```

The `---` fence is the multiline counterpart to `-` (statement). The body
between the two `---` lines is taken verbatim (dedented by its minimum
indentation) and compiled to executable JavaScript via the CoffeeScript
bridge. The resulting statements run at **render time**, produce no markup,
and behave exactly like a block of `-` lines.

```haml
---
total = price * quantity
tax = total * 0.08
---
%p= total + tax
```

Semantics:

- **Escapes indentation**: body lines are dedented to column 0, so a block
  nested under an element still writes CoffeeScript flush-left.
- **Render-time**: the compiled statements run on every render, before the
  JSX at their position. In a wrapped component they are hoisted into the
  render expression alongside `-` statements; with `wrap: 'none'` they emit
  in place.
- **No markup**: the block contributes nothing to the render tree.

Note: `---` is a CoffeeHaml fence marker, not an element. For a horizontal
rule use `%hr` — mapping `---` to `<hr>` is a Markdown/YAML convention, not
Haml.

---

## Yield Blocks (`===`)

```
CoffeeYield     := '===' Newline
                   CoffeeBody
                   '===' Newline
```

The `===` fence is the multiline counterpart to `=` (output): its body is
CoffeeScript, dedented to column 0 exactly like a `---` block, but instead
of running as statements its **final value is yielded as content**
at that position in the render tree (escaped, matching `=`).

```haml
%p
  ===
  name = computeName()
  "Hello, #{name}"
  ===
```

A single-expression body compiles directly; a multi-statement body is
wrapped in an IIFE so the last expression is the yielded value.

---

## Preamble Blocks (`~~~`)

```
CoffeePreamble  := '~~~' Newline
                   CoffeeBody
                   '~~~' Newline
```

The `~~~` fence holds module-scope code. Its body is CoffeeScript, dedented
to column 0, and compiled to statements hoisted to **module scope** — outside
any component wrapper. This is where imports, helper functions and one-time
setup code live (the once-at-import region).

```haml
~~~
import { useState } from 'react'
formatPrice = (n) -> "$#{n.toFixed 2}"
~~~
%p= formatPrice total
```

Semantics:

- **Module scope**: the compiled statements run once at module load, before
  any JSX. `import`/`export` stay valid (they are not wrapped in a function).
- **No markup**: the block contributes nothing to the render tree.
- **Runs once**: unlike `---` (render-time), the body does not rerun per
  render — ideal for imports, constants and pure helpers.

---

## Raw Text / Passthrough

```
RawText        := [^%#\-=/:!. \t\n] [^\n]* Newline
               // Any line not matching a recognized pattern.
               // Treated as literal text (wrapped in a text node).
```

Lines that don't start with a special character (`%`, `.`, `#`, `-`, `=`,
`/`, `:`, `!`) are treated as raw text. This enables "plain text" regions
within templates.

---

## Expression Reference

All `Expression` productions delegate to the CoffeeScript parser. Any
valid CoffeeScript expression is accepted — the CoffeeHaml parser does not
re-invent CoffeeScript's expression grammar.

Key CoffeeScript expression forms used in CoffeeHaml:

```
// Functions
->                   // zero-arg arrow
(x) ->               // single-arg arrow
(x, y) ->            // multi-arg arrow
=>                   // bound arrow (fat arrow)

// Conditionals (expression form)
if cond then a else b
unless cond then a

// Existential
obj?.prop
obj?[key]

// Comprehensions
(x for x in arr)
(x for x in arr when x > 0)

// Splats
arr...
{obj..., newKey: val}

// String interpolation
"Hello, #{name}"

// Heregex
```

---

## Continuation (v0.4.2 / v0.5.0 / v0.6.0)

CoffeeHaml supports three forms of indented continuation, extending the
grammar beyond single-line constructs.

### Statement Continuation (`-`, v0.4.2)

```
Statement      := '-' Whitespace CodeStatement Newline
                  (Indent CodeLine+ Dedent)?

CodeLine       := [^\\n]+ Newline
                // Indented code lines concatenated to the primary statement.
                // Enables multi-line CoffeeScript inside - blocks.
```

The indented lines are joined to the primary statement with newline
separators before CoffeeScript compilation:

```haml
- x = computeValue()
  x *= multiplier        # continuation
  x += offset            # continuation
%p= x
```

Compiles as a single CoffeeScript block: `x = computeValue(); x *= multiplier; x += offset`.

### Expression Continuation (`=`, v0.5.0)

```
Output         := '=' Whitespace Expression Newline
                  (Indent TextLine+ Dedent)?

TextLine       := [^\\n]+ Newline
                // TEXT children of = outputs are joined to the expression.
```

Indented TEXT children of `=` outputs are concatenated into the expression:

```haml
%h1= "Hello, " +
  user.firstName +
  " " +
  user.lastName
```

### Arrow Continuation (`= expr ->`, v0.6.0)

```
ArrowOutput    := '=' Whitespace Expression ArrowSuffix Newline
                  (Indent Node+ Dedent)?

ArrowSuffix    := '->' | '=>' Arguments?
                // Detected after CoffeeScript expression parsing.
                // Triggers element-body mode for indented children.
```

When `=` is followed by an expression ending in a CoffeeScript arrow
(`->` or `=>`), the indented children become the arrow body — compiled
as JSX elements within the arrow function:

```haml
= items.map (item) ->
  %li{ key: item.id }
    %span= item.label
```

Compiles to:

```js
items.map(item => jsx("li", { key: item.id },
  jsx("span", null, item.label)
))
```

This works both at the module level (`emitOutput`) and inline within
elements (`emitChildToJs`), enabling functional mapping with full element
syntax in both positions.

### Continuation Precedence

1. Arrow continuation takes priority — if the expression ends with `->` or `=>`, children are treated as the arrow body.
2. Otherwise, TEXT children are joined as expression continuations (for multi-line expressions).
3. Statement continuations are always code lines, joined before CoffeeScript compilation.
/// pattern /g

// Destructuring
{a, b} = obj
[first, rest...] = arr
```

---

## Tokens (Lexer)

The lexer produces these token types:

| Token | Pattern | Notes |
|-------|---------|-------|
| `TAG` | `%[a-zA-Z_$]` | Element tag |
| `CLASS` | `\.[a-zA-Z_$]` | Class modifier |
| `ID` | `#[a-zA-Z_$]` | ID modifier |
| `ATTR_OPEN_BRACE` | `{` | Attribute block start |
| `ATTR_OPEN_PAREN` | `(` | Attribute block start (alt) |
| `ATTR_CLOSE_BRACE` | `}` | Attribute block end |
| `ATTR_CLOSE_PAREN` | `)` | Attribute block end (alt) |
| `OUTPUT` | `=` | Inline expression output |
| `OUTPUT_RAW` | `==` | Unescaped output |
| `CONTROL` | `-` | Control flow prefix |
| `COMMENT` | `-#` | Haml comment |
| `HTML_COMMENT` | `/` | HTML comment |
| `FILTER` | `:` | Filter prefix |
| `DOCTYPE` | `!!!` | Doctype declaration |
| `SELF_CLOSE` | `/` | Self-closing tag |
| `TEXT` | (fallback) | Literal text |
| `NEWLINE` | `\n`, `\r\n` | Line terminator |
| `INDENT` | (virtual) | Increased indentation |
| `DEDENT` | (virtual) | Decreased indentation |
| `WHITESPACE` | `[ \t]` | Spaces/tabs (handled by indenter) |
| `EOF` | (virtual) | End of file |

The lexer is **context-aware** after certain tokens:
- After `{` or `(`, scan for matching closing delimiter, extracting the
  contained source as a raw string for CoffeeScript parsing.
- After `=`, scan to end of line for expression source.
- After `-`, scan to end of line for control expression source.
- After `:`, scan for filter name.

---

## Indentation Rules

CoffeeHaml uses **significant indentation** exactly like Haml and
CoffeeScript:

1. The first non-blank line of a document establishes the base indent
   level (typically 0).
2. A line with **more** indentation than the current line opens a new
   nesting level → emit `INDENT`.
3. A line with **less** indentation closes one or more nesting levels →
   emit `DEDENT` for each closed level.
4. Tabs and spaces **cannot** be mixed. The indent character is detected
   from the first indented line.
5. Blank lines are ignored for indentation purposes.
6. Multi-line expressions (attribute blocks, expressions) defer the
   `NEWLINE` token until the construct is complete.

```
%div           // indent 0
  %p           // indent 2 → INDENT
    Hello      // indent 4 → INDENT
  %p           // indent 2 → DEDENT
    World      // indent 4 → INDENT
               // EOF → DEDENT, DEDENT
```

---

## Precedence of Line-Level Patterns

When the lexer encounters the start of a line (after optional whitespace),
it checks patterns in this order:

1. `!!!` → Doctype
2. `%` + identifier → Element
3. `.` + identifier → ImplicitDiv (class)
4. `#` + identifier → ImplicitDiv (id)
5. `-#` → HamlComment
6. `/` → HTMLComment (unless followed by `/` or `*`)
7. `-` → ControlFlow
8. `=` / `==` → Output
9. `:` + identifier → Filter
10. anything else → RawText