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
- CHICKEN Scheme 5.0 or later
- Dependencies: srfi-1, srfi-13, sxml-transforms
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>< & ></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="{"theme":"dark"}">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><em>emphasized</em></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('Hello'); ;; 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.