diff --git a/README.md b/README.md index 4055a2a..b036ba3 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,20 @@ # 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 ### 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 (hsx @@ -28,21 +32,30 @@ This generates: </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 (hsx (div (p :id (format nil "id-~a" (random 100))) (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) - (p "Condition met!") - (p "Condition not met!")))) + (hsx (p "Condition met!")) + (hsx (p "Condition not met!"))))) ``` This might generate: @@ -81,22 +94,22 @@ This generates: <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 -(defcomp card (&key title children) +(defcomp ~card (&key title children) (hsx (div :class "card" (h1 title) children))) ``` -Or using a property list: +Alternatively, you can use a property list: ```lisp -(defcomp card (&rest props) +(defcomp ~card (&rest props) (hsx (div :class "card" (h1 (getf props :title)) @@ -107,7 +120,7 @@ Usage example: ```lisp (hsx - (card :title "Card Title" + (~card :title "Card Title" (p "This is a card component."))) ``` @@ -120,18 +133,6 @@ 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. diff --git a/hsx.asd b/hsx.asd index aba1850..c5ab13d 100644 --- a/hsx.asd +++ b/hsx.asd @@ -1,6 +1,6 @@ (defsystem "hsx" - :version "0.1.0" - :description "Hypertext S-expression" + :version "0.4.0" + :description "Simple and powerful HTML generation library." :author "skyizwhite, Bo Yao" :maintainer "skyizwhite <paku@skyizwhite.dev>" :license "MIT" diff --git a/src/dsl.lisp b/src/dsl.lisp index 680c85a..27d785a 100644 --- a/src/dsl.lisp +++ b/src/dsl.lisp @@ -13,21 +13,37 @@ ;;;; hsx macro (defmacro hsx (form) - "Detect built-in HSX elements and automatically import them." - (find-builtin-symbols form)) + "Detect HSX elements and automatically import them." + (detect-elements 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))))) +(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))) ;;;; defhsx macro @@ -58,16 +74,19 @@ `(eval-when (:compile-toplevel :load-toplevel :execute) (defhsx ,name ,(make-keyword name)))) -(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." +(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 (~~).")) (unless (or (null props) (member '&key props) (member '&rest props)) - (error "Component properties must be declared with either &key, &rest, or both.")) - (let ((%name (symbolicate '% name))) + (error "Component properties must be declared using &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))))) diff --git a/tests/dsl.lisp b/tests/dsl.lisp index d8bce5d..31abea8 100644 --- a/tests/dsl.lisp +++ b/tests/dsl.lisp @@ -1,67 +1,79 @@ (defpackage #:hsx-test/dsl (:use #:cl #:rove - #:hsx/dsl - #:hsx/builtin) + #:hsx/dsl) + (:import-from #: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 (div))) + (let ((elm (hsx (div)))) (ok (null (element-props elm))) (ok (null (element-children elm))))) (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") (element-props elm))) (ok (null (element-children elm))))) (testing "hsx-with-dynamic-props" (let* ((props '(:prop1 "value1" :prop2 "value2")) - (elm (div props))) + (elm (hsx (div props)))) (ok (equal props (element-props elm))) (ok (null (element-children elm))))) (testing "hsx-with-children" - (let ((elm (div - "child1" - "child2"))) + (let ((elm (hsx (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 (div :prop1 "value1" :prop2 "value2" - "child1" - "child2"))) + (let ((elm (hsx (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 (div props - "child1" - "child2"))) + (elm (hsx (div props + "child1" + "child2")))) (ok (equal props (element-props elm))) (ok (equal (list "child1" "child2") (element-children elm))))))