;;; HTML generation
;;;
;;; Copyright (C) 2000 by Sam Steingold
;;; This is Free Software, covered by the GNU GPL (v2)
;;; See http://www.gnu.org/copyleft/gpl.html
;;;
;;; $Id: htmlgen.lisp,v 1.15 2002/04/03 19:25:29 sds Exp $
;;; $Source: /cvsroot/clocc/clocc/src/cllib/htmlgen.lisp,v $
(eval-when (compile load eval)
(require :cllib-base (translate-logical-pathname "clocc:src;cllib;base"))
;; `dttm->string' - needed only for `directory-index'
;; (require :cllib-date (translate-logical-pathname "cllib:date"))
;; "Gray streams"
(require :port-gray (translate-logical-pathname "port:gray")))
(in-package :cllib)
(export '(html-stream-out with-html-output with-http-output with-tag
*with-html-output-doctype* http-error directory-index))
;;;
;;; preparation
;;;
(defcustom *html-chars* list '((#\< . "<") (#\> . ">") (#\& . "&"))
"The characters which must be replaced before putting a string into HTML.")
(defclass html-stream-out (fundamental-character-output-stream)
((target-stream :initarg :stream :type stream)))
(defmethod stream-write-char ((stream html-stream-out) ch)
(with-slots (target-stream) stream
(let ((char-cons (assoc ch *html-chars* :test #'char=)))
(if char-cons (write-string (cdr char-cons) target-stream)
(write-char ch target-stream)))))
(defmethod stream-line-column ((stream html-stream-out)) nil)
(defmethod stream-finish-output ((stream html-stream-out))
(with-slots (target-stream) stream (finish-output target-stream)))
(defmethod stream-force-output ((stream html-stream-out))
(with-slots (target-stream) stream (force-output target-stream)))
(defmethod stream-clear-output ((stream html-stream-out))
(with-slots (target-stream) stream (clear-output target-stream)))
(defmethod close ((stream html-stream-out) &rest opts)
(with-slots (target-stream) stream (apply #'close target-stream opts))
(call-next-method))
;;;
;;; HTML generation
;;;
(defvar *with-html-output-doctype*
'("html" "PUBLIC" "\"-//W3C//DTD XHTML 1.0 Strict//EN\""
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\""))
(defmacro with-html-output ((var stream
&key (doctype '*with-html-output-doctype*)
(meta '(:http-equiv "Content-Type"
:content "text/html"))
base comment (title "untitled") (footer t)
head)
&body body)
"Create an `html-stream-out' stream out of STREAM, bind it to VAR.
Two local macros are defined inside this one - `with-tag' and `with-tagl'.
Both print a tag but the second one does not do a `terpri' afterwards."
(with-gensyms ("HTML-" raw mailto)
`(let ((,raw ,stream)
(,mailto (concatenate 'string "mailto:" *user-mail-address*)))
(macrolet ((with-tag ((tag &rest options) &body forms)
`(progn (format ,',raw "<~a~@{ ~a=~s~}>" ,tag ,@options)
,@forms (format ,',raw "~a>~%" ,tag)))
(with-tagl ((tag &rest options) &body forms)
`(progn (format ,',raw "<~a~@{ ~a=~s~}>" ,tag ,@options)
,@forms (format ,',raw "~a>" ,tag))))
(with-open-stream (,var (make-instance 'html-stream-out :stream ,raw))
(format ,raw "~%" ,doctype)
;; print the comment
(format ,raw "~2%"
(getenv "USER") (machine-instance)
(lisp-implementation-type) (lisp-implementation-version)
,comment)
(when ,base
(with-tag (:base :href ,base)))
(with-tag (:html)
(with-tag (:head ,@head)
(with-tag (:meta ,@meta))
(with-tag (:link :rev 'made :href ,mailto))
(with-tag (:title) (princ ,title ,var)))
(with-tag (:body)
,@body
,(when footer
`(when ,footer
(with-tag (:p)
(with-tag (:hr))
(with-tag (:address)
(with-tag (:a :href ,mailto)
(princ *user-mail-address* ,var)))
(with-tagl (:strong) (current-time ,var))))))))))))
(defun crlf (sock)
"Write CR/LF into the socket SOCK."
(write-char (code-char 13) sock)
(write-char (code-char 10) sock))
(defmacro with-http-output ((var raw &rest opts &key keep-alive (debug 0)
(return-code 200) (return-name "OK")
&allow-other-keys)
&body body)
"Write some HTML to an http client on socket stream RAW.
Supplies some HTTP/1.0 headers and calls `with-html-output'."
(with-gensyms ("HTTP-" string stream sock header line dbg alive)
(remf opts :keep-alive) (remf opts :debug)
(remf opts :return-code) (remf opts :return-name)
`(let* ((,sock ,raw)
(,dbg ,debug) (,alive ,keep-alive)
(,string (with-output-to-string (,stream)
(with-html-output (,var ,stream ,@opts) ,@body)))
(,header (list (format nil "HTTP/1.0 ~d ~a"
,return-code ,return-name)
"Content-type: text/html"
(format nil "Content-length: ~d" (length ,string))
(format nil "Connection: ~:[Close~;Keep-Alive~]"
,alive))))
(dolist (,line ,header)
(write-string ,line ,sock)
(when (and ,dbg (> ,dbg 0))
(format t "<- ~a~%" ,line))
(crlf ,sock))
(crlf ,sock)
(write-string ,string ,sock)
(when (and ,dbg (> ,dbg 3))
(format t "<- ~s~%" ,string))
(unless ,alive
(when (and ,dbg (> ,dbg 0))
(format t "~s: closing ~s~%" 'with-http-output ,sock))
(close ,sock)))))
(defun http-error (sock url &key (name "Not Found") (code 404)
(keep-alive nil) (debug 0))
"Report a request error."
(with-http-output (out sock :keep-alive keep-alive :debug debug
:return-code code :return-name name)
(with-tag (:h1) (princ name out))
(with-tag (:p)
(format out "The requested URL ~s was not found on this server." url))))
;;;
;;; this is an example on how to use `with-open-html' and `with-tag'.
;;;
(defun directory-index (dir file &rest opts
&key (title (format nil "Index of ~a" dir)))
"Output the index for a directory."
;; (directory-index "/etc/*" "/tmp/z.html")
(with-html-output (out (open file :direction :output)
:title title :comment
(format nil " Called: (directory-index ~s ~s~{ ~s~})"
dir file opts))
(with-tag (:h1) (format out "Index of ~a" dir))
(with-tag (:table :border "1")
(dolist (fi (sort (directory dir #+cmu :follow-links #+cmu nil)
#'string< :key #'namestring))
(with-tag (:tr)
(with-tag (:th :align "left")
(with-tag (:a :href (namestring fi)) (princ fi out)))
(with-tag (:td :align "right")
(format out "~:d" (ignore-errors (file-size fi))))
(with-tag (:td :align "right")
(princ (ignore-errors (dttm->string (file-write-date fi)
:format :short))
out)))))))
(provide :cllib-htmlgen)
;;; file htmlgen.lisp ends here