Compare commits

..

No commits in common. "a73af8d936260585e92035827c161ba0bc3075b8" and "7ce7751900ce6eacb9264b3109402bcc17aa40b2" have entirely different histories.

4 changed files with 82 additions and 114 deletions

View file

@ -1,20 +1,16 @@
# HSX
HSX (Hypertext S-expression) is a simple and powerful HTML (Living Standard) generation library for Common Lisp.
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/).
This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/).
## Introduction
## 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.
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.
## Getting Started
### Basic Usage
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.
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.
```lisp
(hsx
@ -32,30 +28,21 @@ This generates:
</div>
```
To convert an HSX object into an HTML string, use the `render-to-string` function:
## Examples
```lisp
(render-to-string
(hsx ...))
```
### 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.
HSX allows embedding Common Lisp code directly within your HTML structure, making it easy to generate dynamic content.
```lisp
(hsx
(div
(p :id (format nil "id-~a" (random 100)))
(ul
(loop
:for i :from 1 :to 5 :collect
(hsx (li (format nil "Item ~a" i)))))
(loop :for i :from 1 :to 5 :collect (li (format nil "Item ~a" i))))
(if (> (random 10) 5)
(hsx (p "Condition met!"))
(hsx (p "Condition not met!")))))
(p "Condition met!")
(p "Condition not met!"))))
```
This might generate:
@ -94,22 +81,22 @@ This generates:
<p>Second paragraph.</p>
```
### Creating Components
## Creating Components
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.
You can define reusable components with the `defcomp` macro. Components are functions that can take keyword arguments and properties.
```lisp
(defcomp ~card (&key title children)
(defcomp card (&key title children)
(hsx
(div :class "card"
(h1 title)
children)))
```
Alternatively, you can use a property list:
Or using a property list:
```lisp
(defcomp ~card (&rest props)
(defcomp card (&rest props)
(hsx
(div :class "card"
(h1 (getf props :title))
@ -120,7 +107,7 @@ Usage example:
```lisp
(hsx
(~card :title "Card Title"
(card :title "Card Title"
(p "This is a card component.")))
```
@ -133,6 +120,18 @@ Generates:
</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
This project is licensed under the MIT License.

View file

@ -1,6 +1,6 @@
(defsystem "hsx"
:version "0.4.0"
:description "Simple and powerful HTML generation library."
:version "0.1.0"
:description "Hypertext S-expression"
:author "skyizwhite, Bo Yao"
:maintainer "skyizwhite <paku@skyizwhite.dev>"
:license "MIT"

View file

@ -13,37 +13,21 @@
;;;; hsx macro
(defmacro hsx (form)
"Detect HSX elements and automatically import them."
(detect-elements form))
"Detect built-in HSX elements and automatically import them."
(find-builtin-symbols form))
(defun detect-builtin-symbol (sym)
(multiple-value-bind (builtin-sym kind)
(find-symbol (string sym) :hsx/builtin)
(and (eq kind :external) builtin-sym)))
(defun start-with-tilde-p (sym)
(string= "~" (subseq (string sym) 0 1)))
(defun detect-component-symbol (sym)
(and (start-with-tilde-p sym) sym))
(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)))
(defun find-builtin-symbols (node)
(if (atom node)
(or (and (symbolp node)
(not (keywordp node))
(find-symbol (string node) :hsx/builtin))
node)
(cons (find-builtin-symbols (car node))
(mapcar (lambda (n)
(if (listp n)
(find-builtin-symbols n)
n))
(cdr node)))))
;;;; defhsx macro
@ -74,19 +58,16 @@
`(eval-when (:compile-toplevel :load-toplevel :execute)
(defhsx ,name ,(make-keyword name))))
(defmacro defcomp (~name props &body body)
"Define an HSX function component.
The component name must start with a tilde (~).
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 (~~)."))
(defmacro defcomp (name props &body body)
"Define a function component for use in HSX.
The props must be declared with either &key or &rest (or both).
The body must return an HSX element."
(unless (or (null props)
(member '&key props)
(member '&rest props))
(error "Component properties must be declared using &key, &rest, or both."))
(let ((%name (symbolicate '% ~name)))
(error "Component properties must be declared with either &key, &rest, or both."))
(let ((%name (symbolicate '% name)))
`(eval-when (:compile-toplevel :load-toplevel :execute)
(defun ,%name ,props
,@body)
(defhsx ,~name (fdefinition ',%name)))))
(defhsx ,name (fdefinition ',%name)))))

View file

@ -1,79 +1,67 @@
(defpackage #:hsx-test/dsl
(:use #:cl
#:rove
#:hsx/dsl)
(:import-from #:hsx/builtin)
#:hsx/dsl
#:hsx/builtin)
(:import-from #:hsx/element
#:element-props
#:element-children))
(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
(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"
(let ((elm (hsx (div))))
(let ((elm (div)))
(ok (null (element-props elm)))
(ok (null (element-children elm)))))
(testing "hsx-with-static-props"
(let ((elm (hsx (div :prop1 "value1" :prop2 "value2"))))
(let ((elm (div :prop1 "value1" :prop2 "value2")))
(ok (equal '(:prop1 "value1" :prop2 "value2")
(element-props elm)))
(ok (null (element-children elm)))))
(testing "hsx-with-dynamic-props"
(let* ((props '(:prop1 "value1" :prop2 "value2"))
(elm (hsx (div props))))
(elm (div props)))
(ok (equal props (element-props elm)))
(ok (null (element-children elm)))))
(testing "hsx-with-children"
(let ((elm (hsx (div
"child1"
"child2"))))
(let ((elm (div
"child1"
"child2")))
(ok (null (element-props elm)))
(ok (equal (list "child1" "child2") (element-children elm)))))
(testing "hsx-with-static-props-and-children"
(let ((elm (hsx (div :prop1 "value1" :prop2 "value2"
"child1"
"child2"))))
(let ((elm (div :prop1 "value1" :prop2 "value2"
"child1"
"child2")))
(ok (equal '(:prop1 "value1" :prop2 "value2")
(element-props elm)))
(ok (equal (list "child1" "child2") (element-children elm)))))
(testing "hsx-with-dynamic-props-and-children"
(let* ((props '(:prop1 "value1" :prop2 "value2"))
(elm (hsx (div props
"child1"
"child2"))))
(elm (div props
"child1"
"child2")))
(ok (equal props (element-props elm)))
(ok (equal (list "child1" "child2") (element-children elm))))))