Chiccup

A modern HTML rendering system for CHICKEN Scheme with CSS selector integration.

Description

Chiccup is a lightweight HTML rendering library that allows you to write HTML using Lisp syntax with CSS-inspired selectors. It converts simplified Chiccup-style lists to SXML and then to HTML using sxml-transforms, providing both security through automatic HTML escaping and flexibility for advanced transformations.

Requirements

Installation

chicken-install chiccup

Basic Usage

(import chiccup)

;; Simple elements
(ccup->html `[h1 "Hello World"])
;; => <h1>Hello World</h1>

;; CSS classes and IDs
(ccup->html `[.container.mx-auto#main "Content"])
;; => <div class="container mx-auto" id="main">Content</div>

;; Attributes
(ccup->html `[a (@ (href "/page")) "Link"])
;; => <a href="/page">Link</a>

API

[procedure] (ccup->html s-html)

Convert Chiccup list to HTML string. String content is automatically HTML-sanitized to prevent XSS attacks.

;; Basic elements
(ccup->html `[div "Hello World"])
;; => <div>Hello World</div>

;; Automatic escaping for security
(ccup->html `[div "< & >"])
;; => <div>&lt; &amp; &gt;</div>

;; CSS classes and IDs - default element is "div"
;; Note: ID (#id) must come after classes
(ccup->html `[.container.mx-auto#main "Content"])
;; => <div class="container mx-auto" id="main">Content</div>
[procedure] (ccup->sxml s-html)

Convert Chiccup list to SXML for advanced processing with the sxml-transforms ecosystem.

(ccup->sxml `[div.class "content"])
;; => (div (@ (class "class")) "content")

CSS Selector Syntax

Chiccup supports CSS-style selectors in element names:

Tag Names: Any valid HTML tag name

`[h1 "Title"]        ; => <h1>Title</h1>
`[button "Click"]    ; => <button>Click</button>

Classes: Use dot notation, multiple classes allowed

`[.primary "Content"]           ; => <div class="primary">Content</div>
`[button.btn.btn-primary "OK"]  ; => <button class="btn btn-primary">OK</button>

IDs: Use hash notation (must come after classes)

`[\#header "Title"]              ; => <div id="header">Title</div>
`[.container#main "Content"]    ; => <div class="container" id="main">Content</div>

Note: if you only specify the id, you need to escape the hash symbol.

Attributes

Use the `@` syntax for explicit attributes:

;; Basic attributes
`[input (@ (type "text") (name "username"))]
;; => <input type="text" name="username">

;; Boolean attributes (no values)
`[input (@ (type "checkbox") (checked) (disabled))]
;; => <input type="checkbox" checked disabled>

;; Attribute values are automatically HTML-escaped
`[div (@ (data-config "{\"theme\":\"dark\"}")) "Content"]
;; => <div data-config="{&quot;theme&quot;:&quot;dark&quot;}">Content</div>

;; Conditional attributes
(let ((is-disabled #t))
  `[button (@ (type "submit") ,@(if is-disabled '((disabled)) '())) "Submit"])
;; => <button type="submit" disabled>Submit</button>

When both CSS selector classes and explicit `@` attributes contain classes, they are merged:

`[div.container (@ (class "active")) "Content"]
;; => <div class="container active">Content</div>

Dynamic Content

;; Lists and iteration
(let ((items '("Apple" "Banana" "Cherry")))
  `[ul ,@(map (lambda (item) `[li ,item]) items)])
;; => <ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>

;; Conditional content
(let ((logged-in? #t))
  `[div ,(if logged-in? `[p "Welcome back!"] `[a (@ (href "/login")) "Login"])])

Raw HTML Content

For unescaped HTML content, use the `raw` marker:

`[div (raw "<em>emphasized</em>")]
;; => <div><em>emphasized</em></div>

;; Compare with normal escaped content:
`[div "<em>emphasized</em>"]
;; => <div>&lt;em&gt;emphasized&lt;/em&gt;</div>

Void Elements

Chiccup automatically handles HTML void elements (self-closing tags):

`[br]           ; => <br>
`[hr]           ; => <hr>
`[img (@ (src "/logo.png") (alt "Logo"))]  ; => <img src="/logo.png" alt="Logo">

Supported void elements: area, base, br, col, embed, hr, img, input, link, meta, param, source, track, wbr

Configuration Parameters

[parameter] (ccup/doctype)

Controls the DOCTYPE declaration for HTML documents (default: "<!doctype html>").

;; Only applied when root element is 'html
(ccup->html `[html [body "Hello"]])
;; => <!doctype html><html><body>Hello</body></html>

;; Custom doctype
(parameterize ((ccup/doctype "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0//EN\">"))
  (ccup->html `[html [body "Hello"]]))
[parameter] (ccup/raw-content-tags)

List of tags whose content should not be HTML-escaped (default: '(script style)).

`[script "alert('Hello');"]     ; Content not escaped
`[div "alert('Hello');"]        ; Content escaped: alert(&#x27;Hello&#x27;);

;; Add more raw content tags
(parameterize ((ccup/raw-content-tags '(script style pre)))
  (ccup->html `[pre "<code>example</code>"]))
;; => <pre><code>example</code></pre>

Complete Examples

Simple Page

(ccup->html 
  `[html
    [head
     [title "My Page"]
     [meta (@ (charset "utf-8"))]]
    [body
     [.container
      [h1 "Welcome"]
      [p "This is a simple page."]
      [a (@ (href "/about")) "Learn more"]]]])

Form with Validation

(define (render-form errors)
  `[form (@ (method "POST") (action "/submit"))
    [.form-group
     [label (@ (for "email")) "Email:"]
     [input (@ (type "email") (name "email") (id "email")
               ,@(if (member 'email errors) '((class "error")) '()))]
     ,@(if (member 'email errors)
           `([.error-msg "Please enter a valid email"])
           '())]
    [button.btn.btn-primary (@ (type "submit")) "Submit"]])

(ccup->html (render-form '(email)))

Dynamic Navigation

(define (nav-item label url current?)
  `[li ,(if current? '(class "active") '())
       [a (@ (href ,url)) ,label]])

(define (render-nav current-page)
  `[nav.navbar
    [ul.nav-list
     ,(nav-item "Home" "/" (eq? current-page 'home))
     ,(nav-item "About" "/about" (eq? current-page 'about))
     ,(nav-item "Contact" "/contact" (eq? current-page 'contact))]])

Security

Chiccup automatically escapes string content to prevent XSS attacks, similar to modern frontend frameworks like React or Vue. Only use `raw` when you trust the content or have sanitized it yourself.

;; Safe by default
`[div ,user-input]  ; Automatically escaped

;; Only when you trust the content
`[div (raw ,trusted-html)]  ; Not escaped - use with caution

Integration with SXML

Since Chiccup generates SXML, you can combine it with other SXML tools:

(import sxml-transforms)

;; Generate SXML and further process it
(let ((sxml (ccup->sxml `[article [h1 "Title"] [p "Content"]])))
  ;; Apply SXML transformations
  (SRV:send-reply (post-order sxml custom-rules)))

License

Copyright © 2025 Rolando Abarca. Released under BSD-3-Clause License.

Repository

GitHub Repository