aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md84
-rw-r--r--packages.tl1
-rw-r--r--specials.tl3
-rw-r--r--test/simple.tl54
-rw-r--r--who.tl56
5 files changed, 197 insertions, 1 deletions
diff --git a/README.md b/README.md
index 8ef3b5c..2f8d5f1 100644
--- a/README.md
+++ b/README.md
@@ -168,6 +168,13 @@ Here are the differences to be aware of:
a mess of the output if used; unlike CL-WHO, TL-WHO warns when they
are used in attributes.
+* TL-WHO provides a `deftag` macro for defining macro-expanding tags.
+ This is inspired by a `deftag` described in the
+ [Spinneret](https://github.com/ruricolist/spinneret) documentation.
+ Though ordinary macros can easily be used inside `with-html-output`,
+ `deftag` is an expansion mechanism which transforms the tag
+ markup, rather than embedded Lisp code. It is documented below.
+
* The CL-WHO `conc` function is missing. TXR Lisp has a function
like this, which is called `join`, and that is what is used in TL-WHO.
@@ -223,6 +230,83 @@ following issue: the CL-WHO documentation is not accurately maintained and
makes some references to material that no longer exists in CL-WHO, such as the
macro `show-html-expansion`, which was removed from CL-WHO in 2009.
+## The `deftag` macro
+
+### Syntax:
+
+ ::text
+ (deftag <keyword> <attr-param-list> <tag-body>
+ <body> ...)
+
+ <attr-param-list> ::= (<key-param>* [ . <rest-param>])
+
+ <key-param> := <symbol> | (<name> [<default> [<pres-var>]])
+
+### Description:
+
+A `deftag` macro rewrites HTML markup written in TL-WHO syntax
+into other markup.
+
+Simple example:
+
+ ::text
+ (deftag :boldface (. attrs) body
+ ^(:span :font "bold" ,*attrs ,*body))
+
+ (with-html-output-to-string (out)
+ (:boldface :id "hello-id" "Hello!"))
+
+ --> <span font='bold' id='hello-id'>Hello!</span>
+
+Complex example, adapted from Spinneret documentation:
+
+ ::text
+ (deftag :easy-input (label (name (gensym))
+ (id name) (type "text") . other-attrs) default
+ ^(progn
+ (:label :for ,name ,label)
+ (:input :name ,name :id ,id :type ,type
+ ,*other-attrs :value (progn ,*default))))
+
+Note that `progn` here isn't the Lisp `progn` operator; it is recognized
+by the `deftag` expansion mechanism as a way of producing multiple elements.
+To invoke `:easy-input`, we might do this:
+
+ ::text
+ 5> (with-html-output (*stdout* nil :indent t)
+ (:div :class "cls"
+ (:easy-input :name "foo" :id "foo-23"
+ :style "style" :label "lab" "123")))
+ <div class='cls'>
+ <label for='foo'>lab
+ </label>
+ <input name='foo' id='foo-23' type='text' style='style' value='123' />
+ </div>
+
+Note a small flexibility: the `body` argument `"123"` of `:easy-input` wasn't
+inserted as an element in the middle of the tag as in the simple example, but
+as an attribute value.
+
+Note that the `other-attrs` rest parameter of `:easy-input` received
+a list which only contained the `:style` attribute; all the others
+were captured by their respective keyword parameters.
+
+### Parameters:
+
+The `attr-param-list` is an implicit keyword parameter list
+which destructures attributes. It can specify default values for
+attributes that are not passed. Its rest parameter is bound
+to the remaining attributes that were not captured by the parameters.
+
+Each `key-param` is an ordinary TXR Lisp key parameter, implemented
+by the TXR Lisp `:key` parameter list macro. It may be just a
+name, or a name with `default` expression giving a value for
+the parameter if the corresponds keyword argument is missing,
+possibly followed by another variable name which, if present,
+will be a Boolean value indicating, if true, that the keyword
+argument was present, or if false that it was missing (and
+thus defaulted).
+
## Dependencies
TL-WHO has no external dependencies other than TXR itself.
diff --git a/packages.tl b/packages.tl
index bad0c40..0eaca09 100644
--- a/packages.tl
+++ b/packages.tl
@@ -38,6 +38,7 @@
"conc"
"convert-attributes"
"convert-tag-to-string-list"
+ "deftag"
"esc"
"escq"
"escj"
diff --git a/specials.tl b/specials.tl
index 941bdf4..e01de8d 100644
--- a/specials.tl
+++ b/specials.tl
@@ -121,5 +121,8 @@
;; behaviors revert to the CL-WHO behaviors of not escaping.
(defvar *cl-who-compat* nil)
+;; Hash table with tag macro bindings for deftag.
+(defvar *tag-macro* (hash))
+
;; Used for indentation.
(defconstant +newline+ "\n")
diff --git a/test/simple.tl b/test/simple.tl
index 6f9464d..0efad02 100644
--- a/test/simple.tl
+++ b/test/simple.tl
@@ -524,3 +524,57 @@
(contains "requires" x))
:good)))
:good)
+
+
+;;; 50
+;;; Test deftag
+(deftag :abc (foo (bar 2) . other-attrs) body
+ ^(:p :foo ,foo :bar ,bar ,*body " world!"))
+
+(test (with-html-output-to-string (out)
+ (:abc :foo 42 :bar "x" "Hello,"))
+ "<p foo='42' bar='x'>Hello, world!</p>")
+
+;;; 51
+;;; Test chained expansion, and attr keyword arg defaulting
+(deftag :xyz (. other-attrs) body
+ ^(:abc :foo 1 "xyz" ,*body))
+
+(test (with-html-output-to-string (out)
+ (:xyz "abc"))
+ "<p foo='1' bar='2'>xyzabc world!</p>")
+
+;;; 52
+;;; Test invalid return: not tag material.
+(deftag :bad (. rest) body
+ ^(notkeyword))
+
+(test (catch
+ (eval ^(with-html-output-to-string (out) (:bad)))
+ (error (x) (if (contains "non-tag" x)
+ :good)))
+ :good)
+
+;;; 53
+;;; Test deftag producing multiple consecutive tags
+(deftag :multi (. rest) body
+ ^(progn (:p) (:q) (:r)))
+
+(test (with-html-output-to-string (out)
+ (:multi))
+ "<p></p><q></q><r></r>")
+
+
+;;; 54
+;;; Complex deftag
+(deftag :easy-input (label (name (gensym))
+ (id name) (type "text") . other-attrs) default
+ ^(progn
+ (:label :for ,name ,label)
+ (:input :name ,name :id ,id :type ,type
+ ,*other-attrs :value (progn ,*default))))
+
+(test (with-html-output-to-string (out)
+ (:easy-input :name "foo" :id "foo-23" :style "style" :label "lab" "123"))
+ "<label for='foo'>lab</label> \
+ <input name='foo' id='foo-23' type='text' style='style' value='123' />")
diff --git a/who.tl b/who.tl
index b605ba2..1ce80d7 100644
--- a/who.tl
+++ b/who.tl
@@ -83,7 +83,31 @@
(if (keywordp (first rest))
(push (cons (first rest) (second rest)) attr)))
(set body (rest sexp)))))
- (convert-tag-to-string-list tag attr-list body body-fn)))
+ ;; tag macro expansion
+ (flet ((finish ()
+ (convert-tag-to-string-list tag attr-list body body-fn))
+ (alist-to-plist (al)
+ (mappend (tb ((a . b)) (list a b)) al))
+ (process-elem (elem)
+ (match-case elem
+ (@(or @(keywordp)
+ (@(keywordp) . @nil)
+ ((@(keywordp) . @nil) . @nil))
+ [process-tag elem body-fn])
+ (@else (compile-error sexp "produced non-tag form ~s"
+ else)))))
+ (iflet ((fn [*tag-macro* tag]))
+ ;; macro found
+ (let* ((attr-plist (alist-to-plist attr-list))
+ (new-tag-elem [fn body . attr-plist]))
+ (match-case new-tag-elem
+ ;; returned keyword form
+ ((progn . @stuff)
+ (append-each ((elem stuff))
+ (process-elem elem)))
+ (@else (process-elem else))))
+ ;; no macro: convert to string list
+ (finish)))))
(defun attr-warning-macrolet (form)
(with-gensyms (warn)
@@ -291,3 +315,33 @@
^(with-output-to-string (,var ,string-form)
(with-html-output (,var nil :prologue ,prologue :indent ,indent)
,*body)))
+
+(defun scrub-kw-args (key-params args)
+ (let* ((kwp (find-package "keyword"))
+ (keys [mapcar (op intern
+ (symbol-name
+ (tree-case @1
+ ((prop t) prop)
+ (prop prop)))
+ kwp)
+ key-params]))
+ (build
+ (for ((iter args)) (iter) ((set iter (cddr iter)))
+ (tree-bind (prop val . t) iter
+ (unless (memq prop keys)
+ (add prop val)))))))
+
+(defmacro deftag (:form f keyword attr-param-list tag-body . body)
+ (unless (keywordp keyword)
+ (compile-error f "~s argument must be a keyword" keyword))
+ (let ((rest-param (if (consp attr-param-list)
+ (last attr-param-list 0)
+ attr-param-list))
+ (key-params (if (consp attr-param-list)
+ (butlast attr-param-list 0))))
+ ^(set [*tag-macro* ,keyword]
+ (lambda (:key ,tag-body -- ,*attr-param-list)
+ ,*(if (and rest-param key-params)
+ ^((set ,rest-param
+ (scrub-kw-args ',key-params ,rest-param))))
+ ,*body))))