public
Description: BDD for Common Lisp
Homepage:
Clone URL: git://github.com/osteele/cl-spec.git
osteele (author)
Tue Jan 15 07:31:35 -0800 2008
commit  86ec419166158682ab6de9dbe6e4d8458435ddca
tree    98705085f67e3a78d86bbd57d4313892bb1eb478
parent  90633abd1672f695d57edb3e1e06830213ac1a7f
cl-spec / templates.lisp
100644 170 lines (161 sloc) 7.668 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
;;; Copyright 2008 by Oliver Steele. Released under the MIT License.
 
(in-package #:cl-spec)
 
;; A quick and dirty template facilityq that does just enough to
;; enable HTML output from cl-spec. A proper implementation would
;; compile to a function, as well as use a proper parser.
 
(defun copy-template (source-pathname target-pathname dictionary)
  "Copy the contents of SOURCE-PATHNAME to TARGET-PATHNAME, interpolating
${expr} constructs against the environment in DICTIONARY."
  (let* ((template (read-template source-pathname)))
    (with-open-file (s target-pathname :direction :output :if-exists :supersede)
      (apply-template template dictionary s)
      target-pathname)))
 
;; TODO: this be somewhat more efficient by only searching lower
;; positions than any match so far; and much more efficient by
;; compiling the test strings into a regular expression..
(defmacro with-next-substring ((string &optional (pos (gensym "pos")))
                               &body clauses)
  "This is similar to COND, except that if a test form is a string,
it is considered true if it is the first of the string test forms
to appear in STRING.
 
If POS is supplied, it is bound to the index of the position of the
first string to appear in STRING."
  ;; not all positions are used, but it's easier to keep this the same
  ;; length as clauses
  (let ((positions (loop for (token . body) in clauses
                        if (stringp token)
                      collect (gensym)
                      else
                      collect nil))
        (min pos)
        (occurrences (gensym "occurrences")))
    `(let* (,@(loop for (token . body) in clauses
                for pos in positions
                if (stringp token)
                collect `(,pos (search ,token ,string)))
           (,occurrences (delete nil (list ,@(remove nil positions))))
             (,min (if ,occurrences (apply #'min ,occurrences))))
       (cond ,@(loop for (token . body) in clauses
                    for pos in positions
                    if (stringp token)
                    collect `((and ,min (eql ,min ,pos)) ,@body)
                    else
                    collect `(,token ,@body))))))
 
;; FIXME: doesn't know to avoid punctuation in strings
(defun read-template (pathname)
  "Parse PATHNAME into a template. A template is a list of chunks;
each chunk is either a string or character; or a symbol (which is a
key in the application-time dictionary); a DICTIONARY that represents
a FORMAT directive and arguments; or a DICTIONARY that represents the
name of a sequence to iterate, and nested template to recursively apply."
  (let ((stack nil)
          (chunks nil)
          context)
      ;; Hand-crafted state-machine parser. Run away! Run away!
      ;; (More realistically, find a version of yacc or ragel that's
      ;; not too heavy-weight.)
      (labels ((process-line (line &optional (crlf t))
                 (with-next-substring (line pos)
                   ((not pos)
                    (literal line crlf))
                   ((< 0 pos)
                    (process-line (subseq line 0 pos) nil)
                    (process-line (subseq line pos) crlf))
                   ("${"
                    (begin-interpolation (subseq line 2) crlf))
                   ("$}"
                    (end-iteration)
                    (process-line (subseq line 2) crlf))))
               (literal (string crlf)
                 (unless (string= string "")
                   (push string chunks))
                 (if crlf
                     (push #\newline chunks)))
               (begin-interpolation (string crlf)
                 (with-next-substring (string pos)
                   ("}"
                    (compile-interpolation (subseq string 0 pos))
                    (process-line (subseq string (1+ pos)) crlf))
                   (t
                   (begin-iteration string crlf))))
               (compile-interpolation (string)
                 (with-next-substring (string pos)
                   ("|"
                   (push {:type :format
                         :format-string
                         (trim (subseq string 0 pos))
                         :format-args
                         (loop for symbol in (split (subseq string (1+ pos)) #\space)
                            unless (string= "" symbol)
                            collect (intern (string-upcase (trim symbol))))
                         }
                         chunks))
                   (t
                   (push (intern (string-upcase string)) chunks))))
               (begin-iteration (string crlf)
                 (with-next-substring (string pos)
                   ("=>"
                   (let ((variable (intern (string-upcase (trim (subseq string 0 pos)))))
                         (residue (subseq string (+ pos 2))))
                     (push (cons chunks context) stack)
                     (setf chunks nil
                           context {:type :iteration :sequence-variable variable}
                           )
                     (process-line residue crlf)))
                   (t
                   (error "unrecognized interpolation format: ~S" string))))
               (end-iteration ()
                 (let ((iterator context))
                   (destructuring-bind (previous-chunks . previous-context)
                   (pop stack)
                   (setref context :body (nreverse chunks))
                   (setf chunks previous-chunks
                         context previous-context)
                   (push iterator chunks)))))
        (with-open-file (s pathname :direction :input)
          (map-lines #'process-line s)))
      (nreverse chunks)))
 
(defvar *trace-templates* nil
  "Set this to true to debug templates.")
 
(defun apply-template (template &optional (dictionary {}) (output-stream t))
  (if (stringp template)
      (setf template (read-template template)))
  (labels ((lookup (key)
             ;; FIXME: kludge
             (unless (has-key-p dictionary key)
               (setf key (intern (symbol-name key) :keyword)))
             (assert (has-key-p dictionary key) (key)
                     "The environment does not contain ~S (keys = ~S)"
                     key (keys dictionary))
             (ref1 dictionary key)))
    (dolist (chunk template)
      (flet ((field (key)
               (assert (has-key-p chunk key) (key)
                       "The context does not have a field named ~S (keys = ~S)"
                       key (keys chunk))
               (ref1 chunk key)))
        (typecase chunk
          ((or string character)
           (princ chunk output-stream))
          (symbol
           (when *trace-templates*
             (format t "looking up ~S -> ~S~%"
                     chunk (ref1 dictionary chunk)))
           (princ (lookup chunk) output-stream))
          (t
           (case (field :type)
             (:format
              (when *trace-templates*
                (format t "formatting ~S~%" (field :format-args)))
              (let ((format-string (field :format-string))
                    (format-args (mapcar #'lookup (field :format-args))))
                (apply #'format output-stream format-string format-args)))
             (:iteration
              (let ((sequence (lookup (field :sequence-variable)))
                    (body (field :body)))
                (when *trace-templates*
                  (format t "iterating over ~S~%" sequence))
                (dolist (item sequence)
                  (apply-template body item output-stream))))
             (t
              (error "don't know that format")))))))))