[ht-ajax-cvs] r1 - doc static test

Xan Lopez xlopez at common-lisp.net
Fri Nov 14 21:17:43 UTC 2008


Author: xlopez
Date: Fri Nov 14 21:17:43 2008
New Revision: 1

Log:
Initial commit, version 0.0.7.


Added:
   ChangeLog
   LICENSE
   doc/
   doc/ht-ajax.html
   ht-ajax-test.asd
   ht-ajax.asd
   ht-ajax.lisp
   join-strings.lisp
   jsmin.lisp
   optimization.lisp
   packages.lisp
   processor-dojo.lisp
   processor-lokris.lisp
   processor-prototype.lisp
   processor-simple.lisp
   processor-yui.lisp
   static/
   static/lokris.js
   static/prototype.js
   test/
   test/packages.lisp
   test/test-ajax.tmpl.html
   test/test-ht-ajax.lisp
   utils.lisp
   version.lisp

Added: ChangeLog
==============================================================================
--- (empty file)
+++ ChangeLog	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,25 @@
+* HT-AJAX ChangeLog
+** Version 0.0.7 <2007-03-08>
+**** Fixed bug in jsmin.lisp
+
+** Version 0.0.6  <2007-02-20>
+**** Added unexport function
+**** Added the "virtual .js file" feature
+**** Code reorganization
+**** Changed the JS-FILE-URI parameter to JS-FILE-URIS
+**** Added support for Yahoo UI library
+**** Support for returning complex objects to Javascript with JSON
+
+** Version 0.0.5  /unreleased/
+**** Added error calbacks to the public interface
+**** Added the option to compress the generated Javascript 
+     to minimize the download size
+**** Processor for Dojo Toolkit ( http://dojotoolkit.org/ )
+	
+** Version 0.0.4  <2007-02-09>
+**** Applied patch by Pierre THIERRY for better control over 
+     Javascript debugging 
+**** Code & documentation cleanups
+
+** Version 0.0.3  <2007-02-05>
+**** Added Prototype processor

Added: LICENSE
==============================================================================
--- (empty file)
+++ LICENSE	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,29 @@
+Copyright (c) 2007, Ury Marshak
+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.  
+
+    * Neither the name of the author(s) nor the names of contributors
+      may be used to endorse or promote products derived from this
+      software without specific prior written permission.
+
+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.

Added: doc/ht-ajax.html
==============================================================================
--- (empty file)
+++ doc/ht-ajax.html	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,523 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"> 
+<!--
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+-->
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
+  <title>HT-AJAX - Common Lisp AJAX framework for Hunchentoot</title>
+  <meta name="description" content="HT-AJAX is an AJAX framework in Common Lisp for Hunchentoot web server under BSD license, portable, is known to run on SBCL and Lispworks. Supports such Javascript libraries as Lokris, Prototype, YUI and Dojo. Supports JSON, virtual Javascript files,Javascript minimization." />
+  <meta name="keywords" content="HT-AJAX,AJAX,Common Lisp,Lisp,Hunchentoot,framework,opensource,BSD license,library,SBCL,Lispworks,Lokris,Prototype,Dojo,YUI,JSON, virtual Javascript files, Javascript minimization" />
+
+  <style type="text/css">
+  pre { padding:5px; background-color:#e0e0e0; 
+        margin-left: 1cm; margin-right: 3cm;
+        margin-top: 0cm; margin-bottom: 0cm;
+  }
+  h3, h4 { text-decoration: underline; }
+  a { text-decoration: none; }
+  a.noborder { border:0px }
+  a.noborder:hover { border:0px }  a.none { border:1px solid white; }
+  a.none { border:1px solid white; }
+  a.none:hover { border:1px solid white; }
+
+  a.note { border:1px solid white; text-decoration: bold; }
+
+  a { border:1px solid white; }
+  a:hover   { border: 1px solid black; } 
+  a.noborder { border:0px }
+  a.noborder:hover { border:0px }
+  .browser { background-color:#f0f0f0; border: 1px solid black;
+        margin-left: 1cm; margin-right: 3cm;
+        margin-top: 0cm; margin-bottom: 0cm;
+  }
+  </style>
+</head>
+
+<body bgcolor="white">
+
+<h2> HT-AJAX - AJAX framework for Hunchentoot</h2>
+
+<div style="margin-left: 4cm; margin-right: 4cm;">
+<br /> <br /><h3><a name="abstract" class="none">Abstract</a></h3>
+
+<p>
+HT-AJAX is a small <a href="http://www.lisp.org">Common Lisp</a> framework that is designed to ease dynamic 
+interaction of your web pages with the server. It runs under the 
+<a  href="http://weitz.de/hunchentoot/">Hunchentoot</a> web server by Dr. Edi Weitz.
+</p>
+
+<p>
+Basically it allows 'exporting' of your lisp functions so that they can be easily called
+from the Javascript code on your web page. The details of the data transfer can be
+handled by different backends, or as we'll call them 'AJAX processors'. At the moment three
+such processors are supported: one simple, built-in, that generates code inside the
+web page and does not require external libraries. The others are using a javascript
+<a href="#supported-libraries">library</a>, such as 
+<a href="http://prototypejs.org/">Prototype</a> or
+<a href="http://dojotoolkit.org/">Dojo</a>
+<a href="#supported-libraries">(full list)</a>.
+</p>
+
+<p>
+The code comes with
+a <a href="http://www.opensource.org/licenses/bsd-license.php">BSD-style
+license</a> so you can basically do with it whatever you want.
+</p>
+
+<p>
+<font color="red">Download shortcut:</font> <a href="http://85.65.214.241/misc/ht-ajax.tar.gz">http://85.65.214.241/misc/ht-ajax.tar.gz</a>.
+</p>
+</div>
+
+<br /> <br /><h3><a class="none" name="contents">Contents</a></h3>
+<ol>
+  <li><a href="#download">Download and installation</a></li>
+  <li><a href="#support">Support</a></li>
+  <li><a href="#getting-started">Getting started (mini-tutorial)</a></li>
+  <li><a href="#choosing-processor">Choosing the right ajax-processor</a></li>
+  <li><a href="#generated-js">Using the generated Javascript functions</a></li>
+  <li><a href="#dictionary">The HT-AJAX dictionary</a>
+    <ol>
+      <li><a href="#make-ajax-processor"><code>make-ajax-processor</code></a></li>
+      <li><a href="#export-func"><code>export-func</code></a></li>
+      <li><a href="#unexport-func"><code>unexport-func</code></a></li>
+      <li><a href="#defun-ajax"><code>defun-ajax</code></a></li>
+      <li><a href="#generate-prologue"><code>generate-prologue</code></a></li>
+      <li><a href="#get-handler"><code>get-handler</code></a></li>
+    </ol>
+  </li>
+  <li><a href="#supported-libraries">Supported Javascript libraries</a></li>
+  <li><a href="#portability">Portability</a></li>
+  <li><a href="#notes">Notes</a></li>
+  <li><a href="#ack">Acknowledgements</a></li>
+</ol>
+
+<br /> <br /><h3><a class="none" name="download">Download and installation</a></h3>
+
+HT-AJAX together with this documentation can be downloaded from 
+<a href="http://85.65.214.241/misc/ht-ajax.tar.gz">http://85.65.214.241/misc/ht-ajax.tar.gz</a>. 
+The current version is 0.0.7 .
+
+<br />
+If you have <a href="http://www.cliki.net/ASDF-Install">ASDF-Install</a> working you can use
+it to install HT-AJAX:
+<pre>
+(require 'asdf-install)
+(asdf-install:install :ht-ajax)
+</pre>
+
+<br />
+Otherwise, download and untar the distribution and use the normal procedure for your system to
+install a library that comes with an ASDF system definition. For example, make an appropriate
+symbolic link to the ht-ajax.asd file and run
+<pre>(require 'asdf)
+(asdf:oos 'asdf:load-op :ht-ajax)</pre>
+
+<br /> <br /><h3><a class="none" name="support">Support</a></h3>
+<p>Questions, bug reports, requests, criticism and just plain information that you found
+this package useful (or useless) are to be sent to 
+<span>ur</span>ym<span>@t</span>wo-by<span>te</span>s<span>.</span>c<span>o</span>m
+
+</p>
+
+<br /> <br /><h3><a class="none" name="getting-started">Getting started (mini-tutorial)</a></h3>
+<p>In this tutorial we assume that the reader is reasonably familiar with Hunchentoot 
+and AJAX.</p>
+
+
+<p>So, let's suppose we already have some lisp code working under Hunchentoot and start
+modifying it to use AJAX.
+Note that normally we'll <b>(use-package :ht-ajax)</b>, so that we won't have to prefix 
+the symbols with <b>ht-ajax:</b>, but here we'll still do it to show clearly which symbols 
+come from the HT-AJAX package.</p>
+
+
+<p>At first some setup:</p>
+<pre>
+(defparameter *ajax-handler-url* "/hunchentoot/some/suitable/uri/ajax-hdlr")
+</pre>
+<p>Here we select some URL that will handle our AJAX requests. Later we'll need to arrange
+for an appropriate handler to be called when we get a request for this URL (and all
+URLs starting with it). Replace the URL with whatever makes sense for your application</p>
+
+<br /> <br />
+<p>After this we create an instance of so-called ajax-processor that will handle all our
+AJAX needs. One ajax-processor per application should be enough. We pass the following 
+parameters: <b>:type :lokris</b> to select which backend to use, in this case it's
+the <a href="http://www.ajax-buch.de/lokris/">Lokris</a> library. Also we pass the
+<b>:server-uri</b> that we've selected and the <b>:js-file-uris</b> that shows where
+to find the appropriate library file, lokris.js in this case (the URL may be relative
+to the URL of the page):</p>
+<pre>
+(defparameter *ajax-processor* (ht-ajax:make-ajax-processor
+                                :type :lokris
+                                :server-uri *ajax-handler-url*
+                                :js-file-uris "static/lokris.js"))
+</pre>
+
+<br /> 
+
+<p>Now we create the function that we want to be called from the web page:</p>
+<pre>
+(ht-ajax:defun-ajax testfunc (command) (*ajax-processor* :method :post)
+    (prin1-to-string (eval (read-from-string command nil))))
+</pre>
+<p>We've used here the <b>defun-ajax</b> macro that performs two tasks: defines the function 
+as per <b>defun</b> and 'exports' it - makes available to the web page's javascript. 
+This fragment could've been written more verbosely as:</p>
+<pre>
+(defun testfunc (command)
+    (prin1-to-string (eval (read-from-string command nil))))
+
+(ht-ajax:export-func *ajax-processor* 'testfunc :method :post)
+</pre>
+<p>The function itself contains the code to evaluate the string parameter <i>command</i> 
+and return the result as a string. (It's possible to return more complex objects to the
+Javascript code by using <a href="http://www.json.org/">JSON</a>). 
+While processig the request HT-AJAX will call Hunchentoot's function 
+<a href="http://weitz.de/hunchentoot/#no-cache"><b>no-cache</b></a> to make sure the browser 
+will make a request to the server each time and not cache the results, so we don't have to 
+do it here. If we want to manually control the caching we can pass <b>:allow-cache t</b> 
+parameter when exporting the function.
+</p>
+
+<p>The only thing left to prepare the server side of the things
+is to create the dispatcher for our <b>*ajax-handler-url*</b> and to add it to
+Hunchentoot's dispatch table. The specifics of this can vary, but it might include something 
+like:
+</p>
+<pre>(create-prefix-dispatcher *ajax-handler-url* (ht-ajax:get-handler *ajax-processor*)
+</pre>
+<p> The call to <b>(ht-ajax:get-handler *ajax-processor*)</b> returns the handler that the 
+handler URL
+needs to be dispatched to.<a href="#note1"><sup>[1]</sup></a></p></p>
+<br /> 
+<p>Now we need to make sure that the dynamic web pages we generate are correctly set up. 
+This means that the result of the call <b>(ht-ajax:generate-prologue *ajax-processor*)</b>
+needs to be inserted somewhere in the HTML page (right after the <b><body></b> tag 
+seems like
+a good place). Once again how to do this depends on the facilities that are used for HTML 
+generation.
+For example when using <a href="http://weitz.de/html-template/">HTML-TEMPLATE</a> we'll have 
+something like the following in our template:</p>
+<pre>
+<body>
+<!-- TMPL_VAR prologue -->
+</pre>
+<p>and then pass the output of <b>(ht-ajax:get-handler *ajax-processor*)</b> as the 
+<i>prologue</i> parameter to the <b>fill-and-print-template</b> call.<a href="#note2"><sup>[2]</sup></a></p>
+
+<p>After that, whatever means for HTML generation we're using, let's put the following HTML 
+somewhere in the page:</p>
+<pre>
+
+<table width="50%">
+  <tr>
+    <td colspan="2">
+      <span id="result"> <i>no results yet</i>
+      </span>
+    </td>
+  </tr>
+  <tr>
+    <td width="70%">
+      <input type="text" size="70" name="command" id="command" />
+    </td>
+    <td>
+      <input type="button" value="Eval" onclick="javascript:command_clicked();"/>
+    </td>
+  </tr>
+</table>
+
+</pre>
+
+<p>This will produce something like:</p>
+<div class="browser">
+<table width="50%">
+  <tr>
+    <td colspan="2">
+      <span id="result"> <i>no results yet</i>
+      </span>
+    </td>
+  </tr>
+  <tr>
+    <td width="70%">
+      <input type="text" size="70" name="command" id="command" />
+    </td>
+    <td>
+      <input type="button" value="Eval" />
+    </td>
+  </tr>
+</table>
+</div>
+
+<br /> 
+Now write into the template the javascript function that will be called when you click the 
+button:
+<pre>
+<script type="text/javascript">
+function command_clicked(txt) {
+// get the current value of the text input field
+var command = document.getElementById('command').value;
+
+// call function testfunc on the server with the parameter
+// command and set the element with the id 'result' to the return
+// value of the function
+ajax_testfunc_set_element('result', command); 
+}
+</script>
+</pre>
+<p>The function <b>ajax_testfunc_set_element</b> that we call here was generated for us by 
+HT-AJAX.
+It takes one required parameter - the id of the element that we want to be set to the result of
+the remote call. All other parameters will be passed to corresponding exported lisp function, 
+<b>testfunc</b> in this case (watch out for the number of arguments). The resulting string will
+be assigned to the <b>.innerHTML</b> property of the element with the id 'result'.
+</p>
+
+<br />
+
+<p>This is it. Now to save the files, compile the lisp code, make sure the 
+Hunchentoot server is started and open the web page in the browser. 
+Enter something like <b>(+ 1 42)</b> in the text field and click 'Eval'.
+If all's well the results of the evaluation will be displayed.</p>
+
+<br />
+
+
+
+<br /> <br /><h3><a class="none" name="choosing-processor">Choosing the right ajax-processor</a></h3>
+<p>If one of the <a href="#supported-libraries">supported libraries</a> 
+(like <a href="http://prototypejs.org/">Prototype</a>) is 
+already used in Javascript code then the decision is is easy - use the appropriate ajax-processor.
+Otherwise consider if you need to make server calls using HTTP GET or POST method. If GET works 
+for you then SIMPLE may be enough, otherwise for POST method use Lokris.
+</p>
+
+<br /> <br /><h3><a class="none" name="generated-js">Using the generated Javascript functions</a></h3>
+<p>
+Exporting the function (and later including the result of the GENERATE-PROLOGUE call in the 
+web page) makes available two functions for the Javascript code on the page. Assuming the
+exported function was called TESTFUNC and the standard prefix (ajax_) was used they are:
+</p>
+<pre>
+ajax_testfunc_callback(<i>callback_specification</i>, [params for the server's TESTFUNC....])
+</pre>
+and
+<pre>
+ajax_testfunc_set_element(element_id, [params for the server's TESTFUNC....])
+</pre>
+<p>
+Both functions will call the server-side function TESTFUNC, the <b>ajax_testfunc_callback</b>
+version will call the provided <i>callback</i> function with the result of the server call 
+as a single parameter, the <b>ajax_testfunc_set_element</b> version with find the document
+element with the id <i>element_id</i> and set it's <b>innerHTML</b> to the result of the 
+server call.<br />
+The result of the server call is normally a string and is passed to the callback as-is, 
+unless the Content-Type header was set to <b>application/json</b> which is the 
+<a href="http://www.iana.org/assignments/media-types/application/">official</a> IANA
+media type. In case of JSON the result is evaluated using "unsafe"
+<a href="#note3"><sup>[3]</sup></a> <b>eval</b> 
+call and the resulting object is passed to the callback.
+<br />
+</p>
+<p><a class="none" name="callback-specification"></a>
+The <i>callback_specification</i> parameter can be used 
+to specify two kinds of callacks (at the same time). The success callback function will
+be called 
+after a successful interaction with the server and passed the server call result 
+as a parameter. The error callback function will be called in case of an error
+and passed a string with the information about the error. 
+<br />So the <i>callback_specification</i> can take the following forms:</p>
+<ul>
+ <li>A function. This will be used as a success callback.</li> 
+ <li>An array of two functions. The first will be used as a success callback,
+the second as an error callback.</li>
+ <li>An object. The value of the "success" property of the object
+will be used as a success callback, the value of the "error" property - as an
+error callback. </li> 
+</ul>
+
+
+<br /> <br /><h3><a class="none" name="dictionary">The HT-AJAX dictionary</a></h3>
+
+
+<!-- Entry for MAKE-AJAX-PROCESSOR -->
+
+<p><br />[Function]<br /><a class="none" name='make-ajax-processor'><b>make-ajax-processor</b> <i><tt>&rest</tt> rest <tt>&key</tt> type <tt>&allow-other-keys</tt></i> => <i>new-ajax-processor</i></a></p>
+<blockquote><br />
+
+Creates an ajax-processor object. Parameters: <br />
+   TYPE - selects the kind of ajax-processor to use (should be 
+          one of:SIMPLE or :LOKRIS, :PROTOTYPE, :YIU or :DOJO) (required). <br />
+   SERVER-URI - url that the ajax function calls will use (required). <br />
+   JS-FILE-URIS - a list of URLs on your server of the .js files that the
+                used library requires , such as lokris.js or prototype.js 
+                (parameter required for all processors except :SIMPLE). If
+                only one file needs to be included then instead of a list a single 
+                string may be passed. Also if this parameter is a string that ends 
+                in a forward slash ( #\/ ) then it is assumed to be a directory 
+                and the default file names for the processor are appended to it. <br />
+   AJAX-FUNCTION-PREFIX - the string to be prepended to the generated js functions,
+                (default prefix is "ajax_"). <br />
+   JS-DEBUG - enable the Javascript debugging function debug_alert(). Overrides
+              such parameters as JS-COMPRESSION and VIRTUAL-FILES. <br />
+   JS-COMPRESSION - enable Javascript compression of the generated code
+                 to minimize the download size. <br />
+   VIRTUAL-JS-FILE - enable creation of virtual Javascript file instead of
+                inline Javascript code that may be
+                cached on the client to minimize traffic. <br />
+</blockquote>
+
+<!-- End of entry for MAKE-AJAX-PROCESSOR -->
+
+
+<!-- Entry for EXPORT-FUNC -->
+
+<p><br />[Generic function]<br /><a class="none" name='export-func'><b>export-func</b> <i>processor funcallable <tt>&key</tt> method name content-type allow-cache</i> =>| </a></p>
+<blockquote><br />
+
+Makes the function designated by FUNCALLABLE exported (available to call from js)
+Parameters: <br />
+  METHOD - :get (default) or :post (:post is not supported under SIMPLE processor). <br />
+  NAME - export the function under a different name. <br />
+  CONTENT-TYPE - Value of Content-Type header so set on the reply 
+                (default: text/plain). <br />
+  ALLOW-CACHE - (default nil) if true then HT-AJAX will not call NO-CACHE function and
+                allow to control cache manually. <br />
+  JSON - (default nil) if true, the function returns a JSON-encoded object that will
+         be decoded on the client and passed to the callback as an object<br />
+<br />
+Exporting the function (and later including the result of the GENERATE-PROLOGUE call in the 
+web page) makes available two functions for the Javascript code on the page:
+<b>ajax_testfunc_callback</b> and <b>ajax_testfunc_set_element</b>. See 
+"<a href="#generated-js">Using the generated Javascript functions</a>" 
+for more details.
+</blockquote>
+
+<!-- End of entry for EXPORT-FUNC -->
+
+<!-- Entry for UNEXPORT-FUNC -->
+
+<p><br />[Generic function]<br /><a class="none" name='unexport-func'><b>unexport-func</b> <i>processor symbol-or-name</i> =>| </a></p>
+<blockquote><br />
+
+Removes the previously exported function, should be called
+with either the name (string) under which it was exported or the symbol
+designating the function
+
+</blockquote>
+
+<!-- End of entry for UNEXPORT-FUNC -->
+
+
+<!-- Entry for DEFUN-AJAX -->
+
+<p><br />[Macro]<br /><a class="none" name='defun-ajax'><b>defun-ajax</b> <i>name params (processor <tt>&rest</tt> export-args) declaration* statement*</i> </a></p>
+<blockquote><br />
+
+Macro, defining a function exported to AJAX
+Example: (defun-ajax func1 (arg1 arg2) (*ajax-processor*)
+   (do-stuff))
+
+</blockquote>
+
+<!-- End of entry for DEFUN-AJAX -->
+
+
+<!-- Entry for GENERATE-PROLOGUE -->
+
+<p><br />[Generic function]<br /><a class="none" name='generate-prologue'><b>generate-prologue</b> <i>processor <tt>&key</tt> use-cache</i> => <i>html-prologue</i></a></p>
+<blockquote><br />
+
+Generates the necessary HTML+JS to be included in the web page.
+Provides caching if USE-CACHE is true (default).
+
+</blockquote>
+
+<!-- End of entry for GENERATE-PROLOGUE -->
+
+
+<!-- Entry for GET-HANDLER -->
+
+<p><br />[Generic function]<br /><a class="none" name='get-handler'><b>get-handler</b> <i>processor</i> => <i>handler</i></a></p>
+<blockquote><br />
+
+Get the hunchentoot handler for AJAX url. 
+The url that was passed as the SERVER-URI parameter (and all URLs starting with it)
+should be dispatched to this handler.
+
+</blockquote>
+
+<!-- End of entry for GET-HANDLER -->
+
+
+<br /> <br /><h3><a class="none" name="supported-libraries"></a>Supported Javascript libraries</h3>
+<ul>
+  <li>"Simple" - does not use an external library
+  </li>
+  <li><a href="http://www.ajax-buch.de/lokris/">Lokris</a> version 1.2 2006/08/02
+  </li>
+  <li><a href="http://prototypejs.org/">Prototype</a> version 1.5.0
+  </li>
+  <li><a href="http://dojotoolkit.org/">Dojo</a> version 0.4.1
+  </li>
+  <li><a href="http://developer.yahoo.com/yui/">Yahoo! User Interface (YUI) Library</a> versions 0.12.2 and 2.2.0
+  </li>
+</ul>
+
+
+<br /> <br /><h3><a class="none" name="portability">Portability</a></h3>
+<p> At the moment HT-AJAX is known to run on SBCL and Lispworks, but it aims to be
+portable across all the implementations Hunchentoot runs on. Please report all incompatibilities.
+</p>
+
+<br /> <br /><h3><a class="none" name="notes">Notes</a></h3>
+<p><a class="note" name="note1">[1]</a>
+When not using CREATE-PREFIX-DISPATCHER, note that not only the <b>SERVER-URI</b> itself 
+but also all the URLs starting with it need to be dispatched to the handler in order for
+"virtual .js files" mechanism to function.
+</p>
+
+<p><a class="note" name="note2">[2]</a>
+By default HTML-TEMPLATE escapes some characters while expanding. In the case of the prologue of
+HT-AJAX there's no need to do it since HT-AJAX already wraps the generated Javascript code in the
+proper CDATA sections (which also makes it possible to generate documents compliant with for
+example XHTML Strict requirements). So one of the options is to wrap the template expansion 
+in the following binding:
+</p>
+
+<pre>
+(let ((*string-modifier* #'CL:IDENTITY))
+      <i>...template expansion...</i>  )
+</pre>
+<br />
+
+<p><a class="note" name="note3">[3]</a>
+The word "unsafe" means that it might not be generally safe to evaluate 
+arbitrary Javascript code coming from an untrusted source; in our case it's ok since 
+we control both the client and the server.
+</p>
+
+<br /> <br /><h3><a class="none" name="ack">Acknowledgements</a></h3>
+
+<p>
+This documentation was prepared with the help of 
+<a href="http://weitz.de/documentation-template/">DOCUMENTATION-TEMPLATE</a> 
+by Edi Weitz (the code was hacked to run on SBCL).<br />
+The initial inspiration for the SIMPLE processor came from Richard Newman's <a href="http://www.cliki.net/cl-ajax">CL-AJAX</a> which is designed for use with Araneida.
+</p>
+
+<br />
+<a href="index.html">Back to the Lisp page</a>
+<br />
+<p style="font-size: x-small; text-color:#404040">
+;;; Copyright (c) 2007, Ury Marshak
+<br />
+</p>
+</body>
+</html>

Added: ht-ajax-test.asd
==============================================================================
--- (empty file)
+++ ht-ajax-test.asd	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,18 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+
+(asdf:defsystem :ht-ajax-test
+  :description "Test files for HT-AJAX"
+  :version "0.0.1"
+  :serial t
+  :components ((:module "test"
+                        :serial t
+                        :components ( (:file "packages")
+                                      (:file "test-ht-ajax"))))
+  :depends-on (:html-template
+               :ht-ajax))

Added: ht-ajax.asd
==============================================================================
--- (empty file)
+++ ht-ajax.asd	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,27 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+
+(asdf:defsystem :ht-ajax
+  :description "AJAX for Hunchentoot"
+  :version "0.0.7"
+  :serial t
+  :components ((:file "packages")
+               (:file "optimization")
+               (:file "version")
+               (:file "jsmin")
+               (:file "ht-ajax")
+               (:file "utils")
+               (:file "join-strings")
+               (:file "processor-simple")
+               (:file "processor-lokris")
+               (:file "processor-prototype")
+               (:file "processor-dojo")
+               (:file "processor-yui")
+               )
+  :depends-on (:hunchentoot
+               :cl-ppcre))

Added: ht-ajax.lisp
==============================================================================
--- (empty file)
+++ ht-ajax.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,342 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax)
+
+(declaim #.*optimization*)
+
+;;
+
+(defclass ajax-processor ()
+  ((exported-funcs :initform nil :accessor exported-funcs)
+   (server-uri :initarg :server-uri :accessor server-uri)
+   (hunchentoot-handler :accessor hunchentoot-handler)
+   (cached-prologue :accessor cached-prologue :initform nil)
+   (js-debug :accessor js-debug :initarg :js-debug :initform nil)
+   (js-compression :accessor js-compression :initarg :js-compression :initform nil)
+   (ajax-function-prefix :initarg :ajax-function-prefix
+                         :accessor ajax-function-prefix :initform "ajax_")
+   (default-content-type :initarg :default-content-type
+     :accessor default-content-type :initform "text/plain; charset=\"utf-8\"")
+   (default-reply-external-format :initarg :default-reply-external-format
+     :accessor default-reply-external-format :initform hunchentoot::+utf-8+)
+   (virtual-js-file :initarg :virtual-js-file
+                    :accessor virtual-js-file :initform nil)
+   (virtual-files :accessor virtual-files :initform nil))
+  
+  (:documentation "The class containing all ajax-related handling"))
+
+
+(defmethod initialize-instance :after ((processor ajax-processor) &key)
+  (setf (exported-funcs processor)  (make-hash-table :test 'equal))
+  (unless (and (slot-boundp processor 'server-uri)
+               (server-uri processor))
+    (error "Initializing AJAX-PROCESSOR without SERVER-URI.")))
+
+
+;;
+
+
+(defgeneric handle-request (processor)
+  (:documentation "Process the incoming request from hunchentoot"))
+
+
+(defgeneric export-func (processor funcallable
+                                   &key method name content-type allow-cache)
+  (:documentation "Makes the function designated by FUNCALLABLE exported (available to call from js)
+Parameters:
+  METHOD - :get (default) or :post (:post is not supported under SIMPLE processor)
+  NAME - export the function under a different name
+  CONTENT-TYPE - Value of Content-Type header so set on the reply (default: text/plain)
+  ALLOW-CACHE - (default nil) if true then HT-AJAX will not call NO-CACHE function and
+                allow to control cache manually
+  JSON - (default nil) if true, the function returns a JSON-encoded object that will
+         be decoded on the client and passed to the callback
+"))
+
+(defgeneric unexport-func (processor symbol-or-name)
+  (:documentation "Removes the previously exported function, should be called
+with either the name (string) under which it was exported or the symbol
+designating the function"))
+
+
+(defmacro defun-ajax (name params (processor &rest export-args) &body body)
+  "Macro, defining a function exported to AJAX
+Example: (defun-ajax func1 (arg1 arg2) (*ajax-processor*)
+   (do-stuff))"
+  (let ((f (gensym)))
+      `(let ((,f (defun ,name ,params , at body)))
+	 (if ,f (export-func ,processor ',name , at export-args)))))
+
+
+(defgeneric generate-prologue (processor &key use-cache)
+  (:documentation "Generates the necessary HTML+JS to be included in the web page.
+Provides caching if USE-CACHE is true (default)"))
+
+
+(defgeneric %generate-includes (processor)
+  (:documentation "Internal generic function to be implemented in specific 
+ajax processor"))
+
+(defgeneric %generate-js-code (processor)
+  (:documentation "Internal generic function to be implemented in specific 
+ajax processor"))
+
+
+(defgeneric get-handler (processor)
+  (:documentation "Get the hunchentoot handler for AJAX url. 
+The url that was passed as the SERVER-URI parameter should be
+dispatched to this handler."))
+
+
+(defgeneric reset-prologue-cache (processor)
+  (:documentation ""))
+
+
+(defgeneric js-function-name (processor function-name)
+  (:documentation ""))
+
+(defgeneric prepare-js-ajax-function (processor fun-name js-fun-name
+                                     &rest rest &key method &allow-other-keys)
+  (:documentation ""))
+
+;;; 
+
+(defmethod export-func ((processor ajax-processor) funcallable
+                        &key (method :get) name content-type allow-cache json)
+  (let ((func-name (or name
+                       (when (symbolp funcallable)
+                         (symbol-name funcallable)))))
+    (unless func-name
+      (error "Name not provided for ~A" funcallable))
+    
+    (setf (gethash (string-upcase func-name) (exported-funcs processor))
+          `(:funcallable ,funcallable
+                         :method ,method
+                         :content-type ,content-type
+                         :allow-cache ,allow-cache
+                         :json ,json))
+    (reset-prologue-cache processor)
+    (values)))
+
+
+(defmethod unexport-func ((processor ajax-processor) symbol-or-name)
+  (let ((func-name (or (when (symbolp symbol-or-name)
+                         (symbol-name symbol-or-name))
+                       symbol-or-name)))
+    (unless (and func-name
+                 (stringp func-name))
+      (error "Invalid name ~S in UNEXPORT-FUNC" symbol-or-name))
+
+    (remhash (string-upcase func-name) (exported-funcs processor))
+    (reset-prologue-cache processor)
+    (values)))
+
+
+
+(defmethod handle-request ((processor ajax-processor))
+  ;; See if it's a request for a virtual .JS file
+  (let ((virtual-file-result (handle-virtual-file processor)))
+    (when virtual-file-result
+      (return-from handle-request virtual-file-result)))
+
+  ;; Not a vitual file, process as a function call
+  (let ((func-name (parameter "ajax-fun"))
+        (num-args (parameter "ajax-num-args")))
+    (unless (and func-name num-args)
+      (error "Error in HANDLE-REQUEST: required parameters missing"))
+
+    (let* ((args (loop for i from 0 below (parse-integer num-args)
+                    for arg-name = (concatenate 'string "ajax-arg" (princ-to-string i))
+                    for arg = (parameter arg-name)
+                    collect arg))
+           (funcallable-plist (gethash func-name (exported-funcs processor)))
+           (funcallable (getf funcallable-plist :funcallable)))
+      (unless funcallable
+        (error "Error in HANDLE-REQUEST: no such function: ~A" func-name))
+
+      (let ((content-type (getf funcallable-plist :content-type)))
+        ;; Can't use the default parameter of getf since it may be present but null
+        (setf (content-type) (or content-type
+                                 (when (getf funcallable-plist :json) (json-content-type))
+                                 (default-content-type processor))))
+      (when (default-reply-external-format processor)
+        (setf (reply-external-format) (default-reply-external-format processor)))
+      (unless (getf funcallable-plist :allow-cache)
+        (no-cache))
+
+      (apply funcallable args))))
+
+
+(defun handle-virtual-file (processor)
+  (let* ((file-name (string-downcase (script-name)))
+         (file-record (assoc file-name (virtual-files processor) :test 'equal)))
+    (when file-record
+      (let ((time (cddr file-record)))
+        (handle-if-modified-since time) ; Does not return if the file was not modified
+        
+        (setf (content-type) "text/javascript")
+        (setf (header-out "Last-Modified") (rfc-1123-date time))
+        ;;(setf (header-out "Expires") (rfc-1123-date (+ time #.(* 60 60 2))))
+        (cadr file-record)))))
+
+
+(defun store-virtual-js-file (processor file-contents)
+  "Makes a new unique name for a file, makes an alist of file name and a cons of 
+contents and time, stores the alist in the processor's slot and returns the 
+file name"
+  (let ((file-name (string-downcase (concatenate 'string
+                                                 (server-uri processor)
+                                                 "/"
+                                                 (symbol-name (gensym))
+                                                 ".js"))))
+    (setf (virtual-files processor) (list (cons file-name
+                                                (cons file-contents (get-universal-time)))))
+    file-name))
+
+
+(defmethod get-handler ((processor ajax-processor))
+  (if (slot-boundp processor 'hunchentoot-handler)
+      (hunchentoot-handler processor)
+      (setf (hunchentoot-handler processor) #'(lambda ()
+                                                (handle-request processor)))))
+
+
+(defun make-ajax-processor (&rest rest &key (type :simple) &allow-other-keys)
+  "Creates an ajax-processor object. Parameters: 
+   TYPE - selects the kind of ajax-processor to use (should be 
+          one of:SIMPLE or :LOKRIS, :PROTOTYPE, :YUI or :DOJO) (required)
+   SERVER-URI - url that the ajax function calls will use (required)
+   JS-FILE-URIS - a list of URLs on your server of the .js files that the
+                used library requires , such as lokris.js or prototype.js 
+                (parameter required for all processors except :SIMPLE). If
+                only one file needs to be included then instead of a list a single 
+                string may be passed. Also if this parameter is a string that ends 
+                in a forward slash ( #\/ ) then it is assumed to be a directory 
+                and the default file names for the processor are appended to it.
+   AJAX-FUNCTION-PREFIX - the string to be prepended to the generated js functions,
+                (default prefix is \"ajax_\")
+   JS-DEBUG - enable the Javascript debugging function debug_alert(). Overrides
+              such parameters as JS-COMPRESSION and VIRTUAL-FILES
+   JS-COMPRESSION - enable Javascript compression to minimize the download size
+   VIRTUAL-JS-FILE - enable creation of virtual Javascript file instead of
+                inline Javascript code that may be cached on the client to 
+                minimize traffic
+   "
+  (let ((params (copy-seq rest)))
+    (remf params :type)
+
+    ;; make a class name depending on TYPE and create an instance
+    (let* ((class-name (concatenate 'string (symbol-name type) "-ajax-processor"))
+           (class-sym (intern (string-upcase class-name) #.*package*)))
+      (apply #'make-instance class-sym params))))
+
+
+(defmethod generate-prologue ((processor ajax-processor) &key (use-cache t))
+  (let ((cached-prologue (cached-prologue processor)))
+    (if (and cached-prologue use-cache)
+        cached-prologue
+        (let ((prologue (%generate-includes processor))
+              (js-code (%generate-js-code processor)))
+          
+          (when (and (js-compression processor) (js-debug processor))
+            (setf (js-compression processor) nil)
+            (warn "JS-COMPRESSION conflicts with JS-DEBUG, JS-COMPRESSION disabled."))
+          
+          (when (js-compression processor)
+            (setf js-code (jsmin js-code)))
+
+          (when (and (virtual-js-file processor) (js-debug processor))
+            (setf (virtual-js-file processor) nil)
+            (warn "VIRTUAL-JS-FILE conflicts with JS-DEBUG, VIRTUAL-JS-FILE disabled."))
+
+          (if (virtual-js-file processor)
+              ;; Create a virtual file and use a link to it
+              (let ((file-name (store-virtual-js-file processor js-code)))
+                (setf prologue (concatenate 'string
+                                            "<!-- HT-AJAX " +version+ "-->"
+                                            prologue
+                                            (prepare-js-file-include file-name))))
+              ;; Not using virtual file, create inline <script> tag
+              (setf prologue (concatenate 'string
+                                          "<!-- HT-AJAX " +version+ "-->"
+                                          prologue
+                                          (wrap-js-in-script-tags js-code))))
+
+          (setf (cached-prologue processor) prologue)))))
+
+
+
+(defmethod reset-prologue-cache ((processor ajax-processor))
+  (setf (cached-prologue processor) nil))
+
+
+(defmethod js-function-name ((processor ajax-processor) function-name)
+  (concatenate 'string
+               (ajax-function-prefix processor)
+               (string-downcase (make-safe-js-name function-name))))
+
+
+(defun maybe-rewrite-url-for-session (url &key (cookie-name *session-cookie-name*)
+                                      (value (hunchentoot::session-cookie-value)))
+  "Modelled after (well, copied from) HUNCHENTOOT::MAYBE-REWRITE-URLS-FOR-SESSION.
+Rewrites the URL such that the name/value pair
+COOKIE-NAME/COOKIE-VALUE is inserted if the client hasn't sent a
+cookie of the same name but only if *REWRITE-FOR-SESSION-URLS* is
+true."
+  (cond
+    ((or (not *rewrite-for-session-urls*)
+         (null value)
+         (cookie-in cookie-name))
+     url)
+    (t
+     (hunchentoot::add-cookie-value-to-url url
+                                           :cookie-name cookie-name
+                                           :value value))))
+
+
+ ;;
+
+(defclass library-ajax-processor (ajax-processor)
+  ((js-file-uris :initarg :js-file-uris :accessor js-file-uris))
+  (:documentation "The class representing a processor that uses an 
+external Javascript library"))
+
+(defgeneric default-library-file-names (library-ajax-processor)
+  (:documentation "Returns the default filename for Javascript library to
+be included in the HTML"))
+
+
+(defmethod initialize-instance :after ((processor library-ajax-processor) &key)
+  (unless (and (slot-boundp processor 'js-file-uris)
+               (js-file-uris processor))
+    (error "Initializing ~A without JS-FILE-URIS" (class-name (class-of processor))))
+  (let ((file-uri (js-file-uris processor)))
+    (when (and (stringp file-uri)
+               (eql (char file-uri (1- (length file-uri))) #\/)) ; Just a path
+      ;; Store default filenames for this processor
+      (setf (js-file-uris processor)
+            (mapcar #'(lambda (fname) (concatenate 'string
+                                                   file-uri fname))
+                    (default-library-file-names processor))))
+    ;; If it's a string then wrap it in a list
+    (when (stringp (js-file-uris processor))
+      (setf (js-file-uris processor) (list (js-file-uris processor))))))
+
+
+(defmethod %generate-includes ((processor library-ajax-processor))
+  (apply #'concatenate 'string
+         (mapcar #'prepare-js-file-include (js-file-uris processor))))
+
+
+(defmethod prepare-js-ajax-function ((processor library-ajax-processor) fun-name js-fun-name
+                                     &rest rest &key method &allow-other-keys)
+  (declare (ignore processor))
+  (let ((request-func (ecase method
+                        (:get "ajax_call_uri")
+                        (:post "ajax_post_uri"))))
+    (apply #'prepare-js-ajax-function-definitions request-func fun-name js-fun-name rest)))

Added: join-strings.lisp
==============================================================================
--- (empty file)
+++ join-strings.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,25 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+
+(in-package #:ht-ajax)
+
+;; Written by Riastradh ( http://mumble.net/~campbell/blog.txt )
+;; posted at http://paste.lisp.org/display/35412
+;; and placed in the public domain
+
+(defun join-strings (string-list separator)
+  (if (null string-list)
+      ""
+      (let* ((separator-length (length separator))
+             (total-length
+              (reduce (lambda (left-length right-length)
+                        (+ left-length separator-length right-length))
+                      string-list
+                      :key #'length))
+             (result (make-string total-length)))
+        (replace result (car string-list))
+        (let ((offset (length (car string-list))))
+          (dolist (string (cdr string-list))
+            (replace result separator :start1 offset)
+            (replace result string :start1 (+ offset separator-length))
+            (incf offset (+ separator-length (length string)))))
+        result)))

Added: jsmin.lisp
==============================================================================
--- (empty file)
+++ jsmin.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,205 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; This is a port of original C code by Douglas Crockford to
+;;; Common Lisp. There was no attempt to make the code more
+;;; "lispy", it is just a rather faithful translation. This code
+;;; may be used under the same conditions as the C original, which
+;;; has the following copyright notice:
+;;; 
+;;; /* jsmin.c
+;;;    2007-01-08
+;;;
+;;; Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
+;;;
+;;; Permission is hereby granted, free of charge, to any person obtaining a copy of
+;;; this software and associated documentation files (the "Software"), to deal in
+;;; the Software without restriction, including without limitation the rights to
+;;; use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+;;; of the Software, and to permit persons to whom the Software is furnished to do
+;;; so, subject to the following conditions:
+;;;
+;;; The above copyright notice and this permission notice shall be included in all
+;;; copies or substantial portions of the Software.
+;;;
+;;; The Software shall be used for Good, not Evil.
+;;;
+;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+;;; SOFTWARE.
+;;; */
+;;;
+
+(in-package #:ht-ajax)
+
+(declaim #.*optimization*)
+;;
+
+(defun is-alphanum (c)
+  "isAlphanum -- return true if the character is a letter, digit, underscore,
+        dollar sign, or non-ASCII character"
+  (and c
+       (or (and (char>= c #\a) (char<= c #\z))
+           (and (char>= c #\0) (char<= c #\9))
+           (and (char>= c #\A) (char<= c #\Z))
+           (char= c #\_)
+           (char= c #\$)
+           (char= c #\\)
+           (> (char-code c) 126))))
+
+
+
+(defun %jsmin (in out)
+  (let (the-a the-b the-lookahead (pos 0))
+    (labels ((get-c ()
+               ;; return the next character from stdin. Watch out for lookahead. If
+               ;; the character is a control character, translate it to a space or
+               ;; linefeed.
+               (let ((c the-lookahead))
+                 (setf the-lookahead nil)
+                 (unless c
+                   (setf c (read-char in nil nil))
+                   (incf pos))
+                 (cond
+                   ((or (null c)
+                        (char= c #\Newline)
+                        (char>= c #\Space)) c)
+                   ((char= c (code-char 13)) #\Newline)
+                   (t #\Space))))
+           
+             (peek ()
+               ;; get the next character without getting it
+               (setf the-lookahead (get-c)))
+           
+             (next ()
+               ;; get the next character, excluding comments. peek()
+               ;; is used to see if a '/' is followed by a '/' or '*'.
+               (let ((c (get-c)))
+                 (if (and c
+                          (char= c #\/))
+                     (case (peek)
+                       (#\/
+                        (loop for cc = (get-c)
+                           while (and cc
+                                      (char> cc #\Newline))
+                             finally (return cc)))
+                       (#\*
+                        (get-c)
+                        (loop for cc = (get-c)
+                           unless cc
+                           do (error "JSMIN: Unterminated comment.")
+                           when (and (char= cc #\*)
+                                     (char= (peek) #\/))
+                           do (progn (get-c) (return #\Space))))
+                       (otherwise
+                        c))
+                     c)))
+
+
+             (action (d)
+               ;; action -- do something! What you do is determined by the argument:
+               ;;         1   Output A. Copy B to A. Get the next B.
+               ;;         2   Copy B to A. Get the next B. (Delete A).
+               ;;         3   Get the next B. (Delete B).
+               ;;    action treats a string as a single character. Wow!               
+               ;;    action recognizes a regular expression if it is
+               ;;    preceded by ( or , or =.
+
+               
+               (when (= d 1)
+                 (write-char the-a out))
+               (when (<= d 2)
+                 (setf the-a the-b)
+                 (when (and the-a
+                            (or (char= the-a #\')
+                                (char= the-a #\")))
+                   (loop
+                      (progn
+                        (write-char the-a out)
+                        (setf the-a (get-c))
+                        (when (and the-a (char= the-a the-b))
+                          (return))
+                        (when (or (null the-a)
+                                  (char<= the-a #\Newline))
+                          (error "JSMIN unterminated string literal: ~C at position ~A" the-b pos))
+                        (when (char= the-a #\\)
+                          (write-char the-a out)
+                          (setf the-a (get-c)))))))
+               (when (<= d 3)
+                 (setf the-b (next))
+                 (when (and the-b
+                            (char= the-b #\/)
+                            (position the-a "(,=:[!&|?"))
+                   (write-char the-a out)
+                   (write-char the-b out)
+                   (loop
+                      (progn
+                        (setf the-a (get-c))
+                        (when (and the-a
+                                   (char= the-a #\/))
+                          (return))
+                        (when (and the-a
+                                   (char= the-a #\\))
+                          (write-char the-a out)
+                          (setf the-a (get-c)))
+                        (when (or (null the-a)
+                                  (char<= the-a #\Newline))
+                          (error "JSMIN: unterminated Regular Expression literal."))
+                        (write-char the-a out)))
+                   (setf the-b (next))))))
+      ;; jsmin -- Copy the input to the output, deleting the characters
+      ;;   which are insignificant to JavaScript. Comments will be
+      ;;   removed. Tabs will be replaced with spaces. Carriage returns will
+      ;;   be replaced with linefeeds.  Most spaces and linefeeds will be
+      ;;   removed.
+      (setf the-a #\Newline)
+      (action 3)
+      (loop while the-a
+         do (case the-a
+             (#\Space
+              (if (is-alphanum the-b)
+                  (action 1)
+                  (action 2)))
+             (#\Newline
+              (case the-b
+                ((#\{ #\[ #\( #\+ #\-)
+                 (action 1))
+                (#\Space
+                 (action 3))
+                (otherwise
+                 (if (is-alphanum the-b)
+                     (action 1)
+                     (action 2)))))
+             (otherwise
+              (case the-b
+                (#\Space
+                 (if (is-alphanum the-a)
+                     (action 1)
+                     (action 3)))
+                (#\Newline
+                 (case the-a
+                   ((#\} #\] #\) #\+ #\- #\" #\')
+                    (action 1))
+                   (otherwise
+                    (if (is-alphanum the-a)
+                        (action 1)
+                        (action 3)))))
+                (otherwise
+                 (action 1))))))
+      )))
+
+
+(defun jsmin (js)
+  (with-output-to-string (out)
+    (with-input-from-string (in js)
+      (%jsmin in out))))
+
+(defun jsmin-file (infile outfile)
+  (with-open-file (in infile :direction :input)
+    (with-open-file (out outfile :direction :output :if-exists :overwrite
+                         :if-does-not-exist :create)
+      (%jsmin in out))))

Added: optimization.lisp
==============================================================================
--- (empty file)
+++ optimization.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,14 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax)
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  (defvar *optimization* '(optimize (space 0) (speed 0) (safety 3) (debug 3))))
+
+
+

Added: packages.lisp
==============================================================================
--- (empty file)
+++ packages.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,23 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+
+(cl:in-package #:cl-user)
+
+(defpackage #:ht-ajax
+  (:use #:cl
+        #:hunchentoot
+        #:cl-ppcre)
+  (:export #:make-ajax-processor
+           ;;#:handle-request
+           #:export-func
+           #:unexport-func
+           #:generate-prologue
+           #:get-handler
+           #:defun-ajax
+           #:js-debug))
+

Added: processor-dojo.lisp
==============================================================================
--- (empty file)
+++ processor-dojo.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,117 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax)
+
+(declaim #.*optimization*)
+;;
+
+
+;;; This is the AJAX processor interfacing to the Dojo Toolkit
+;;; ( http://dojotoolkit.org/ )
+
+
+(defclass dojo-ajax-processor (library-ajax-processor)
+  ())
+
+(defmethod default-library-file-names ((processor dojo-ajax-processor))
+  (declare (ignore processor))
+  
+  '("dojo.js"))
+
+;;
+
+
+(defun prepare-js-dojo-ajax-preamble (server-uri) 
+  "Output a string containing the call function."
+  (concatenate 'string
+   "
+dojo.require('dojo.io.*');
+
+var ajax_server_uri = '" server-uri "';
+
+function ajax_call_uri(func, callback_spec, args) {
+  var uri = ajax_server_uri;
+  var i;
+
+  if (uri.indexOf('?') == -1)
+    uri = uri + '?';
+  else
+    uri = uri + '&';
+
+  uri = uri + ajax_encode_args(func, args);
+  var callbacks = ajax_parse_callbacks(callback_spec);
+
+  dojo.io.bind({ url: uri,
+                 transport: 'XMLHTTPTransport',
+                 load: function (type, data, evt) {
+                     ajax_call_maybe_evaluate_json(callbacks[0], 
+                                     data, 
+                                     evt.getResponseHeader('Content-Type'));
+
+                 },
+                 error: function (type, errorObject) {
+                   if(callbacks[1]) {
+                     callbacks[1](dojo.errorToString(errorObject)); 
+                   }
+                   else {
+                     debug_alert(dojo.errorToString(errorObject)); 
+                   }
+                 }
+  });
+}
+
+
+function ajax_post_uri(func, callback_spec, args) {
+  var params = ajax_encode_args(func, args);
+  var callbacks = ajax_parse_callbacks(callback_spec);
+
+  dojo.io.bind({ url: ajax_server_uri,
+                 transport: 'XMLHTTPTransport',
+                 method: 'POST',
+                 load: function (type, data, evt) {
+                     ajax_call_maybe_evaluate_json(callbacks[0], 
+                                     data, 
+                                     evt.getResponseHeader('Content-Type'));
+
+                 },
+                 error: function (type, errorObject) {
+                   if(callbacks[1]) {
+                     callbacks[1](dojo.errorToString(errorObject));
+                   }
+                   else {
+                     debug_alert(dojo.errorToString(errorObject));
+                   }
+                 },
+                 postContent: params
+  });
+}"))
+
+
+
+
+(defmethod %generate-js-code ((processor dojo-ajax-processor))
+  (concatenate 'string
+               (apply #'concatenate 'string
+                      (prepare-js-debug-function processor)
+                      (prepare-js-ajax-encode-args)
+                      (prepare-js-parse-callbacks)
+                      (prepare-js-ajax-is-json)                      
+                      (prepare-js-ajax-call-maybe-evaluate-json)
+                      (prepare-js-dojo-ajax-preamble (maybe-rewrite-url-for-session
+                                                      (server-uri processor)))
+
+
+                      (loop
+                         for fun-name being the hash-keys
+                         in (exported-funcs processor)
+                         collect (apply #'prepare-js-ajax-function
+                                        processor
+                                        fun-name
+                                        (js-function-name processor fun-name)
+                                        (gethash fun-name (exported-funcs processor))))
+                      )))

Added: processor-lokris.lisp
==============================================================================
--- (empty file)
+++ processor-lokris.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,122 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax)
+
+(declaim #.*optimization*)
+;;
+
+
+;;; This is the AJAX processor interfacing to the Lokris library
+;;; ( http://www.ajaxbuch.de/lokris/ )
+
+
+(defclass lokris-ajax-processor (library-ajax-processor)
+  ())
+
+(defmethod default-library-file-names ((processor lokris-ajax-processor))
+  (declare (ignore processor))
+  
+  '("lokris.js"))
+
+;;
+
+
+
+(defun prepare-js-lokris-ajax-preamble (server-uri) 
+  "Output a string containing the call functions."
+  (concatenate 'string "
+var ajax_server_uri = '" server-uri "';
+
+function ajax_call_uri(func, callback_spec, args) {
+  var uri = ajax_server_uri;
+  var i;
+
+  if (uri.indexOf('?') == -1)
+    uri = uri + '?';
+  else
+    uri = uri + '&';
+
+  uri = uri + ajax_encode_args(func, args);
+  var callbacks = ajax_parse_callbacks(callback_spec);
+
+  var error_cb = function (req) {
+    if(callbacks[1]) {
+        callbacks[1](req.status + ' ' + req.statusText)
+    }
+    else {
+        debug_alert(req.status + ' ' + req.statusText);
+    }
+  }
+
+  var request = Lokris.AjaxCall(uri, 
+     function (response) {
+       var data = response.responseText;
+       ajax_call_maybe_evaluate_json(callbacks[0], 
+                                     data, 
+                                     response.getResponseHeader('Content-Type'));
+     },
+     {
+       errorHandler: error_cb,
+       rawResponse:true
+     }
+     );
+}
+
+function ajax_post_uri(func, callback_spec, args) {
+  var params = ajax_encode_args(func, args);
+  var callbacks = ajax_parse_callbacks(callback_spec);
+
+  var error_cb = function (req) {
+    if(callbacks[1]) {
+        callbacks[1](req.status + ' ' + req.statusText)
+    }
+    else {
+        debug_alert(req.status + ' ' + req.statusText);
+    }
+  }
+
+  var request = Lokris.AjaxCall(ajax_server_uri,
+     function (response) {
+       var data = response.responseText;
+       ajax_call_maybe_evaluate_json(callbacks[0], 
+                                     data, 
+                                     response.getResponseHeader('Content-Type'));
+     },
+
+     {postBody: params, 
+      method: 'POST',
+      errorHandler: error_cb,
+      rawResponse:true
+      });
+}"
+))
+
+
+
+
+(defmethod %generate-js-code ((processor lokris-ajax-processor))
+  (concatenate 'string
+               (apply #'concatenate 'string
+                      (prepare-js-debug-function processor)
+                      (prepare-js-ajax-encode-args)
+                      (prepare-js-parse-callbacks)
+                      (prepare-js-ajax-is-json)
+                      (prepare-js-ajax-call-maybe-evaluate-json)
+                      (prepare-js-lokris-ajax-preamble (maybe-rewrite-url-for-session
+                                                        (server-uri processor)))
+
+
+                      (loop
+                         for fun-name being the hash-keys
+                         in (exported-funcs processor)
+                         collect (apply #'prepare-js-ajax-function
+                                        processor
+                                        fun-name
+                                        (js-function-name processor fun-name)
+                                        (gethash fun-name (exported-funcs processor))))
+                      )))

Added: processor-prototype.lisp
==============================================================================
--- (empty file)
+++ processor-prototype.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,118 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax)
+
+(declaim #.*optimization*)
+;;
+
+
+;;; This is the AJAX processor interfacing to the Prototype library
+;;; ( http://prototypejs.org )
+
+
+(defclass prototype-ajax-processor (library-ajax-processor)
+  ())
+
+(defmethod default-library-file-names ((processor prototype-ajax-processor))
+  (declare (ignore processor))
+  
+  '("prototype.js"))
+
+;;
+
+
+(defun prepare-js-prototype-ajax-preamble (server-uri) 
+  "Output a string containing the call functions."
+  (concatenate 'string
+   "
+var ajax_server_uri = '" server-uri "';
+
+function ajax_call_uri(func, callback_spec, args) {
+  var uri = ajax_server_uri;
+  var i;
+
+  if (uri.indexOf('?') == -1)
+    uri = uri + '?';
+  else
+    uri = uri + '&';
+
+  uri = uri + ajax_encode_args(func, args);
+  var callbacks = ajax_parse_callbacks(callback_spec);
+
+  new Ajax.Request(uri,
+  {
+    method:'get',
+    onSuccess: function(transport){
+      var data = transport.responseText;
+      ajax_call_maybe_evaluate_json(callbacks[0], 
+                                     data, 
+                                     transport.getResponseHeader('Content-Type'));
+    },
+    onFailure: function(){ 
+      if(callbacks[1]) {
+          callbacks[1]('URI: '+uri);
+      }
+      else {
+          debug_alert('Error for URI: '+uri); 
+      }
+    }
+  });
+}
+
+function ajax_post_uri(func, callback_spec, args) {
+  var params = ajax_encode_args(func, args);
+  var callbacks = ajax_parse_callbacks(callback_spec);
+
+  new Ajax.Request(ajax_server_uri,
+  {
+    method:'post',
+    parameters: params,
+    onSuccess: function(transport){
+      var data = transport.responseText;
+      ajax_call_maybe_evaluate_json(callbacks[0], 
+                                     data, 
+                                     transport.getResponseHeader('Content-Type'));
+    },
+    onFailure: function(){ 
+      if(callbacks[1]) {
+          callbacks[1]('URI: '+uri);
+      }
+      else {
+          debug_alert('Error for URI: '+uri);
+      }
+    }
+  });
+}"))
+
+
+
+(defmethod %generate-js-code ((processor prototype-ajax-processor))
+  (concatenate 'string
+               (apply #'concatenate 'string
+                      (prepare-js-debug-function processor)
+                      (prepare-js-ajax-encode-args)
+                      (prepare-js-parse-callbacks)
+                      (prepare-js-ajax-is-json)
+                      (prepare-js-ajax-call-maybe-evaluate-json)
+                      (prepare-js-prototype-ajax-preamble (maybe-rewrite-url-for-session
+                                                           (server-uri processor)))
+
+
+                      (loop
+                         for fun-name being the hash-keys
+                         in (exported-funcs processor)
+                         collect (apply #'prepare-js-ajax-function
+                                        processor
+                                        fun-name
+                                        (js-function-name processor fun-name)
+                                        (gethash fun-name (exported-funcs processor))))
+                      )))
+
+
+
+

Added: processor-simple.lisp
==============================================================================
--- (empty file)
+++ processor-simple.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,149 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax)
+
+(declaim #.*optimization*)
+
+;;
+
+
+;;; This is loosely based on the CL-AJAX package by Richard Newman
+;;; (http://www.cliki.net/cl-ajax, http://www.holygoat.co.uk/applications/cl-ajax/cl-ajax)
+;;; but probably does not deserve the name "port of CL-AJAX for Hunchentoot",
+;;; also in any case the code taken from CL-AJAX was heavily modified, so
+;;; the bugs are probably mine
+
+
+
+(defclass simple-ajax-processor (ajax-processor)
+  ())
+
+
+(defun prepare-js-simple-init-request ()
+  "
+function init_request() {
+//  debug_alert(\"Initialising request...\");
+  var r;
+  if (window.XMLHttpRequest) { r = new XMLHttpRequest(); }
+  else {
+    try { r = new ActiveXObject(\"Msxml2.XMLHTTP\"); } catch (e) {
+      try { r = new ActiveXObject(\"Microsoft.XMLHTTP\"); } catch (ee) {
+        r = null;
+      }}}
+  if (!r) debug_alert(\"Browser couldn't make a connection object.\");
+  return r;
+}
+")
+
+
+
+(defun prepare-js-simple-ajax-preamble (server-uri) 
+  "Output a string containing the call function."
+  (format nil "
+
+function ajax_call_uri(func, callback_spec, args) {
+  var uri = '~A';
+  var i;
+  var response = null;
+  var callbacks = ajax_parse_callbacks(callback_spec);
+
+  if (uri.indexOf('?') == -1)
+    uri = uri + '?';
+  else
+    uri = uri + '&';
+
+  uri = uri + ajax_encode_args(func, args);
+
+  var re = init_request();
+
+  re.open('GET', uri, true);
+  re.onreadystatechange = function() {
+    if (re.readyState != 4) return;
+    if (((re.status>=200) && (re.status<300)) || (re.status == 304)) {
+      var data = re.responseText;
+      ajax_call_maybe_evaluate_json(callbacks[0], 
+                                    data, 
+                                    re.getResponseHeader('Content-Type'));
+    }
+    else {
+      if(callbacks[1]) {
+          callbacks[1](re.status + ' ' + re.statusText);
+      }
+      else {
+          debug_alert('Error for URI '+uri + ' ' + re.status + ' ' + re.statusText);
+      }
+
+    }
+  }
+  re.send(null);
+  delete re;
+}"
+    server-uri))
+
+
+(defmethod prepare-js-ajax-function ((processor simple-ajax-processor) fun-name js-fun-name
+                                     &rest rest &key method &allow-other-keys)
+  (declare (ignore processor))
+  (unless (eq method :get)
+    (error "SIMPLE-AJAX-PROCESSOR does not support methods other than GET"))
+  (apply #'prepare-js-ajax-function-definitions "ajax_call_uri" fun-name js-fun-name rest))
+
+
+
+;; (defun wrap-result-in-xml (result element-id)
+;;   (no-cache)
+;;   (format nil
+;;           "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>
+;; <response>~A<result xmlns=\"http://www.w3.org/1999/xhtml\">~A</result></response>"
+;;           (if element-id
+;;               (concatenate 'string "<elem_id>" element-id "</elem_id>")
+;;               "")
+;;           result)
+;;   )
+
+
+;; (defmethod handle-request ((processor simple-ajax-processor))
+;;   (let ((ajax-xml (string-to-js-boolean (parameter "ajax-xml")))
+;;         (ajax-elem (parameter "ajax-elem")))
+;;     (let ((result (call-next-method)))
+;;       (if ajax-xml
+;;           (progn
+;;             (setf (content-type) "text/xml")
+;;             (wrap-result-in-xml result ajax-elem))
+;;           result)
+;;       ))
+;;   )
+
+
+(defmethod %generate-includes ((processor simple-ajax-processor))
+  "No includes for SIMPLE processor"
+  ;;
+  "")
+
+
+(defmethod %generate-js-code ((processor simple-ajax-processor))
+  (apply #'concatenate 'string
+         (prepare-js-debug-function processor)
+         (prepare-js-ajax-encode-args)
+         (prepare-js-parse-callbacks)
+         (prepare-js-ajax-is-json)
+         (prepare-js-ajax-call-maybe-evaluate-json)
+         (prepare-js-simple-ajax-preamble (maybe-rewrite-url-for-session
+                                           (server-uri processor)))
+         (prepare-js-simple-init-request)
+
+         (loop
+            for fun-name being the hash-keys
+            in (exported-funcs processor)
+            collect (apply #'prepare-js-ajax-function
+                           processor
+                           fun-name
+                           (js-function-name processor fun-name)
+                           (gethash fun-name (exported-funcs processor))))
+         ))
+

Added: processor-yui.lisp
==============================================================================
--- (empty file)
+++ processor-yui.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,118 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax)
+
+(declaim #.*optimization*)
+;;
+
+
+;;; This is the AJAX processor interfacing to the Yahoo User Interface Library
+;;; ( http://developer.yahoo.com/yui/ )
+
+
+(defclass yui-ajax-processor (library-ajax-processor)
+  ())
+
+(defmethod default-library-file-names ((processor yui-ajax-processor))
+  (declare (ignore processor))
+  
+  '("yahoo.js" "connection.js"))
+
+;;
+
+
+(defun prepare-js-yui-ajax-preamble (server-uri) 
+  "Output a string containing the call function."
+  (concatenate 'string
+   "
+
+var ajax_server_uri = '" server-uri "';
+
+
+
+function ajax_call_uri(func, callback_spec, args) {
+  var uri = ajax_server_uri;
+  var i;
+
+  if (uri.indexOf('?') == -1)
+    uri = uri + '?';
+  else
+    uri = uri + '&';
+
+  uri = uri + ajax_encode_args(func, args);
+  var callbacks = ajax_parse_callbacks(callback_spec);
+
+  var transaction = YAHOO.util.Connect.asyncRequest('GET',
+          uri, 
+          { success: function (response) {
+              var data = response.responseText;
+              ajax_call_maybe_evaluate_json(callbacks[0], 
+                                     data, 
+                                     response.getResponseHeader['Content-Type']);
+            },
+            failure: function (response) {
+              if(callbacks[1]) {
+                 callbacks[1](response.statusText); 
+              }
+              else {
+                debug_alert(response.statusText); 
+              }
+            }
+          }, 
+          null);
+}
+
+
+function ajax_post_uri(func, callback_spec, args) {
+  var params = ajax_encode_args(func, args);
+  var callbacks = ajax_parse_callbacks(callback_spec);
+
+  var transaction = YAHOO.util.Connect.asyncRequest('POST',
+          ajax_server_uri, 
+          { success: function (response) {
+              var data = response.responseText;
+              ajax_call_maybe_evaluate_json(callbacks[0], 
+                                     data, 
+                                     response.getResponseHeader['Content-Type']);
+            },
+            failure: function (response) {
+              if(callbacks[1]) {
+                 callbacks[1](response.statusText); 
+              }
+              else {
+                debug_alert(response.statusText); 
+              }
+            }
+          }, 
+          params);
+}"))
+
+
+
+
+(defmethod %generate-js-code ((processor yui-ajax-processor))
+  (concatenate 'string
+               (apply #'concatenate 'string
+                      (prepare-js-debug-function processor)
+                      (prepare-js-ajax-encode-args)
+                      (prepare-js-parse-callbacks)
+                      (prepare-js-ajax-is-json)
+                      (prepare-js-ajax-call-maybe-evaluate-json)
+                      (prepare-js-yui-ajax-preamble (maybe-rewrite-url-for-session
+                                                      (server-uri processor)))
+
+
+                      (loop
+                         for fun-name being the hash-keys
+                         in (exported-funcs processor)
+                         collect (apply #'prepare-js-ajax-function
+                                        processor
+                                        fun-name
+                                        (js-function-name processor fun-name)
+                                        (gethash fun-name (exported-funcs processor))))
+                      )))

Added: static/lokris.js
==============================================================================
--- (empty file)
+++ static/lokris.js	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,168 @@
+/* ==========================================================================
+
+   = Lokris - An Ajax library for Javascript =
+
+   Lokris provides some basic Ajax functions.
+   Originally developed for use on ajaxbuch.de.
+
+   It's named after "Ajax the Lesser", son of the King of Locris
+   (http://en.wikipedia.org/wiki/Ajax_the_Lesser).
+   The spelling (with k) follows the transcription of the 
+   ancient greek name of the region of Locris.
+
+   (c) 2006 Linkwerk.com, Christoph Leisegang, Stefan Mintert
+   Licence:   http://www.ajaxbuch.de/lokris/lokris-licence.txt
+   Home page: http://www.ajaxbuch.de/lokris/
+
+   $Id: lokris.js,v 1.2 2006/08/02 21:24:07 sm Exp $
+
+   ========================================================================== */
+
+
+var Lokris = new Object();
+
+/* Setting global defaults */
+Lokris.Defaults = {
+    rawResponse:    false,
+    async:          true,
+    method:         "GET",
+    postBody:       null,
+    user:           undefined,
+    password:       undefined,
+    timeoutHandler: undefined,
+    timeout:        60000,
+    postMime:       "application/x-www-form-urlencoded",
+    errorHandler:   function(req) {alert("HTTP error: "+req.status)}
+};
+
+/* Defining IE prog IDs
+   For details see http://msdn.microsoft.com/library/en-us/xmlsdk/html/5016cf75-4358-4c1f-912e-c071aa0a0991.asp */
+Lokris.MSIEIDS = ["Msxml2.XMLHTTP.6.0", "Msxml2.XMLHTTP.5.0", "Msxml2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "Microsoft.XMLHTTP"];
+
+/* Lokris.AjaxCall - The Ajax function */
+Lokris.AjaxCall = function (uri, callbackFunction, options) {
+
+    var lwAjax = new Object; // "Host" Obbject for XmlHttpRequest and own properties
+    var req    = null;       // Define local XmlHttpRequest
+    Lokris.XMLHTTPRequestImplementation = "";
+
+    // Evaluate Options
+    var raw 	= (options != undefined && options.rawResponse != undefined) ? options.rawResponse : Lokris.Defaults.rawResponse;
+    var async 	= (options != undefined && options.async != undefined) ? options.async : Lokris.Defaults.async;
+    var method 	= (options != undefined && options.method != undefined) ? options.method : Lokris.Defaults.method;
+    var body 	= (options != undefined && options.postBody != undefined) ? options.postBody : Lokris.Defaults.postBody;
+    var user    = (options != undefined && options.user != undefined) ? options.user : Lokris.Defaults.user;
+    var password= (options != undefined && options.password != undefined) ? options.password : Lokris.Defaults.password;
+    var timeoutHandler 	= (options != undefined && options.timeoutHandler != undefined) ? options.timeoutHandler : Lokris.Defaults.timeoutHandler;
+    var timeout 	= (options != undefined && options.timeout != undefined) ? options.timeout : Lokris.Defaults.timeout;
+    var postMime        = (options != undefined && options.mime != undefined) ? options.mime : Lokris.Defaults.postMime;
+    var errorHandler    = (options != undefined && options.errorHandler != undefined) ? options.errorHandler : Lokris.Defaults.errorHandler;
+
+
+    if (window.XMLHttpRequest) { // Check for native XmlHttpRequest ...
+        req = new XMLHttpRequest();
+	Lokris.XMLHTTPRequestImplementation = "XMLHttpRequest";
+    } else if (window.ActiveXObject) { // ... or ActiveX
+	for (var i = 0; i < Lokris.MSIEIDS.length; i++) {
+	    try {
+		req = new ActiveXObject(Lokris.MSIEIDS[i]);
+		Lokris.XMLHTTPRequestImplementation = Lokris.MSIEIDS[i];
+		break;
+	    } catch (e) { }
+	}
+    } 
+    if ( req === null) { // Sorry, no Ajax
+        alert("Ajax not available");
+        return null;
+    }
+
+    lwAjax.request = req; 
+    if (timeoutHandler != undefined) {
+        lwAjax.timeoutHandler = timeoutHandler;
+        lwAjax.timeoutId = window.setTimeout(function() { lwAjax.request.abort(); lwAjax.timeoutHandler(lwAjax.request) }, timeout);
+    };
+    // Register Event Handler
+    lwAjax.request.onreadystatechange = Lokris.getReadyStateHandler(lwAjax, callbackFunction, raw, errorHandler);
+
+    // Send Request
+    lwAjax.request.open(method, uri, async, user, password);
+
+    // Content-Type for Post Requests
+    if (method.toLowerCase() == "post") {
+        lwAjax.request.setRequestHeader("Content-Type",postMime);
+    }
+
+    lwAjax.request.send(body);
+
+    return lwAjax.request;
+}
+
+
+/* ==========================================================================
+   lwGetReadyStateHandler(XMLHttpRequest: req, function: responseXmlHandler, bool: raw)
+
+   Returns a callback function, to be called each time req.readystate
+   changes. If the XMLHttpRequest finishes successfully
+   responseHandler() is called on req.responseXML or req.responseText,
+   depending on the content type
+
+   Inspired by: http://www-128.ibm.com/developerworks/library/j-ajax1/?ca=dgr-lnxw01Ajax
+   ========================================================================== */
+
+    Lokris.getReadyStateHandler = function (lwAjax, responseHandler, raw, errorHandler) {
+
+  if (responseHandler == null || responseHandler === undefined) {
+    return function() {};  // Dummy function
+    // Background: When async==false, MSIE calls getreadystatechange.
+    // Mozilla doesn't! 
+    // Therefore: Call Lokris.AjaxCall with "null" as 
+    // 2nd argument (responseHandler).
+    // This if-clause returns a dummy function to be called
+    // from MSIE (and Opera, mybe other browsers).
+    // Check manual for an example of a synchronous call.
+  }
+
+
+  // Return an anonymous function that listens to the 
+  // XMLHttpRequest instance
+  return function () {
+
+    // If the request's status is "complete"
+    if (lwAjax.request.readyState == 4) {
+      if (lwAjax.timeoutId != undefined) {
+	window.clearTimeout(lwAjax.timeoutId);
+      }
+      
+      // Check that a successful server response was received
+      if (lwAjax.request.status == 200) {
+
+	if (raw != undefined && raw) {
+	  responseHandler(lwAjax.request);
+	} else {
+	  var mimeType = String("" + lwAjax.request.getResponseHeader("Content-Type")).split(';')[0];
+
+	  if ( mimeType == "text/xml" ) {
+	    // Pass the XML payload of the response to the 
+            // handler function
+            responseHandler(lwAjax.request.responseXML);
+	  } else {
+            // Pass the text payload of the response to the
+            // handler function
+	    responseHandler(lwAjax.request.responseText);
+	  }
+	}
+      } else {
+	  // An HTTP problem has occurred
+	  errorHandler(lwAjax.request);
+      }
+    }
+  }
+}
+
+
+
+/* ==== Backward Compatibility ==== 
+   For pages using the old name of the Ajax function 
+*/
+
+lwAjaxCall = Lokris.AjaxCall;
\ No newline at end of file

Added: static/prototype.js
==============================================================================
--- (empty file)
+++ static/prototype.js	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,2515 @@
+/*  Prototype JavaScript framework, version 1.5.0
+ *  (c) 2005-2007 Sam Stephenson
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.5.0',
+  BrowserFeatures: {
+    XPath: !!document.evaluate
+  },
+
+  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
+  emptyFunction: function() {},
+  K: function(x) { return x }
+}
+
+var Class = {
+  create: function() {
+    return function() {
+      this.initialize.apply(this, arguments);
+    }
+  }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+  for (var property in source) {
+    destination[property] = source[property];
+  }
+  return destination;
+}
+
+Object.extend(Object, {
+  inspect: function(object) {
+    try {
+      if (object === undefined) return 'undefined';
+      if (object === null) return 'null';
+      return object.inspect ? object.inspect() : object.toString();
+    } catch (e) {
+      if (e instanceof RangeError) return '...';
+      throw e;
+    }
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({}, object);
+  }
+});
+
+Function.prototype.bind = function() {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function() {
+    return __method.apply(object, args.concat($A(arguments)));
+  }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function(event) {
+    return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
+  }
+}
+
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    var digits = this.toString(16);
+    if (this < 16) return '0' + digits;
+    return digits;
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  }
+});
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) {}
+    }
+
+    return returnValue;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.callback(this);
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+}
+String.interpret = function(value){
+  return value == null ? '' : String(value);
+}
+
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += String.interpret(replacement(match));
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
+
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = count === undefined ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
+
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return this;
+  },
+
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = truncation === undefined ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : this;
+  },
+
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
+
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(function(script) { return eval(script) });
+  },
+
+  escapeHTML: function() {
+    var div = document.createElement('div');
+    var text = document.createTextNode(this);
+    div.appendChild(text);
+    return div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = document.createElement('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? (div.childNodes.length > 1 ?
+      $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) :
+      div.childNodes[0].nodeValue) : '';
+  },
+
+  toQueryParams: function(separator) {
+    var match = this.strip().match(/([^?#]*)(#.*)?$/);
+    if (!match) return {};
+
+    return match[1].split(separator || '&').inject({}, function(hash, pair) {
+      if ((pair = pair.split('='))[0]) {
+        var name = decodeURIComponent(pair[0]);
+        var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;
+
+        if (hash[name] !== undefined) {
+          if (hash[name].constructor != Array)
+            hash[name] = [hash[name]];
+          if (value) hash[name].push(value);
+        }
+        else hash[name] = value;
+      }
+      return hash;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  succ: function() {
+    return this.slice(0, this.length - 1) +
+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+  },
+
+  camelize: function() {
+    var parts = this.split('-'), len = parts.length;
+    if (len == 1) return parts[0];
+
+    var camelized = this.charAt(0) == '-'
+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+      : parts[0];
+
+    for (var i = 1; i < len; i++)
+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+    return camelized;
+  },
+
+  capitalize: function(){
+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+  },
+
+  underscore: function() {
+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+  },
+
+  dasherize: function() {
+    return this.gsub(/_/,'-');
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.replace(/\\/g, '\\\\');
+    if (useDoubleQuotes)
+      return '"' + escapedString.replace(/"/g, '\\"') + '"';
+    else
+      return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+  }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (typeof replacement == 'function') return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern  = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    return this.template.gsub(this.pattern, function(match) {
+      var before = match[1];
+      if (before == '\\') return match[2];
+      return before + String.interpret(object[match[3]]);
+    });
+  }
+}
+
+var $break    = new Object();
+var $continue = new Object();
+
+var Enumerable = {
+  each: function(iterator) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        try {
+          iterator(value, index++);
+        } catch (e) {
+          if (e != $continue) throw e;
+        }
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+    return this;
+  },
+
+  eachSlice: function(number, iterator) {
+    var index = -number, slices = [], array = this.toArray();
+    while ((index += number) < array.length)
+      slices.push(array.slice(index, index+number));
+    return slices.map(iterator);
+  },
+
+  all: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      result = result && !!(iterator || Prototype.K)(value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator) {
+    var result = false;
+    this.each(function(value, index) {
+      if (result = !!(iterator || Prototype.K)(value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push((iterator || Prototype.K)(value, index));
+    });
+    return results;
+  },
+
+  detect: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(pattern, iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      var stringValue = value.toString();
+      if (stringValue.match(pattern))
+        results.push((iterator || Prototype.K)(value, index));
+    })
+    return results;
+  },
+
+  include: function(object) {
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inGroupsOf: function(number, fillWith) {
+    fillWith = fillWith === undefined ? null : fillWith;
+    return this.eachSlice(number, function(slice) {
+      while(slice.length < number) slice.push(fillWith);
+      return slice;
+    });
+  },
+
+  inject: function(memo, iterator) {
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.map(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value >= result)
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value < result)
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator) {
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      ((iterator || Prototype.K)(value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator) {
+    return this.map(function(value, index) {
+      return {value: value, criteria: iterator(value, index)};
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }).pluck('value');
+  },
+
+  toArray: function() {
+    return this.map();
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (typeof args.last() == 'function')
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      return iterator(collections.pluck(index));
+    });
+  },
+
+  size: function() {
+    return this.toArray().length;
+  },
+
+  inspect: function() {
+    return '#<Enumerable:' + this.toArray().inspect() + '>';
+  }
+}
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+  if (!iterable) return [];
+  if (iterable.toArray) {
+    return iterable.toArray();
+  } else {
+    var results = [];
+    for (var i = 0, length = iterable.length; i < length; i++)
+      results.push(iterable[i]);
+    return results;
+  }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+  Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0, length = this.length; i < length; i++)
+      iterator(this[i]);
+  },
+
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(value && value.constructor == Array ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  indexOf: function(object) {
+    for (var i = 0, length = this.length; i < length; i++)
+      if (this[i] == object) return i;
+    return -1;
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  reduce: function() {
+    return this.length > 1 ? this : this[0];
+  },
+
+  uniq: function() {
+    return this.inject([], function(array, value) {
+      return array.include(value) ? array : array.concat([value]);
+    });
+  },
+
+  clone: function() {
+    return [].concat(this);
+  },
+
+  size: function() {
+    return this.length;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  }
+});
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string){
+  string = string.strip();
+  return string ? string.split(/\s+/) : [];
+}
+
+if(window.opera){
+  Array.prototype.concat = function(){
+    var array = [];
+    for(var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+    for(var i = 0, length = arguments.length; i < length; i++) {
+      if(arguments[i].constructor == Array) {
+        for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+          array.push(arguments[i][j]);
+      } else {
+        array.push(arguments[i]);
+      }
+    }
+    return array;
+  }
+}
+var Hash = function(obj) {
+  Object.extend(this, obj || {});
+};
+
+Object.extend(Hash, {
+  toQueryString: function(obj) {
+    var parts = [];
+
+	  this.prototype._each.call(obj, function(pair) {
+      if (!pair.key) return;
+
+      if (pair.value && pair.value.constructor == Array) {
+        var values = pair.value.compact();
+        if (values.length < 2) pair.value = values.reduce();
+        else {
+        	key = encodeURIComponent(pair.key);
+          values.each(function(value) {
+            value = value != undefined ? encodeURIComponent(value) : '';
+            parts.push(key + '=' + encodeURIComponent(value));
+          });
+          return;
+        }
+      }
+      if (pair.value == undefined) pair[1] = '';
+      parts.push(pair.map(encodeURIComponent).join('='));
+	  });
+
+    return parts.join('&');
+  }
+});
+
+Object.extend(Hash.prototype, Enumerable);
+Object.extend(Hash.prototype, {
+  _each: function(iterator) {
+    for (var key in this) {
+      var value = this[key];
+      if (value && value == Hash.prototype[key]) continue;
+
+      var pair = [key, value];
+      pair.key = key;
+      pair.value = value;
+      iterator(pair);
+    }
+  },
+
+  keys: function() {
+    return this.pluck('key');
+  },
+
+  values: function() {
+    return this.pluck('value');
+  },
+
+  merge: function(hash) {
+    return $H(hash).inject(this, function(mergedHash, pair) {
+      mergedHash[pair.key] = pair.value;
+      return mergedHash;
+    });
+  },
+
+  remove: function() {
+    var result;
+    for(var i = 0, length = arguments.length; i < length; i++) {
+      var value = this[arguments[i]];
+      if (value !== undefined){
+        if (result === undefined) result = value;
+        else {
+          if (result.constructor != Array) result = [result];
+          result.push(value)
+        }
+      }
+      delete this[arguments[i]];
+    }
+    return result;
+  },
+
+  toQueryString: function() {
+    return Hash.toQueryString(this);
+  },
+
+  inspect: function() {
+    return '#<Hash:{' + this.map(function(pair) {
+      return pair.map(Object.inspect).join(': ');
+    }).join(', ') + '}>';
+  }
+});
+
+function $H(object) {
+  if (object && object.constructor == Hash) return object;
+  return new Hash(object);
+};
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new XMLHttpRequest()},
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+}
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responder) {
+    if (!this.include(responder))
+      this.responders.push(responder);
+  },
+
+  unregister: function(responder) {
+    this.responders = this.responders.without(responder);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (typeof responder[callback] == 'function') {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) {}
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate: function() {
+    Ajax.activeRequestCount++;
+  },
+  onComplete: function() {
+    Ajax.activeRequestCount--;
+  }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+  setOptions: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      contentType:  'application/x-www-form-urlencoded',
+      encoding:     'UTF-8',
+      parameters:   ''
+    }
+    Object.extend(this.options, options || {});
+
+    this.options.method = this.options.method.toLowerCase();
+    if (typeof this.options.parameters == 'string')
+      this.options.parameters = this.options.parameters.toQueryParams();
+  }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+  _complete: false,
+
+  initialize: function(url, options) {
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+    this.request(url);
+  },
+
+  request: function(url) {
+    this.url = url;
+    this.method = this.options.method;
+    var params = this.options.parameters;
+
+    if (!['get', 'post'].include(this.method)) {
+      // simulate other verbs over post
+      params['_method'] = this.method;
+      this.method = 'post';
+    }
+
+    params = Hash.toQueryString(params);
+    if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='
+
+    // when GET, append parameters to URL
+    if (this.method == 'get' && params)
+      this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params;
+
+    try {
+      Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+      this.transport.open(this.method.toUpperCase(), this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous)
+        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      this.setRequestHeaders();
+
+      var body = this.method == 'post' ? (this.options.postBody || params) : null;
+
+      this.transport.send(body);
+
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous && this.transport.overrideMimeType)
+        this.onStateChange();
+
+    }
+    catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState > 1 && !((readyState == 4) && this._complete))
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  setRequestHeaders: function() {
+    var headers = {
+      'X-Requested-With': 'XMLHttpRequest',
+      'X-Prototype-Version': Prototype.Version,
+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+    };
+
+    if (this.method == 'post') {
+      headers['Content-type'] = this.options.contentType +
+        (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+      /* Force "Connection: close" for older Mozilla browsers to work
+       * around a bug where XMLHttpRequest sends an incorrect
+       * Content-length header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType &&
+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+            headers['Connection'] = 'close';
+    }
+
+    // user-defined headers
+    if (typeof this.options.requestHeaders == 'object') {
+      var extras = this.options.requestHeaders;
+
+      if (typeof extras.push == 'function')
+        for (var i = 0, length = extras.length; i < length; i += 2)
+          headers[extras[i]] = extras[i+1];
+      else
+        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+    }
+
+    for (var name in headers)
+      this.transport.setRequestHeader(name, headers[name]);
+  },
+
+  success: function() {
+    return !this.transport.status
+        || (this.transport.status >= 200 && this.transport.status < 300);
+  },
+
+  respondToReadyState: function(readyState) {
+    var state = Ajax.Request.Events[readyState];
+    var transport = this.transport, json = this.evalJSON();
+
+    if (state == 'Complete') {
+      try {
+        this._complete = true;
+        (this.options['on' + this.transport.status]
+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(transport, json);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      if ((this.getHeader('Content-type') || 'text/javascript').strip().
+        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
+          this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
+      Ajax.Responders.dispatch('on' + state, this, transport, json);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    if (state == 'Complete') {
+      // avoid memory leak in MSIE: clean up
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+    }
+  },
+
+  getHeader: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) { return null }
+  },
+
+  evalJSON: function() {
+    try {
+      var json = this.getHeader('X-JSON');
+      return json ? eval('(' + json + ')') : null;
+    } catch (e) { return null }
+  },
+
+  evalResponse: function() {
+    try {
+      return eval(this.transport.responseText);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+  initialize: function(container, url, options) {
+    this.container = {
+      success: (container.success || container),
+      failure: (container.failure || (container.success ? null : container))
+    }
+
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+
+    var onComplete = this.options.onComplete || Prototype.emptyFunction;
+    this.options.onComplete = (function(transport, param) {
+      this.updateContent();
+      onComplete(transport, param);
+    }).bind(this);
+
+    this.request(url);
+  },
+
+  updateContent: function() {
+    var receiver = this.container[this.success() ? 'success' : 'failure'];
+    var response = this.transport.responseText;
+
+    if (!this.options.evalScripts) response = response.stripScripts();
+
+    if (receiver = $(receiver)) {
+      if (this.options.insertion)
+        new this.options.insertion(receiver, response);
+      else
+        receiver.update(response);
+    }
+
+    if (this.success()) {
+      if (this.onComplete)
+        setTimeout(this.onComplete.bind(this), 10);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(container, url, options) {
+    this.setOptions(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = {};
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.options.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(request) {
+    if (this.options.decay) {
+      this.decay = (request.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = request.responseText;
+    }
+    this.timer = setTimeout(this.onTimerEvent.bind(this),
+      this.decay * this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+function $(element) {
+  if (arguments.length > 1) {
+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+      elements.push($(arguments[i]));
+    return elements;
+  }
+  if (typeof element == 'string')
+    element = document.getElementById(element);
+  return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+  document._getElementsByXPath = function(expression, parentElement) {
+    var results = [];
+    var query = document.evaluate(expression, $(parentElement) || document,
+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    for (var i = 0, length = query.snapshotLength; i < length; i++)
+      results.push(query.snapshotItem(i));
+    return results;
+  };
+}
+
+document.getElementsByClassName = function(className, parentElement) {
+  if (Prototype.BrowserFeatures.XPath) {
+    var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
+    return document._getElementsByXPath(q, parentElement);
+  } else {
+    var children = ($(parentElement) || document.body).getElementsByTagName('*');
+    var elements = [], child;
+    for (var i = 0, length = children.length; i < length; i++) {
+      child = children[i];
+      if (Element.hasClassName(child, className))
+        elements.push(Element.extend(child));
+    }
+    return elements;
+  }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element)
+  var Element = new Object();
+
+Element.extend = function(element) {
+  if (!element || _nativeExtensions || element.nodeType == 3) return element;
+
+  if (!element._extended && element.tagName && element != window) {
+    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;
+
+    if (element.tagName == 'FORM')
+      Object.extend(methods, Form.Methods);
+    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
+      Object.extend(methods, Form.Element.Methods);
+
+    Object.extend(methods, Element.Methods.Simulated);
+
+    for (var property in methods) {
+      var value = methods[property];
+      if (typeof value == 'function' && !(property in element))
+        element[property] = cache.findOrStore(value);
+    }
+  }
+
+  element._extended = true;
+  return element;
+};
+
+Element.extend.cache = {
+  findOrStore: function(value) {
+    return this[value] = this[value] || function() {
+      return value.apply(null, [this].concat($A(arguments)));
+    }
+  }
+};
+
+Element.Methods = {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
+  },
+
+  hide: function(element) {
+    $(element).style.display = 'none';
+    return element;
+  },
+
+  show: function(element) {
+    $(element).style.display = '';
+    return element;
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+    return element;
+  },
+
+  update: function(element, html) {
+    html = typeof html == 'undefined' ? '' : html.toString();
+    $(element).innerHTML = html.stripScripts();
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  replace: function(element, html) {
+    element = $(element);
+    html = typeof html == 'undefined' ? '' : html.toString();
+    if (element.outerHTML) {
+      element.outerHTML = html.stripScripts();
+    } else {
+      var range = element.ownerDocument.createRange();
+      range.selectNodeContents(element);
+      element.parentNode.replaceChild(
+        range.createContextualFragment(html.stripScripts()), element);
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  inspect: function(element) {
+    element = $(element);
+    var result = '<' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '>';
+  },
+
+  recursivelyCollect: function(element, property) {
+    element = $(element);
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
+  },
+
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
+  },
+
+  descendants: function(element) {
+    return $A($(element).getElementsByTagName('*'));
+  },
+
+  immediateDescendants: function(element) {
+    if (!(element = $(element).firstChild)) return [];
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    if (element) return [element].concat($(element).nextSiblings());
+    return [];
+  },
+
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
+  },
+
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
+  },
+
+  siblings: function(element) {
+    element = $(element);
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
+
+  match: function(element, selector) {
+    if (typeof selector == 'string')
+      selector = new Selector(selector);
+    return selector.match($(element));
+  },
+
+  up: function(element, expression, index) {
+    return Selector.findElement($(element).ancestors(), expression, index);
+  },
+
+  down: function(element, expression, index) {
+    return Selector.findElement($(element).descendants(), expression, index);
+  },
+
+  previous: function(element, expression, index) {
+    return Selector.findElement($(element).previousSiblings(), expression, index);
+  },
+
+  next: function(element, expression, index) {
+    return Selector.findElement($(element).nextSiblings(), expression, index);
+  },
+
+  getElementsBySelector: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  getElementsByClassName: function(element, className) {
+    return document.getElementsByClassName(className, element);
+  },
+
+  readAttribute: function(element, name) {
+    element = $(element);
+    if (document.all && !window.opera) {
+      var t = Element._attributeTranslations;
+      if (t.values[name]) return t.values[name](element, name);
+      if (t.names[name])  name = t.names[name];
+      var attribute = element.attributes[name];
+      if(attribute) return attribute.nodeValue;
+    }
+    return element.getAttribute(name);
+  },
+
+  getHeight: function(element) {
+    return $(element).getDimensions().height;
+  },
+
+  getWidth: function(element) {
+    return $(element).getDimensions().width;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    var elementClassName = element.className;
+    if (elementClassName.length == 0) return false;
+    if (elementClassName == className ||
+        elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+      return true;
+    return false;
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).add(className);
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).remove(className);
+    return element;
+  },
+
+  toggleClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
+    return element;
+  },
+
+  observe: function() {
+    Event.observe.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  stopObserving: function() {
+    Event.stopObserving.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.match(/^\s*$/);
+  },
+
+  descendantOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var pos = Position.cumulativeOffset(element);
+    window.scrollTo(pos[0], pos[1]);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    if (['float','cssFloat'].include(style))
+      style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat');
+    style = style.camelize();
+    var value = element.style[style];
+    if (!value) {
+      if (document.defaultView && document.defaultView.getComputedStyle) {
+        var css = document.defaultView.getComputedStyle(element, null);
+        value = css ? css[style] : null;
+      } else if (element.currentStyle) {
+        value = element.currentStyle[style];
+      }
+    }
+
+    if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none'))
+      value = element['offset'+style.capitalize()] + 'px';
+
+    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
+      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+    if(style == 'opacity') {
+      if(value) return parseFloat(value);
+      if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+        if(value[1]) return parseFloat(value[1]) / 100;
+      return 1.0;
+    }
+    return value == 'auto' ? null : value;
+  },
+
+  setStyle: function(element, style) {
+    element = $(element);
+    for (var name in style) {
+      var value = style[name];
+      if(name == 'opacity') {
+        if (value == 1) {
+          value = (/Gecko/.test(navigator.userAgent) &&
+            !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0;
+          if(/MSIE/.test(navigator.userAgent) && !window.opera)
+            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
+        } else if(value == '') {
+          if(/MSIE/.test(navigator.userAgent) && !window.opera)
+            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
+        } else {
+          if(value < 0.00001) value = 0;
+          if(/MSIE/.test(navigator.userAgent) && !window.opera)
+            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
+              'alpha(opacity='+value*100+')';
+        }
+      } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat';
+      element.style[name.camelize()] = value;
+    }
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    var display = $(element).getStyle('display');
+    if (display != 'none' && display != null) // Safari bug
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    var originalDisplay = els.display;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = 'block';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = originalDisplay;
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+    return element;
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return element;
+    element._overflow = element.style.overflow || 'auto';
+    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return element;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  }
+};
+
+Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf});
+
+Element._attributeTranslations = {};
+
+Element._attributeTranslations.names = {
+  colspan:   "colSpan",
+  rowspan:   "rowSpan",
+  valign:    "vAlign",
+  datetime:  "dateTime",
+  accesskey: "accessKey",
+  tabindex:  "tabIndex",
+  enctype:   "encType",
+  maxlength: "maxLength",
+  readonly:  "readOnly",
+  longdesc:  "longDesc"
+};
+
+Element._attributeTranslations.values = {
+  _getAttr: function(element, attribute) {
+    return element.getAttribute(attribute, 2);
+  },
+
+  _flag: function(element, attribute) {
+    return $(element).hasAttribute(attribute) ? attribute : null;
+  },
+
+  style: function(element) {
+    return element.style.cssText.toLowerCase();
+  },
+
+  title: function(element) {
+    var node = element.getAttributeNode('title');
+    return node.specified ? node.nodeValue : null;
+  }
+};
+
+Object.extend(Element._attributeTranslations.values, {
+  href: Element._attributeTranslations.values._getAttr,
+  src:  Element._attributeTranslations.values._getAttr,
+  disabled: Element._attributeTranslations.values._flag,
+  checked:  Element._attributeTranslations.values._flag,
+  readonly: Element._attributeTranslations.values._flag,
+  multiple: Element._attributeTranslations.values._flag
+});
+
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    var t = Element._attributeTranslations;
+    attribute = t.names[attribute] || attribute;
+    return $(element).getAttributeNode(attribute).specified;
+  }
+};
+
+// IE is missing .innerHTML support for TABLE-related elements
+if (document.all && !window.opera){
+  Element.Methods.update = function(element, html) {
+    element = $(element);
+    html = typeof html == 'undefined' ? '' : html.toString();
+    var tagName = element.tagName.toUpperCase();
+    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
+      var div = document.createElement('div');
+      switch (tagName) {
+        case 'THEAD':
+        case 'TBODY':
+          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
+          depth = 2;
+          break;
+        case 'TR':
+          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
+          depth = 3;
+          break;
+        case 'TD':
+          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
+          depth = 4;
+      }
+      $A(element.childNodes).each(function(node){
+        element.removeChild(node)
+      });
+      depth.times(function(){ div = div.firstChild });
+
+      $A(div.childNodes).each(
+        function(node){ element.appendChild(node) });
+    } else {
+      element.innerHTML = html.stripScripts();
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  }
+};
+
+Object.extend(Element, Element.Methods);
+
+var _nativeExtensions = false;
+
+if(/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
+    var className = 'HTML' + tag + 'Element';
+    if(window[className]) return;
+    var klass = window[className] = {};
+    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
+  });
+
+Element.addMethods = function(methods) {
+  Object.extend(Element.Methods, methods || {});
+
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    var cache = Element.extend.cache;
+    for (var property in methods) {
+      var value = methods[property];
+      if (!onlyIfAbsent || !(property in destination))
+        destination[property] = cache.findOrStore(value);
+    }
+  }
+
+  if (typeof HTMLElement != 'undefined') {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+    copy(Form.Methods, HTMLFormElement.prototype);
+    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
+      copy(Form.Element.Methods, klass.prototype);
+    });
+    _nativeExtensions = true;
+  }
+}
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+  this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+  initialize: function(element, content) {
+    this.element = $(element);
+    this.content = content.stripScripts();
+
+    if (this.adjacency && this.element.insertAdjacentHTML) {
+      try {
+        this.element.insertAdjacentHTML(this.adjacency, this.content);
+      } catch (e) {
+        var tagName = this.element.tagName.toUpperCase();
+        if (['TBODY', 'TR'].include(tagName)) {
+          this.insertContent(this.contentFromAnonymousTable());
+        } else {
+          throw e;
+        }
+      }
+    } else {
+      this.range = this.element.ownerDocument.createRange();
+      if (this.initializeRange) this.initializeRange();
+      this.insertContent([this.range.createContextualFragment(this.content)]);
+    }
+
+    setTimeout(function() {content.evalScripts()}, 10);
+  },
+
+  contentFromAnonymousTable: function() {
+    var div = document.createElement('div');
+    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+    return $A(div.childNodes[0].childNodes[0].childNodes);
+  }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+  initializeRange: function() {
+    this.range.setStartBefore(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment, this.element);
+    }).bind(this));
+  }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(true);
+  },
+
+  insertContent: function(fragments) {
+    fragments.reverse(false).each((function(fragment) {
+      this.element.insertBefore(fragment, this.element.firstChild);
+    }).bind(this));
+  }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.appendChild(fragment);
+    }).bind(this));
+  }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+  initializeRange: function() {
+    this.range.setStartAfter(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment,
+        this.element.nextSibling);
+    }).bind(this));
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
+
+  toString: function() {
+    return $A(this).join(' ');
+  }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+var Selector = Class.create();
+Selector.prototype = {
+  initialize: function(expression) {
+    this.params = {classNames: []};
+    this.expression = expression.toString().strip();
+    this.parseExpression();
+    this.compileMatcher();
+  },
+
+  parseExpression: function() {
+    function abort(message) { throw 'Parse error in selector: ' + message; }
+
+    if (this.expression == '')  abort('empty expression');
+
+    var params = this.params, expr = this.expression, match, modifier, clause, rest;
+    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
+      params.attributes = params.attributes || [];
+      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
+      expr = match[1];
+    }
+
+    if (expr == '*') return this.params.wildcard = true;
+
+    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
+      modifier = match[1], clause = match[2], rest = match[3];
+      switch (modifier) {
+        case '#':       params.id = clause; break;
+        case '.':       params.classNames.push(clause); break;
+        case '':
+        case undefined: params.tagName = clause.toUpperCase(); break;
+        default:        abort(expr.inspect());
+      }
+      expr = rest;
+    }
+
+    if (expr.length > 0) abort(expr.inspect());
+  },
+
+  buildMatchExpression: function() {
+    var params = this.params, conditions = [], clause;
+
+    if (params.wildcard)
+      conditions.push('true');
+    if (clause = params.id)
+      conditions.push('element.readAttribute("id") == ' + clause.inspect());
+    if (clause = params.tagName)
+      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
+    if ((clause = params.classNames).length > 0)
+      for (var i = 0, length = clause.length; i < length; i++)
+        conditions.push('element.hasClassName(' + clause[i].inspect() + ')');
+    if (clause = params.attributes) {
+      clause.each(function(attribute) {
+        var value = 'element.readAttribute(' + attribute.name.inspect() + ')';
+        var splitValueBy = function(delimiter) {
+          return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
+        }
+
+        switch (attribute.operator) {
+          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
+          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
+          case '|=':      conditions.push(
+                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
+                          ); break;
+          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
+          case '':
+          case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break;
+          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
+        }
+      });
+    }
+
+    return conditions.join(' && ');
+  },
+
+  compileMatcher: function() {
+    this.match = new Function('element', 'if (!element.tagName) return false; \
+      element = $(element); \
+      return ' + this.buildMatchExpression());
+  },
+
+  findElements: function(scope) {
+    var element;
+
+    if (element = $(this.params.id))
+      if (this.match(element))
+        if (!scope || Element.childOf(element, scope))
+          return [element];
+
+    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
+
+    var results = [];
+    for (var i = 0, length = scope.length; i < length; i++)
+      if (this.match(element = scope[i]))
+        results.push(Element.extend(element));
+
+    return results;
+  },
+
+  toString: function() {
+    return this.expression;
+  }
+}
+
+Object.extend(Selector, {
+  matchElements: function(elements, expression) {
+    var selector = new Selector(expression);
+    return elements.select(selector.match.bind(selector)).map(Element.extend);
+  },
+
+  findElement: function(elements, expression, index) {
+    if (typeof expression == 'number') index = expression, expression = false;
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
+
+  findChildElements: function(element, expressions) {
+    return expressions.map(function(expression) {
+      return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) {
+        var selector = new Selector(expr);
+        return results.inject([], function(elements, result) {
+          return elements.concat(selector.findElements(result || element));
+        });
+      });
+    }).flatten();
+  }
+});
+
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  },
+
+  serializeElements: function(elements, getHash) {
+    var data = elements.inject({}, function(result, element) {
+      if (!element.disabled && element.name) {
+        var key = element.name, value = $(element).getValue();
+        if (value != undefined) {
+          if (result[key]) {
+            if (result[key].constructor != Array) result[key] = [result[key]];
+            result[key].push(value);
+          }
+          else result[key] = value;
+        }
+      }
+      return result;
+    });
+
+    return getHash ? data : Hash.toQueryString(data);
+  }
+};
+
+Form.Methods = {
+  serialize: function(form, getHash) {
+    return Form.serializeElements(Form.getElements(form), getHash);
+  },
+
+  getElements: function(form) {
+    return $A($(form).getElementsByTagName('*')).inject([],
+      function(elements, child) {
+        if (Form.Element.Serializers[child.tagName.toLowerCase()])
+          elements.push(Element.extend(child));
+        return elements;
+      }
+    );
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) || (name && input.name != name))
+        continue;
+      matchingInputs.push(Element.extend(input));
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    form = $(form);
+    form.getElements().each(function(element) {
+      element.blur();
+      element.disabled = 'true';
+    });
+    return form;
+  },
+
+  enable: function(form) {
+    form = $(form);
+    form.getElements().each(function(element) {
+      element.disabled = '';
+    });
+    return form;
+  },
+
+  findFirstElement: function(form) {
+    return $(form).getElements().find(function(element) {
+      return element.type != 'hidden' && !element.disabled &&
+        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    form = $(form);
+    form.findFirstElement().activate();
+    return form;
+  }
+}
+
+Object.extend(Form, Form.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
+
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+}
+
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    if (!element.disabled && element.name) {
+      var value = element.getValue();
+      if (value != undefined) {
+        var pair = {};
+        pair[element.name] = value;
+        return Hash.toQueryString(pair);
+      }
+    }
+    return '';
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    return Form.Element.Serializers[method](element);
+  },
+
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    element.focus();
+    if (element.select && ( element.tagName.toLowerCase() != 'input' ||
+      !['button', 'reset', 'submit'].include(element.type) ) )
+      element.select();
+    return element;
+  },
+
+  disable: function(element) {
+    element = $(element);
+    element.disabled = true;
+    return element;
+  },
+
+  enable: function(element) {
+    element = $(element);
+    element.blur();
+    element.disabled = false;
+    return element;
+  }
+}
+
+Object.extend(Form.Element, Form.Element.Methods);
+var Field = Form.Element;
+var $F = Form.Element.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element);
+      default:
+        return Form.Element.Serializers.textarea(element);
+    }
+  },
+
+  inputSelector: function(element) {
+    return element.checked ? element.value : null;
+  },
+
+  textarea: function(element) {
+    return element.value;
+  },
+
+  select: function(element) {
+    return this[element.type == 'select-one' ?
+      'selectOne' : 'selectMany'](element);
+  },
+
+  selectOne: function(element) {
+    var index = element.selectedIndex;
+    return index >= 0 ? this.optionValue(element.options[index]) : null;
+  },
+
+  selectMany: function(element) {
+    var values, length = element.length;
+    if (!length) return null;
+
+    for (var i = 0, values = []; i < length; i++) {
+      var opt = element.options[i];
+      if (opt.selected) values.push(this.optionValue(opt));
+    }
+    return values;
+  },
+
+  optionValue: function(opt) {
+    // extend element because hasAttribute may not be native
+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+  initialize: function(element, frequency, callback) {
+    this.frequency = frequency;
+    this.element   = $(element);
+    this.callback  = callback;
+
+    this.lastValue = this.getValue();
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    var value = this.getValue();
+    var changed = ('string' == typeof this.lastValue && 'string' == typeof value
+      ? this.lastValue != value : String(this.lastValue) != String(value));
+    if (changed) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    Form.getElements(this.element).each(this.registerCallback.bind(this));
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        default:
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) {
+  var Event = new Object();
+}
+
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+
+  element: function(event) {
+    return event.target || event.srcElement;
+  },
+
+  isLeftClick: function(event) {
+    return (((event.which) && (event.which == 1)) ||
+            ((event.button) && (event.button == 1)));
+  },
+
+  pointerX: function(event) {
+    return event.pageX || (event.clientX +
+      (document.documentElement.scrollLeft || document.body.scrollLeft));
+  },
+
+  pointerY: function(event) {
+    return event.pageY || (event.clientY +
+      (document.documentElement.scrollTop || document.body.scrollTop));
+  },
+
+  stop: function(event) {
+    if (event.preventDefault) {
+      event.preventDefault();
+      event.stopPropagation();
+    } else {
+      event.returnValue = false;
+      event.cancelBubble = true;
+    }
+  },
+
+  // find the first node with the given tagName, starting from the
+  // node the event was triggered on; traverses the DOM upwards
+  findElement: function(event, tagName) {
+    var element = Event.element(event);
+    while (element.parentNode && (!element.tagName ||
+        (element.tagName.toUpperCase() != tagName.toUpperCase())))
+      element = element.parentNode;
+    return element;
+  },
+
+  observers: false,
+
+  _observeAndCache: function(element, name, observer, useCapture) {
+    if (!this.observers) this.observers = [];
+    if (element.addEventListener) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.addEventListener(name, observer, useCapture);
+    } else if (element.attachEvent) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.attachEvent('on' + name, observer);
+    }
+  },
+
+  unloadCache: function() {
+    if (!Event.observers) return;
+    for (var i = 0, length = Event.observers.length; i < length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
+  },
+
+  observe: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.attachEvent))
+      name = 'keydown';
+
+    Event._observeAndCache(element, name, observer, useCapture);
+  },
+
+  stopObserving: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.detachEvent))
+      name = 'keydown';
+
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      try {
+        element.detachEvent('on' + name, observer);
+      } catch (e) {}
+    }
+  }
+});
+
+/* prevent memory leaks in IE */
+if (navigator.appVersion.match(/\bMSIE\b/))
+  Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false,
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset
+                || document.documentElement.scrollLeft
+                || document.body.scrollLeft
+                || 0;
+    this.deltaY =  window.pageYOffset
+                || document.documentElement.scrollTop
+                || document.body.scrollTop
+                || 0;
+  },
+
+  realOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        if(element.tagName=='BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p == 'relative' || p == 'absolute') break;
+      }
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  offsetParent: function(element) {
+    if (element.offsetParent) return element.offsetParent;
+    if (element == document.body) return element;
+
+    while ((element = element.parentNode) && element != document.body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return element;
+
+    return document.body;
+  },
+
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = this.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] &&
+            x <  this.offset[0] + element.offsetWidth);
+  },
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = this.realOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = this.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] &&
+            this.xcomp <  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  page: function(forElement) {
+    var valueT = 0, valueL = 0;
+
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+
+      // Safari fix
+      if (element.offsetParent==document.body)
+        if (Element.getStyle(element,'position')=='absolute') break;
+
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      if (!window.opera || element.tagName=='BODY') {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
+      }
+    } while (element = element.parentNode);
+
+    return [valueL, valueT];
+  },
+
+  clone: function(source, target) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || {})
+
+    // find page position of source
+    source = $(source);
+    var p = Position.page(source);
+
+    // find coordinate system to use
+    target = $(target);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(target,'position') == 'absolute') {
+      parent = Position.offsetParent(target);
+      delta = Position.page(parent);
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
+    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.style.position == 'absolute') return;
+    Position.prepare();
+
+    var offsets = Position.positionedOffset(element);
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.width  = width + 'px';
+    element.style.height = height + 'px';
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.style.position == 'relative') return;
+    Position.prepare();
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+  }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned.  For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+  Position.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return [valueL, valueT];
+  }
+}
+
+Element.addMethods();
\ No newline at end of file

Added: test/packages.lisp
==============================================================================
--- (empty file)
+++ test/packages.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,17 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+
+(cl:in-package #:cl-user)
+
+(defpackage #:ht-ajax-test
+  (:use #:cl
+        #:hunchentoot
+        #:cl-ppcre
+        #:ht-ajax
+        #:html-template))
+

Added: test/test-ajax.tmpl.html
==============================================================================
--- (empty file)
+++ test/test-ajax.tmpl.html	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,134 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+-->
+<head>
+<title>HT_AJAX Test page</title>
+<!-- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> -->
+<style type="text/css">
+<!--
+.header1 {
+	background-color: #FFFFCC;
+	font-size: x-large;
+	font-weight: bold;
+        border-width: 2;
+}
+body {
+	background-color: #FFCC99;
+}
+.main {
+	background-color: #FFFFCC;
+}
+
+table {border:none}
+td {border:none}
+fieldset {border:none}
+
+table.jsontable {
+  border: 1px solid;
+  cellspacing:3px;
+  cellpadding:5px;
+  background-color: #FFCC77;
+}
+.jsontable td {
+  border: 1px solid;
+  text-align: right;
+}
+-->
+</style>
+</head>
+
+<body>
+<!-- TMPL_VAR prologue -->
+<noscript><h1 style="color:red;">This page requires Javascript</h1><br /></noscript>
+
+<div class="header1"><h3 style="text-align:center;" class="header1">Test</h3>
+</div>
+<script type="text/javascript">
+function ajax1() {
+    var s = function (res) {
+        el = document.getElementById('res');
+        el.innerHTML = res;
+    }
+    var e = function (info) {
+        debug_alert('Handler caught error: '+info)
+    }
+    ajax_get_counter_callback([s, e]);
+}
+</script>
+<a href="javascript:ajax1();">GET COUNTER</a>
+<br />
+<span id="res">text</span>
+
+<hr />
+
+<br />
+<br />
+<table width="50%">
+  <tr>
+    <td colspan="2">
+      <span id="result"> <i>no results yet</i>
+      </span>
+    </td>
+  </tr>
+  <tr>
+    <td width="70%">
+      <input type="text" size="70" name="command" id="command" />
+    </td>
+    <td>
+      <input type="button" value="Eval" onclick="javascript:command_clicked();"/>
+    </td>
+  </tr>
+</table>
+
+<br />
+<script type="text/javascript">
+function command_clicked(txt) {
+// get the current value of the text input field
+var command = document.getElementById('command').value;
+
+// call function testfunc on the server with the parameter
+// command and set the element with the id 'result' to the return
+// value of the function
+ajax_testfunc_set_element('result', command); 
+
+}
+</script>
+
+
+
+<hr />
+<script type="text/javascript">
+function do_test_json() {
+    ajax_testjson_callback(function (obj) {
+        document.getElementById("object-p").innerHTML=obj.p;
+        document.getElementById("element-of-object-p").innerHTML=obj.p[3];
+    });
+}
+</script>
+<p>Test JSON</p>
+<br />
+<table class="jsontable">
+  <tr>
+    <td> </td>
+    <td>object.p</td>
+    <td>object.p[3]</td>
+  </tr>
+  <tr>
+    <td onclick="do_test_json();" style="background-color: #EE7777">Click here</td>
+    <td id="object-p"> </td>
+    <td id="element-of-object-p"> </td>
+  </tr>
+</table>
+<br />
+
+
+<hr />
+<p>This is just a test page</p>
+<hr />
+</body>
+</html>

Added: test/test-ht-ajax.lisp
==============================================================================
--- (empty file)
+++ test/test-ht-ajax.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,143 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax-test)
+
+(declaim (optimize (space 0) (speed 0) (safety 3) (debug 3)))
+
+
+(defvar *this-file* (load-time-value
+                     (or #.*compile-file-pathname* *load-pathname*)))
+
+(defvar *this-dir* (make-pathname :host (pathname-host *this-file*)
+                                  :device (pathname-device *this-file*)
+                                  :directory (pathname-directory *this-file*)))
+
+
+;; (defmacro debug-output (value)
+;;   `(ignore-errors
+;;     (swank::with-connection ((swank::default-connection)) (print ,value))))
+
+
+
+;;
+(defparameter +templates-root+ (namestring *this-dir*))
+
+(defparameter +web-root-base+ "/hunchentoot/test")
+(defparameter +web-root+ (concatenate 'string +web-root-base+ "/"))
+(defparameter +static-web-root+ (concatenate 'string +web-root+ "static/"))
+
+(defparameter +ajax-handler-url+ (concatenate 'string +web-root+ "ajax-hdlr"))
+
+(defparameter +static-files-root+ (concatenate 'string +templates-root+ "../static/"))
+
+
+
+;;
+(defun expand-web-addr (short-addr)
+  (concatenate 'string +web-root+ short-addr ))
+
+
+(defun expand-template (templ-short-name &optional args)
+  (let ((templ-full-name (merge-pathnames templ-short-name +templates-root+)))
+    (with-output-to-string (*default-template-output*)
+      (funcall #'fill-and-print-template templ-full-name args :external-format :utf-8))))
+
+
+(defun expand-template-with-prologue (templ-short-name &optional args prologue)
+  (let ((page (expand-template templ-short-name args)))
+    (regex-replace "(?s)<body[^>]*>" page (list :match prologue))))
+
+;;
+
+
+(defparameter *ajax-processor* (ht-ajax:make-ajax-processor
+                                :type :prototype
+                                :server-uri +ajax-handler-url+
+                                :js-file-uris "static/"
+                                :js-debug nil
+                                :js-compression t
+                                :virtual-js-file t))
+
+
+;;
+
+
+(defun test ()
+  (no-cache)
+;;   (setf (content-type) "text/html; charset=utf-8")
+;;   (setf (reply-external-format)  hunchentoot::+utf-8+)
+  
+  (expand-template-with-prologue "test-ajax.tmpl.html" '()
+                                 (ht-ajax:generate-prologue *ajax-processor*)))
+
+
+(let ((counter 0))
+  (ht-ajax:defun-ajax get-counter () (*ajax-processor*)
+    (concatenate 'string
+                 "<span>" "counter: "
+                 (princ-to-string (incf counter))
+                 "</span>")))
+
+
+(ht-ajax:defun-ajax testfunc (command) (*ajax-processor* :method :post)
+  (prin1-to-string (handler-case (eval (read-from-string command nil))
+                     (error (c) (format nil "~A" c)))))
+
+
+(ht-ajax:defun-ajax testjson () (*ajax-processor* :method :get
+                                                  :json t)
+  "{\"p\":[1,2,3,5,7,11]}")
+
+
+;;
+
+(defun string-starts-with (string prefix)
+  ;; (from Hunchentoot)
+  (let ((mismatch (mismatch string prefix :test #'char=)))
+    (or (null mismatch)
+        (>= mismatch (length prefix)))))
+
+;;
+(defun page404 ()
+  (no-cache)
+  (setf (return-code *reply*) +http-not-found+)
+  (throw 'handler-done nil))
+
+
+(defparameter +urls-alist+ '(("test" . test)) )
+
+
+(defun serve-static ()
+  "Handle a request for a file under static/ directory"
+  (let* ((script-name (script-name))
+         (fname (subseq script-name (length +static-web-root+)))
+         (fullname (concatenate 'string +static-files-root+ fname)))
+    (handle-static-file fullname)))
+
+
+(defun dispatch (request)
+  (let ((script-name (script-name request)))
+    (cond
+      ((or (string-equal script-name +web-root-base+)
+           (string-equal script-name +web-root+)) 'root-url) ; go to the start page
+      ((string-starts-with script-name +ajax-handler-url+)          ; process AJAX requests
+         (ht-ajax:get-handler *ajax-processor*))
+      ((not (string-starts-with script-name +web-root+)) nil) ; do not handle this request
+      ((string-starts-with script-name +static-web-root+) 'serve-static) ; serve static file
+      
+      (t                                ; normal processing
+       (let* ((name (subseq script-name (length +web-root+)))
+              (handler (assoc name +urls-alist+ :test #'string-equal)))
+
+         (if handler
+             (cdr handler)
+             'page404))))))
+
+
+
+(pushnew 'dispatch *dispatch-table* :test #'eq)

Added: utils.lisp
==============================================================================
--- (empty file)
+++ utils.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,206 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax)
+
+(declaim #.*optimization*)
+
+
+;;
+;; Common functions
+;;
+
+(defun make-safe-js-name (function-name)
+  "Primitive function to try to turn rich lisp names into suitable
+ for Javascript"
+  (loop for c across "-<>"
+     do (setf function-name (substitute #\_ c function-name))
+     finally (return function-name)))
+
+
+(defun prepare-js-debug-function (processor)
+  "Output the debugging function."
+  (concatenate 'string "
+function debug_alert(text) {"
+               (when (js-debug processor)
+                   "  alert(\"HT-AJAX: \" + text);" )
+               "}
+"))
+
+
+;; (defun js-string-to-boolean (str)
+;;   (when str
+;;     (not (or (string= str "")
+;;              (string= str "null")
+;;              (string= str "false")
+;;              (string= str "0"))
+;;          )))
+
+
+
+;; (defun entity-escape (s)
+;;   (setf s (regex-replace-all "(?s)&" s "&"))
+;;   (setf s (regex-replace-all "(?s)<" s "<"))
+;;   (setf s (regex-replace-all "(?s)>" s ">"))
+;;   )
+
+
+
+(defun prepare-js-collect-varargs-to-array (num-standard-args &optional (array-name "args"))
+  (let ((start_args (symbol-name (gensym)))
+        (end_args (symbol-name (gensym)))
+        (i (symbol-name (gensym))))
+    (concatenate 'string
+                 "
+  var " start_args " = " (princ-to-string num-standard-args) ";
+  var " end_args " = arguments.length;
+
+  var " array-name " = new Array();
+  var " i " = " start_args ";
+  for (var " i "=" start_args "; " i " < " end_args "; ++" i ")
+    " array-name ".push(arguments[" i "]);
+"    )))
+
+
+(defun prepare-js-ajax-encode-args ()
+  ;;
+  "
+function ajax_encode_args(func, args) {
+  var res = 'ajax-fun=' + encodeURIComponent(func);
+  var i;
+  if (args)
+    for (i = 0; i < args.length; ++i) {
+      res = res + '&ajax-arg' + i + '=' + encodeURIComponent(args[i]);
+    }
+  res = res + '&ajax-num-args=' + args.length;
+
+  res = res + '&ajax-xml=false';
+
+  return res;
+}
+")
+
+
+(defun wrap-js-in-script-tags (js)
+  (concatenate 'string
+               "
+<script type=\"text/javascript\">
+//<![CDATA[
+"
+               js
+               "
+//]]>
+</script>
+" ))
+
+
+
+(defun prepare-js-file-include (js-file-uri)
+  (concatenate 'string
+          "<script src=\"" js-file-uri "\" type=\"text/javascript\"></script>"))
+
+
+(defun prepare-js-parse-callbacks ()
+  "Create a Javascript function that receives a specification for
+  callbacks and returns an array of two functions, the first is the
+  success callback and the second is the error callback. The callback
+  specification may be: 
+    Function. It is assumed to be the success callback, the error callback
+        is assumed to be null
+    Array. Returned as is, i.e. it should be [success_callback, error_callback]
+    Object. If the object has a success property it is used as success callback.
+        The error property if present becomes the error callback."
+  ;;
+  "
+function ajax_parse_callbacks(obj) {
+  if (typeof obj === 'function') {
+    return [obj, null];
+  }
+  if (typeof obj === 'object' && typeof obj.length === 'number') {
+    // array
+    return obj;
+  }
+  var error_callback = null;
+  var success_callback = null;
+  if (obj.error !== undefined) {
+    error_callback = obj.error;
+  }
+  if (obj.success !== undefined) {
+    success_callback = obj.success;
+  }
+  
+  return [success_callback, error_callback];
+}
+")
+
+
+(defun json-content-type ()
+  "Official IANA http://www.iana.org/assignments/media-types/application/"
+  ;;
+  "application/json")
+
+
+(defun prepare-js-ajax-is-json ()
+  (concatenate 'string
+               "
+function ajax_trim_CR(s) {
+  if (s.charCodeAt(s.length-1)==13) {
+    s = s.substring(0,s.length-1)
+  }
+  return s;
+}
+
+function ajax_is_json(content_type) {
+  content_type = ajax_trim_CR(content_type); // YUI under IE needs this
+  return (content_type == '" (json-content-type) "');
+}
+"))
+
+
+(defun prepare-js-ajax-call-maybe-evaluate-json ()
+  (concatenate 'string
+               "
+function ajax_call_maybe_evaluate_json(callback, data, content_type) {
+  if (ajax_is_json(content_type)) {
+    try {
+      data = eval('(' + data + ')');
+    } 
+    catch (e) {
+      debug_alert(e.message);
+    }
+  }
+  callback(data);
+}
+"))
+
+
+(defun prepare-js-ajax-function-definitions(request-func fun-name js-fun-name &key method &allow-other-keys)
+  "Output a string containing the appropriate Javascript for accessing fun-name
+   on server-uri."
+  (concatenate 'string
+               "
+function " js-fun-name "_callback(callback)
+ {
+" (prepare-js-collect-varargs-to-array 1 "args") "
+  " request-func "('" fun-name "', callback, args);
+}
+"
+               "
+function " js-fun-name "_set_element(elem_id)
+ {
+" (prepare-js-collect-varargs-to-array 1 "args") "
+
+  var elem = document.getElementById(elem_id);
+  if (!elem) {
+    debug_alert('!elem');
+  }
+
+  " request-func "('" fun-name "', function(res) {elem.innerHTML=res;} , args);
+}
+"))
+
+

Added: version.lisp
==============================================================================
--- (empty file)
+++ version.lisp	Fri Nov 14 21:17:43 2008
@@ -0,0 +1,11 @@
+;;; -*- Mode: LISP; Syntax: COMMON-LISP; Base: 10 -*-
+;;;
+;;; Copyright (c) 2007, Ury Marshak
+;;; The code comes with a BSD-style license, so you can basically do
+;;; with it whatever you want. See the file LICENSE for details.
+;;;
+
+(in-package #:ht-ajax)
+
+(defparameter +version+ "0.0.7")
+




More information about the Ht-ajax-cvs mailing list