make distribution by default
[rrq/hourglass.git] / manager / expand-string.lsp
1 ;; @module expand-string.lsp
2 ;; @author Ralph Ronnquist, Real Thing Entertainment Pty. Ltd.
3 ;; @location http://www.realthing.com.au/files/newlisp/expand-string.lsp
4 ;; @version 1.3, 2015-09-14 Added markdown blocks
5 ;; @version 1.2, 2015-08-22
6 ;; @description Inclusion module providing string templating using expansion.
7 ;
8 ;; This is an inclusion module that provides an <expand-string>
9 ;; function to process a string template and replace key tokens as
10 ;; declared in a rules list of token-to-replacement associations with
11 ;; their associated values. It offers a similar function to <expand>,
12 ;; but for strings, and using string pattern match without
13 ;; tokenization to determine the replacement points. Further, it
14 ;; evaluates the rule value parts to make the replacements.
15 ;;
16 ;; The <expand-string> module was developed as a means to separate
17 ;; logic and rendering in newlisp CGI scripting, and thereby
18 ;; facilitate a higher degree of modularisation, in the aim of
19 ;; increasing the maintainability.  It supports the design principle
20 ;; where the handling of a request is directed to a newlisp "logic
21 ;; script" that implements the request logic, and finishes by response
22 ;; rendering via an <expand-file> call.
23 ;;
24 ;; By virtue of the replacement value evaluation, the response
25 ;; rendering is more than a substitution of keywords. Rather it
26 ;; selectively transitions back into newlisp evaluation, e.g., to pick
27 ;; up particulars from the logic context, and transform and combine
28 ;; them for rendering purposes.
29 ;;
30 ;; This makes it easy to achieve a consistent appearance across all
31 ;; response pages, by sharing inclusion fragments. For example,
32 ;; response templates may include a common ingress fragment, common
33 ;; component fragments, and a common egress fragment. Such fragments
34 ;; may be included generically by using &lt;?newlisp ?&gt; or
35 ;; &lt;EVAL&gt;..&lt;/EVAL&gt; phrases to invoke <expand-file> for the
36 ;; inclusion fragments, or it may be done specifically by special
37 ;; replacement rules.
38 ;
39 ;; <hr/>
40 ;
41 ;; @syntax (expand-string <text> <rules>)
42 ;; The <expand-string> function processes the given text for the
43 ;; occurrences of the rule keys, and replaces these with the values
44 ;; obtained by evaluating the associated value expressions. The result
45 ;; is the new string with replacements. Note that a value expression
46 ;; may affect variable <txt>, which is the rest of the input following
47 ;; the expanding key, to optionally consume additional text in the
48 ;; replacement. See function <.expand-map> for an example.
49 ;
50 ;; @syntax (expand-file file rules)
51 ;; The <expand-file> function reads the file and expand it using
52 ;; <expand-string> with the rules.
53 ;
54 ;; @syntax (.expand-eval <ctx> <end>)
55 ;; This function is intended as expansion value function for an
56 ;; <expand-string> rule, to implement template expression
57 ;; evaluation. The <ctx> parameter tells the context for symbol
58 ;; creations. The optional <end> parameter tells the end of the
59 ;; replacement fragment. This function extracts the text fragment
60 ;; until the nearest <end> text, then evaluates this with
61 ;; <eval-string>, makes the result a string, and uses that as value to
62 ;; replace the whole block. See <default-expand-rules> below how a
63 ;; rule using this function may look.
64 ;
65 ;; @syntax (.expand-cond <ctx> <mid> <end>)
66 ;;
67 ;; This function is intended as an expansion value function for an
68 ;; <expand-string> rule, to implement template fragment conditional
69 ;; cascade. The optional <mid> parameter tells the pattern that
70 ;; divides the cascaded parts, which is "&lt;ELSEIF/&gt;" by default,
71 ;; and the optional <end> parameter tells the end of the whole cascade
72 ;; fragment, which is "&lt;/IF&gt;" by default. Note that the <mid>
73 ;; pattern is a divider between the conditional parts. and thereby
74 ;; both the end of the preceding part, and the beginning of the
75 ;; succeeding part.
76 ;;
77 ;; The cascaded parts are processed in order, for selecting one to
78 ;; include and expand recursively. To this end, each part starts with
79 ;; an s-expression, that gets evaluated to determine whether the part
80 ;; should be included or not. If the value is nil, the the part is
81 ;; ignored, and the processing continues with the next part. If the
82 ;; value is non-nil, the rest of the part is expanded recursively via
83 ;; <expand-string>, and this is then returned as the expansion result
84 ;; of the cascade. If none of the parts is selected, the cascade
85 ;; expansion results in the empty string.
86 ;
87 ;; @syntax (.expand-map <ctx> <end>)
88 ;; This function is intended as expansion value function for an
89 ;; <expand-string> rule, to implement template fragment
90 ;; repetition. The optional <ctx> parameter tells the context for
91 ;; symbol creations. The optional <end> parameter tells the end of the
92 ;; fragment portion, which is "&lt;/MAP&gt;" by default. The function
93 ;; pulls two s-expression from the template using <read-expr>. The
94 ;; first is a list of keys, and the second a list of binding lists for
95 ;; those keys. The rest of the fragment is then expanded recursively,
96 ;; repeatedly, with the keys having their subsequent bindings, and the
97 ;; block is replaced by the concatenation of these results. See
98 ;; <default-expand-rules> below how a rule using this function may
99 ;; look.
100 ;
101 ;; @syntax (.expand-markdown <ctx> <end>)
102 ;
103 ;; This function is intended as expansion value function for an
104 ;; <expand-string> rule, to implement template fragment markdown
105 ;; processing. It first passes the fragment for recursive
106 ;; expand-string processing, the uses the
107 ;; @link http://daringfireball.net/projects/markdown markdown
108 ;; program to translate the fragment into html.
109 ;
110 ;; @syntax (mapv <name>)
111 ;; This is a utility macro to both lookup a name in the current rules,
112 ;; and bind its value as the value for that variable. This is
113 ;; typically used for referring to a &lt;MAP&gt; variable.
114 ;
115 ;; @syntax default-expand-rules
116 ;; This constant holds a few default rules for using repetition end
117 ;; expression evaluation. Currently set to the following:
118 ;; <pre>
119 ;; (constant 'default-expand-rules
120 ;;           '(("&lt;MAP1&gt;" (.expand-map MAIN "&lt;/MAP1&gt;"))
121 ;;             ("&lt;MAP2&gt;" (.expand-map MAIN "&lt;/MAP2&gt;"))
122 ;;             ("&lt;MAP3&gt;" (.expand-map MAIN "&lt;/MAP3&gt;"))
123 ;;             ("&lt;MAP&gt;" (.expand-map MAIN "&lt;/MAP&gt;"))
124 ;;             ("&lt;EVAL&gt;" (.expand-eval MAIN "&lt;/EVAL&gt;"))))
125 ;;             ("&lt;?newlisp" (.expand-eval MAIN "?&gt;"))
126 ;;             ("&lt;markdown&gt;" (.expand-eval MAIN "&lt;/markdown&gt;"))
127 ;;            ))
128 ;; </pre> These default rules obviously favours HTML templates.
129 ;;
130 ;; <center>&sect;</center>
131 ;; <h2>Examples</h2>
132 ;; <b>Example:</b> The following is an illustration of <expand-string>
133 ;; using <.expand-map>:
134 ;; <pre>(expand-string
135 ;;          "&lt;MAP&gt;(A B) '((1 2) (3 4)) A B B A&lt;/MAP&gt;"
136 ;;          '(("&lt;MAP&gt;" (.expand-map)) ))
137 ;; </pre>
138 ;; The example results in the string " 1 2 2 1 3 4 4 3".
139 ;;
140 ;; Note that the binding lists expression is evaluated in the given
141 ;; context, or MAIN, if nil is given. Thus, the rule above is
142 ;; equivalent with the following: <tt>(.expand-map MAIN "&lt;/MAP&gt;")</tt>
143 ;;
144 ;; Note also that the fragment blocks cannot be nested. To achieve
145 ;; nested repetition, use several tag pairs, as in the following rule set:
146 ;; <pre> '(("&lt;MAP1&gt;" (.expand-map nil "&lt;/MAP1&gt;"))
147 ;;   ("&lt;MAP2&gt;" (.expand-map nil "&lt;/MAP2&gt;"))
148 ;;   ("&lt;MAP3&gt;" (.expand-map nil "&lt;/MAP3&gt;")) )</pre>
149 ;; In that case, the outer expansion keys may be used in the inner
150 ;; repetition although they are not actually bound to the values.
151 ;;
152 ;; <b>Example:</b>
153 ;; <pre>(expand-string
154 ;;          {&lt;EVAL&gt;(first (exec "uname -mrs"))&lt;/EVAL&gt;}
155 ;;          default-expand-rules )</pre>
156 ;; This example results in the machine details as reported by the
157 ;; <uname> program with the <-mrs> command line argument.
158 ;;
159 ;; <b>Example:</b> This example illustrates HTML rendering, with a
160 ;; template file that includes certain keys for expansion. In this
161 ;; case I have a list if paragraps as value of variable <texts>, and
162 ;; want them inserted nicely into an HTML page. Note that the spaces
163 ;; following the two s-expressions in the &lt;MAP&gt;..&lt;/MAP&gt;
164 ;; construct are compulsory, and they get consumed by the <read-expr>
165 ;; function.
166 ;
167 ;; <pre> @PAGEDOCTYPE@
168 ;; &lt;html&gt;&lt;head&gt;&lt;title&gt;@TITLE@&lt;/title&gt;&lt;/head&gt;
169 ;; &lt;body&gt;&lt;h1&gt;@TITLE@&lt;/h1&gt;
170 ;; &lt;MAP&gt;(text) texts &lt;p&gt;text&lt;/p&gt;&lt;/MAP&gt;
171 ;; &lt;/body&gt;&lt;/html&gt;</pre>
172 ;
173 ;; This template would be used in a context that provides suitable
174 ;; expansion rules for the "@PAGEDOCTYPE@" and "@TITLE@" keys, as well
175 ;; as the default "&lt;MAP&gt;" expansion rule.
176 ;;
177 ;; <b>Example:</b> This example illustrates the use of a conditional
178 ;; cascade template fragment. It expands to one of the sentences
179 ;; depending on the conditions.
180 ;;
181 ;; <pre> &lt;IF&gt; (&gt; (setf cnt (length (index fresh strawberries))) 10)
182 ;; Mostly fresh strawberries.
183 ;; &lt;ELSEIF/&gt; (&gt; cnt 5) Many strawberries are fresh.
184 ;; &lt;ELSEIF/&gt; (&gt; cnt) At least some strawberries are fresh.
185 ;; &lt;ELSEIF/&gt; true None of the strawberries are fresh.
186 ;; &lt;/IF&gt;</pre>
187 ;
188 ;; Thus in the example, if the list <strawberries> has more than 10
189 ;; elements qualified as <fresh>, then the expansion is "Mostly fresh
190 ;; strawberries.". As a side effect, the count is cached by the first
191 ;; evaluation, regardless of the value, and this is then used in the
192 ;; subsequent expressions.
193 ;;
194 ;; <b>Example:</b> This example illustrates the use of a markdown,
195 ;; where the template is somewhat more readable.
196 ;; <pre>
197 ;; &lt;markdown&gt;
198 ;; # This is a H1 header<br>
199 ;; A first paragraph.<br>
200 ;; ## Then a H2 header<br>
201 ;; And a second paragraph with [this link](http://www.realthing.com.au) to somewhere.<br>
202 ;; * A list item
203 ;; * and another list item
204 ;;   1. with a numbered sub item in the item
205 ;;   1. and a second sub item<br>
206 ;; and so on...
207 ;; &lt;/markdown&gt;
208 ;; </pre>
209 ;; Note that the markdown block is expanded recursively before being
210 ;; passed to the markdown processor. Thus, that inner expansion may
211 ;; result in markdown as well as raw HTML (which the markdown
212 ;; processor digests without ado).
213
214 ############################################################
215
216 (define (rule-key rule)
217   (replace "[\\?*.()]" (first rule) (string "\\" $it) 0))
218
219 (define (expand-string txt (rules default-expand-rules))
220   (if (null? rules) txt
221     (let ((pat (string "(" (join (map string (map rule-key rules)) "|") ")"))
222           (out "") (i 0))
223       (while (setf i (find pat txt 0))
224         (extend out (0 i txt))
225         (setf txt ((+ i (length $1)) txt))
226         (extend out (string (eval (lookup $1 rules)))))
227       (extend out txt))))
228
229 (define (expand-file file (rules default-expand-rules))
230   ;(write-line 2 (string "expand-file " file " " rules))
231   (expand-string (read-file file) rules))
232
233 (define (.expand-map ctx (end "</MAP>")) ; uses txt rules
234   (let ((A (map term (read-expr txt (or ctx MAIN) nil 0)))
235         (dlist (read-expr txt (or ctx MAIN) nil $count))
236         (frag ($count (- (find end txt nil $count) $count) txt))
237         (out ""))
238     (setf txt ((+ $count (length frag) (length end)) txt))
239     (dolist (d (eval dlist))
240       (extend out (expand-string frag (extend (map list A d) rules))))
241     out))
242
243 (define (.expand-eval ctx (end "</EVAL>")) ; uses txt rules
244   (let ((frag (0 (find end txt nil 0) txt)))  
245     (setf txt ((+ (length frag) (length end)) txt))
246     (string (eval-string frag))))
247
248 (define (.expand-cond ctx (mid "<ELSEIF/>") (end "</IF>"))
249   (let ((frag (0 (find end txt nil 0) txt)) (fi 0) (ti 0) (ex nil) (out ""))
250     (setf txt ((+ (length frag) (length end)) txt))
251     (while (and (null? ex) (< fi (length frag)))
252       (setf ex (read-expr frag (or ctx MAIN) nil fi))
253       (setf fi $count)
254       (setf ti (or (find mid frag nil fi) (length frag)))
255       (if (setf ex (eval ex))
256             (setf out (fi (- ti fi) frag))
257           (setf fi (+ ti (length mid)))))
258     (expand-string out rules)))
259
260 ; Process a block for markdown after recursive expansion
261 (define (.expand-markdown ctx end) ; uses <txt>
262   (let ((frag (0 (find end txt nil 0) txt)))
263     (setf txt ((+ (length frag) (length end)) txt))
264     (letn (f (string "/tmp/markdown-" (date-value)))
265       (when (exec (format "/usr/bin/markdown --html4tags > %s" f)
266                   (expand-string frag rules))
267         (read-file f)))))
268
269 (constant 'default-expand-rules
270           '(("<MAP1>" (.expand-map MAIN "</MAP1>"))
271             ("<MAP2>" (.expand-map MAIN "</MAP2>"))
272             ("<MAP3>" (.expand-map MAIN "</MAP3>"))
273             ("<MAP>" (.expand-map MAIN "</MAP>"))
274             ("<EVAL>" (.expand-eval MAIN "</EVAL>"))
275             ("<IF>" (.expand-cond MAIN "<ELSEIF/>" "</IF>"))
276             ("<?newlisp" (.expand-eval MAIN "?>"))
277             ("<markdown>" (.expand-markdown MAIN "</markdown>"))
278             ))
279
280 (define-macro (mapv name) (set name (lookup (string name) rules)))
281
282 (global 'expand-string 'expand-file '.expand-map 'default-expand-rules 'mapv)
283
284 "expand-string.lsp"
285