Reworked printing implementation to get rid of dwim-join and gratuitious consing.
[clinton/parenscript.git] / src / js-translation.lisp
dissimilarity index 96%
index 6ee8692..65ce5e2 100644 (file)
-(in-package :parenscript.javascript)
-
-(defgeneric js-to-strings (expression start-pos)
-  (:documentation "Transform an enscript-javascript expression to a string"))
-
-(defgeneric js-to-statement-strings (code-fragment start-pos)
-  (:documentation "Transform an enscript-javascript code fragment to a string"))
-
-;;; indenter
-
-(defun special-append-to-last (form elt)
-  (flet ((special-append (form elt)
-          (let ((len (length form)))
-            (if (and (> len 0)
-                      (string= (char form (1- len)) elt))
-                form
-                (concatenate 'string form elt)))))
-    (cond ((stringp form)
-          (special-append form elt))
-         ((consp form)
-          (let ((last (last form)))
-            (if (stringp (car last))
-                (rplaca last (special-append (car last) elt))
-                (append-to-last (car last) elt))
-          form))
-         (t (error "unsupported form ~S" form)))))
-
-(defun dwim-join (value-string-lists max-length
-                 &key (start "")
-                       end
-                      (join-before "")
-                       join-after
-                      (white-space (make-string (length start) :initial-element #\Space))
-                       (separator " ")
-                 (append-to-last #'append-to-last)
-                 (collect t))
-    #+nil
-    (format t "value-string-lists: ~S~%" value-string-lists)
-
-    ;;; collect single value-string-lists until line full
-
-    (do* ((string-lists value-string-lists (cdr string-lists))
-         (string-list (car string-lists) (car string-lists))
-         (cur-elt start)
-          (is-first t nil)
-         (cur-empty t)
-         (res nil))
-        ((null string-lists)
-         (unless cur-empty
-           (push cur-elt res))
-         (if (null res)
-             (list (concatenate 'string start end))
-             (progn
-               (when end
-                 (setf (first res)
-                       (funcall append-to-last (first res) end)))
-               (nreverse res))))
-      #+nil
-      (format t "string-list: ~S~%" string-list)
-
-      (when join-after
-       (unless (null (cdr string-lists))
-         (funcall append-to-last string-list join-after)))
-
-      (if (and collect (= (length string-list) 1))
-         (progn
-           #+nil
-           (format t "cur-elt: ~S line-length ~D, max-length ~D, string: ~S~%"
-                   cur-elt
-                   (+ (length (first string-list))
-                      (length cur-elt))
-                   max-length
-                   (first string-list))
-           (if (or cur-empty
-                   (< (+ (length (first string-list))
-                         (length cur-elt)) max-length))
-               (setf cur-elt
-                     (concatenate 'string cur-elt
-                                  (if (or is-first (and cur-empty (string= join-before "")))
-                                        "" (concatenate 'string separator join-before))
-                                  (first string-list))
-                     cur-empty nil)
-               (progn
-                 (push cur-elt res)
-                 (setf cur-elt (concatenate 'string white-space
-                                            join-before (first string-list))
-                       cur-empty nil))))
-
-         (progn
-           (unless cur-empty
-             (push cur-elt res)
-             (setf cur-elt white-space
-                   cur-empty t))
-           (setf res (nconc (nreverse
-                             (cons (concatenate 'string
-                                                cur-elt
-                                                 (if (null res)
-                                                    "" join-before)
-                                                (first string-list))
-                                   (mapcar #'(lambda (x) (concatenate 'string white-space x))
-                                           (cdr string-list))))
-                             res))
-           (setf cur-elt white-space cur-empty t)))))
-
-(defmethod js-to-strings ((expression expression) start-pos)
-  (declare (ignore start-pos))
-  (list (princ-to-string (value expression))))
-
-(defmethod js-to-statement-strings ((expression expression) start-pos)
-  (js-to-strings expression start-pos))
-
-(defmethod js-to-statement-strings ((statement statement) start-pos)
-  (declare (ignore start-pos))
-  (list (princ-to-string (value statement))))
-
-(defmethod js-to-strings ((expression script-quote) start-pos)
-  (declare (ignore start-pos))
-  (list
-   (if (eql nil (value expression))
-       "null"
-       (case (value expression)
-        (t (error "Cannot translate quoted value ~S to javascript" (value expression)))))))
-
-;;; array literals
-
-(defmethod js-to-strings ((array array-literal) start-pos)
-  (let ((value-string-lists
-        (mapcar #'(lambda (x) (js-to-strings x (+ start-pos 2)))
-                (array-values array)))
-       (max-length (- 80 start-pos 2)))
-    (dwim-join value-string-lists max-length
-              :start "[ " :end " ]"
-              :join-after ",")))
-
-(defmethod js-to-strings ((aref js-aref) start-pos)
-  (dwim-join (cons (js-to-strings (aref-array aref) start-pos)
-                  (mapcar #'(lambda (x) (dwim-join (list (js-to-strings x (+ start-pos 2)))
-                                                   (- 80 start-pos 2)
-                                                   :start "[" :end "]"))
-                          (aref-index aref)))
-            (- 80 start-pos 2) :separator ""
-            :white-space "  "))
-
-;;; object literals (maps and hash-tables)
-
-(defmethod js-to-strings ((obj object-literal) start-pos)
-  (dwim-join
-   (loop
-    for (key . value) in (object-values obj)
-    append (list
-           (dwim-join (list (list (format nil "~A:" (js-translate-symbol key)))
-                            (js-to-strings value (+ start-pos 2)))
-                      (- 80 start-pos 2)
-                      :start "" :end "" :join-after "")))
-   (- 80 start-pos 2)
-   :start "{ " :end " }"
-   :join-after ","))
-
-;;; string literals
-
-(defvar *js-quote-char* #\'
-  "Specifies which character JS sholud use for delimiting strings.
-
-This variable is usefull when have to embed some javascript code
-in an html attribute delimited by #\\\" as opposed to #\\', or
-vice-versa.")
-
-(defparameter *js-lisp-escaped-chars*
-  '((#\' . #\')
-    (#\\ . #\\)
-    (#\b . #\Backspace)
-    (#\f . #.(code-char 12))
-    (#\n . #\Newline)
-    (#\r . #\Return)
-    (#\t . #\Tab)))
-
-(defun lisp-special-char-to-js (lisp-char)
-    (car (rassoc lisp-char *js-lisp-escaped-chars*)))
-
-(defmethod js-to-strings ((string string-literal) start-pos)
-  (declare (ignore start-pos)
-           (inline lisp-special-char-to-js))
-  (list (with-output-to-string (escaped)
-          (write-char *js-quote-char*  escaped)
-          (loop
-           for char across (value string)
-           for code = (char-code char)
-           for special = (lisp-special-char-to-js char)
-           do
-           (cond
-             (special
-              (write-char #\\ escaped)
-              (write-char special escaped))
-             ((or (<= code #x1f) (>= code #x80))
-              (format escaped "\\u~4,'0x" code))
-             (t (write-char char escaped)))
-           finally (write-char *js-quote-char* escaped)))))
-
-;;; variables
-(defgeneric js-translate-symbol-contextually (symbol package env)
-  (:documentation "Translates a symbol to a string in the given environment & package
-and for the given symbol."))
-
-(defparameter *obfuscate-standard-identifiers* nil)
-
-(defparameter *obfuscation-table* (make-hash-table))
-
-(defun obfuscated-symbol (symbol)
-  (or (gethash symbol *obfuscation-table*)
-      (setf (gethash symbol *obfuscation-table*) (string (gensym)))))
-
-(defmethod js-translate-symbol-contextually ((symbol symbol)
-                                            (package ps::script-package)
-                                            (env ps::compilation-environment))
-  (cond
-    ((member (ps::script-package-lisp-package package)
-            (mapcar #'find-package '(:keyword :parenscript.global)))
-     (symbol-to-js symbol))
-    (*obfuscate-standard-identifiers*
-     (obfuscated-symbol symbol))
-    (t
-     (case *package-prefix-style*
-       (:prefix
-       (format nil "~A~A"
-               (or (ps::script-package-prefix package) (concatenate 'string (ps::script-package-name package) "_"))
-               (symbol-to-js symbol)))
-       (t
-       (symbol-to-js (value symbol)))))))
-
-(defgeneric js-translate-symbol (var)
-  (:documentation "Given a JS-VARIABLE returns an output
-JavaScript version of it as a string."))
-
-(defmethod js-translate-symbol ((var js-variable))
-  (js-translate-symbol (value var)))
-
-(defmethod js-translate-symbol ((var-name symbol))
-  (js-translate-symbol-contextually var-name (ps::symbol-script-package var-name) ps::*compilation-environment*))
-
-(defmethod js-to-strings ((v js-variable) start-form)
-  (declare (ignore start-form))
-  (list (js-translate-symbol v)))
-
-;;; arithmetic operators
-(defun script-convert-op-name (op)
-  (case op
-    (and '\&\&)
-    (or '\|\|)
-    (not '!)
-    (eql '\=\=)
-    (=   '\=\=)
-    (t op)))
-
-(defun op-form-p (form)
-  (and (listp form)
-       (not (script-special-form-p form))
-       (not (null (op-precedence (first form))))))
-
-(defun klammer (string-list)
-  (prepend-to-first string-list "(")
-  (append-to-last string-list ")")
-  string-list)
-
-(defmethod expression-precedence ((expression expression))
-  0)
-
-(defmethod expression-precedence ((form op-form))
-  (op-precedence (operator form)))
-
-(defmethod js-to-strings ((form op-form) start-pos)
-  (let* ((precedence (expression-precedence form))
-        (value-string-lists
-         (mapcar #'(lambda (x)
-                     (let ((string-list (js-to-strings x (+ start-pos 2))))
-                       (if (>= (expression-precedence x) precedence)
-                           (klammer string-list)
-                           string-list)))
-                 (op-args form)))
-        (max-length (- 80 start-pos 2))
-        (op-string (format nil "~A " (operator form))))
-    (dwim-join value-string-lists max-length :join-before op-string)    
-    ))
-
-(defmethod js-to-strings ((one-op one-op) start-pos)
-  (let* ((value (value one-op))
-        (value-strings (js-to-strings value start-pos)))
-    (when (typep value 'op-form)
-      (setf value-strings (klammer value-strings)))
-    (if (one-op-pre-p one-op)
-      (prepend-to-first value-strings
-                       (one-op one-op))
-      (append-to-last value-strings
-                     (one-op one-op)))))
-
-;;; function calls
-
-(defmethod js-to-strings ((form function-call) start-pos)
-  (let* ((value-string-lists
-         (mapcar #'(lambda (x) (js-to-strings x (+ start-pos 2)))
-                 (f-args form)))
-        (max-length (- 80 start-pos 2))
-        (args (dwim-join value-string-lists max-length
-                         :start "(" :end ")" :join-after ",")))
-    (etypecase (f-function form)
-      (js-lambda
-       (dwim-join (list (append (dwim-join (list (js-to-strings (f-function form) (+ start-pos 2)))
-                                           max-length
-                                           :start "(" :end ")" :separator "")
-                                args))
-                  max-length
-                  :separator ""))
-      ((or js-variable js-aref js-slot-value)
-       (dwim-join (list (js-to-strings (f-function form) (+ start-pos 2))
-                        args)
-                  max-length
-                  :separator ""))
-      (function-call
-       ;; TODO it adds superfluous newlines after each ()
-       ;; and it's nearly the same as the js-lambda case above
-       (dwim-join (list (append (dwim-join (list (js-to-strings (f-function form) (+ start-pos 2)))
-                                           max-length :separator "")
-                                args))
-                  max-length :separator "")))))
-
-(defmethod js-to-strings ((form method-call) start-pos)
-  (let ((object (js-to-strings (m-object form) (+ start-pos 2))))
-    ;; TODO: this may not be the best way to add ()'s around lambdas
-    ;; probably there is or should be a more general solution working
-    ;; in other situations involving lambda's
-    (when (member (m-object form) (list 'js-lambda 'number-literal 'js-object 'op-form)
-                 :test #'typep)  
-      (push "(" object)
-      (nconc object (list ")")))
-    (let* ((fname (dwim-join (list object
-                                   (list (js-translate-symbol (m-method form))))
-                             (- 80 start-pos 2)
-                             :end "("
-                             :separator ""))
-           (butlast (butlast fname))
-           (last (car (last fname)))
-           (method-and-args (dwim-join (mapcar #'(lambda (x) (js-to-strings x (+ start-pos 2)))
-                                               (m-args form))
-                                       (- 80 start-pos 2)
-                                       :start last
-                                       :end ")"
-                                       :join-after ","))
-           (ensure-no-newline-before-dot (concatenate 'string
-                                                      (car (last butlast))
-                                                      (first method-and-args))))
-      (nconc (butlast butlast)
-             (list ensure-no-newline-before-dot)
-             (rest method-and-args)))))
-
-;;; optimization that gets rid of nested blocks, which have no meaningful effect
-;;; in javascript
-(defgeneric expanded-subblocks (block)
-  (:method (block)
-    (list block))
-  (:method ((block js-block))
-    (mapcan #'expanded-subblocks (block-statements block))))
-
-(defun consolidate-subblocks (block)
-  (setf (block-statements block) (expanded-subblocks block))
-  block)
-
-
-(defmethod js-to-statement-strings ((body js-block) start-pos)
-  (consolidate-subblocks body)
-  (dwim-join (mapcar #'(lambda (x) (js-to-statement-strings x (+ start-pos 2)))
-                    (block-statements body))
-            (- 80 start-pos 2)
-            :join-after ";"
-            :append-to-last #'special-append-to-last
-            :start (block-indent body) :collect nil
-            :end ";"))
-
-(defmethod js-to-strings ((body js-block) start-pos)
-  (dwim-join (mapcar #'(lambda (x) (js-to-strings x (+ start-pos 2)))
-                    (block-statements body))
-            (- 80 start-pos 2)
-            :append-to-last #'special-append-to-last
-            :join-after ","
-            :start (block-indent body)))
-
-
-(defmethod js-to-statement-strings ((body js-sub-block) start-pos)
-  (declare (ignore start-pos))
-  (nconc (list "{") (call-next-method) (list "}")))
-
-;;; function definition
-(defmethod js-to-strings ((lambda js-lambda) start-pos)
-  (let ((fun-header (dwim-join (mapcar #'(lambda (x)
-                                           (list (js-translate-symbol x)))
-                                      (lambda-args lambda))
-                              (- 80 start-pos 2)
-                              :start (function-start-string lambda)
-                              :end ") {" :join-after ","))
-       (fun-body (js-to-statement-strings (lambda-body lambda) (+ start-pos 2))))
-    (nconc fun-header fun-body (list "}"))))
-
-(defgeneric function-start-string (function)
-  (:documentation "Returns the string that starts the function - this varies according to whether
-this is a lambda or a defun"))
-
-(defmethod function-start-string ((lambda js-lambda))
-  "function (")
-
-(defmethod js-to-statement-strings ((lambda js-lambda) start-pos)
-  (js-to-strings lambda start-pos))
-
-(defmethod function-start-string ((defun js-defun))
-  (format nil "function ~A(" (js-translate-symbol (defun-name defun))))
-
-;;; object creation
-(defmethod js-to-strings ((object js-object) start-pos)
-  (let ((value-string-lists
-        (mapcar #'(lambda (slot)
-                    (let* ((slot-name (first slot))
-                           (slot-string-name
-                           (if (typep slot-name 'script-quote)
-                               (if (symbolp (value slot-name))
-                                   (format nil "~A" (js-translate-symbol (value slot-name)))
-                                   (format nil "~A" (first (js-to-strings slot-name 0))))
-                               (car (js-to-strings slot-name 0)))))
-                      (dwim-join (list (js-to-strings (second slot) (+ start-pos 4)))
-                                 (- 80 start-pos 2)
-                                 :start (concatenate 'string slot-string-name  " : ")
-                                 :white-space "    ")))
-                (o-slots object)))
-       (max-length (- 80 start-pos 2)))
-    (dwim-join value-string-lists max-length
-              :start "{ "
-              :end " }"
-              :join-after ", "
-              :white-space "  "
-              :collect nil)))
-
-(defmethod js-to-strings ((sv js-slot-value) start-pos)
-  (append-to-last (if (typep (sv-object sv) 'js-variable)
-                      (js-to-strings (sv-object sv) start-pos)
-                      (list (format nil "~A" (js-to-strings (sv-object sv) start-pos))))
-                  (if (typep (sv-slot sv) 'script-quote)
-                      (if (symbolp (value (sv-slot sv)))
-                          (format nil ".~A" (js-translate-symbol (value (sv-slot sv))))
-                          (format nil ".~A" (first (js-to-strings (sv-slot sv) 0))))
-                      (format nil "[~A]" (first (js-to-strings (sv-slot sv) 0))))))
-
-;;; cond
-(defmethod js-to-statement-strings ((cond js-cond) start-pos)
-  (loop :for body :on (cond-bodies cond)
-       :for first = (eq body (cond-bodies cond))
-       :for last = (not (cdr body))
-       :for test :in (cond-tests cond)
-       :append (if (and last (not first) (string= (value test) "true"))
-                   '("else {")
-                   (dwim-join (list (js-to-strings test 0)) (- 80 start-pos 2)
-                              :start (if first "if (" "else if (") :end ") {"))
-       :append (js-to-statement-strings (car body) (+ start-pos 2))
-       :collect "}"))
-
-(defmethod js-to-statement-strings ((if js-if) start-pos)
-  (let ((if-strings (dwim-join (list (js-to-strings (if-test if) 0))
-                              (- 80 start-pos 2)
-                              :start "if ("
-                              :end ") {"))
-       (then-strings (js-to-statement-strings (if-then if) (+ start-pos 2)))
-       (else-strings (when (if-else if)
-                       (js-to-statement-strings (if-else if)
-                                                (+ start-pos 2)))))
-    (nconc if-strings then-strings (if else-strings
-                                      (nconc (list "} else {") else-strings (list "}"))
-                                      (list "}")))))
-
-(defmethod js-to-strings ((if js-if) start-pos)
-  (assert (typep (if-then if) 'expression))
-  (when (if-else if)
-    (assert (typep (if-else if) 'expression)))
-  (dwim-join (list (append-to-last (js-to-strings (if-test if) start-pos) " ?")
-                  (let* ((new-then (make-instance 'js-block
-                                                  :statements (block-statements (if-then if))
-                                                  :indent ""))
-                         (res (js-to-strings new-then start-pos)))
-                    (if (>= (expression-precedence (if-then if))
-                            (expression-precedence if))
-                            (klammer res)
-                            res))
-                  (list ":")
-                  (if (if-else if)
-                      (let* ((new-else (make-instance 'js-block
-                                                      :statements (block-statements (if-else if))
-                                                      :indent ""))
-                             (res (js-to-strings new-else start-pos)))
-                        (if (>= (expression-precedence (if-else if))
-                                (expression-precedence if))
-                            (klammer res)
-                            res))
-                      (list "undefined")))
-            (- 80 start-pos 2)
-            :white-space "  "))
-
-;;; setf
-(defmethod js-to-strings ((setf js-setf) start-pos)
-  (dwim-join (cons (js-to-strings (setf-lhs setf) start-pos)
-                  (mapcar #'(lambda (x) (js-to-strings x start-pos)) (setf-rhsides setf)))
-            (- 80 start-pos 2)
-            :join-after " ="))
-
-;;; defvar
-(defmethod js-to-statement-strings ((defvar js-defvar) start-pos)
-  (dwim-join (nconc (mapcar #'(lambda (x) (list (js-translate-symbol x))) (var-names defvar))
-                   (when (var-value defvar)
-                     (list (js-to-strings (var-value defvar) start-pos))))
-            (- 80 start-pos 2)
-            :join-after " ="
-            :start "var " :end ";"))
-
-;;; iteration
-(defmethod js-to-statement-strings ((for js-for) start-pos)
-  (let* ((init (dwim-join (mapcar #'(lambda (x)
-                                     (dwim-join (list (list (js-translate-symbol (first (var-names x))))
-                                                      (js-to-strings (var-value x)
-                                                                     (+ start-pos 2)))
-                                                (- 80 start-pos 2)
-                                                :join-after " ="))
-                                 (for-vars for))
-                         (- 80 start-pos 2)
-                         :start "var " :join-after ","))
-        (check (js-to-strings (for-check for) (+ start-pos 2)))
-        (steps (dwim-join (mapcar #'(lambda (x var)
-                                      (dwim-join
-                                       (list (list (js-translate-symbol (first (var-names var))))
-                                             (js-to-strings x (- start-pos 2)))
-                                       (- 80 start-pos 2)
-                                       :join-after " ="))
-                                  (for-steps for)
-                                  (for-vars for))
-                          (- 80 start-pos 2)
-                          :join-after ","))
-        (header (dwim-join (list init check steps)
-                           (- 80 start-pos 2)
-                           :start "for (" :end ") {"
-                           :join-after ";"))
-        (body (js-to-statement-strings (for-body for) (+ start-pos 2))))
-    (nconc header body (list "}"))))
-
-
-(defmethod js-to-statement-strings ((fe for-each) start-pos)
-  (let ((header (dwim-join (list (list (js-translate-symbol (fe-name fe)))
-                                (list "in")
-                                (js-to-strings (fe-value fe) (+ start-pos 2)))
-                          (- 80 start-pos 2)
-                          :start "for (var "
-                          :end ") {"))
-       (body (js-to-statement-strings (fe-body fe) (+ start-pos 2))))
-    (nconc header body (list "}"))))
-
-(defmethod js-to-statement-strings ((while js-while) start-pos)
-  (let ((header (dwim-join (list (js-to-strings (while-check while) (+ start-pos 2)))
-                          (- 80 start-pos 2)
-                          :start "while ("
-                          :end ") {"))
-       (body (js-to-statement-strings (while-body while) (+ start-pos 2))))
-    (nconc header body (list "}"))))
-
-;;; with
-(defmethod js-to-statement-strings ((with js-with) start-pos)
-  (nconc (dwim-join (list (js-to-strings (with-obj with) (+ start-pos 2)))
-                   (- 80 start-pos 2)
-                   :start "with (" :end ") {")
-        (js-to-statement-strings (with-body with) (+ start-pos 2))
-        (list "}")))
-
-;;; switch
-(defmethod js-to-statement-strings ((case js-switch) start-pos)
-  (let ((body   (mapcan #'(lambda (clause)
-                    (let ((val (car clause))
-                          (body (second clause)))
-                      (dwim-join (list (if (eql val 'default)
-                                           (list "")
-                                           (js-to-strings val (+ start-pos 2)))
-                                       (js-to-statement-strings body (+ start-pos 2)))
-                                 (- 80 start-pos 2)
-                                 :start (if (eql val 'default) "  default" "  case ")
-                                 :white-space "   "
-                                 :join-after ":"))) (case-clauses case))))
-    (nconc (dwim-join (list (js-to-strings (case-value case) (+ start-pos 2)))
-                   (- 80 start-pos 2)
-                   :start "switch (" :end ") {")
-          body
-          (list "}"))))
-
-;;; try-catch
-(defmethod js-to-statement-strings ((try js-try) start-pos)
-  (let* ((catch (try-catch try))
-        (finally (try-finally try))
-        (catch-list (when catch
-                      (nconc
-                       (dwim-join (list (list (js-translate-symbol (first catch))))
-                                  (- 80 start-pos 2)
-                                  :start "} catch ("
-                                  :end ") {")
-                       (js-to-statement-strings (second catch) (+ start-pos 2)))))
-        (finally-list (when finally
-                        (nconc (list "} finally {")
-                               (js-to-statement-strings finally (+ start-pos 2))))))
-    (nconc (list "try {")
-          (js-to-statement-strings (try-body try) (+ start-pos 2))
-          catch-list
-          finally-list
-          (list "}"))))
-
-;;; regex
-(defun first-slash-p (string)
-  (and (> (length string) 0)
-       (eq (char string 0) '#\/)))
-
-(defmethod js-to-strings ((regex regex) start-pos)
-   (declare (ignore start-pos))
-   (let ((slash (if (first-slash-p (value regex)) nil "/")))
-     (list (format nil (concatenate 'string slash "~A" slash) (value regex)))))
-
-;;; conditional compilation
-(defmethod js-to-statement-strings ((cc cc-if) start-pos)
-  (nconc (list (format nil "/*@if ~A" (cc-if-test cc)))
-        (mapcan #'(lambda (x) (js-to-strings x start-pos)) (cc-if-body cc))
-        (list "@end @*/")))
-
-
-;;; TODO instanceof
-(defmethod js-to-strings ((instanceof js-instanceof) start-pos)
-  (dwim-join
-   (list (js-to-strings (value instanceof) (+ start-pos 2))
-         (list "instanceof")
-         (js-to-strings (slot-value instanceof 'type) (+ start-pos 2)))
-   (- 80 start-pos 2)
-   :start "("
-   :end ")"
-   :white-space
-   "  "))
-
-;;; single operations
-(defmacro define-translate-js-single-op (name &optional (superclass 'expression))
-    (let ((script-name (intern (concatenate 'string "JS-" (symbol-name name)) #.*package*)))
-      `(defmethod ,(if (eql superclass 'expression)
-                       'js-to-strings
-                      'js-to-statement-strings)
-       ((,name ,script-name) start-pos)
-       (dwim-join (list (js-to-strings (value ,name) (+ start-pos 2)))
-        (- 80 start-pos 2)
-        :start ,(concatenate 'string (string-downcase (symbol-name name)) " ")
-        :white-space "  "))))
-
-(define-translate-js-single-op return statement)
-(define-translate-js-single-op throw statement)
-(define-translate-js-single-op delete)
-(define-translate-js-single-op void)
-(define-translate-js-single-op typeof)
-(define-translate-js-single-op new)
\ No newline at end of file
+(in-package :parenscript)
+
+(defvar *ps-output-stream*)
+
+(defmethod parenscript-print (ps-form &optional *ps-output-stream*)
+  (flet ((print-ps (form)
+           (let ((*standard-output* *ps-output-stream*))
+             (ps-print form))))
+    (if *ps-output-stream*
+        (print-ps ps-form)
+        (with-output-to-string (*ps-output-stream*)
+          (print-ps ps-form)))))
+
+(defgeneric ps-print% (special-form-name special-form-args))
+
+(defmacro defprinter (special-form content-args &body body)
+  "Given a special-form name and a destructuring lambda-list for its
+arguments, defines a printer for that form using the given body."
+  (let ((sf (gensym))
+        (sf-args (gensym)))
+    `(defmethod ps-print% ((,sf (eql ',special-form)) ,sf-args)
+      (declare (ignore ,sf))
+      (destructuring-bind ,content-args
+          ,sf-args
+        ,@body))))
+
+(defgeneric ps-print (compiled-form))
+
+(defmethod ps-print ((compiled-form cons))
+  "Prints the given compiled ParenScript form starting at the given
+indent position."
+  (ps-print% (car compiled-form) (cdr compiled-form)))
+
+;;; string literals
+(defvar *js-quote-char* #\'
+  "Specifies which character JS should use for delimiting strings.
+
+This variable is useful when have to embed some javascript code
+in an html attribute delimited by #\\\" as opposed to #\\', or
+vice-versa.")
+
+(defparameter *js-lisp-escaped-chars*
+  '((#\' . #\')
+    (#\\ . #\\)
+    (#\b . #\Backspace)
+    (#\f . #.(code-char 12))
+    (#\n . #\Newline)
+    (#\r . #\Return)
+    (#\t . #\Tab)))
+
+(defmethod ps-print ((string string))
+  (flet ((lisp-special-char-to-js (lisp-char)
+           (car (rassoc lisp-char *js-lisp-escaped-chars*))))
+    (write-char *js-quote-char*)
+    (loop for char across string
+          for code = (char-code char)
+          for special = (lisp-special-char-to-js char)
+          do (cond (special (write-char #\\)
+                            (write-char special))
+                   ((or (<= code #x1f) (>= code #x80))
+                    (format *ps-output-stream* "\\u~4,'0x" code))
+                   (t (write-char char)))
+          finally (write-char *js-quote-char*))))
+
+(defmethod ps-print ((number number))
+  (format *ps-output-stream* (if (integerp number) "~S" "~F") number))
+
+;;; expression and operator precedence rules
+
+(defun expression-precedence (expr)
+  (if (consp expr)
+      (case (car expr)
+        (js-block (if (= (length (cdr expr)) 1)
+                      (expression-precedence (first (cdr expr)))
+                      (op-precedence 'comma)))
+        (js-expression-if (op-precedence 'js-expression-if))
+        (js-assign (op-precedence '=))
+        (operator (op-precedence (second expr)))
+        (otherwise 0))
+      0))
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  (defparameter *op-precedence-hash* (make-hash-table :test #'equal))
+
+  ;;; generate the operator precedences from *OP-PRECEDENCES*
+  (let ((precedence 1))
+    (dolist (ops '((js-aref)
+                   (js-slot-value)
+                   (! not ~)
+                   (* / %)
+                   (+ -)
+                   (<< >>)
+                   (>>>)
+                   (< > <= >=)
+                   (in js-expression-if)
+                   (eql == != =)
+                   (=== !==)
+                   (&)
+                   (^)
+                   (\|)
+                   (\&\& and)
+                   (\|\| or)
+                   (js-assign *= /= %= += -= <<= >>= >>>= \&= ^= \|=)
+                   (comma)))
+      (dolist (op ops)
+        (let ((op-name (symbol-name op)))
+          (setf (gethash op-name *op-precedence-hash*) precedence)))
+      (incf precedence)))
+
+  (defun op-precedence (op)
+    (gethash (if (symbolp op)
+                 (symbol-name op)
+                 op)
+             *op-precedence-hash*)))
+
+(defprinter script-quote (val)
+  (if (null val)
+      (write-string "null")
+      (error "Cannot translate quoted value ~S to javascript" val)))
+
+(defprinter js-literal (str)
+  (write-string str))
+
+(defprinter js-keyword (str)
+  (write-string str))
+
+(defun print-comma-list (ps-forms)
+  (loop for (form . rest) on ps-forms
+        with after = ", "
+        unless rest do (setf after "")
+        doing (progn (ps-print form)
+                     (write-string after))))
+
+(defprinter array-literal (&rest initial-contents)
+  (write-char #\[)
+  (print-comma-list initial-contents)
+  (write-char #\]))
+
+(defprinter js-aref (array indices)
+  (ps-print array)
+  (loop for idx in indices do
+        (progn (write-char #\[)
+               (ps-print idx)
+               (write-char #\]))))
+
+(defprinter object-literal (&rest slot-definitions)
+  (write-char #\{)
+  (loop for ((key . value) . rest) on slot-definitions
+        with after = ", "
+        unless rest do (setf after "")
+        doing (progn (format *ps-output-stream* "~A: " (js-translate-symbol key))
+                     (ps-print value)
+                     (write-string after)))
+  (write-string " }"))
+
+(defprinter js-variable (var)
+  (write-string (js-translate-symbol var)))
+
+;;; arithmetic operators
+(defun script-convert-op-name (op)
+  (case op
+    (and '\&\&)
+    (or '\|\|)
+    (not '!)
+    (eql '\=\=)
+    (=   '\=\=)
+    (t op)))
+
+(defun parenthesize-print (ps-form)
+  (write-char #\()
+  (ps-print ps-form)
+  (write-char #\)))
+
+(defprinter operator (op args)
+  (loop for (arg . rest) on args
+        with precedence = (op-precedence op)
+        with op-string = (format nil " ~A " op)
+        unless rest do (setf op-string "")
+        do (progn (if (>= (expression-precedence arg) precedence)
+                      (parenthesize-print arg)
+                      (ps-print arg))
+                  (write-string op-string))))
+
+(defprinter unary-operator (op arg &key prefix)
+  (when prefix
+    (write-string op))
+  (if (eql 'operator (car arg))
+      (parenthesize-print arg)
+      (ps-print arg))
+  (unless prefix
+    (write-string op)))
+
+;;; function and method calls
+(defprinter js-funcall (fun-designator args)
+  (cond ((member (car fun-designator) '(js-variable js-aref js-slot-value))
+         (ps-print fun-designator))
+        ((eql 'js-lambda (car fun-designator))
+         (write-char #\()
+         (ps-print fun-designator)
+         (write-char #\)))
+        ((eql 'js-funcall (car fun-designator))
+         (ps-print fun-designator)))
+  (write-char #\()
+  (print-comma-list args)
+  (write-char #\)))
+
+(defprinter js-method-call (method object args)
+  ;; TODO: this may not be the best way to add ()'s around lambdas
+  ;; probably there is or should be a more general solution working
+  ;; in other situations involving lambda's
+  (if (or (numberp object) (and (consp object) (member (car object) '(js-lambda js-object operator js-expression-if js-block))))
+      (parenthesize-print object)
+      (ps-print object))
+  (write-string (js-translate-symbol method))
+  (write-char #\()
+  (print-comma-list args)
+  (write-char #\)))
+
+(defprinter js-block (statement-p statements)
+  (loop for (statement . rest) on statements
+        with indent = (if statement-p "    " "")
+        with after = (if statement-p
+                         ";
+"
+                         ", ")
+        unless rest do (setf after (if statement-p after ""))
+        do (progn (write-string indent)
+                  (ps-print statement)
+                  (write-string after))))
+
+(defprinter js-lambda (args body)
+  (print-fun-def nil args body))
+
+(defprinter js-defun (name args body)
+  (print-fun-def name args body))
+
+(defun print-fun-def (name args body)
+  (format *ps-output-stream* "function ~:[~;~A~](" name (js-translate-symbol name))
+  (loop for (arg . rest) on args
+        with after = ", "
+        unless rest do (setf after "")
+        do (progn (write-string (js-translate-symbol arg))
+                  (write-string after))
+        finally (write-string ") {"))
+  (fresh-line)
+  (ps-print body)
+  (write-char #\}))
+
+;;; object creation
+(defprinter js-object (slot-defs)
+  (write-string "{ ")
+  (loop for ((slot-name slot-value) . rest) on slot-defs
+        with after = ", "
+        unless rest do (setf after "")
+        do (progn (if (and (listp slot-name) (eql 'script-quote (car slot-name)) (symbolp (second slot-name)))
+                      (write-string (js-translate-symbol (second slot-name)))
+                      (ps-print slot-name))
+                  (write-string " : ")
+                  (ps-print slot-value)
+                  (write-string after)))
+  (write-string " }"))
+
+(defprinter js-slot-value (obj slot)
+  (if (and (listp obj) (member (car obj) '(js-block js-expression-if)))
+      (parenthesize-print obj)
+      (ps-print obj))
+  (if (and (listp slot) (eql 'script-quote (car slot)))
+      (progn (write-char #\.)
+             (if (symbolp (second slot))
+                 (write-string (js-translate-symbol (second slot)))
+                 (ps-print slot)))
+      (progn (write-char #\[)
+             (ps-print slot)
+             (write-char #\]))))
+
+;;; cond
+(defprinter js-cond (clauses)
+  (loop for (test body-block) in clauses
+        for start = "if (" then "else if ("
+        do (progn (if (string= test "true")
+                      (progn (write-string "else {")
+                             (fresh-line))
+                      (progn (ps-print test)
+                             (write-string ") {")
+                             (fresh-line)))
+                  (ps-print body-block)
+                  (write-char #\}))))
+
+(defprinter js-statement-if (test then else)
+  (write-string "if (")
+  (ps-print test)
+  (write-string ") {")
+  (fresh-line)
+  (ps-print then)
+  (fresh-line)
+  (when else
+      (write-string "} else {")
+      (fresh-line)
+      (ps-print else))
+  (write-char #\}))
+
+(defprinter js-expression-if (test then else)
+  (ps-print test)
+  (write-string " ? ")
+  (if (>= (expression-precedence then) (op-precedence 'js-expression-if))
+      (parenthesize-print then)
+      (ps-print then))
+  (write-string " : ")
+  (if else
+      (if (>= (expression-precedence else) (op-precedence 'js-expression-if))
+          (parenthesize-print else)
+          (ps-print else))
+      (write-string "undefined")))
+
+(defprinter js-assign (lhs rhs)
+  (ps-print lhs)
+  (write-string " = ")
+  (ps-print rhs))
+
+(defprinter js-defvar (var-name &rest var-value)
+  (write-string "var ")
+  (write-string (js-translate-symbol var-name))
+  (when var-value
+    (write-string " = ")
+    (ps-print (car var-value))))
+
+;;; iteration
+(defprinter js-for (vars steps test body-block)
+  (write-string "for (")
+  (loop for ((var-name . var-init) . rest) on vars
+        for decl = "var " then ""
+        with after = ", "
+        unless rest do (setf after "")
+        do (progn (write-string decl)
+                  (write-string (js-translate-symbol var-name))
+                  (write-string " = ")
+                  (ps-print var-init)
+                  (write-string after)))
+  (write-string "; ")
+  (ps-print test)
+  (write-string "; ")
+  (loop for ((var-name . var-init) . rest) on vars
+        for step in steps
+        with after = ", "
+        unless rest do (setf after "")
+        do (progn (write-string (js-translate-symbol var-name))
+                  (write-string " = ")
+                  (ps-print step)
+                  (write-string after)))
+  (write-string ") {")
+  (fresh-line)
+  (ps-print body-block)
+  (write-char #\}))
+
+(defprinter js-for-each (var object body-block)
+  (write-string "for (var ")
+  (write-string (js-translate-symbol var))
+  (write-string " in ")
+  (ps-print object)
+  (write-string ") {")
+  (fresh-line)
+  (ps-print body-block)
+  (write-char #\}))
+
+(defprinter js-while (test body-block)
+  (write-string "while (")
+  (ps-print test)
+  (write-string ") {")
+  (fresh-line)
+  (ps-print body-block)
+  (write-char #\}))
+
+(defprinter js-with (expression body-block)
+  (write-string "with (")
+  (ps-print expression)
+  (write-string ") {")
+  (fresh-line)
+  (ps-print body-block)
+  (write-char #\}))
+
+(defprinter js-switch (test clauses)
+  (write-string "switch (")
+  (ps-print test)
+  (write-string ") {")
+  (fresh-line)
+  (loop for (val body-block) in clauses
+        do (if (eql val 'default)
+               (progn (write-string "default: ")
+                      (ps-print body-block))
+               (progn (write-string "case ")
+                      (ps-print val)
+                      (write-char #\:)
+                      (fresh-line)
+                      (ps-print body-block))))
+  (write-char #\}))
+
+(defprinter js-try (body &key catch finally)
+  (write-string "try {")
+  (fresh-line)
+  (ps-print body)
+  (when catch
+    (write-string "} catch (")
+    (write-string (js-translate-symbol (first catch)))
+    (write-string ") {")
+    (ps-print (second catch)))
+  (when finally
+    (write-string "} finally {")
+    (ps-print finally))
+  (write-char #\}))
+
+;;; regex
+(defprinter js-regex (regex)
+  (flet ((first-slash-p (string)
+           (and (> (length string) 0) (char= (char string 0) #\/))))
+    (let ((slash (unless (first-slash-p regex) "/")))
+      (format *ps-output-stream* (concatenate 'string slash "~A" slash) regex))))
+
+(defprinter js-return (value)
+  (write-sequence "return " *ps-output-stream*)
+  (ps-print value))
+
+;;; conditional compilation
+(defprinter cc-if (test body-forms)
+  (write-string "/*@if ")
+  (ps-print test)
+  (fresh-line)
+  (dolist (form body-forms)
+    (ps-print form))
+  (fresh-line)
+  (write-string "@end @*/"))
+
+(defprinter js-instanceof (value type)
+  (write-char #\()
+  (ps-print value)
+  (write-string " instanceof ")
+  (ps-print type)
+  (write-char #\)))
+
+(defprinter js-named-operator (op value)
+  (format *ps-output-stream* "~(~A~) " op)
+  (ps-print value))