sema-html
0.1.0S-expression to HTML generation, inspired by Hiccup
sema-html
S-expression to HTML generation, inspired by Hiccup.
Install
sema pkg add sema-html
Quick start
(import "sema-html")
(html/render
(list :div {:class "container"}
(list :h1 "Hello World")
(list :p {:id "intro"} "Welcome to Sema")
(list :a {:href "https://sema-lang.com"} "Learn more")))
; => "<div class=\"container\"><h1>Hello World</h1><p id=\"intro\">Welcome to Sema</p><a href=\"https://sema-lang.com\">Learn more</a></div>"
Node shape
An element node is a list whose first element is a keyword tag, followed by an optional attribute map, followed by children:
(list :h1 "Hello") ; => <h1>Hello</h1>
(list :h1 {:class "big"} "Hello") ; => <h1 class="big">Hello</h1>
If the second element is a map it is the attributes; otherwise it is the first
child. Children may be strings (escaped), numbers, nil (renders nothing),
nested element nodes, or html/raw values.
A child list whose first element is not a keyword is a sequence of nodes
and is spliced in place — so map results drop straight in:
(html/render
(list :ul (map (fn (x) (list :li x)) (list "a" "b"))))
; => "<ul><li>a</li><li>b</li></ul>"
Attributes render in sorted key order, so output is deterministic.
API
| Function | Description |
|---|---|
(html/render node) |
Convert a node tree to an HTML string |
(html/escape s) |
Escape &, <, >, ", ' |
(html/raw s) |
Mark a string as raw HTML (no escaping) |
(html/doctype) |
Returns "<!DOCTYPE html>" |
(html/page head body) |
Full HTML5 page from head/body node lists |
html/render
(html/render (list :p "1 < 2"))
; => "<p>1 < 2</p>"
Text children and attribute values are HTML-escaped. Keyword attribute values render their name, numbers are stringified:
(html/render (list :input {:type :checkbox}))
; => "<input type=\"checkbox\">"
(html/render (list :td {:colspan 2} "x"))
; => "<td colspan=\"2\">x</td>"
Boolean attributes: #t renders the bare attribute, #f (or nil) omits it:
(html/render (list :input {:type "text" :disabled #t}))
; => "<input disabled type=\"text\">"
(html/render (list :input {:type "text" :disabled #f}))
; => "<input type=\"text\">"
Void elements (br, hr, img, input, meta, link, area, base,
col, embed, source, track, wbr) self-close and ignore children:
(html/render (list :img {:src "logo.png" :alt "Logo"}))
; => "<img alt=\"Logo\" src=\"logo.png\">"
html/escape
(html/escape "a < b & 'c'")
; => "a < b & 'c'"
Escapes &, <, >, ", and '. Single quotes are escaped as ' for
safety even though attribute values are always double-quoted.
html/raw
(html/render (list :div (html/raw "<b>bold</b>")))
; => "<div><b>bold</b></div>"
Wraps a string so html/render emits it verbatim. Internally this is a
sentinel list (list "__sema_html_raw__" s) — a user data list that happens to
have exactly that shape would be treated as raw HTML. This collision is
accepted as a known limitation; don't put that literal string in child data.
html/doctype
(html/doctype)
; => "<!DOCTYPE html>"
html/page
(html/page
(list (list :title "My Site")
(list :meta {:charset "utf-8"}))
(list (list :h1 "Welcome")
(list :p "Hello from Sema!")))
; => "<!DOCTYPE html>\n<html><head><title>My Site</title><meta charset=\"utf-8\"></head><body><h1>Welcome</h1><p>Hello from Sema!</p></body></html>"
Takes a list of head nodes and a list of body nodes; returns the full document string.
Testing
sema pkg add sema-test # once
sema tests.sema
License
MIT
| Version | Size | Published |
|---|---|---|
| 0.1.0 | 5 KB | 2026-07-07 21:28:06 |