Compare commits
10 commits
7ce7751900
...
a73af8d936
Author | SHA1 | Date | |
---|---|---|---|
a73af8d936 | |||
fa7fc1605e | |||
f60259ec4a | |||
4490c74197 | |||
dfc074ec71 | |||
3193054e04 | |||
33dd8e8205 | |||
6abb647246 | |||
a170c58530 | |||
011ccd6b2a |
4 changed files with 114 additions and 82 deletions
57
README.md
57
README.md
|
@ -1,16 +1,20 @@
|
||||||
# HSX
|
# HSX
|
||||||
|
|
||||||
HSX (Hypertext S-expression) is a simple yet powerful HTML5 generation library for Common Lisp. It was forked from [flute](https://github.com/ailisp/flute/).
|
HSX (Hypertext S-expression) is a simple and powerful HTML (Living Standard) generation library for Common Lisp.
|
||||||
|
|
||||||
## Introduction
|
This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/).
|
||||||
|
|
||||||
HSX allows you to generate HTML using S-expressions, providing a more Lisp-friendly way to create web content. By using the `hsx` macro, you can define HTML elements and their attributes in a concise and readable manner.
|
## Warning
|
||||||
|
|
||||||
|
This software is still in ALPHA quality. The APIs are likely to change.
|
||||||
|
|
||||||
|
Please check the [release notes](https://github.com/skyizwhite/hsx/releases) for updates.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
Use the `hsx` macro to create HTML elements. Attributes are specified using a property list following the element name, and child elements are nested directly within.
|
Use the `hsx` macro to create HTML elements. Attributes are specified using a property list after the element name, and child elements are nested directly inside.
|
||||||
|
|
||||||
```lisp
|
```lisp
|
||||||
(hsx
|
(hsx
|
||||||
|
@ -28,21 +32,30 @@ This generates:
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
To convert an HSX object into an HTML string, use the `render-to-string` function:
|
||||||
|
|
||||||
### Dynamic Content
|
```lisp
|
||||||
|
(render-to-string
|
||||||
|
(hsx ...))
|
||||||
|
```
|
||||||
|
|
||||||
HSX allows embedding Common Lisp code directly within your HTML structure, making it easy to generate dynamic content.
|
### Embedding Content
|
||||||
|
|
||||||
|
HSX allows you to embed Common Lisp forms directly within your HTML structure.
|
||||||
|
|
||||||
|
When working with HSX elements inside embedded Lisp forms, you should use the `hsx` macro again.
|
||||||
|
|
||||||
```lisp
|
```lisp
|
||||||
(hsx
|
(hsx
|
||||||
(div
|
(div
|
||||||
(p :id (format nil "id-~a" (random 100)))
|
(p :id (format nil "id-~a" (random 100)))
|
||||||
(ul
|
(ul
|
||||||
(loop :for i :from 1 :to 5 :collect (li (format nil "Item ~a" i))))
|
(loop
|
||||||
|
:for i :from 1 :to 5 :collect
|
||||||
|
(hsx (li (format nil "Item ~a" i)))))
|
||||||
(if (> (random 10) 5)
|
(if (> (random 10) 5)
|
||||||
(p "Condition met!")
|
(hsx (p "Condition met!"))
|
||||||
(p "Condition not met!"))))
|
(hsx (p "Condition not met!")))))
|
||||||
```
|
```
|
||||||
|
|
||||||
This might generate:
|
This might generate:
|
||||||
|
@ -81,22 +94,22 @@ This generates:
|
||||||
<p>Second paragraph.</p>
|
<p>Second paragraph.</p>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Creating Components
|
### Creating Components
|
||||||
|
|
||||||
You can define reusable components with the `defcomp` macro. Components are functions that can take keyword arguments and properties.
|
You can define reusable components using the `defcomp` macro. Component names must begin with a tilde (`~`). Properties should be declared using `&key`, `&rest`, or both. The body must return an HSX element.
|
||||||
|
|
||||||
```lisp
|
```lisp
|
||||||
(defcomp card (&key title children)
|
(defcomp ~card (&key title children)
|
||||||
(hsx
|
(hsx
|
||||||
(div :class "card"
|
(div :class "card"
|
||||||
(h1 title)
|
(h1 title)
|
||||||
children)))
|
children)))
|
||||||
```
|
```
|
||||||
|
|
||||||
Or using a property list:
|
Alternatively, you can use a property list:
|
||||||
|
|
||||||
```lisp
|
```lisp
|
||||||
(defcomp card (&rest props)
|
(defcomp ~card (&rest props)
|
||||||
(hsx
|
(hsx
|
||||||
(div :class "card"
|
(div :class "card"
|
||||||
(h1 (getf props :title))
|
(h1 (getf props :title))
|
||||||
|
@ -107,7 +120,7 @@ Usage example:
|
||||||
|
|
||||||
```lisp
|
```lisp
|
||||||
(hsx
|
(hsx
|
||||||
(card :title "Card Title"
|
(~card :title "Card Title"
|
||||||
(p "This is a card component.")))
|
(p "This is a card component.")))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -120,18 +133,6 @@ Generates:
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rendering HTML
|
|
||||||
|
|
||||||
To render HSX to an HTML string, use the `render-to-string` function.
|
|
||||||
|
|
||||||
```lisp
|
|
||||||
(render-to-string
|
|
||||||
(hsx
|
|
||||||
(div :class "content"
|
|
||||||
(h1 "Rendered to String")
|
|
||||||
(p "This HTML is generated as a string."))))
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
This project is licensed under the MIT License.
|
||||||
|
|
4
hsx.asd
4
hsx.asd
|
@ -1,6 +1,6 @@
|
||||||
(defsystem "hsx"
|
(defsystem "hsx"
|
||||||
:version "0.1.0"
|
:version "0.4.0"
|
||||||
:description "Hypertext S-expression"
|
:description "Simple and powerful HTML generation library."
|
||||||
:author "skyizwhite, Bo Yao"
|
:author "skyizwhite, Bo Yao"
|
||||||
:maintainer "skyizwhite <paku@skyizwhite.dev>"
|
:maintainer "skyizwhite <paku@skyizwhite.dev>"
|
||||||
:license "MIT"
|
:license "MIT"
|
||||||
|
|
61
src/dsl.lisp
61
src/dsl.lisp
|
@ -13,21 +13,37 @@
|
||||||
;;;; hsx macro
|
;;;; hsx macro
|
||||||
|
|
||||||
(defmacro hsx (form)
|
(defmacro hsx (form)
|
||||||
"Detect built-in HSX elements and automatically import them."
|
"Detect HSX elements and automatically import them."
|
||||||
(find-builtin-symbols form))
|
(detect-elements form))
|
||||||
|
|
||||||
(defun find-builtin-symbols (node)
|
(defun detect-builtin-symbol (sym)
|
||||||
(if (atom node)
|
(multiple-value-bind (builtin-sym kind)
|
||||||
(or (and (symbolp node)
|
(find-symbol (string sym) :hsx/builtin)
|
||||||
(not (keywordp node))
|
(and (eq kind :external) builtin-sym)))
|
||||||
(find-symbol (string node) :hsx/builtin))
|
|
||||||
node)
|
(defun start-with-tilde-p (sym)
|
||||||
(cons (find-builtin-symbols (car node))
|
(string= "~" (subseq (string sym) 0 1)))
|
||||||
(mapcar (lambda (n)
|
|
||||||
(if (listp n)
|
(defun detect-component-symbol (sym)
|
||||||
(find-builtin-symbols n)
|
(and (start-with-tilde-p sym) sym))
|
||||||
n))
|
|
||||||
(cdr node)))))
|
(defun detect-elements (form)
|
||||||
|
(check-type form cons)
|
||||||
|
(let* ((head (first form))
|
||||||
|
(tail (rest form))
|
||||||
|
(well-formed-p (listp tail))
|
||||||
|
(detected-sym (and (symbolp head)
|
||||||
|
(not (keywordp head))
|
||||||
|
(or (detect-builtin-symbol head)
|
||||||
|
(detect-component-symbol head)))))
|
||||||
|
(if (and well-formed-p detected-sym)
|
||||||
|
(cons detected-sym
|
||||||
|
(mapcar (lambda (sub-form)
|
||||||
|
(if (consp sub-form)
|
||||||
|
(detect-elements sub-form)
|
||||||
|
sub-form))
|
||||||
|
tail))
|
||||||
|
form)))
|
||||||
|
|
||||||
;;;; defhsx macro
|
;;;; defhsx macro
|
||||||
|
|
||||||
|
@ -58,16 +74,19 @@
|
||||||
`(eval-when (:compile-toplevel :load-toplevel :execute)
|
`(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||||
(defhsx ,name ,(make-keyword name))))
|
(defhsx ,name ,(make-keyword name))))
|
||||||
|
|
||||||
(defmacro defcomp (name props &body body)
|
(defmacro defcomp (~name props &body body)
|
||||||
"Define a function component for use in HSX.
|
"Define an HSX function component.
|
||||||
The props must be declared with either &key or &rest (or both).
|
The component name must start with a tilde (~).
|
||||||
The body must return an HSX element."
|
Component properties must be declared using &key, &rest, or both.
|
||||||
|
The body of the component must produce a valid HSX element."
|
||||||
|
(unless (start-with-tilde-p ~name)
|
||||||
|
(error "The component name must start with a tilde (~~)."))
|
||||||
(unless (or (null props)
|
(unless (or (null props)
|
||||||
(member '&key props)
|
(member '&key props)
|
||||||
(member '&rest props))
|
(member '&rest props))
|
||||||
(error "Component properties must be declared with either &key, &rest, or both."))
|
(error "Component properties must be declared using &key, &rest, or both."))
|
||||||
(let ((%name (symbolicate '% name)))
|
(let ((%name (symbolicate '% ~name)))
|
||||||
`(eval-when (:compile-toplevel :load-toplevel :execute)
|
`(eval-when (:compile-toplevel :load-toplevel :execute)
|
||||||
(defun ,%name ,props
|
(defun ,%name ,props
|
||||||
,@body)
|
,@body)
|
||||||
(defhsx ,name (fdefinition ',%name)))))
|
(defhsx ,~name (fdefinition ',%name)))))
|
||||||
|
|
|
@ -1,67 +1,79 @@
|
||||||
(defpackage #:hsx-test/dsl
|
(defpackage #:hsx-test/dsl
|
||||||
(:use #:cl
|
(:use #:cl
|
||||||
#:rove
|
#:rove
|
||||||
#:hsx/dsl
|
#:hsx/dsl)
|
||||||
#:hsx/builtin)
|
(:import-from #:hsx/builtin)
|
||||||
(:import-from #:hsx/element
|
(:import-from #:hsx/element
|
||||||
#:element-props
|
#:element-props
|
||||||
#:element-children))
|
#:element-children))
|
||||||
(in-package #:hsx-test/dsl)
|
(in-package #:hsx-test/dsl)
|
||||||
|
|
||||||
|
(defcomp ~comp1 (&key children)
|
||||||
|
(hsx (div children)))
|
||||||
|
|
||||||
|
(deftest detect-elements-test
|
||||||
|
(testing "detect-tags"
|
||||||
|
(ok (expands '(hsx (div div div))
|
||||||
|
'(hsx/builtin:div div div)))
|
||||||
|
(ok (expands '(hsx (div (div div (div))))
|
||||||
|
'(hsx/builtin:div
|
||||||
|
(hsx/builtin:div
|
||||||
|
div
|
||||||
|
(hsx/builtin:div))))))
|
||||||
|
|
||||||
|
(testing "detect-components"
|
||||||
|
(ok (expands '(hsx (~comp1 (div)))
|
||||||
|
'(~comp1 (hsx/builtin:div)))))
|
||||||
|
|
||||||
|
(testing "ignore-malformed-form"
|
||||||
|
(ok (expands '(hsx (div . div))
|
||||||
|
'(div . div)))
|
||||||
|
(ok (expands '(hsx ((div)))
|
||||||
|
'((div)))))
|
||||||
|
|
||||||
|
(testing "ignore-cl-form"
|
||||||
|
(ok (expands '(hsx (labels ((div () "div"))
|
||||||
|
(div)))
|
||||||
|
'(labels ((div () "div"))
|
||||||
|
(div))))))
|
||||||
|
|
||||||
(deftest dsl-test
|
(deftest dsl-test
|
||||||
(testing "find-symbols"
|
|
||||||
(ok (expands
|
|
||||||
'(hsx (div '(:div "div")
|
|
||||||
div
|
|
||||||
(div
|
|
||||||
'div
|
|
||||||
(div)
|
|
||||||
:div)
|
|
||||||
"div"))
|
|
||||||
'(hsx/builtin:div '(:div "div")
|
|
||||||
div
|
|
||||||
(hsx/builtin:div
|
|
||||||
'div
|
|
||||||
(hsx/builtin:div)
|
|
||||||
:div)
|
|
||||||
"div"))))
|
|
||||||
|
|
||||||
(testing "empty-hsx"
|
(testing "empty-hsx"
|
||||||
(let ((elm (div)))
|
(let ((elm (hsx (div))))
|
||||||
(ok (null (element-props elm)))
|
(ok (null (element-props elm)))
|
||||||
(ok (null (element-children elm)))))
|
(ok (null (element-children elm)))))
|
||||||
|
|
||||||
(testing "hsx-with-static-props"
|
(testing "hsx-with-static-props"
|
||||||
(let ((elm (div :prop1 "value1" :prop2 "value2")))
|
(let ((elm (hsx (div :prop1 "value1" :prop2 "value2"))))
|
||||||
(ok (equal '(:prop1 "value1" :prop2 "value2")
|
(ok (equal '(:prop1 "value1" :prop2 "value2")
|
||||||
(element-props elm)))
|
(element-props elm)))
|
||||||
(ok (null (element-children elm)))))
|
(ok (null (element-children elm)))))
|
||||||
|
|
||||||
(testing "hsx-with-dynamic-props"
|
(testing "hsx-with-dynamic-props"
|
||||||
(let* ((props '(:prop1 "value1" :prop2 "value2"))
|
(let* ((props '(:prop1 "value1" :prop2 "value2"))
|
||||||
(elm (div props)))
|
(elm (hsx (div props))))
|
||||||
(ok (equal props (element-props elm)))
|
(ok (equal props (element-props elm)))
|
||||||
(ok (null (element-children elm)))))
|
(ok (null (element-children elm)))))
|
||||||
|
|
||||||
(testing "hsx-with-children"
|
(testing "hsx-with-children"
|
||||||
(let ((elm (div
|
(let ((elm (hsx (div
|
||||||
"child1"
|
"child1"
|
||||||
"child2")))
|
"child2"))))
|
||||||
(ok (null (element-props elm)))
|
(ok (null (element-props elm)))
|
||||||
(ok (equal (list "child1" "child2") (element-children elm)))))
|
(ok (equal (list "child1" "child2") (element-children elm)))))
|
||||||
|
|
||||||
(testing "hsx-with-static-props-and-children"
|
(testing "hsx-with-static-props-and-children"
|
||||||
(let ((elm (div :prop1 "value1" :prop2 "value2"
|
(let ((elm (hsx (div :prop1 "value1" :prop2 "value2"
|
||||||
"child1"
|
"child1"
|
||||||
"child2")))
|
"child2"))))
|
||||||
(ok (equal '(:prop1 "value1" :prop2 "value2")
|
(ok (equal '(:prop1 "value1" :prop2 "value2")
|
||||||
(element-props elm)))
|
(element-props elm)))
|
||||||
(ok (equal (list "child1" "child2") (element-children elm)))))
|
(ok (equal (list "child1" "child2") (element-children elm)))))
|
||||||
|
|
||||||
(testing "hsx-with-dynamic-props-and-children"
|
(testing "hsx-with-dynamic-props-and-children"
|
||||||
(let* ((props '(:prop1 "value1" :prop2 "value2"))
|
(let* ((props '(:prop1 "value1" :prop2 "value2"))
|
||||||
(elm (div props
|
(elm (hsx (div props
|
||||||
"child1"
|
"child1"
|
||||||
"child2")))
|
"child2"))))
|
||||||
(ok (equal props (element-props elm)))
|
(ok (equal props (element-props elm)))
|
||||||
(ok (equal (list "child1" "child2") (element-children elm))))))
|
(ok (equal (list "child1" "child2") (element-children elm))))))
|
||||||
|
|
Loading…
Reference in a new issue