;;;;; Copyright (c) 2009-2010, Martin Loetzsch
;;;;; All rights reserved.
;;;;; Redistribution and use in source and binary forms, with or
;;;;; without modification, are permitted provided that the following
;;;;; conditions are met:
;;;;; Redistributions of source code must retain the above copyright
;;;;; notice, this list of conditions and the following disclaimer.
;;;;; Redistributions in binary form must reproduce the above
;;;;; copyright notice, this list of conditions and the following
;;;;; disclaimer in the documentation and/or other materials provided
;;;;; with the distribution.
;;;;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
;;;;; CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
;;;;; INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
;;;;; MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
;;;;; DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
;;;;; CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
;;;;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
;;;;; LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
;;;;; USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
;;;;; AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
;;;;; LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
;;;;; IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
;;;;; THE POSSIBILITY OF SUCH DAMAGE.
(in-package :gtfl)
(export '(*gtfl-address* *gtfl-port* start-gtfl
gtfl-out replace-element-content append-to-element
reset-gtfl *reset-functions*
who who2s who-lambda
define-css define-js
make-id-string))
;; we require the version of hunchentoot to be >= 1.1.0
(unless (string>= (asdf:component-version (asdf:find-system :hunchentoot)) "1.1.0")
(error "Please install a hunchentoot with version >= 1.1.0"))
;; #########################################################
;; web server definition and startup
;; ---------------------------------------------------------
;; Address and port. Change this to something else when needed
(defvar *gtfl-address* "localhost")
(defvar *gtfl-port* 8000)
;; the hunchentoot server instance
(defvar *gtfl-server* nil)
;; starts the web server
(defun start-gtfl ()
(if *gtfl-server*
(format t "~% ***** gtfl already running at http://~a:~d *****~%"
*gtfl-address* *gtfl-port*)
(progn
(setf *gtfl-server*
(start (make-instance 'acceptor
:port *gtfl-port* :address *gtfl-address*)))
(format t "~% ***** started gtfl at http://~a:~d *****~%"
*gtfl-address* *gtfl-port*))))
;; we opt for instead of
(setf *attribute-quote-char* #\")
;; we want to see when something goes wrong
(setq *catch-errors-p* nil)
;; #########################################################
;; ajax processor and dispatch table
;; ---------------------------------------------------------
(defvar *ajax-processor* (make-instance 'ajax-processor))
(setq *dispatch-table*
(list 'dispatch-easy-handlers
(create-ajax-dispatcher *ajax-processor*)))
;; #########################################################
;; cl-who shortcuts
;; ---------------------------------------------------------
;; we always use *standard-output* as the stream for writing
;; html expressions to. This way we avoid passing streams
;; around and it is very easy to test html generating
;; functions.
(defmacro who (&rest expressions)
"shortcut for with-html-output"
`(progn
(with-html-output (*standard-output*) ,@expressions)
nil))
(defmacro who2s (&rest expressions)
"shortcut for with-html-output-to-string"
`(with-html-output-to-string (*standard-output*)
,@expressions))
(defmacro who-lambda (&rest expressions)
"makes an anonymous function that generates cl-who output for expression"
`(lambda ()
(progn (with-html-output (*standard-output*) ,@expressions) nil)))
;; #########################################################
;; define-css and define-js
;; ---------------------------------------------------------
;; collections of css and javascript code fragments that are
;; automatically included in the head of the client html page
(defvar *css-definitions* (make-hash-table))
(defvar *js-definitions* (make-hash-table))
(defun define-css (id css)
"adds css code fragments to the client page"
(setf (gethash id *css-definitions*) css))
(defun define-js (id js)
(setf (gethash id *js-definitions*) js))
;; #########################################################
;; client/ server communication
;; ---------------------------------------------------------
;; This is a list of "requests". The lisp side puts stuff into that list.
;; The client regularily polls this list and
;; handles them in function 'processRequestQueue' (see javascript below).
(defvar *requests* nil)
;; empty the requests list when the page is reset
(defun reset-requests () (setf *requests* nil))
;; the client side script frequently polls the requests and processes them
(define-js 'request-handling "
// handles the response from lisp and then polls the next requests after 200 ms
function requestsCallBack (result) {
for (i=0;i
")
;; the client html page
(define-easy-handler (client-page :uri "/") ()
(reset-gtfl)
(with-html-output-to-string (*standard-output* nil :prologue t)
(:html :xmlns "http://www.w3.org/1999/xhtml"
(:head
(:title "gtfl")
(princ (generate-prologue *ajax-processor*))
(:script :type "text/javascript" "//")
(:style :type "text/css"
(loop for definition being the hash-values of *css-definitions*
do (write-string definition))))
(:body :onLoad "window.setTimeout(getRequests,500);"
(:div :id "content" :class "gtfl")
(:p :class "gtfl"
(:a :class "button" :href "javascript:ajax_reset();" "reset"))))))
;; #########################################################
;; how to get stuff on the client page
;; ---------------------------------------------------------
(defmacro gtfl-out (&rest expressions)
"Adds some content to the bottom of the client page. expressions
can be anything that is ok in cl-who's with-html-output macro."
`(progn
(push (who2s (:add-element ,@expressions)) *requests*)
nil))
(defmacro replace-element-content (id &rest expressions)
"replaces the content of the element with id (a string) by expressions"
`(progn
(push (who2s (:replace-element-content (:id (str ,id))
(:content ,@expressions)))
*requests*)
nil))
(defmacro append-to-element (id &rest html-expressions)
"appends expressions to the element with id (a string)"
`(progn
(push (who2s (:append-to-element (:id (str ,id))
(:content ,@html-expressions)))
*requests*)
nil))
;; #########################################################
;; make-id-string
;; ---------------------------------------------------------
;; counters for strings
(defvar *nid-table* (make-hash-table :test #'equal))
(defun make-id-string (&optional (base "id"))
"Creates an uniquely numbered id string"
(format nil "~a-~d" base (if (gethash base *nid-table*)
(incf (gethash base *nid-table*))
(setf (gethash base *nid-table*) 1))))
;; #########################################################
;; resetting gtfl
;; ---------------------------------------------------------
(defvar *reset-functions* nil
"a list of functions that are called whenever gtfl is reset,
i.e. when the 'reset' button is clicked or when (reset-gtfl) is
called. For resetting own stuff in these cases add your
reset-function here, for example
(pushnew #'my-reset-function *reset-functions*)")
(pushnew #'reset-requests *reset-functions*)
(defun reset-gtfl ()
"Clears the content area of the client page and resets things"
(mapcar #'funcall *reset-functions*)
(push " " *requests*)
nil)
;; the ajax handler for the 'reset' button
(defun-ajax reset () (*ajax-processor*)
(reset-gtfl)
nil)