├── .gitattributes ├── LICENSE ├── README.md ├── attr-processors.lisp ├── clip-logo.png ├── clip.asd ├── clipboard.lisp ├── conditions.lisp ├── docs ├── clip-logo.png └── index.html ├── example.ctml ├── package.lisp ├── processor.lisp ├── tag-processors.lisp ├── test.ctml └── toolkit.lisp /.gitattributes: -------------------------------------------------------------------------------- 1 | doc/ linguist-vendored 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Yukari Hafner 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About Clip 2 | Clip is an attempt at a templating library that allows you to write templates in a way that is both accessible to direct webdesign and flexible. The main idea is to incorporate transformation commands into an HTML file through tags and attributes. Clip is heavily dependant on [Plump](https://shinmera.github.io/plump) and [lQuery](https://shinmera.github.io/lquery). 3 | 4 | ## How To 5 | Load Clip through ASDF or Quicklisp. 6 | 7 | ``` 8 | (ql:quickload :clip) 9 | ``` 10 | 11 | To process a template, simply call `PROCESS`: 12 | 13 | ``` 14 | (clip:process #p"my-template.ctml") 15 | ``` 16 | 17 | You may also pass in pure strings or plump nodes. Most likely you will want to include some kind of data in your template. Data in Clip is managed through a central `CLIPBOARD`. The additional arguments you pass to `PROCESS` are entered into the initial clipboard like a plist (key and value alternating). 18 | 19 | Depending on the current tag environment and how the template is processed at the time these values can come into play. Most of the time, entering the name as passed into `PROCESS` in the template as an attribute value will then evaluate to the according value using `RESOLVE-VALUE`. In the case of a symbol this then delegates to `CLIP`, which returns the value stored in the clipboard. 20 | 21 | The value returned by `PROCESS` is the node you passed into it. You can parse this back into a string using `PLUMP:SERIALIZE` or lQuery's `WRITE-TO-FILE`. 22 | 23 | ## Standard Tags 24 | 25 | * `C:EXPAND`
26 | This tag expands its attributes and then calls `PROCESS-NODE` on itself again. This is useful to generate attributes to be expanded. 27 | * `C:IF`
28 | Looks for either a `TEST` attribute or a `C:TEST` tag as one of its direct children. If the test as by `RESOLVE-VALUE` is non-NIL, all children of the `C:THEN` tag are spliced in place of the `C:IF` block. Otherwise it looks for `C:ELSEIF` blocks and checks their `TEST` attributes in turn. The contents of the first block whose `TEST` passes is spliced. If none pass, the `C:ELSE` child block, if any, is spliced. 29 | * `C:ITERATE`
30 | Looks for one attribute called `OVER` and then works like the `ITERATE` attribute processor using the value of the `OVER` attribute. 31 | * `C:LET`
32 | Creates a new clipboard environment with all the tag attributes bound in the following manner: The attribute key is put into the clipboard directly and associated with the value of `RESOLVE-VALUE` of the attribute value. Acts like `SPLICE`. 33 | * `C:NOOP`
34 | This tag only processes its attributes, but none of its children. 35 | * `C:SPLICE`
36 | Splices all nodes within it into the parent's child list at the position of itself (essentially replacing it with its children). 37 | * `C:UNLESS`
38 | Same as WHEN, but inverted. 39 | * `C:USING`
40 | Binds the clipboard to the resolved-value of its `VALUE` attribute. Acts like `SPLICE`. 41 | * `C:WHEN`
42 | Looks for a `TEST` attribute and if the value of it as by `RESOLVE-VALUE` is non-NIL, acts like `SPLICE`. Otherwise it removes itself including its children from the DOM. 43 | * `C:CASE`
44 | Takes a `VALUE` attribute that each element in the body is compared against, just like CL's `case`. Each element in the body may have a `VALUE` attribute to compare again, or a `VALUES` attribute that is a space-separated list, too. 45 | * `C:S`
46 | A shorthand for `` where ... is the contents of the element. 47 | * `C:H`
48 | A shorthand for `` where ... is the contents of the element. 49 | 50 | If you specify attributes that are not known on a standard tag, a warning of type `UNKNOWN-ATTRIBUTE` is signalled. If you do not specify a required attribute on a standard tag, an error of type `MISSING-ATTRIBUTE` is signalled. 51 | 52 | ## Standard Attributes 53 | 54 | * `AS`
55 | Simply changes that node's tag-name to the value of this attribute. 56 | * `COUNT`
57 | Inserts the value of `*TARGET-COUNT*` as the attribute value. This is useful to follow processing order during debugging. 58 | * `EVAL`
59 | Simply calls `EVAL` on the value of `READ-FROM-STRING` of the attribute value. 60 | * `ITERATE`
61 | The value (as by `RESOLVE-VALUE`) is used as an iteration list or vector. The first node within the node this attribute belongs to is copied once for each item in the iteration list and processed with that item used as the clipboard. 62 | * `LQUERY`
63 | Calls lQuery functions on the node as if by `($ node ..)`. Note that only lQuery functions are available, not its macros. 64 | * `FILL`
65 | The attribute value is read as a plist, the keys of which designate other attribute names and the values are resolved to the objects to use. For each named attribute, its value is modified by replacing `{thing}` by the result of `clip` on the respective object's field `thing`. 66 | 67 | ## Extending Clip 68 | You can define new tag and attribute processors with the macros `DEFINE-TAG-PROCESSOR` and `DEFINE-ATTRIBUTE-PROCESSOR`. For tag processors you will usually want to make sure to call `PROCESS-ATTRIBUTES` and `PROCESS-CHILDREN` to ensure that tags and attributes within are properly processed. To retrieve values most of the time you need to use `RESOLVE-VALUE` (or its shorthand `RESOLVE-ATTRIBUTE`) unless you want to whip up your own system for one reason or another. All tags that you define will automatically be prefixed with `C:` in order to help highlighting template tags and ensure that there are no collisions with existing tags. 69 | 70 | ## Editor Support 71 | The Emacs extension [Web-Mode](http://web-mode.org/)(version 9.0.77+) provides syntax highlighting for Clip templates. In order for it to recognise the templates, use the `.ctml` file extension. A huge thanks goes to [Bois Francois-Xavier](https://github.com/fxbois) for adding the support. 72 | 73 | ## Tutorials 74 | These are short tutorials to help explaining the effects of each tag and to illustrate some basic templating techniques. The examples will only work with Clip>=0.5.1 and lQuery>=3.1.1 . 75 | 76 | ### Updating a node with values 77 | 78 | (clip:process-to-string 79 | "" 80 | :text "Hi!" :class "clip-text") 81 | 82 | Explanation: The `LQUERY` attribute allows you to perform lQuery operations on the node it is an attribute of. In this case, the `TEXT` function sets the text of the node to the value of `TEXT`, which we told Clip to be `"Hi!"`. Similarly for `ADD-CLASS`. Any non-keyword symbol within the template is automatically resolved to a field on the current clipboard. You may think of the clipboard as a form of lexical environment for the template, which we currently set to have the variables `TEXT` and `CLASS` bound. The default `CLIPBOARD` object is special in the sense that it does not differentiate between accessing it with keywords, symbols or strings and is case-insensitive. This makes it easier to access in templates. 83 | 84 | Please see the [lQuery](https://shinmera.github.io/lquery) documentation for all possible node manipulation functions. 85 | 86 | ### Populating from a list 87 | 88 | (clip:process-to-string 89 | "
" 90 | :todo-list '("Write tutorials" "Make tiramisu" "Visit grandma")) 91 | 92 | The `ITERATE` attribute goes over the list or vector of elements its attribute-value resolves to and uses each item as the current clipboard for the iteration element. Since in this case these values themselves are direct strings we cannot retrieve further values from them and instead need to use `*` to refer to the entire clipboard. 93 | 94 | ### Conditionals 95 | 96 | (clip:process-to-string 97 | "" 100 | :users '((:username "Some Guy" :anonymous T) (:username "Some Other Guy" :anonymous NIL) (:username "You" :anonymous NIL))) 101 | 102 | Clip offers a couple of constructs to perform conditionals. These constructs are `C:WHEN` `C:UNLESS` and `C:IF`, after their CL equivalents. Each take an attribute called `TEST` that has to resolve to a non-NIL value to be taken as true. In the case of `C:IF`, three special local child tags are used: `C:THEN`, `C:ELSE` and `C:TEST`. The `C:TEST` tag can be used as an alternative to the test attribute. The other two should be self-explanatory. Note that none of the child-tags or attributes of an unchosen branch are processed. 103 | 104 | ### Bindings 105 | 106 | (clip:process-to-string 107 | " 108 | 109 | 110 | 111 | " 112 | :num 2) 113 | 114 | In order to manipulate the clipboard bindings you can use the `C:USING` and `C:LET` special tags. `C:USING` replaces the clipboard environment with what the value of its `VALUE` attribute resolves to. `C:LET` on the other hand creates a new `CLIPBOARD` object, setting the specified symbol/value pairs from its attributes. 115 | 116 | ### Clipboard Stack 117 | 118 | (clip:process-to-string 119 | "" 125 | :author "Max Mastermind" :articles '("Whoa I am blogging!!" "I don't know what to write, sadface.")) 126 | 127 | Sometimes you need to refer to values in clipboards outside of the current binding. No worries, this is easy to do as the clipboards are organised using a stack. You can reach clipboards higher up in the stack using the asterisk symbols. Each asterisk more is one clipboard higher. Using the asterisk symbol as a variable returns the clipboard directly, using it as a function call is akin to doing `(CLIP ** 'thing)`. In order to avoid clashing with the `*` multiplication function, the asterisk function shorthand is only active for two or more asterisks. 128 | 129 | ### Function References 130 | 131 | (defun seconds () (decode-universal-time (get-universal-time))) 132 | (clip:process-to-string 133 | "") 134 | 135 | Whenever you require to use functions within clip documents, you need to be aware of the current value of `*package*`. As values that are resolved are first parsed using `read`, they are influenced by `*package*`. You can of course use fully qualified symbol names, but often times it is useful to bind the variable to the package you need to reduce verbosity. 136 | 137 | You must also be aware of the special resolving for symbols used as function calls within standard resolvings. As mentioned in the previous section, symbols only consisting of asterisks are specially handled. Additionally, the symbols `cl:quote`, `cl:function`, `cl:or`, `cl:and`, `cl:if`, `cl:when`, and `cl:unless` are handled to work like their usual macro/special equivalents. Any other symbol is treated as follows: If a function's symbol with the same symbol-name is externalised from the `clip` package, the `clip` function is used. If not, the function named by the symbol in the symbol's package is used. This is done so that, no matter your package, you will always have access to functions like `clip` and `clipboard`. As an unfortunate side-effect of a symbol not knowing whether it was fully qualified or not, this means that even if you use the full symbol name with package in your template, as long as the name is external in `clip`, the `clip` function is used instead. You will have to use a combination of `funcall` and `#'` to circumvent this limitation.. 138 | 139 | Further Reading 140 | --------------- 141 | * [Plump](https://shinmera.github.io/plump) 142 | * [lQuery](https://shinmera.github.io/lquery) 143 | -------------------------------------------------------------------------------- /attr-processors.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:org.tymoonnext.clip) 2 | 3 | (defvar *attribute-processors* (make-hash-table :test 'equalp) 4 | "Global registry of attribute processors. 5 | 6 | This has to be an EQUALP hash-table with the attribute name as keys 7 | and functions that accept two arguments (node attribute-value) as 8 | values. Binding this variable can be useful to establish local 9 | attributes.") 10 | 11 | (defvar *target* NIL 12 | "This variable is bound to whatever node is currently being processed.") 13 | 14 | (defvar *target-counter* 0 15 | "This counter is upped whenever process-node is called.") 16 | 17 | (defun attribute-processor (attribute) 18 | "Returns the processor function for the requested attribute if one is registered. 19 | Otherwise returns NIL. See *ATTRIBUTE-PROCESSORS*." 20 | (gethash attribute *attribute-processors*)) 21 | 22 | (defun (setf attribute-processor) (func attribute) 23 | "Sets the attribute-processor bound to the given attribute to the specified function. 24 | See *ATTRIBUTE-PROCESSORS*." 25 | (setf (gethash attribute *attribute-processors*) func)) 26 | 27 | (defun process-attribute (node attribute value) 28 | "Processes the specified attribute using the given value. 29 | If no attribute processor can be found, nothing is done. 30 | See *ATTRIBUTE-PROCESSORS*." 31 | (let ((func (attribute-processor attribute))) 32 | (when func (funcall func node value)))) 33 | 34 | (defmacro define-attribute-processor (attribute (node value) &body body) 35 | "Defines a new attribute processor. 36 | 37 | ATTRIBTUE --- A symbol or string that matches the attribute to process (case-insensitive) 38 | NODE --- The current node is bound to this symbol. 39 | VALUE --- The value of the attribute is bound to this symbol. 40 | BODY ::= form*" 41 | `(setf (attribute-processor ,(string attribute)) 42 | #'(lambda (,node ,value) ,@body))) 43 | 44 | (defun process-attributes (node) 45 | "Processes all attributes on the node. 46 | See PROCESS-ATTRIBUTE." 47 | (maphash #'(lambda (attr val) (process-attribute node attr val)) 48 | (plump:attributes node))) 49 | 50 | (define-attribute-processor lquery (node value) 51 | (let ((actions (typecase value 52 | (list value) 53 | (string (read-from-string (format NIL "(~a)" value)))))) 54 | (plump:remove-attribute node "lquery") 55 | (loop with node = (make-proper-vector :size 1 :initial-element node :fill-pointer T) 56 | for (func . args) in actions 57 | do (apply (or (find-symbol (string func) :lquery-funcs) func) 58 | node (mapcar #'resolve-value args))))) 59 | 60 | (define-attribute-processor eval (node value) 61 | (eval (read-from-string value)) 62 | (plump:remove-attribute node "eval")) 63 | 64 | (define-attribute-processor iterate (node value) 65 | (let ((val (parse-and-resolve value)) 66 | (new-children (plump:make-child-array)) 67 | (target node)) 68 | (flet ((process (item) 69 | (with-clipboard-bound (item) 70 | (let* ((clone (plump:clone-node target T)) 71 | (children (plump:children clone))) 72 | (loop for i from 0 73 | while (< i (length children)) 74 | do (process-node (aref children i))) 75 | (array-utils:vector-append new-children children))))) 76 | (etypecase val 77 | (list (loop for item in val do (process item))) 78 | (vector (loop for item across val do (process item))))) 79 | (setf (plump:children node) new-children) 80 | (plump:remove-attribute node "iterate"))) 81 | 82 | (define-attribute-processor as (node value) 83 | (setf (plump:tag-name node) value) 84 | (plump:remove-attribute node "as")) 85 | 86 | (define-attribute-processor count (node value) 87 | (declare (ignore value)) 88 | (setf (plump:attribute node "count") (princ-to-string *target-counter*))) 89 | 90 | 91 | (defun replace-region (string start end replacer) 92 | (with-output-to-string (output) 93 | (loop with filler = (make-string-output-stream) 94 | with target = output 95 | for char across string 96 | do (cond 97 | ((char= char start) 98 | (setf target filler)) 99 | 100 | ((char= char end) 101 | (unless (eql target filler) 102 | (error "Unmatched closing.")) 103 | (princ (funcall replacer (get-output-stream-string filler)) output) 104 | (close filler) 105 | (setf target output) 106 | (setf filler (make-string-output-stream))) 107 | 108 | (T (write-char char target))) 109 | finally (if (eql target filler) 110 | (error "Unmatched opening.") 111 | (close filler))))) 112 | 113 | (define-attribute-processor fill (node value) 114 | (loop for (attribute object) on (read-from-string (format NIL "(~a)" value)) 115 | for attr = (string attribute) 116 | do (let ((value (plump:attribute node attr))) 117 | (when value 118 | (setf (plump:attribute node attr) 119 | (replace-region 120 | value #\{ #\} 121 | #'(lambda (value) 122 | (clip (resolve-value object) value))))))) 123 | (plump:remove-attribute node "fill")) 124 | -------------------------------------------------------------------------------- /clip-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shinmera/clip/abe3c31ddf4c6efe59daf70d19415caf668d56ea/clip-logo.png -------------------------------------------------------------------------------- /clip.asd: -------------------------------------------------------------------------------- 1 | (defsystem clip 2 | :name "Clip HTML Processor" 3 | :version "0.7.0" 4 | :license "zlib" 5 | :author "Yukari Hafner " 6 | :maintainer "Yukari Hafner " 7 | :description "An HTML templating engine using Plump." 8 | :homepage "https://Shinmera.github.io/clip/" 9 | :bug-tracker "https://github.com/Shinmera/clip/issues" 10 | :source-control (:git "https://github.com/Shinmera/clip.git") 11 | :serial T 12 | :components ((:file "package") 13 | (:file "toolkit") 14 | (:file "conditions") 15 | (:file "clipboard") 16 | (:file "attr-processors") 17 | (:file "tag-processors") 18 | (:file "processor")) 19 | :depends-on (:array-utils 20 | :lquery)) 21 | -------------------------------------------------------------------------------- /clipboard.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:org.tymoonnext.clip) 2 | 3 | (defvar *clipboard-stack* NIL 4 | "Template storage stack. When new clipboards are bound, they are pushed onto the stack. 5 | Once the binding is left, they are popped off the stack again.") 6 | 7 | (defclass clipboard () 8 | ((%clipboard-env :initarg :env :initform (make-hash-table :test 'equalp) :accessor clipboard-env)) 9 | (:documentation "Special class for clipboard environments. Use CLIPBOARD or CLIP to access and set values within. 10 | Field names are automatically transformed into strings as per STRING. Access is case-insensitive.")) 11 | 12 | (defun make-clipboard (&rest fields) 13 | "Creates a new clipboard using the specified fields (like a plist)." 14 | (let ((table (make-hash-table :test 'equalp))) 15 | (loop for (key val) on fields by #'cddr 16 | do (setf (gethash (string key) table) val)) 17 | (make-instance 'clipboard :env table))) 18 | 19 | (defmacro with-clipboard-bound ((new-clipboard &rest fields) &body body) 20 | "Executes the body with the new clipboard on the *CLIPBOARD-STACK*. 21 | 22 | If fields are provided, they are set on the NEW-CLIPBOARD in plist fashion as per consecutive SETF. 23 | This means that side-effects of an early field set affect later fields. The fields are evaluated 24 | before the NEW-CLIPBOARD is pushed onto the *CLIPBOARD-STACK*." 25 | (let ((clipboard (gensym "CLIPBOARD"))) 26 | `(let ((,clipboard ,new-clipboard)) 27 | ,@(loop for (field value) on fields by #'cddr collect `(setf (clip ,clipboard ,field) ,value)) 28 | (let ((*clipboard-stack* (cons ,clipboard *clipboard-stack*))) 29 | ,@body)))) 30 | 31 | (defgeneric clip (object field) 32 | (:documentation "Generic object accessor. 33 | If you want to get special treatment of objects or types, define your own methods on this.")) 34 | 35 | (defgeneric (setf clip) (value object field) 36 | (:documentation "Generic object setter. 37 | If you want to get special treatment of objects or types, define your own methods on this.")) 38 | 39 | (defun clipboard (field) 40 | "Shorthand for (CLIP (FIRST *CLIPBOARD-STACK*) FIELD)" 41 | (clip (first *clipboard-stack*) field)) 42 | 43 | (defun (setf clipboard) (value field) 44 | "Shorthand for (SETF (CLIP (FIRST *CLIPBOARD-STACK*) FIELD) VALUE)" 45 | (setf (clip (first *clipboard-stack*) field) value)) 46 | 47 | (defmethod clip ((board clipboard) field) 48 | "Accessor for the clipboard object." 49 | (gethash (string field) (clipboard-env board))) 50 | 51 | (defmethod (setf clip) (value (board clipboard) field) 52 | (setf (gethash (string field) (clipboard-env board)) value)) 53 | 54 | (defmethod clip ((table hash-table) field) 55 | "Generic hash-table accessor." 56 | (gethash field table)) 57 | 58 | (defmethod (setf clip) (value (table hash-table) field) 59 | (setf (gethash field table) value)) 60 | 61 | (defmethod clip ((model list) field) 62 | "Generic alist or plist accessor." 63 | (cond 64 | ((keywordp (first model)) 65 | (getf model (make-keyword field))) 66 | ((listp (first model)) 67 | (cdr (assoc field model :test #'string-equal))) 68 | (T 69 | (error "Model is of type LIST, but is neither an ALIST or PLIST.")))) 70 | 71 | (defmethod (setf clip) (value (model list) field) 72 | (cond 73 | ((keywordp (first model)) 74 | (setf (getf model (make-keyword field)) value)) 75 | ((listp (first model)) 76 | (setf (cdr (assoc field model :test #'string-equal)) value)) 77 | (T 78 | (error "Model is of type LIST, but is neither an ALIST or PLIST.")))) 79 | 80 | (defmethod clip ((model standard-object) field) 81 | "Generic slot accessor." 82 | (let ((field (find-symbol (string field) 83 | (symbol-package (class-name (class-of model)))))) 84 | (if field (slot-value model field)))) 85 | 86 | (defmethod (setf clip) (value (model standard-object) field) 87 | (let ((field (find-symbol (string field) 88 | (symbol-package (class-name (class-of model)))))) 89 | (if field (setf (slot-value model field) value)))) 90 | 91 | (defmethod clip ((model structure-object) field) 92 | "Generic slot accessor." 93 | (let ((field (find-symbol (string field) 94 | (symbol-package (class-name (class-of model)))))) 95 | (if field (slot-value model field)))) 96 | 97 | (defmethod (setf clip) (value (model structure-object) field) 98 | (let ((field (find-symbol (string field) 99 | (symbol-package (class-name (class-of model)))))) 100 | (if field (setf (slot-value model field) value)))) 101 | 102 | (defgeneric resolve-value (object) 103 | (:documentation "Attempts to resolve the object to a specific value. 104 | This is usually used in combination with READ-FROM-STRING of an attribute value.")) 105 | 106 | (defmethod resolve-value (object) 107 | "Default fallback for unrecognized objects; simply returns it." 108 | object) 109 | 110 | (defmethod resolve-value ((symbol symbol)) 111 | "Handler for symbols. 112 | If the symbol is EQL to '* the *CLIPBOARD* is returned, 113 | If the symbol is a keyword the symbol itself is returned, 114 | otherwise the value of (CLIPBOARD SYMBOL) is returned." 115 | (cond ((loop for char across (symbol-name symbol) 116 | always (char= char #\*)) 117 | (nth (1- (length (symbol-name symbol))) *clipboard-stack*)) 118 | ((keywordp symbol) 119 | symbol) 120 | ((eq symbol T) 121 | T) 122 | ((eq symbol NIL) 123 | NIL) 124 | (T (clipboard symbol)))) 125 | 126 | (defmethod resolve-value ((list list)) 127 | "Handler for lists, aka function calls. 128 | 129 | The function call is decided upon the CAR of the list. 130 | The following cases are handled: 131 | 132 | QUOTE Returns the first argument 133 | FUNCTION Returns the symbol-function of the first argument 134 | OR Simulated version of the OR macro. 135 | AND Simulated version of the AND macro. 136 | 137 | Otherwise the symbol is looked for in the :CLIP package 138 | and then the current *PACKAGE*. If found, the function is 139 | applied with all arguments of the list (which are first 140 | all individually passed to RESOLVE-VALUE too)." 141 | (let ((func (car list)) 142 | (args (cdr list))) 143 | (cond ((and (loop for char across (symbol-name func) 144 | always (char= char #\*)) 145 | (<= 2 (length (symbol-name func)))) 146 | (clip (nth (1- (length (symbol-name func))) *clipboard-stack*) 147 | (resolve-value (first args)))) 148 | (T 149 | (flet ((run-progn (args) 150 | (loop for arg in args 151 | for val = (resolve-value arg) 152 | finally (return val)))) 153 | (case func 154 | (quote (first args)) 155 | (function (symbol-function (first args))) 156 | (or (loop for arg in args 157 | thereis (resolve-value arg))) 158 | (and (loop for arg in args 159 | for val = (resolve-value arg) 160 | when (not val) 161 | do (return) 162 | finally (return val))) 163 | (progn (run-progn args)) 164 | (if (if (resolve-value (first args)) 165 | (resolve-value (second args)) 166 | (resolve-value (third args)))) 167 | (when (when (resolve-value (first args)) 168 | (loop for form in (rest args) 169 | for val = (resolve-value form) 170 | finally (return val)))) 171 | (unless (unless (resolve-value (first args)) 172 | (loop for form in (rest args) 173 | for val = (resolve-value form) 174 | finally (return val)))) 175 | (cond (loop for (test . body) in args 176 | do (when (resolve-value test) 177 | (return (run-progn body))))) 178 | (case (loop with value = (resolve-value (first args)) 179 | for (test . body) in (rest args) 180 | do (cond ((listp test) 181 | (when (find value test) 182 | (return (run-progn body)))) 183 | ((eql T test) 184 | (return (run-progn body))) 185 | ((eql value test) 186 | (return (run-progn body)))))) 187 | (T (apply (or (multiple-value-bind (s e) (find-symbol (string func) :clip) 188 | (when (and s (eql e :external) (fboundp s)) s)) func) 189 | (mapcar #'resolve-value args))))))))) 190 | 191 | (defun resolve-attribute (node attr) 192 | "Shorthand to resolve the value of an attibute. 193 | See RESOLVE-VALUE." 194 | (resolve-value (read-from-string (plump:attribute node attr)))) 195 | 196 | (defun parse-and-resolve (value) 197 | "If the passed value is a STRING it is parsed using READ-FROM-STRING and subsequently passed to RESOLVE-VALUE. 198 | If it is not a string, the value itself is returned." 199 | (if (stringp value) 200 | (resolve-value (read-from-string value)) 201 | value)) 202 | -------------------------------------------------------------------------------- /conditions.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:org.tymoonnext.clip) 2 | 3 | (define-condition clip-condition () 4 | () 5 | (:documentation "Superclass for all conditions related to Clip.")) 6 | 7 | (define-condition node-condition (clip-condition) 8 | ((node :initarg :node)) 9 | (:documentation "Superclass for all conditions related to problems with a node. 10 | 11 | See CLIP-CONDITION")) 12 | 13 | (define-condition attribute-condition (node-condition) 14 | ((attribute :initarg :attribute)) 15 | (:documentation "Superclass for all conditions related to problems with a node's attribute. 16 | 17 | See NODE-CONDITION")) 18 | 19 | (define-condition missing-attribute (error attribute-condition) 20 | () 21 | (:report (lambda (c s) (format s "The attribute ~s is required on the node ~a." 22 | (slot-value c 'attribute) (slot-value c 'node)))) 23 | (:documentation "Condition signalled when a required attribute is missing. 24 | 25 | See ATTRIBUTE-CONDITION")) 26 | 27 | (define-condition unknown-attribute (warning attribute-condition) 28 | () 29 | (:report (lambda (c s) (format s "The attribute ~s is present on the node ~a~%but does not have a specified effect." 30 | (slot-value c 'attribute) (slot-value c 'node)))) 31 | (:documentation "Condition signalled when an unknown attribute is present. 32 | 33 | See ATTRIBUTE-CONDITION")) 34 | 35 | (defun check-attribute (node attribute) 36 | "Checks whether the given attribute is present on the node. 37 | 38 | If it is, the attribute's value is returned. 39 | Otherwise, an error of type MISSING-ATTRIBUTE is signalled. 40 | 41 | See MISSING-ATTRIBUTE" 42 | (or (plump:attribute node attribute) 43 | (error 'missing-attribute :attribute attribute :node node))) 44 | 45 | (defun check-no-unknown-attributes (node &rest known-attributes) 46 | "Checks whether there are any unknown attributes present on the node. 47 | 48 | If an unknown attribute is present, a warning of type 49 | UNKNOWN-ATTRIBUTE is signalled. Otherwise, NIL is returned. 50 | 51 | See UNKNOWN-ATTRIBUTE" 52 | (loop for attribute being the hash-keys of (plump:attributes node) 53 | do (unless (find attribute known-attributes :test #'string-equal) 54 | (warn 'unknown-attribute :attribute attribute :node node)))) 55 | 56 | (defun check-sole-attribute (node attribute) 57 | "Checks whether the given attribute is the only attribute on the node. 58 | 59 | If it is not present, or not the only one, an error is signalled. 60 | Otherwise, the attribute's value is returned. 61 | 62 | See CHECK-NO-UNKNOWN-ATTRIBUTES 63 | See CHECK-ATTRIBUTE" 64 | (check-no-unknown-attributes node attribute) 65 | (check-attribute node attribute)) 66 | -------------------------------------------------------------------------------- /docs/clip-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shinmera/clip/abe3c31ddf4c6efe59daf70d19415caf668d56ea/docs/clip-logo.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Clip
0.7.0

