;;; 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 "~%" ,tag))) (with-tagl ((tag &rest options) &body forms) `(progn (format ,',raw "<~a~@{ ~a=~s~}>" ,tag ,@options) ,@forms (format ,',raw "" ,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