-
Notifications
You must be signed in to change notification settings - Fork 0
/
html.lisp
497 lines (386 loc) · 17.1 KB
/
html.lisp
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
(in-package #:com.gigamonkeys.html)
(defvar *pretty* t)
(defvar *html-output* *standard-output*)
(defvar *html-pretty-printer* nil)
(defparameter *xhtml* nil)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Public API
(defmacro with-html-output ((stream &key (pretty *pretty*)) &body body)
`(let* ((*html-output* ,stream)
(*pretty* ,pretty))
,@body))
(defmacro with-html-to-file ((file &key (pretty *pretty*)) &body body)
(with-gensyms (stream)
`(with-open-file (,stream ,file :direction :output :if-exists :supersede)
(with-html-output (,stream :pretty ,pretty)
,@body))))
(defmacro in-html-style (syntax)
(eval-when (:compile-toplevel :load-toplevel :execute)
(case syntax
(:html (setf *xhtml* nil))
(:xhtml (setf *xhtml* t)))))
(defun emit-html (sexp) (process (get-pretty-printer) sexp))
(defmacro html (&whole whole &body body)
(declare (ignore body))
`(if *pretty*
(macrolet ((html (&body body) (codegen-html (sexp->ops body) t)))
(let ((*html-pretty-printer* (get-pretty-printer))) ,whole))
(macrolet ((html (&body body) (codegen-html (sexp->ops body) nil)))
,whole)))
;;; Helpers for public API
(defun get-pretty-printer ()
(or *html-pretty-printer*
(make-instance
'html-pretty-printer
:printer (make-instance 'indenting-printer :out *html-output*))))
(defun codegen-html (ops pretty)
(let ((*pretty* pretty))
`(progn ,@(generate-code (optimize-static-output ops)) nil)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; String escaping
(defparameter *element-escapes* "<>&")
(defparameter *attribute-escapes* "<>&\"'")
(defvar *escapes* *element-escapes*)
(defun escape-char (char)
(case char
(#\& "&")
(#\< "<")
(#\> ">")
(#\' "'")
(#\" """)
(t (format nil "&#~d;" (char-code char)))))
(defun escape (in to-escape)
(flet ((needs-escape-p (char) (find char to-escape)))
(with-output-to-string (out)
(loop for start = 0 then (1+ pos)
for pos = (position-if #'needs-escape-p in :start start)
do (write-sequence in out :start start :end pos)
when pos do (write-sequence (escape-char (char in pos)) out)
while pos))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; indenting-printer
(defclass indenting-printer ()
((out :accessor out :initarg :out)
(beginning-of-line-p :accessor beginning-of-line-p :initform t)
(indentation :accessor indentation :initform 0)
(indenting-p :accessor indenting-p :initform t)))
(defun emit (ip string)
(loop for start = 0 then (1+ pos)
for pos = (position #\Newline string :start start)
do (emit/no-newlines ip string :start start :end pos)
when pos do (emit-newline ip)
while pos))
(defun emit/no-newlines (ip string &key (start 0) end)
(indent-if-necessary ip)
(write-sequence string (out ip) :start start :end end)
(unless (zerop (- (or end (length string)) start))
(setf (beginning-of-line-p ip) nil)))
(defun emit-newline (ip)
(write-char #\Newline (out ip))
(setf (beginning-of-line-p ip) t))
(defun emit-freshline (ip)
(unless (beginning-of-line-p ip) (emit-newline ip)))
(defun indent-if-necessary (ip)
(when (and (beginning-of-line-p ip) (indenting-p ip))
(loop repeat (indentation ip) do (write-char #\Space (out ip)))
(setf (beginning-of-line-p ip) nil)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; html processor interface
(defgeneric raw-string (processor string &optional check-for-newlines))
(defgeneric newline (processor))
(defgeneric freshline (processor))
(defgeneric indent (processor))
(defgeneric unindent (processor))
(defgeneric toggle-indenting (processor))
(defgeneric embed-value (processor value))
(defgeneric embed-code (processor code))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; html-pretty-printer
(defclass html-pretty-printer ()
((printer :accessor printer :initarg :printer)
(tab-width :accessor tab-width :initarg :tab-width :initform 2)))
(defmethod raw-string ((pp html-pretty-printer) string &optional newlines-p)
(if newlines-p
(emit (printer pp) string)
(emit/no-newlines (printer pp) string)))
(defmethod newline ((pp html-pretty-printer))
(emit-newline (printer pp)))
(defmethod freshline ((pp html-pretty-printer))
(when *pretty* (emit-freshline (printer pp))))
(defmethod indent ((pp html-pretty-printer))
(when *pretty*
(incf (indentation (printer pp)) (tab-width pp))))
(defmethod unindent ((pp html-pretty-printer))
(when *pretty*
(decf (indentation (printer pp)) (tab-width pp))))
(defmethod toggle-indenting ((pp html-pretty-printer))
(when *pretty*
(with-slots (indenting-p) (printer pp)
(setf indenting-p (not indenting-p)))))
(defmethod embed-value ((pp html-pretty-printer) value)
(error "Can't embed values when interpreting. Value: ~s" value))
(defmethod embed-code ((pp html-pretty-printer) code)
(error "Can't embed code when interpreting. Code: ~s" code))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Ops buffer
(defun make-op-buffer () (make-array 10 :adjustable t :fill-pointer 0))
(defun push-op (op ops-buffer) (vector-push-extend op ops-buffer))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Compiler
(defclass html-compiler ()
((ops :accessor ops :initform (make-op-buffer))))
(defmethod raw-string ((compiler html-compiler) string &optional newlines-p)
(push-op `(:raw-string ,string ,newlines-p) (ops compiler)))
(defmethod newline ((compiler html-compiler))
(push-op '(:newline) (ops compiler)))
(defmethod freshline ((compiler html-compiler))
(push-op '(:freshline) (ops compiler)))
(defmethod indent ((compiler html-compiler))
(push-op `(:indent) (ops compiler)))
(defmethod unindent ((compiler html-compiler))
(push-op `(:unindent) (ops compiler)))
(defmethod toggle-indenting ((compiler html-compiler))
(push-op `(:toggle-indenting) (ops compiler)))
(defmethod embed-value ((compiler html-compiler) value)
(push-op `(:embed-value ,value ,*escapes*) (ops compiler)))
(defmethod embed-code ((compiler html-compiler) code)
(push-op `(:embed-code ,code) (ops compiler)))
(defun sexp->ops (body)
(loop with compiler = (make-instance 'html-compiler)
for form in body do (process compiler form)
finally (return (ops compiler))))
(defun optimize-static-output (ops)
(let ((new-ops (make-op-buffer)))
(with-output-to-string (buf)
(flet ((add-op (op)
(compile-buffer buf new-ops)
(push-op op new-ops)))
(loop for op across ops do
(ecase (first op)
(:raw-string (write-sequence (second op) buf))
((:newline :embed-value :embed-code) (add-op op))
((:indent :unindent :freshline :toggle-indenting)
(when *pretty* (add-op op)))))
(compile-buffer buf new-ops)))
new-ops))
(defun compile-buffer (buf ops)
"Compile a string possibly containing newlines into a sequence of
:raw-string and :newline ops."
(loop with str = (get-output-stream-string buf)
for start = 0 then (1+ pos)
for pos = (position #\Newline str :start start)
when (< start (length str))
do (push-op `(:raw-string ,(subseq str start pos) nil) ops)
when pos do (push-op '(:newline) ops)
while pos))
(defun generate-code (ops)
(loop for op across ops collect (apply #'op->code op)))
(defgeneric op->code (op &rest operands))
(defmethod op->code ((op (eql :raw-string)) &rest operands)
(destructuring-bind (string check-for-newlines) operands
(if *pretty*
`(raw-string *html-pretty-printer* ,string ,check-for-newlines)
`(write-sequence ,string *html-output*))))
(defmethod op->code ((op (eql :newline)) &rest operands)
(declare (ignore operands))
(if *pretty*
`(newline *html-pretty-printer*)
`(write-char #\Newline *html-output*)))
(defmethod op->code ((op (eql :freshline)) &rest operands)
(declare (ignore operands))
(if *pretty*
`(freshline *html-pretty-printer*)
(error "Bad op when not pretty-printing: ~a" op)))
(defmethod op->code ((op (eql :indent)) &rest operands)
(declare (ignore operands))
(if *pretty*
`(indent *html-pretty-printer*)
(error "Bad op when not pretty-printing: ~a" op)))
(defmethod op->code ((op (eql :unindent)) &rest operands)
(declare (ignore operands))
(if *pretty*
`(unindent *html-pretty-printer*)
(error "Bad op when not pretty-printing: ~a" op)))
(defmethod op->code ((op (eql :toggle-indenting)) &rest operands)
(declare (ignore operands))
(if *pretty*
`(toggle-indenting *html-pretty-printer*)
(error "Bad op when not pretty-printing: ~a" op)))
(defmethod op->code ((op (eql :embed-value)) &rest operands)
(destructuring-bind (value escapes) operands
(if *pretty*
(if escapes
`(raw-string *html-pretty-printer* (escape (princ-to-string ,value) ,escapes) t)
`(raw-string *html-pretty-printer* (princ-to-string ,value) t))
(if escapes
`(write-sequence (escape (princ-to-string ,value) ,escapes) *html-output*)
`(princ ,value *html-output*)))))
(defmethod op->code ((op (eql :embed-code)) &rest operands)
(first operands))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; HTML processor.
(defun process (processor form)
(cond
((special-form-p form) (process-special-form processor form))
((macro-form-p form) (process processor (expand-macro-form form)))
((sexp-html-p form) (process-sexp-html processor form))
((consp form) (embed-code processor form))
(t (embed-value processor form))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Language syntax
(defun sexp-html-p (form)
(or (self-evaluating-p form) (cons-form-p form)))
(defun self-evaluating-p (form)
(and (atom form) (if (symbolp form) (keywordp form) t)))
(defun cons-form-p (form &optional (test #'keywordp))
(and (consp form)
(or (funcall test (car form))
(and (consp (car form)) (funcall test (caar form))))))
(defun macro-form-p (form)
(cons-form-p form #'(lambda (x) (and (symbolp x) (get x 'html-macro)))))
(defun special-form-p (form)
(and (consp form) (symbolp (car form)) (get (car form) 'html-special-operator)))
(defun parse-cons-form (sexp)
(if (consp (first sexp))
(parse-explicit-attributes-sexp sexp)
(parse-implicit-attributes-sexp sexp)))
(defun parse-explicit-attributes-sexp (sexp)
(destructuring-bind ((tag &rest attributes) &body body) sexp
(values tag attributes body)))
(defun parse-implicit-attributes-sexp (sexp)
(loop with tag = (first sexp)
for rest on (rest sexp) by #'cddr
while (and (keywordp (first rest)) (second rest))
when (second rest)
collect (first rest) into attributes and
collect (second rest) into attributes
end
finally (return (values tag attributes rest))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; SEXP-HTML
(defparameter *block-elements*
'(:body :colgroup :dl :fieldset :form :head :html :map :noscript :object
:ol :optgroup :pre :script :select :style :table :tbody :tfoot :thead
:tr :ul))
(defparameter *paragraph-elements*
'(:area :base :blockquote :br :button :caption :col :dd :div :dt :h1
:h2 :h3 :h4 :h5 :h6 :hr :input :li :link :meta :option :p :param
:td :textarea :th :title))
(defparameter *inline-elements*
'(:a :abbr :acronym :address :b :bdo :big :cite :code :del :dfn :em
:i :img :ins :kbd :label :legend :q :samp :small :span :strong :sub
:sup :tt :var))
(defparameter *empty-elements*
'(:area :base :br :col :hr :img :input :link :meta :param))
(defparameter *preserve-whitespace-elements* '(:pre :script :style))
(defun process-sexp-html (processor form)
(if (self-evaluating-p form)
(raw-string processor (escape (princ-to-string form) *escapes*) t)
(process-cons-sexp-html processor form)))
(defun process-cons-sexp-html (processor form)
(when (string= *escapes* *attribute-escapes*)
(error "Can't use cons forms in attributes: ~a" form))
(multiple-value-bind (tag attributes body) (parse-cons-form form)
(emit-open-tag processor tag body attributes)
(emit-element-body processor tag body)
(emit-close-tag processor tag body)))
(defun emit-open-tag (processor tag body-p attributes)
(when (or (paragraph-element-p tag) (block-element-p tag))
(freshline processor))
(raw-string processor (format nil "<~(~a~)" tag))
(emit-attributes processor attributes)
(raw-string processor (if (and *xhtml* (not body-p)) "/>" ">")))
(defun emit-attributes (processor attributes)
(loop for (k v) on attributes by #'cddr do
(raw-string processor (format nil " ~(~a~)='" k))
(let ((*escapes* *attribute-escapes*))
(process processor (if (eql v t) (string-downcase k) v)))
(raw-string processor "'")))
(defun emit-element-body (processor tag body)
(when (block-element-p tag)
(freshline processor)
(indent processor))
(when (preserve-whitespace-p tag) (toggle-indenting processor))
(dolist (item body) (process processor item))
(when (preserve-whitespace-p tag) (toggle-indenting processor))
(when (block-element-p tag)
(unindent processor)
(freshline processor)))
(defun emit-close-tag (processor tag body-p)
(unless (and (or *xhtml* (empty-element-p tag)) (not body-p))
(raw-string processor (format nil "</~(~a~)>" tag)))
(when (or (paragraph-element-p tag) (block-element-p tag))
(freshline processor)))
(defun block-element-p (tag) (find tag *block-elements*))
(defun paragraph-element-p (tag) (find tag *paragraph-elements*))
(defun empty-element-p (tag) (find tag *empty-elements*))
(defun preserve-whitespace-p (tag) (find tag *preserve-whitespace-elements*))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Special operators
(defmacro define-html-special-operator (name (processor &rest other-parameters) &body body)
`(eval-when (:compile-toplevel :load-toplevel :execute)
(setf (get ',name 'html-special-operator)
(lambda (,processor ,@other-parameters) ,@body))))
(defun process-special-form (processor form)
(apply (get (car form) 'html-special-operator) processor (rest form)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Macros
(defmacro define-html-macro (name (&rest args) &body body)
(multiple-value-bind (attribute-var args)
(parse-html-macro-lambda-list args)
(if attribute-var
(generate-macro-with-attributes name attribute-var args body)
(generate-macro-no-attributes name args body))))
(defun generate-macro-with-attributes (name attribute-args args body)
(with-gensyms (attributes form-body)
(if (symbolp attribute-args) (setf attribute-args `(&rest ,attribute-args)))
`(eval-when (:compile-toplevel :load-toplevel :execute)
(setf (get ',name 'html-macro-wants-attributes) t)
(setf (get ',name 'html-macro)
(lambda (,attributes ,form-body)
(destructuring-bind (,@attribute-args) ,attributes
(destructuring-bind (,@args) ,form-body
,@body)))))))
(defun generate-macro-no-attributes (name args body)
(with-gensyms (form-body)
`(eval-when (:compile-toplevel :load-toplevel :execute)
(setf (get ',name 'html-macro-wants-attributes) nil)
(setf (get ',name 'html-macro)
(lambda (,form-body)
(destructuring-bind (,@args) ,form-body ,@body))))))
(defun parse-html-macro-lambda-list (args)
"Parse a lambda list that can include the &attributes lambda-list-keyword."
(let ((attr-cons (member '&attributes args)))
(values
(cadr attr-cons)
(nconc (ldiff args attr-cons) (cddr attr-cons)))))
(defun expand-macro-form (form)
(if (or (consp (first form))
(get (first form) 'html-macro-wants-attributes))
(multiple-value-bind (tag attributes body) (parse-cons-form form)
(funcall (get tag 'html-macro) attributes body))
(destructuring-bind (tag &body body) form
(funcall (get tag 'html-macro) body))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Special Forms
(define-html-special-operator :print (processor form)
(cond
((self-evaluating-p form)
(warn "Redundant :print of self-evaluating form ~s" form)
(process-sexp-html processor form))
(t
(embed-value processor form))))
(define-html-special-operator :format (processor &rest args)
(if (every #'self-evaluating-p args)
(process-sexp-html processor (apply #'format nil args))
(embed-value processor `(format nil ,@args))))
(define-html-special-operator :progn (processor &rest body)
(loop for exp in body do (process processor exp)))
(define-html-special-operator :noescape (processor &rest body)
(let ((*escapes* nil))
(loop for exp in body do (process processor exp))))
(define-html-special-operator :attribute (processor &rest body)
(let ((*escapes* *attribute-escapes*))
(loop for exp in body do (process processor exp))))
(define-html-special-operator :newline (processor)
(newline processor))