An HTML templating engine using Plump.

Table of Contents

About Clip

Clip is an attempt at a templating library that allows you to write templates in a way that is both accessible to direct webdesign and flexible. The main idea is to incorporate transformation commands into an HTML file through tags and attributes. Clip is heavily dependant on Plump and lQuery.

How To

Load Clip through ASDF or Quicklisp.

(ql:quickload :clip)

To process a template, simply call PROCESS:

(clip:process #p"my-template.ctml")

You may also pass in pure strings or plump nodes. Most likely you will want to include some kind of data in your template. Data in Clip is managed through a central CLIPBOARD. The additional arguments you pass to PROCESS are entered into the initial clipboard like a plist (key and value alternating).

Depending on the current tag environment and how the template is processed at the time these values can come into play. Most of the time, entering the name as passed into PROCESS in the template as an attribute value will then evaluate to the according value using RESOLVE-VALUE. In the case of a symbol this then delegates to CLIP, which returns the value stored in the clipboard.

The value returned by PROCESS is the node you passed into it. You can parse this back into a string using PLUMP:SERIALIZE or lQuery's WRITE-TO-FILE.

Standard Tags

  • C:EXPAND
    This tag expands its attributes and then calls PROCESS-NODE on itself again. This is useful to generate attributes to be expanded.
  • C:IF
    Looks for either a TEST attribute or a C:TEST tag as one of its direct children. If the test as by RESOLVE-VALUE is non-NIL, all children of the C:THEN tag are spliced in place of the C:IF block. Otherwise it looks for C:ELSEIF blocks and checks their TEST attributes in turn. The contents of the first block whose TEST passes is spliced. If none pass, the C:ELSE child block, if any, is spliced.
  • C:ITERATE
    Looks for one attribute called OVER and then works like the ITERATE attribute processor using the value of the OVER attribute.
  • C:LET
    Creates a new clipboard environment with all the tag attributes bound in the following manner: The attribute key is put into the clipboard directly and associated with the value of RESOLVE-VALUE of the attribute value. Acts like SPLICE.
  • C:NOOP
    This tag only processes its attributes, but none of its children.
  • C:SPLICE
    Splices all nodes within it into the parent's child list at the position of itself (essentially replacing it with its children).
  • C:UNLESS
    Same as WHEN, but inverted.
  • C:USING
    Binds the clipboard to the resolved-value of its VALUE attribute. Acts like SPLICE.
  • C:WHEN
    Looks for a TEST attribute and if the value of it as by RESOLVE-VALUE is non-NIL, acts like SPLICE. Otherwise it removes itself including its children from the DOM.
  • C:CASE
    Takes a VALUE attribute that each element in the body is compared against, just like CL's case. Each element in the body may have a VALUE attribute to compare again, or a VALUES attribute that is a space-separated list, too.
  • C:S
    A shorthand for <c:splice lquery="(text ...)"></c:splice> where ... is the contents of the element.
  • C:H
    A shorthand for <c:splice lquery="(html ...)"></c:splice> where ... is the contents of the element.

If you specify attributes that are not known on a standard tag, a warning of type UNKNOWN-ATTRIBUTE is signalled. If you do not specify a required attribute on a standard tag, an error of type MISSING-ATTRIBUTE is signalled.

Standard Attributes

  • AS
    Simply changes that node's tag-name to the value of this attribute.
  • COUNT
    Inserts the value of *TARGET-COUNT* as the attribute value. This is useful to follow processing order during debugging.
  • EVAL
    Simply calls EVAL on the value of READ-FROM-STRING of the attribute value.
  • ITERATE
    The value (as by RESOLVE-VALUE) is used as an iteration list or vector. The first node within the node this attribute belongs to is copied once for each item in the iteration list and processed with that item used as the clipboard.
  • LQUERY
    Calls lQuery functions on the node as if by ($ node ..). Note that only lQuery functions are available, not its macros.
  • FILL
    The attribute value is read as a plist, the keys of which designate other attribute names and the values are resolved to the objects to use. For each named attribute, its value is modified by replacing {thing} by the result of clip on the respective object's field thing.

Extending Clip

You can define new tag and attribute processors with the macros DEFINE-TAG-PROCESSOR and DEFINE-ATTRIBUTE-PROCESSOR. For tag processors you will usually want to make sure to call PROCESS-ATTRIBUTES and PROCESS-CHILDREN to ensure that tags and attributes within are properly processed. To retrieve values most of the time you need to use RESOLVE-VALUE (or its shorthand RESOLVE-ATTRIBUTE) unless you want to whip up your own system for one reason or another. All tags that you define will automatically be prefixed with C: in order to help highlighting template tags and ensure that there are no collisions with existing tags.

Editor Support

The Emacs extension Web-Mode(version 9.0.77+) provides syntax highlighting for Clip templates. In order for it to recognise the templates, use the .ctml file extension. A huge thanks goes to Bois Francois-Xavier for adding the support.

Tutorials

These are short tutorials to help explaining the effects of each tag and to illustrate some basic templating techniques. The examples will only work with Clip>=0.5.1 and lQuery>=3.1.1 .

Updating a node with values

(clip:process-to-string
  2 |  "<span lquery=\"(text text) (add-class class)\" />"
  3 |  :text "Hi!" :class "clip-text")
  4 | 

Explanation: The LQUERY attribute allows you to perform lQuery operations on the node it is an attribute of. In this case, the TEXT function sets the text of the node to the value of TEXT, which we told Clip to be "Hi!". Similarly for ADD-CLASS. Any non-keyword symbol within the template is automatically resolved to a field on the current clipboard. You may think of the clipboard as a form of lexical environment for the template, which we currently set to have the variables TEXT and CLASS bound. The default CLIPBOARD object is special in the sense that it does not differentiate between accessing it with keywords, symbols or strings and is case-insensitive. This makes it easier to access in templates.

Please see the lQuery documentation for all possible node manipulation functions.

Populating from a list

(clip:process-to-string
  5 |  "<ol iterate=\"todo-list\"><li lquery=\"(text *)\"></li></ol>"
  6 |  :todo-list '("Write tutorials" "Make tiramisu" "Visit grandma"))
  7 | 

The ITERATE attribute goes over the list or vector of elements its attribute-value resolves to and uses each item as the current clipboard for the iteration element. Since in this case these values themselves are direct strings we cannot retrieve further values from them and instead need to use * to refer to the entire clipboard.

Conditionals

(clip:process-to-string
  8 |  "<ul iterate=\"users\">
  9 |   <li><c:if test=\"anonymous\"><c:then>Username Hidden</c:then><c:else lquery=\"(text username)\"/></c:if></li>
 10 | </ul>"
 11 |  :users '((:username "Some Guy" :anonymous T) (:username "Some Other Guy" :anonymous NIL) (:username "You" :anonymous NIL)))
 12 | 

Clip offers a couple of constructs to perform conditionals. These constructs are C:WHEN C:UNLESS and C:IF, after their CL equivalents. Each take an attribute called TEST that has to resolve to a non-NIL value to be taken as true. In the case of C:IF, three special local child tags are used: C:THEN, C:ELSE and C:TEST. The C:TEST tag can be used as an alternative to the test attribute. The other two should be self-explanatory. Note that none of the child-tags or attributes of an unchosen branch are processed.

Bindings

(clip:process-to-string
 13 |  "<c:using value=\"num\">
 14 |   <c:let orig=\"*\" double=\"(* * 2)\" square=\"(expt * 2)\" root=\"(sqrt *)\">
 15 |     <span lquery=\"(text (list orig double square root))\" />
 16 |   </c:let>
 17 | </c:using>"
 18 |  :num 2)
 19 | 

In order to manipulate the clipboard bindings you can use the C:USING and C:LET special tags. C:USING replaces the clipboard environment with what the value of its VALUE attribute resolves to. C:LET on the other hand creates a new CLIPBOARD object, setting the specified symbol/value pairs from its attributes.

Clipboard Stack

(clip:process-to-string
 20 |  "<ul iterate=\"articles\">
 21 |   <li><article>
 22 |     <header><div class=\"author\" lquery=\"(text (** :author))\">AUTHOR</div></header>
 23 |     <section class=\"content\" lquery=\"(text *)\">CONTENT</section>
 24 |   </article></li>
 25 | </ul>"
 26 |  :author "Max Mastermind" :articles '("Whoa I am blogging!!" "I don't know what to write, sadface."))
 27 | 

Sometimes you need to refer to values in clipboards outside of the current binding. No worries, this is easy to do as the clipboards are organised using a stack. You can reach clipboards higher up in the stack using the asterisk symbols. Each asterisk more is one clipboard higher. Using the asterisk symbol as a variable returns the clipboard directly, using it as a function call is akin to doing (CLIP ** 'thing). In order to avoid clashing with the * multiplication function, the asterisk function shorthand is only active for two or more asterisks.

Function References

(defun seconds () (decode-universal-time (get-universal-time)))
 28 | (clip:process-to-string
 29 |  "<time lquery=\"(text (seconds))\">TIME</time>")
 30 | 

Whenever you require to use functions within clip documents, you need to be aware of the current value of *package*. As values that are resolved are first parsed using read, they are influenced by *package*. You can of course use fully qualified symbol names, but often times it is useful to bind the variable to the package you need to reduce verbosity.

You must also be aware of the special resolving for symbols used as function calls within standard resolvings. As mentioned in the previous section, symbols only consisting of asterisks are specially handled. Additionally, the symbols cl:quote, cl:function, cl:or, cl:and, cl:if, cl:when, and cl:unless are handled to work like their usual macro/special equivalents. Any other symbol is treated as follows: If a function's symbol with the same symbol-name is externalised from the clip package, the clip function is used. If not, the function named by the symbol in the symbol's package is used. This is done so that, no matter your package, you will always have access to functions like clip and clipboard. As an unfortunate side-effect of a symbol not knowing whether it was fully qualified or not, this means that even if you use the full symbol name with package in your template, as long as the name is external in clip, the clip function is used instead. You will have to use a combination of funcall and #' to circumvent this limitation..

Further Reading

System Information

0.7.0
Yukari Hafner
zlib

Definition Index

  • CLIP

    • ORG.TYMOONNEXT.CLIP
    No documentation provided.
    • EXTERNAL SPECIAL-VARIABLE

      *ATTRIBUTE-PROCESSORS*

          Source
          Global registry of attribute processors.
           31 | 
           32 | This has to be an EQUALP hash-table with the attribute name as keys
           33 | and functions that accept two arguments (node attribute-value) as
           34 | values. Binding this variable can be useful to establish local
           35 | attributes.
        • EXTERNAL SPECIAL-VARIABLE

          *CLIPBOARD-STACK*

              Source
              Template storage stack. When new clipboards are bound, they are pushed onto the stack.
               36 | Once the binding is left, they are popped off the stack again.
            • EXTERNAL SPECIAL-VARIABLE

              *TAG-PROCESSORS*

                  Source
                  Global registry of tag processors.
                   37 | 
                   38 | This has to be an EQUALP hash-table with the tag name as keys
                   39 | and functions that accept one argument (the node) as
                   40 | values. Binding this variable can be useful to establish local
                   41 | tags.
                • EXTERNAL SPECIAL-VARIABLE

                  *TARGET*

                      Source
                      This variable is bound to whatever node is currently being processed.
                    • EXTERNAL CLASS

                      CLIPBOARD

                          Source
                          Special class for clipboard environments. Use CLIPBOARD or CLIP to access and set values within.
                           42 | Field names are automatically transformed into strings as per STRING. Access is case-insensitive.
                        • EXTERNAL FUNCTION

                          ATTRIBUTE-PROCESSOR

                            • ATTRIBUTE
                            • &REST
                            Source
                            Returns the processor function for the requested attribute if one is registered.
                             51 | Otherwise returns NIL. See *ATTRIBUTE-PROCESSORS*.
                          • EXTERNAL FUNCTION

                            (SETF ATTRIBUTE-PROCESSOR)

                              • FUNC
                              • ATTRIBUTE
                              • &REST
                              Source
                              Sets the attribute-processor bound to the given attribute to the specified function.
                               52 | See *ATTRIBUTE-PROCESSORS*.
                            • EXTERNAL FUNCTION

                              CHECK-ATTRIBUTE

                                • NODE
                                • ATTRIBUTE
                                • &REST
                                Source
                                Checks whether the given attribute is present on the node.
                                 53 | 
                                 54 | If it is, the attribute's value is returned.
                                 55 | Otherwise, an error of type MISSING-ATTRIBUTE is signalled.
                                 56 | 
                                 57 | See MISSING-ATTRIBUTE
                              • EXTERNAL FUNCTION

                                CHECK-NO-UNKNOWN-ATTRIBUTES

                                  • NODE
                                  • &REST
                                  • KNOWN-ATTRIBUTES
                                  • &REST
                                  Source
                                  Checks whether there are any unknown attributes present on the node.
                                   58 | 
                                   59 | If an unknown attribute is present, a warning of type
                                   60 | UNKNOWN-ATTRIBUTE is signalled. Otherwise, NIL is returned.
                                   61 | 
                                   62 | See UNKNOWN-ATTRIBUTE
                                • EXTERNAL FUNCTION

                                  CLIPBOARD

                                    • FIELD
                                    • &REST
                                    Source
                                    Shorthand for (CLIP (FIRST *CLIPBOARD-STACK*) FIELD)
                                  • EXTERNAL FUNCTION

                                    (SETF CLIPBOARD)

                                      • VALUE
                                      • FIELD
                                      • &REST
                                      Source
                                      Shorthand for (SETF (CLIP (FIRST *CLIPBOARD-STACK*) FIELD) VALUE)
                                    • EXTERNAL FUNCTION

                                      MAKE-CLIPBOARD

                                        • &REST
                                        • FIELDS
                                        • &REST
                                        Source
                                        Creates a new clipboard using the specified fields (like a plist).
                                      • EXTERNAL FUNCTION

                                        PARSE-AND-RESOLVE

                                          • VALUE
                                          • &REST
                                          Source
                                          If the passed value is a STRING it is parsed using READ-FROM-STRING and subsequently passed to RESOLVE-VALUE.
                                           69 | If it is not a string, the value itself is returned.
                                        • EXTERNAL FUNCTION

                                          PROCESS

                                            • TARGET
                                            • &REST
                                            • FIELDS
                                            • &REST
                                            Source
                                            Processes all clip markup on the target with the given FIELDS used to initialise the clipboard.
                                          • EXTERNAL FUNCTION

                                            PROCESS*

                                              • TARGET
                                              • CLIPBOARD
                                              • &REST
                                              Source
                                              Processes all clip markup on the target with the given CLIPBOARD.
                                            • EXTERNAL FUNCTION

                                              PROCESS-ATTRIBUTE

                                                • NODE
                                                • ATTRIBUTE
                                                • VALUE
                                                • &REST
                                                Source
                                                Processes the specified attribute using the given value.
                                                 70 | If no attribute processor can be found, nothing is done.
                                                 71 | See *ATTRIBUTE-PROCESSORS*.
                                              • EXTERNAL FUNCTION

                                                PROCESS-ATTRIBUTES

                                                  • NODE
                                                  • &REST
                                                  Source
                                                  Processes all attributes on the node.
                                                   72 | See PROCESS-ATTRIBUTE.
                                                • EXTERNAL FUNCTION

                                                  PROCESS-CHILDREN

                                                    • NODE
                                                    • &REST
                                                    Source
                                                    Calls PROCESS-NODE on all childrens of the passed node.
                                                     73 | 
                                                     74 | This takes some care to make sure that splicing into the childrens array
                                                     75 | of the node is possible. However, note that inserting children before the
                                                     76 | node that is currently being processed will most likely lead to horrors.
                                                     77 | If such functionality is indeed ever needed (I hope not), this system
                                                     78 | needs to be rewritten to somehow be able to cope with such scenarios. 
                                                  • EXTERNAL FUNCTION

                                                    PROCESS-NODE

                                                      • NODE
                                                      • &REST
                                                      Source
                                                      Processes the passed node.
                                                       79 | 
                                                       80 | Depending on type the following is done:
                                                       81 | PLUMP:ELEMENT       PROCESS-TAG is called.
                                                       82 | PLUMP:NESTING-NODE  PROCESS-CHILDREN is called.
                                                       83 | PLUMP:NODE          Nothing is done.
                                                       84 | T                   An error is signalled.
                                                       85 | Any call to this also increases the *TARGET-COUNTER* regardless of what
                                                       86 | is done.
                                                    • EXTERNAL FUNCTION

                                                      PROCESS-TAG

                                                        • TAG
                                                        • NODE
                                                        • &REST
                                                        Source
                                                        Processes the specified node as the given tag.
                                                         87 | If no tag processor can be found, PROCESS-ATTRIBUTES and PROCESS-CHILDREN is called.
                                                         88 | See *TAG-PROCESSORS*.
                                                      • EXTERNAL FUNCTION

                                                        PROCESS-TO-STRING

                                                          • TARGET
                                                          • &REST
                                                          • FIELDS
                                                          • &REST
                                                          Source
                                                          Same as PROCESS, but automatically performs PLUMP:SERIALIZE on the result to a string.
                                                        • EXTERNAL FUNCTION

                                                          RESOLVE-ATTRIBUTE

                                                            • NODE
                                                            • ATTR
                                                            • &REST
                                                            Source
                                                            Shorthand to resolve the value of an attibute.
                                                             89 | See RESOLVE-VALUE.
                                                          • EXTERNAL FUNCTION

                                                            TAG-PROCESSOR

                                                              • TAG
                                                              • &REST
                                                              Source
                                                              Returns the processor function for the requested tag if one is registered.
                                                               90 | Otherwise returns NIL. See *TAG-PROCESSORS*.
                                                            • EXTERNAL FUNCTION

                                                              (SETF TAG-PROCESSOR)

                                                                • FUNC
                                                                • TAG
                                                                • &REST
                                                                Source
                                                                Sets the tag-processor bound to the given tag-name to the specified function.
                                                                 91 | See *TAG-PROCESSORS*.
                                                              • EXTERNAL GENERIC-FUNCTION

                                                                CLIP

                                                                  • OBJECT
                                                                  • FIELD
                                                                  • &REST
                                                                  Source
                                                                  Generic object accessor.
                                                                   92 | If you want to get special treatment of objects or types, define your own methods on this.
                                                                • EXTERNAL GENERIC-FUNCTION

                                                                  (SETF CLIP)

                                                                    • VALUE
                                                                    • OBJECT
                                                                    • FIELD
                                                                    • &REST
                                                                    Source
                                                                    Generic object setter.
                                                                     93 | If you want to get special treatment of objects or types, define your own methods on this.
                                                                  • EXTERNAL GENERIC-FUNCTION

                                                                    RESOLVE-VALUE

                                                                      • OBJECT
                                                                      • &REST
                                                                      Source
                                                                      Attempts to resolve the object to a specific value.
                                                                       94 | This is usually used in combination with READ-FROM-STRING of an attribute value.
                                                                    • EXTERNAL MACRO

                                                                      DEFINE-ATTRIBUTE-PROCESSOR

                                                                        • ATTRIBUTE
                                                                          • NODE
                                                                          • VALUE
                                                                          • &REST
                                                                        • &BODY
                                                                        • BODY
                                                                        • &REST
                                                                        Source
                                                                        Defines a new attribute processor.
                                                                         95 | 
                                                                         96 | ATTRIBTUE --- A symbol or string that matches the attribute to process (case-insensitive)
                                                                         97 | NODE      --- The current node is bound to this symbol.
                                                                         98 | VALUE     --- The value of the attribute is bound to this symbol.
                                                                         99 | BODY      ::= form*
                                                                      • EXTERNAL MACRO

                                                                        DEFINE-TAG-PROCESSOR

                                                                          • TAG
                                                                            • NODE
                                                                            • &REST
                                                                          • &BODY
                                                                          • BODY
                                                                          • &REST
                                                                          Source
                                                                          Defines a new attribute processor.
                                                                          100 | 
                                                                          101 | TAG    --- A symbol or string that matches the tag name to process (case-insensitive)
                                                                          102 | NODE   --- The node to process is bound to this symbol
                                                                          103 | BODY   ::= form*
                                                                        • EXTERNAL MACRO

                                                                          WITH-CLIPBOARD-BOUND

                                                                              • NEW-CLIPBOARD
                                                                              • &REST
                                                                              • FIELDS
                                                                              • &REST
                                                                            • &BODY
                                                                            • BODY
                                                                            • &REST
                                                                            Source
                                                                            Executes the body with the new clipboard on the *CLIPBOARD-STACK*.
                                                                            104 | 
                                                                            105 | If fields are provided, they are set on the NEW-CLIPBOARD in plist fashion as per consecutive SETF.
                                                                            106 | This means that side-effects of an early field set affect later fields. The fields are evaluated
                                                                            107 | before the NEW-CLIPBOARD is pushed onto the *CLIPBOARD-STACK*.
                                                                        -------------------------------------------------------------------------------- /example.ctml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Clip Test 5 | 6 | 7 |

                                                                        Select an option:

                                                                        8 |
                                                                        9 |
                                                                          10 |
                                                                        • 11 | 12 | 0 13 | Title 14 |
                                                                        • 15 |
                                                                        16 | 17 |
                                                                        18 | 19 | 20 | -------------------------------------------------------------------------------- /package.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:cl-user) 2 | (defpackage #:clip 3 | (:nicknames #:org.tymoonnext.clip) 4 | (:use #:cl #:lquery) 5 | ;; attr-processors.lisp 6 | (:export 7 | #:*attribute-processors* 8 | #:*target* 9 | #:*target-count* 10 | #:attribute-processor 11 | #:process-attribute 12 | #:define-attribute-processor 13 | #:process-attributes) 14 | ;; clipboard.lisp 15 | (:export 16 | #:*clipboard-stack* 17 | #:clipboard 18 | #:make-clipboard 19 | #:with-clipboard-bound 20 | #:clip 21 | #:clipboard 22 | #:resolve-value 23 | #:resolve-attribute 24 | #:parse-and-resolve) 25 | ;; conditions.lisp 26 | (:export 27 | #:clip-condition 28 | #:node-condition 29 | #:attribute-condition 30 | #:missing-attribute 31 | #:unknown-attribute 32 | #:check-attribute 33 | #:check-no-unknown-attributes 34 | #:check-sole-attribute) 35 | ;; processor.lisp 36 | (:export 37 | #:process 38 | #:process* 39 | #:process-to-string) 40 | ;; tag-processors.lisp 41 | (:export 42 | #:*tag-processors* 43 | #:tag-processor 44 | #:process-tag 45 | #:define-tag-processor 46 | #:process-children 47 | #:process-node) 48 | ;; toolkit.lisp 49 | (:export)) 50 | -------------------------------------------------------------------------------- /processor.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:org.tymoonnext.clip) 2 | 3 | (defun process (target &rest fields) 4 | "Processes all clip markup on the target with the given FIELDS used to initialise the clipboard." 5 | (process* target (apply #'make-clipboard fields))) 6 | 7 | (defun process* (target clipboard) 8 | "Processes all clip markup on the target with the given CLIPBOARD." 9 | (let ((*target-counter* 0) 10 | (*target* (etypecase target 11 | (plump:node target) 12 | (pathname (plump:parse target)) 13 | (string (plump:parse target))))) 14 | (with-clipboard-bound (clipboard) 15 | (process-node *target*)) 16 | *target*)) 17 | 18 | (defun process-to-string (target &rest fields) 19 | "Same as PROCESS, but automatically performs PLUMP:SERIALIZE on the result to a string." 20 | (with-output-to-string (stream) 21 | (plump:serialize (apply #'process target fields) stream))) 22 | -------------------------------------------------------------------------------- /tag-processors.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:org.tymoonnext.clip) 2 | 3 | (defvar *tag-processors* (make-hash-table :test 'equalp) 4 | "Global registry of tag processors. 5 | 6 | This has to be an EQUALP hash-table with the tag name as keys 7 | and functions that accept one argument (the node) as 8 | values. Binding this variable can be useful to establish local 9 | tags.") 10 | 11 | (defun tag-processor (tag) 12 | "Returns the processor function for the requested tag if one is registered. 13 | Otherwise returns NIL. See *TAG-PROCESSORS*." 14 | (gethash tag *tag-processors*)) 15 | 16 | (defun (setf tag-processor) (func tag) 17 | "Sets the tag-processor bound to the given tag-name to the specified function. 18 | See *TAG-PROCESSORS*." 19 | (setf (gethash tag *tag-processors*) func)) 20 | 21 | (defun process-tag (tag node) 22 | "Processes the specified node as the given tag. 23 | If no tag processor can be found, PROCESS-ATTRIBUTES and PROCESS-CHILDREN is called. 24 | See *TAG-PROCESSORS*." 25 | (let ((func (tag-processor tag))) 26 | (cond 27 | (func (funcall func node)) 28 | (T (process-attributes node) 29 | (process-children node))))) 30 | 31 | (defmacro define-tag-processor (tag (node) &body body) 32 | "Defines a new attribute processor. 33 | 34 | TAG --- A symbol or string that matches the tag name to process (case-insensitive) 35 | NODE --- The node to process is bound to this symbol 36 | BODY ::= form*" 37 | `(setf (tag-processor ,(format NIL "C:~a" tag)) 38 | (setf (tag-processor ,(string tag)) 39 | #'(lambda (,node) ,@body)))) 40 | 41 | (defun process-children (node) 42 | "Calls PROCESS-NODE on all childrens of the passed node. 43 | 44 | This takes some care to make sure that splicing into the childrens array 45 | of the node is possible. However, note that inserting children before the 46 | node that is currently being processed will most likely lead to horrors. 47 | If such functionality is indeed ever needed (I hope not), this system 48 | needs to be rewritten to somehow be able to cope with such scenarios. " 49 | (loop for i from 0 ;; We do this manually to allow growing size of the array. 50 | while (< i (length (plump:children node))) 51 | do (process-node (aref (plump:children node) i))) 52 | node) 53 | 54 | (defun process-node (node) 55 | "Processes the passed node. 56 | 57 | Depending on type the following is done: 58 | PLUMP:ELEMENT PROCESS-TAG is called. 59 | PLUMP:NESTING-NODE PROCESS-CHILDREN is called. 60 | PLUMP:NODE Nothing is done. 61 | T An error is signalled. 62 | Any call to this also increases the *TARGET-COUNTER* regardless of what 63 | is done." 64 | (let ((*target* node)) 65 | (etypecase node 66 | (plump:element (process-tag (plump:tag-name node) node)) 67 | (plump:nesting-node (process-children node)) 68 | (plump:node)) 69 | (incf *target-counter*) 70 | node)) 71 | 72 | (define-tag-processor noop (node) 73 | (process-attributes node)) 74 | 75 | (define-tag-processor let (node) 76 | (let ((table (make-hash-table :test 'equalp))) 77 | (maphash #'(lambda (key val) 78 | (setf (clip table key) 79 | (parse-and-resolve val))) 80 | (plump:attributes node)) 81 | (with-clipboard-bound ((make-instance 'clipboard :env table)) 82 | (process-children node)) 83 | (clrhash (plump:attributes node)) 84 | (process-tag "splice" node))) 85 | 86 | (define-tag-processor iterate (node) 87 | (process-attributes node) 88 | (let ((var (check-sole-attribute node "over"))) 89 | (plump:remove-attribute node "over") 90 | (process-attribute node "iterate" var))) 91 | 92 | (define-tag-processor expand (node) 93 | (process-attributes node) 94 | (process-node node)) 95 | 96 | (defun splice-into (node pos new-children) 97 | (let ((family (plump:children node)) 98 | (childcount (length new-children))) 99 | (when (< 0 childcount) 100 | (array-utils:array-shift family :n (1- childcount) :from pos) 101 | (loop for i from 0 below childcount 102 | for child = (aref new-children i) 103 | do (setf (plump:parent child) node 104 | (aref family (+ pos i)) child))))) 105 | 106 | (define-tag-processor splice (node) 107 | (process-attributes node) 108 | (check-no-unknown-attributes node) 109 | (let* ((first (plump:first-element node)) 110 | (parent (plump:parent node)) 111 | (family (plump:children parent)) 112 | (pos (position node family)) 113 | (childcount (length (plump:children node)))) 114 | (when first (process-node first)) 115 | (cond 116 | ((< 0 childcount) 117 | (splice-into parent pos (plump:children node)) 118 | (setf (plump:parent node) NIL)) 119 | (T (plump:remove-child node))))) 120 | 121 | (define-tag-processor splice-inner (node) 122 | (process-attributes node) 123 | (check-no-unknown-attributes node) 124 | (let* ((first (plump:first-element node)) 125 | (parent (plump:parent node)) 126 | (family (plump:children parent)) 127 | (pos (position node family)) 128 | (childcount (length (plump:children node)))) 129 | (when first (process-node first)) 130 | (cond 131 | ((< 0 childcount) 132 | (splice-into parent pos (loop with totals = (make-array 0 :adjustable T :fill-pointer T) 133 | for child across (plump:children node) 134 | do (loop for inner across (plump:children child) 135 | do (vector-push-extend inner totals)) 136 | finally (return totals))) 137 | (setf (plump:parent node) NIL)) 138 | (T (plump:remove-child node))))) 139 | 140 | (define-tag-processor when (node) 141 | (process-attributes node) 142 | (let ((test (parse-and-resolve (check-sole-attribute node "test")))) 143 | (plump:remove-attribute node "test") 144 | (if test 145 | (process-tag "splice" node) 146 | (plump:remove-child node)))) 147 | 148 | (define-tag-processor unless (node) 149 | (process-attributes node) 150 | (let ((test (parse-and-resolve (check-sole-attribute node "test")))) 151 | (plump:remove-attribute node "test") 152 | (if test 153 | (plump:remove-child node) 154 | (process-tag "splice" node)))) 155 | 156 | (define-tag-processor if (node) 157 | (process-attributes node) 158 | (check-no-unknown-attributes node "test") 159 | (let* ((parent (plump:parent node)) 160 | (pos (position node (plump:children parent))) 161 | (then) (elseif) (else) (test (plump:attribute node "test"))) 162 | (plump:remove-attribute node "test") 163 | ;; Gather elements 164 | (loop for child across (plump:children node) 165 | when (plump:element-p child) 166 | do (when (or (string-equal (plump:tag-name child) "test") 167 | (string-equal (plump:tag-name child) "c:test")) 168 | (setf test (plump:text child))) 169 | (when (and (not then) (or (string-equal (plump:tag-name child) "then") 170 | (string-equal (plump:tag-name child) "c:then"))) 171 | (setf then child)) 172 | (when (or (string-equal (plump:tag-name child) "elseif") 173 | (string-equal (plump:tag-name child) "c:elseif")) 174 | (push child elseif)) 175 | (when (and (not else) (or (string-equal (plump:tag-name child) "else") 176 | (string-equal (plump:tag-name child) "c:else"))) 177 | (setf else child))) 178 | (unless test 179 | (error 'missing-attribute :attribute "test" :node node)) 180 | ;; Parse test 181 | (when (stringp test) 182 | (setf test (resolve-value (read-from-string test)))) 183 | ;; Perform splice 184 | (flet ((splice (node) 185 | (process-attributes node) 186 | (let ((children (plump:children node))) 187 | (when (< 0 (length children)) 188 | (splice-into parent pos children) 189 | ;; We need to splice the first since it is in-place of the if. 190 | (process-node (aref children 0)))))) 191 | (if test 192 | (if then 193 | (splice then) 194 | (plump:remove-child node)) 195 | (loop for child in (nreverse elseif) 196 | when (resolve-value (read-from-string (plump:attribute child "test"))) 197 | do (splice child) 198 | (return T) 199 | finally (if else 200 | (splice else) 201 | (plump:remove-child node))))))) 202 | 203 | (define-tag-processor case (node) 204 | (process-attributes node) 205 | (let* ((parent (plump:parent node)) 206 | (pos (position node (plump:children parent))) 207 | (value (parse-and-resolve (check-sole-attribute node "value")))) 208 | (plump:remove-attribute node "value") 209 | (or (loop for child across (plump:children node) 210 | when (plump:element-p child) 211 | do (when (or (when (plump:has-attribute child "value") 212 | (equal value (parse-and-resolve (plump:attribute child "value")))) 213 | (when (plump:has-attribute child "values") 214 | (loop for val in (read-from-string (format NIL "(~a)" (plump:attribute child "values"))) 215 | thereis (equal value (resolve-value val))))) 216 | (let ((children (plump:children child))) 217 | (when (< 0 (length children)) 218 | (splice-into parent pos children) 219 | ;; We need to splice the first since it is in-place of the if. 220 | (process-node (aref children 0)))) 221 | (return T))) 222 | (plump:remove-child node)))) 223 | 224 | (define-tag-processor using (node) 225 | (process-attributes node) 226 | (with-clipboard-bound ((parse-and-resolve (check-sole-attribute node "value"))) 227 | (plump:remove-attribute node "value") 228 | (process-children node) 229 | (process-tag "splice" node))) 230 | 231 | (define-tag-processor import (node) 232 | (let ((path (merge-pathnames (resolve-attribute node "file")))) 233 | (plump:remove-attribute node "file") 234 | (plump:parse path :root node) 235 | (process-tag "splice" node))) 236 | 237 | (define-tag-processor s (node) 238 | (process-attributes node) 239 | (lquery:$1 node (text (resolve-value (read-from-string (plump:text node))))) 240 | (process-tag "splice" node)) 241 | 242 | (define-tag-processor h (node) 243 | (process-attributes node) 244 | (lquery:$1 node (html (resolve-value (read-from-string (plump:text node))))) 245 | (process-tag "splice" node)) 246 | -------------------------------------------------------------------------------- /test.ctml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIP Test Page 6 | 7 | this, tag{ 8 | could: do some "minification"; 9 | i: suppose; 10 | } 11 | but, again{ 12 | since: "it is a user-defined tag"; 13 | there: is no knowing; 14 | what: exactly; 15 | it: will do; 16 | } 17 | 18 | 19 | 20 | 21 | 62 |
                                                                        63 | 64 | 65 |
                                                                      • 66 | 90 |
                                                                      • 91 |
                                                                        92 |
                                                                        93 |
                                                                        94 | 95 | FOOTER 96 | 97 | 98 | $(function(){ 99 | console.log("I guess this tag could do some special JS analysis and insert missing libs or something, idk."); 100 | }); 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /toolkit.lisp: -------------------------------------------------------------------------------- 1 | (in-package #:org.tymoonnext.clip) 2 | 3 | (defun concat (list) 4 | "Returns a space concatenated string of the passed list." 5 | (format NIL "~{~a~^ ~}" list)) 6 | 7 | (defun make-keyword (name) 8 | "Returns a keyword of the passed name." 9 | (let ((name (string name))) 10 | (or (find-symbol name "KEYWORD") 11 | (intern name "KEYWORD")))) 12 | --------------------------------------------------------------------------------