sema-html

0.1.0

S-expression to HTML generation, inspired by Hiccup

$ sema pkg install sema-html
Readme Versions 1 Dependencies 0

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 &lt; 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 &lt; b &amp; &#39;c&#39;"

Escapes &, <, >, ", and '. Single quotes are escaped as &#39; 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

VersionSizePublished
0.1.0 5 KB 2026-07-07 21:28:06
No dependencies