r3734: *** empty log message ***
[lml.git] / base.lisp
1 ;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*-
2 ;;;; *************************************************************************
3 ;;;; FILE IDENTIFICATION
4 ;;;;
5 ;;;; Name:          lml.cl
6 ;;;; Purpose:       Lisp Markup Language functions
7 ;;;; Programmer:    Kevin M. Rosenberg
8 ;;;; Date Started:  Aug 2002
9 ;;;;
10 ;;;; $Id: base.lisp,v 1.3 2003/01/04 20:42:02 kevin Exp $
11 ;;;;
12 ;;;; This file, part of LML, is Copyright (c) 2002 by Kevin M. Rosenberg
13 ;;;;
14 ;;;; LML users are granted the rights to distribute and use this software
15 ;;;; as governed by the terms of the GNU General Public License v2
16 ;;;; (http://www.gnu.org/licenses/gpl.html)
17 ;;;; *************************************************************************
18
19 (declaim (optimize (debug 3) (speed 3) (safety 3) (compilation-speed 0)))
20 (in-package :lml)
21
22 (defun html4-prologue-string ()
23   "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">")
24
25 (defun xml-prologue-string ()
26   "<?xml version=\"1.0\" encoding=\"iso-8859-1\" standalone=\"yes\"?>")
27
28 (defun xhtml-prologue-string ()
29   "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">")
30
31 (defvar *print-spaces* nil)
32 (defvar *indent* 0)
33 (defun reset-indent ()
34   (setq *indent* 0))
35
36 (defun lml-print (str &rest args)
37   (when (streamp *html-output*)
38     (when *print-spaces* (indent-spaces *indent* *html-output*))
39     (if args
40         (apply #'format *html-output* str args)
41       (princ str *html-output*))
42     (when *print-spaces* (format *html-output* "~%"))
43     (values)))
44
45 (defmacro lml-line (str &rest args)
46   `(lml-print ,str ,@args))
47
48 (defun lml-print-date (date)
49   (lml-print (date-string date)))
50
51 (defmacro lml-exec-body (&body forms)
52   `(progn
53     ,@(mapcar
54        #'(lambda (form)
55            (etypecase form
56              (string
57               `(lml-print ,form))
58              (number
59               `(lml-print "~D" ,form))
60              (symbol
61               (when form
62               `(lml-print ,form)))
63              (cons
64               form)))
65        forms)))
66
67 (defmacro with-attr-string (tag attr-string &body body)
68   (let ((attr (gensym)))
69   `(let ((,attr ,attr-string))
70      (lml-print "<~(~A~)~A>" ',tag
71               (if (and (stringp ,attr) (plusp (length ,attr)))
72                   (format nil "~A" ,attr)
73                 ""))
74      (incf *indent*)
75      (lml-exec-body ,@body)
76      (decf *indent*)
77      (lml-print "</~(~A~)>" ',tag))))
78
79 (defun one-keyarg-string (key value)
80   "Return attribute string for keys"
81   (format nil "~(~A~)=\"~A\"" key
82           (typecase value
83             (symbol
84              (string-downcase (symbol-name value)))
85             (string
86              value)
87             (t
88              (eval value)))))
89
90 (defmacro with-keyargs (tag keyargs &body body)
91   (let ((attr (gensym))
92         (kv (gensym)))
93   `(progn
94      (let ((,attr '()))
95        (dolist (,kv ,keyargs)
96          (awhen (cdr ,kv)
97            (push (one-keyarg-string (car ,kv) it) ,attr)))
98        (with-attr-string ,tag (list-to-spaced-string (nreverse ,attr)) ,@body)))))
99
100 (defmacro bind-one-keyarg (keyarg)
101   `(list ,(car keyarg) ,(cdr keyarg)))
102
103 (defmacro bind-all-keyargs (keyargs)
104   "Convert a list of keyarg pairs and convert eval/bind arguments"
105   (let* ((npairs (length keyargs))
106          (syms (make-array npairs))
107          (ipair 0)
108          (ipair2 0))
109     (declare (dynamic-extent syms))
110     (dotimes (i npairs)
111       (setf (aref syms i) (gensym)))
112     `(let ,(mapcar #'(lambda (ka)
113                        (prog1
114                            (list (aref syms ipair) (cdr ka))
115                          (incf ipair)))
116                    keyargs)
117       (list ,@(mapcar #'(lambda (ka)
118                           (prog1
119                               `(cons ,(car ka) ,(aref syms ipair2))
120                             (incf ipair2)))
121                       keyargs)))))
122
123 (defmacro with (tag &rest args)
124   "Return a list of keyargs and also the body of LML form"
125   (let ((body '())
126         (keyargs '())
127         (bound-keyargs (gensym)))
128     (do* ((n (length args))
129           (i 0 (+ 2 i))
130           (arg (nth i args) (nth i args))
131           (value (when (< (1+ i) n)
132                    (nth (1+ i) args))
133                  (when (< (1+ i) n)
134                    (nth (1+ i) args))))
135          ((or (not (keyword-symbol? arg))
136               (>= i n))
137           (dotimes (j (- n i))
138             (push (nth (+ i j) args) body)))
139       (push (cons arg value) keyargs))
140     (setq keyargs (nreverse keyargs))
141     (setq body (nreverse body))
142     `(let ((,bound-keyargs ,(macroexpand `(bind-all-keyargs ,keyargs))))
143       ,(macroexpand `(with-keyargs ,tag ,bound-keyargs ,@body)))))
144
145
146 (defmacro keyargs-string (&rest args)
147   "Returns a string of attributes and values. Result contains a leading space."
148   (let ((keyarg-list '()))
149     (loop for ( name val ) on args by #'cddr
150           do
151           (when val
152             (push (one-keyarg-string name val) keyarg-list)))
153     (list-to-spaced-string (nreverse keyarg-list))))
154   
155
156 (defmacro xhtml-prologue ()
157   `(progn
158      (lml-print "~A~%" (xml-prologue-string))
159      (lml-print "~A~%" (xhtml-prologue-string))))
160
161 (defmacro link (dest &body body)
162   `(with a :href ,dest ,@body))
163
164 (defmacro link-c (class dest &body body)
165   `(with a :href ,dest :class (quote ,class) ,@body))
166
167 (defmacro img (dest &key class id alt style width height align)
168   (let ((attr
169          (eval `(keyargs-string :class ,class :id ,id :alt ,alt :style ,style
170                              :width ,width :height ,height :align ,align))))
171     `(lml-print ,(format nil "<img src=\"~A\"~A />" dest attr))))
172
173 (defmacro input (&key name class id type style size maxlength value checked)
174   (let ((attr
175          (eval `(keyargs-string :name ,name :class ,class :id ,id :style ,style
176                              :size ,size :maxlength ,maxlength :value ,value
177                              :type ,type :checked ,checked))))
178     `(lml-print ,(format nil "<input~A />" attr))))
179
180 (defmacro meta (name content)
181   `(with meta :name ,name :content ,content))
182
183 (defmacro meta-key (&key name content http-equiv)
184   `(with meta :name ,name :content ,content :http-equiv ,http-equiv))
185
186 (defmacro br ()
187   `(lml-print "<br />"))
188
189 (defmacro hr ()
190   `(lml-print "<hr />"))
191
192 (defmacro lml-tag-macro (tag)
193   `(progn
194      (defmacro ,tag (&body body)
195        `(with ,',tag ,@body))
196      (export ',tag)))
197
198 (defmacro lml-tag-class-macro (tag)
199   (let ((name (intern (format nil "~A-~A" tag :c))))
200     `(progn
201        (defmacro ,name (&body body)
202          `(with ,',tag :class (quote ,(car body)) ,@(cdr body)))
203        (export ',name))))
204
205 (eval-when (:compile-toplevel :load-toplevel :execute)
206   (defparameter *macro-list*
207     '(a div span h1 h2 h3 h4 h5 h6 i b p li ul ol table tbody td tr body head
208           html title pre tt u dl dt dd kbd code form))
209   (export '(link link-c br hr img input meta meta-key))
210   (export *macro-list*))
211
212 (loop for i in *macro-list*
213       do
214       (eval `(lml-tag-macro ,i))
215       (eval `(lml-tag-class-macro ,i)))
216
217 (defmacro print-page (title &body body)
218   `(html
219     (head
220      (title ,title))
221     (body ,@body)))
222
223 (defmacro page (out-file &body body)
224   `(with-open-file (*html-output*
225                     (lml-file-name ,out-file :output)
226                     :direction :output
227                     :if-exists :supersede)
228      (xhtml-prologue)
229      (html :xmlns "http://www.w3.org/1999/xhtml"
230        ,@body)))
231
232 (defun new-string ()
233   (make-array 1024 :fill-pointer 0 :adjustable t :element-type 'character))
234
235 (set-macro-character #\[
236   #'(lambda (stream char)
237       (declare (ignore char))
238       (let ((forms '())
239             (curr-string (new-string))
240             (paren-level 0)
241             (got-comma nil))
242         (declare (type fixnum paren-level))
243         (do ((ch (read-char stream t nil t) (read-char stream t nil t)))
244             ((eql ch #\]))
245           (if got-comma
246               (if (eql ch #\()
247                   ;; Starting top-level ,(
248                   (progn
249                     #+cmu
250                     (setf curr-string (coerce curr-string `(simple-array character (*))))
251         
252                     (push `(lml-print ,curr-string) forms)
253                     (setq curr-string (new-string))
254                     (setq got-comma nil)
255                     (vector-push #\( curr-string)
256                     (do ((ch (read-char stream t nil t) (Read-char stream t nil t)))
257                         ((and (eql ch #\)) (zerop paren-level)))
258                       (when (eql ch #\])
259                         (format *trace-output* "Syntax error reading #\]")
260                         (return nil))
261                       (case ch
262                         (#\(
263                          (incf paren-level))
264                         (#\)
265                          (decf paren-level)))
266                       (vector-push-extend ch curr-string))
267                     (vector-push-extend #\) curr-string)
268                     (let ((eval-string (read-from-string curr-string))
269                           (res (gensym)))
270                       (push
271                        `(let ((,res ,eval-string))
272                           (when ,res
273                             (lml-print ,res)))
274                        forms))
275                     (setq curr-string (new-string)))
276                 ;; read comma, then non #\( char
277                 (progn
278                   (unless (eql ch #\,)
279                     (setq got-comma nil))
280                   (vector-push-extend #\, curr-string) ;; push previous command
281                   (vector-push-extend ch curr-string)))
282             ;; previous character is not a comma
283             (if (eql ch #\,)
284                 (setq got-comma t)
285               (progn
286                 (setq got-comma nil)
287                 (vector-push-extend ch curr-string)))))
288
289         #+cmu
290         (setf curr-string (coerce curr-string `(simple-array character (*))))
291         
292         (push `(lml-print ,curr-string) forms)
293         `(progn ,@(nreverse forms)))))
294
295