JSGEN is an extension to AllegroServe that allows one to write JavaScript programs in an s-expression syntax. The primary advantage of this approach is that the user has the ability to extend the language using macros, resulting in shorter more manageable programs. A disadvantage is that the debugging of programming errors within a web browser will require working with the generated JavaScript code. Errors raised in the browser will have to be manually traced to their source within corresponding s-expression code. JSGEN is a good choice if you believe the benefits of syntactic abstraction (shorter programs, fewer bugs) outweighs the cost added to the debugging process.
Download: http://www.kantz.com/jason/programs/jsgen/jsgen_v0_4.tar.gz
License: BSD
Programs written in s-expression JavaScript are called sjs programs and should be saved in files with the extension sjs. AllegroServe has been extended so that when a file of type sjs is requested, the file is translated, and the corresponding JavaScript is returned.
(publish-sjs &key host port path class server locator remove authorizer filename timeout content-type plist)
Use publish-sjs to publish an individual sjs file just as you would use AllegroServe's @1{publish-file}. When the published sjs file is first requested it will be translated to JavaScript and the JavaScript source returned. Subsequent requests will only cause a translation if the sjs file has been changed since the last translation.
(sjs-directory-entity-publisher req ent realname info &key suffixes sjs-content-type)
Use sjs-directory-entity as the publisher argument to AllegroServe's publish-directory to cause sjs files within a directory to be published as sjs files and other files in the directory to be published by standard-directory-entity-publisher.
The translation of sjs files is affected by the value of *print-pretty*. The JavaScript is printed without white space or indentation when *print-pretty* is nil and pretty printed for debugging when t.
JSGEN maps all operators and keywords defined in ECMA 262 to s-expressions. One approach to translation is to develop rules for transforming verbose s-expressions to more concise JavaScript. For example the translator could remove parentheses based on operator precedence or perform transformations such as (= i (+ i 1)) -> i += 1. Another approach would be to make sjs into a separate language that has more in common with lisp than JavaScript. However, since there will be a need to match JavaScript code to s-expression code in the debugging process, the approach taken by JSGEN is to avoid these types of transformations and strive for a direct mapping from the s-expression code to JavaScript. Users can selectively deviate from the mapping through the use of user-defined extension macros. In fact, users can use the extension macros to create a whole new language. JSGEN predefines a set of macro extensions for situations where the mapping from JavaScript to s-expressions is rather clumsy.
(aref array index)
Accessing array elements with aref:
(set favorite (aref coffees 1))
You can get away with [] accessors when they appear to JSGEN as part of the symbol,
(set favorite coffees[1])
This works because "coffees[1]" is treated
as a single symbol. "coffees [1]" will not translate as you might expect
because of the space. See also, dot operator.
(array &rest elements)
(set coffees (array "French Roast" "Columbian" "Kona"))
(cond (test . forms)*)
In JavaScript if statements can be chained together using else if. This makes the JavaScript if statement very similar to lisp's special form cond. Since mapping the else if syntax to S-Expressions is a bit clumsy and requires special formatting functions to to maintain proper indentation in programs like emacs, the choice for JSGEN is to map cond to JavaScript's if statement.
(\. object &rest properties)
The dot operator has to be escaped since it has a special meaning within common lisp. JSGEN defines the macro get to be used in preference over \. For example,
(get xml (getElementsByTagName field) [0] firstChild data)
(for initializer_statement continue_test update_statement &body body)
(for variable in object &body body)
An example of the for statement:
(for (var i 0) (< i 10) (++ i)
(document.write (+ i "<br/>")))
(var obj (object :key 1 :value 10))
(for x in obj
(document.write (get obj x)))
(function name (&rest args) &body body)
(function (&rest args) &body body)
(return value)
Define named and anonymous functions with function. return appears in the body of a function and returns VALUE from the function.
(label statement)
(continue &optional label)
(break &optional label)
Create a label for a statement to be used with continue and break.
(new constructor &rest args)
Create a new object from a constructor function, e.g.,
(new ActiveXObject "Msxml2.XMLHTTP")
(object &rest keys)
Object literals are written using the object operator.
(object
:size 10
:name "Left shoe"
:owner "John Doe"
:description (function ()
(return (+ this.owner "'s" this.name))))
An empty object {} is written as (object).
(op &rest args)
where op is one of: / * % + - << >> >>> < > <= >= in instanceof == != === !== = *= /= %= += -= <<= >>= >>>= \&= \|= ^= & ^ \| \&\& \|\| \,
The above infix operators map from prefix operator s-expressions, e.g.,
(* x 10)
-> (x * 10).
The = operator poses a problem for lisp programmers because a form like (= x 10) is likely to be read as a comparison instead of an assignment. To address this problem, JSGEN defines the macro set to be used in preference over =, e.g., (set x 10).
(op arg)
Prefix op: ++ -- ! ~ Postfix op: _++ _--
These are unary operators, e.g., (_++ x). The underscore in _++ and _-- indicates that the operator is translated as a postfix operator.
(switch string_or_number
(case string_or_number form*)*
(default form*))
(try &body body)
(catch obj &body body)
(finally &body body)
try, catch, and finally are JavaScript statements that can follow one another in the above order for control and error handling.
(var name init &rest val-and-inits)
Declare variables with var. The list of arguments are interpreted in two's as variable-name and initializer.
(var x 10)
(var y 20 z 0)
(while test &body body)
(do form* (while test))
while and do...while map to the corresponding JavaScript looping constructs.
(with class &body body)
(with Math
(set a (* PI r r))
(set x (* r (cos PI)))
(set y (* r (sin (/ PI 2)))))
The folowing lisp-like constructs are defined:
(and form1 form2 &rest forms)
(begin &body statements)
begin is an extension macro useful when writing extension macros. It provides a way to package up a sequence of statements that need to be translated and printed sequentially.
(bq expression)
(uq expression)
bq converts expression into a string. Any appearance of an uq form within bq causes the unquoted form to be evalated before being concatenated into the string. uq may only appear within an expression that is an argument to bq. For example,
(bq (+ x1 x2 x3 x4))
-> ("(x1 + x2 + x3 + x4)")
(bq (+ x1 (uq x2) x3 x4))
-> ("(x1 + " + (x2) + " + x3 + x4)")
(doarray (var_name array) &body body)
(dotimes (var_name number) &body body)
(esc_str &rest args)
esc_str turns args into a string concatenation where the resulting string is a string within a string. For example,
(esc_str "Jason" (lastName person))
->
("\"" + "Jason" + lastName (person) + "\"")
get is an alias for the dot operator (defined since the \. needs to be escaped in common lisp).
(get xml (getElementsByTagName field) [0] firstChild data)
(html var_name lhtml)
Use the html macro to build a string containing html. html converts the LHTML to an html string that is appended to VAR_NAME. For example,
(begin
(var foo "")
(html foo ((:a :href "http://www.kantz.com") "Jason Kantz")))
-> var foo = ""; (foo += ("<a" + " href=\"http://www.kantz.com\"" + ">")); (foo += "Jason Kantz"); (foo += "</a>");
(mfunction name (&rest args) &body body)
Use mfunction to create multi-functions. A multi-function is a function with multiple definitions. Each definition is tried until one returns without throwing. When a function is found that returns without throwing, that function is used on subsequent calls. Use multi-functions for functionality that may fail in various browsers. Be cautious how multi-functions modify any global state or the state of variables passed as arguments. Subsequently called functions may depend on state that has been changed by a previous called function that has failed. Here is an example of a function that will try three different ways to create an HttpRequest object:
(mfunction createRequest ())
(mfunction createRequest ()
(return (new XMLHttpRequest)))
(mfunction createRequest ()
(return (new ActiveXObject "Msxml2.XMLHTTP")))
(mfunction createRequest ()
(return (new ActiveXObject "Microsoft.XMLHTTP")))
(not form)
(or form1 form2 &rest forms)
(when test &body body)
#% is a reader macro that quotes the following form and interns its symbols into the :jsgen package.
#%(var x camelCase) -> (JSGEN:|var| JSGEN::|x| JSGEN::|camelCase|)
(def-macro-form op-sym (args-sym) &body body)
Use def-macro-form to define jsgen extension macros. BODY should return an JSGEN s-expression. Note that you will have to escape you JavaScript keywords and identifiers in body to preserve case. For example:
(in-package :jsgen)
(def-macro-form |when| (args)
(destructuring-bind (test . body) args
`(|cond| (,test ,@body))))
(genvar &optional (string ""))
Use genvar to create temporary variable names when defining extension macros. Genvar simply uses a prefix and a counter and concatenates them with STRING. Counter starts over with every js-compile-file so genvars are not guaranteed to be unique across files.
(js stream case_preserved_form)
Translate CASE_PRESERVED_FORM to JavaScript, outputing JavaScript to STREAM. Note that the symbols have to be in the :jsgen package and have their case preserved, so the #% reader macro is provided to do just this:
(jsgen:enable-readtable jsgen:*preserve-case-readtable*)
(jsgen:js t #%(var x 10))
-> var x = 10;
(js-compile-file sjs-file &key (force nil))
Translate SJS-FILE to JavaScript, creating a new file with extension js. If FORCE is non-nil then translation is forced. If FORCE is nil, translation is not performed when the output file is newer than SJS-FILE.
(js-string case_preserved_form)
Translate CASE_PRESERVED_FORM to JavaScript, returning JavaScript as a string.
(pop-readtable)
Restores *readtable* to the readtable in use before the last call to push-readtable.
(push-readtable *preserve-case-readtable*)
Saves away a copy of current *readtable* and installs the given readtable by copying it into *readtable*.
I read Steve Haflich's xml-generator as an example of how to use the common lisp pretty printer. AllegroServe is written and maintained by John Foderaro of Franz Inc. Manuel Odendahl's and Edward Marco Baringer's parenscript provided the motivation to write something similar using the common lisp pretty printer. Acknowledgents to Edi Weitz for writing such good documentation for his open source projects (I've followed his style here).
Questions and comments should be directed to Jason Kantz: jason at kantz dot com.