Pludeck is a small Common Lisp library offering a friendly interface for creating a Plump DOM.
It depends only on Plump (Artistic License 2.0)
Once Pludeck is in a Quicklisp dist, it can be loaded without any further action:
(ql:quickload :pludeck)
If not, you can clone this repository into quicklisp/local-projects
. You may
need to call (ql:register-local-projects)
, after which you can use the same
form as above.
;;; This example generates a document in an imaginary format.
(<>root
(<?xml "version" "1.0")
(<!-- "This is a Pludeck example")
(<> ("document" "markupSyntax" "imaginary" :encoding "utf-8")
(<> "title" "Fixnums are cool")
(<> "blocks"
(<> "paragraph"
"The maximum value of a fixnum on this system is "
most-positive-fixnum
". That's a lot!")
(<> ("image" :src "https://example.com/woah.png")))))
Pludeck exports a number of macros, all of which have names starting with <
.
Some represent specific XML elements you could find in an XML document, in
which case the name is made to look like the opening of such an element. This
includes <?xml
and <!--
, used above. Other macros are more general or
abstract, in which case the name starts with <>
. This includes <>root
and
the general element construction macro <>
.
To insert a text node into the DOM, the macro <>text
can be used. It calls
PRINC-TO-STRING
on its argument. You may notice the above example does not
include any invocations of <>text
, which is because Pludeck will
automatically wrap it around certain expressions in the body of an element. The
rule for this is very simple: If it's not a literal list, it gets wrapped in
<>text
. This replacement does not recurse into the tree. This rule allows you
to insert literal values and variables directly into the tree, without
explicitly marking them as text.
Pludeck also offers a way to define specific macros for the XML elements you want to use. These can help clean up your code if you use the same elements often.
;;; This is the same example, but with a macro for each type of element.
(<>define <document "document" :attributes t :content t)
(<>define <title> "title" :content t)
(<>define <blocks> "blocks" :content t)
(<>define <paragraph> "paragraph" :content t)
(<>define <image "image" :attributes t)
(<>root
(<?xml "version" "1.0")
(<!-- "This is a Pludeck example")
(<document ("markupSyntax" "imaginary" :encoding "utf-8")
(<title> "Fixnums are cool")
(<blocks>
(<paragraph>
"The maximum value of a fixnum on this system is "
most-positive-fixnum
". That's a lot!")
(<image :src "https://example.com/woah.png"))))
The Common Lisp ecosystem has various different solutions for turning S-expressions into HTML. Most notable is probably the CL-WHO family, but there is also YACLML (syntax description).
The CL-WHO syntax requires a tradeoff between being aggressive by assuming
anything in the body that looks like (:tag)
is meant to be a tag (which
Spinneret does), and requiring any tags nested inside other code
to be explicitly wrapped in some form (which CL-WHO does with its
HTM
). Both of these require some level of code walking.
The fact that XML is case-sensitive, unlike HTML, means a CL-WHO-like approach
would need to either support ("Tags" :like "this")
, or require users to use
escaped symbols, (:|Tags| :like "this")
. This makes the aggressive approach
more overbearing than it already is, and can turn even the conservative
approach into a bit of a mess.
The YACLML solution is a bit nicer, since each tag is simply a macro.
It uses a package named <
to give tags a distinct look, which also helps make
things clearer. Pludeck looks more like YACLML than like CL-WHO, though it
doesn't include a package pre-filled with HTML tags. Instead it uses symbol
names starting with <
.
At the top of a DOM tree you need a DOM root:
(defmacro <>root (&body body)
"Bind a new Plump DOM root as the parent for enclosed elements.
Note that this binding is dynamic, meaning an element constructed inside a
function called from the body will also have this DOM root as its parent.
Any non-list in the body (non-recursive) is wrapped in a call to <>TEXT.
Returns the constructed DOM root."
...)
You can use any DOM element as a parent:
(defmacro <>wrap (parent &body body)
"Bind the given element as the parent for enclosed elements.
Note that this binding is dynamic, meaning an element constructed inside a
function called from the body will also have this DOM root as its parent.
Any non-list in the body (non-recursive) is wrapped in a call to <>TEXT.
Returns the given parent element."
...)
Constructing arbitrary DOM elements is easy with <>
:
(defmacro <> (tag/attributes &body body)
"Create a new element, with the closest enclosing element as the parent. This
element will in turn serve as the parent for any elements constructed in its
body.
Note that this binding is dynamic, meaning an element constructed inside a
function called from the body will also have this DOM root as its parent.
TAG/ATTRIBUTES can either be a string, which will be used as the tag name, or a
list. If it is a list, the first item should be a string that serves as the tag
name, and the rest of the items form a plist of attribute names and values.
Both the names and the values are evaluated. If a name evaluates to a symbol,
its lowercased name is used as the attribute name. In all other cases the value
of (PRINC-TO-STRING name) is used. The values are also passed to
PRINC-TO-STRING.
Any non-list in the body (non-recursive) is wrapped in a call to <>TEXT.
Returns the constructed element."
...)
Text nodes can be constructed using <>text
, though this call can often be
implicit:
(defmacro <>text (content)
"Create a new text node with the closest enclosing element as the parent.
CONTENT is passed to PRINC-TO-STRING.
Returns the constructed text node."
...)
Plump also has the construct of a fulltext node, used for <style>
and
<script>
in HTML.
(defmacro <>fulltext (tag/attributes content)
"Create a new fulltext node with the closest enclosing element as the parent.
CONTENT is passed to PRINC-TO-STRING.
A fulltext node is a DOM element that contains text that is not escaped using
HTML entities. This is primarily useful for <style> and <script> in HTML.
Returns the constructed text node."
...)
There is a special macro for inserting XML header nodes:
(defmacro <?xml (&rest attributes)
"Create a new XML header with the closest enclosing element as the parent.
Attributes are handled just like in <>.
Returns the XML header node."
...)
There is a special macro for inserting comments:
(defmacro <!-- (&optional (text ""))
"Create a comment with the closest enclosing element as the parent.
Returns the comment node."
...)
There is a special macro for inserting doctype nodes:
(defmacro <!doctype (doctype)
"Create a doctype element with the closest enclosing element as the parent.
Returns the doctype node."
...)
There is a special macro for inserting CDATA nodes:
(defmacro <![cdata[ (&optional (text ""))
"Create a CDATA element with the closest enclosing element as the parent.
Returns the CDATA node."
...)
Copyright (c) 2018 Joram Schrijver
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.