aether-haml-lit
v1.0.0
Published
ÆtherHAML — HAML-style .lithaml templates compiled to Lit html`…` render functions, with a Vite plugin
Maintainers
Readme
ÆtherHAML · aether-haml-lit
HAML-style templates compiled to Lit render functions.
ÆtherHAML transforms .lithaml files — an indentation-based, HAML-flavored
markup — into ES modules exporting a Lit html tagged-template render function.
Born in PteronautOS as the template engine of
its WebUI, extracted for standalone use.
Zero runtime dependencies. The compiler emits source; lit and (optionally)
vite are supplied by the consuming project.
Install
npm install aether-haml-lit litUsage
Vite
// vite.config.js
import { hamlLitPlugin } from 'aether-haml-lit/vite'
export default {
plugins: [hamlLitPlugin()]
}Any .lithaml import then resolves to a render function:
# views/card.lithaml
.card
%h2= self.title
%p!= self.bodyimport renderCard from './views/card.lithaml'
const templateResult = renderCard({ title: 'Wings', body: '<b>raw</b>' })Each compiled module is:
import { html, unsafeHTML } from 'lit'
export default (self) => html`…`Call the default export with a self object — the template's expression
scope — and feed the returned TemplateResult to Lit's render().
Direct compiler API
import { compileHamlToLit } from 'aether-haml-lit/compiler'
const js = compileHamlToLit(source, 'filename.lithaml')
// → import { html } from 'lit'\nexport default (self) => html`…`Syntax
/ comment → skipped
%tagname.class#id{attr: "v"} → <tagname class="class" id="id" attr="v">
.class → <div class="class">
#id → <div id="id">
text content → text content (escaped into the template)
= self.expr → ${self.expr} (lit-escaped)
!= self.unsafe → ${unsafeHTML(self.unsafe)}
%tag{@event: self.fn} → <tag @event="${self.fn}">
%tag{.prop: self.val} → <tag .prop="${self.val}">
%tag{?attr: self.bool} → <tag ?attr="${self.bool}">
- if self.cond → ${self.cond ? html`…` : html`…`}
- else / - elsif → branches of the nearest - if
- for item of self.list → ${self.list.map(item => html`…`)}
- for item, idx of self.list→ ${self.list.map((item, idx) => html`…`)}
- choose self.expr → ${(expr === val) ? html`…` : …}
- when val / - otherwise → branches of the nearest - choose
- var = expr → IIFE assigning the value, stored on self._var
:plain → raw text blockExpression dialect
Inside =, !=, attributes and code keywords, expressions are
CoffeeScript-friendly JavaScript:
| Haml / CoffeeScript | Emitted JS |
|---|---|
| @name | self.name |
| is, isnt | ===, !== |
| and, or, not | &&, \|\|, ! |
| "Title: #{@name}" | `Title: ${self.name}` |
| self.fn a b | self.fn(a, b) |
Semantics
=expressions are lit-escaped (defaulthtmlinterpolation).!=expressions wrap the value in Lit'sunsafeHTMLdirective; theunsafeHTMLimport is added to the module only when used.- Attribute values: quoted → static; unquoted → dynamic expression.
- Void elements (
br,hr,img,input,link,meta, …) self-close only when empty. - Loop bodies rebind
@-free identifiers to the loop variables. :coffee,:css,:javascriptfilters are recognized but emit nothing — reserved for future support; use:plainfor raw blocks.
Development
npm test # node --test, no dependencies requiredThe test suite evaluates real compiled output against a minimal Lit
stand-in (test/stub-lit.mjs) — deterministic, no DOM, no install step.
License
MIT © PteronautOS contributors
