Fixed js-with
[clinton/parenscript.git] / src / js.lisp
index ae3bcb7..296d704 100644 (file)
@@ -9,7 +9,6 @@
   '((#\! . "Bang")
     (#\? . "What")
     (#\# . "Hash")
-    (#\$ . "Dollar")
     (#\@ . "At")
     (#\% . "Percent")
     (#\+ . "Plus")
   (flet ((special-append (form elt)
           (let ((len (length form)))
             (if (and (> len 0)
-                     (member (char form (1- len))
-                             '(#\; #\, #\})))
+                      (string= (char form (1- len)) elt))
                 form
                 (concatenate 'string form elt)))))
     (cond ((stringp form)
   (defvar *js-compiler-macros* (make-hash-table :test 'equal)
         "*JS-COMPILER-MACROS* is a hash-table containing the functions corresponding
 to javascript special forms, indexed by their name. Javascript special
-forms are compiler macros for JS expressions."))
+forms are compiler macros for JS expressions.")
+
+  (defun undefine-js-compiler-macro (name)
+    (declare (type symbol name))
+    (when (gethash (symbol-name name) *js-compiler-macros*)
+      (warn "Redefining compiler macro ~S" name)
+      (remhash (symbol-name name) *js-compiler-macros*))))
 
 (defmacro define-js-compiler-macro (name lambda-list &rest body)
   "Define a javascript compiler macro NAME. Arguments are destructured
@@ -256,13 +260,20 @@ this macro."
 
 (defmacro defjsmacro (name args &rest body)
   "Define a javascript macro, and store it in the toplevel macro environment."
-  (when (gethash (symbol-name name) *js-compiler-macros*)
-    (warn "Redefining compiler macro ~S" name)
-    (remhash (symbol-name name) *js-compiler-macros*))
   (let ((lambda-list (gensym)))
+    (undefine-js-compiler-macro name)
     `(setf (gethash ,(symbol-name name) *js-macro-toplevel*)
       #'(lambda (&rest ,lambda-list)
-         (destructuring-bind ,args ,lambda-list ,@body)))))
+          (destructuring-bind ,args ,lambda-list ,@body)))))
+
+(defun import-macros-from-lisp (&rest names)
+  "Import the named lisp macros into the js macro expansion"
+  (dolist (name names)
+    (let ((name name))
+      (undefine-js-compiler-macro name)
+      (setf (gethash (symbol-name name) *js-macro-toplevel*)
+            (lambda (&rest args)
+              (macroexpand `(,name ,@args)))))))
 
 (defun js-expand-form (expr)
   "Expand a javascript form."
@@ -283,6 +294,50 @@ this macro."
                 (js-expand-form (apply js-macro (cdr expr)))
                 expr)))))
 
+(defvar *gen-js-name-counter* 0)
+
+(defun gen-js-name-string (&key (prefix "_ps_"))
+  "Generates a unique valid javascript identifier ()"
+  (concatenate 'string
+               prefix (princ-to-string (incf *gen-js-name-counter*))))
+
+(defun gen-js-name (&key (prefix "_ps_"))
+  "Generate a new javascript identifier."
+  (intern (gen-js-name-string :prefix prefix)
+          (find-package :js)))
+
+(defmacro with-unique-js-names (symbols &body body)
+  "Evaluate BODY with the variables on SYMBOLS bound to new javascript identifiers.
+
+Each element of SYMBOLS is either a symbol or a list of (symbol
+prefix)."
+  `(let* ,(mapcar (lambda (symbol)
+                    (destructuring-bind (symbol &optional prefix)
+                        (if (consp symbol)
+                            symbol
+                            (list symbol))
+                      (if prefix
+                          `(,symbol (gen-js-name :prefix ,prefix))
+                          `(,symbol (gen-js-name)))))
+                  symbols)
+     ,@body))
+
+(defjsmacro rebind (variables expression)
+  ;; Creates a new js lexical environment and copies the given
+  ;; variable(s) there.  Executes the body in the new environment. This
+  ;; has the same effect as a new (let () ...) form in lisp but works on
+  ;; the js side for js closures."
+  
+  (unless (listp variables)
+    (setf variables (list variables)))
+  `((lambda ()
+      (let ((new-context (new *object)))
+        ,@(loop for variable in variables
+                do (setf variable (symbol-to-js variable))
+                collect `(setf (slot-value new-context ,variable) (slot-value this ,variable)))
+        (with new-context
+              (return ,expression))))))
+
 (defvar *var-counter* 0)
 
 (defun js-gensym (&optional (name "js"))
@@ -379,17 +434,43 @@ this macro."
 (defjsclass string-literal (expression)
   (value))
 
+(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.")
+
 (defmethod js-to-strings ((string string-literal) start-pos)
-  (declare (ignore 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
-             initially (write-char #\' escaped)
-             for char across (value string)
-             if (char= #\' char)
-               do (write-string "\\'" escaped)
-             else
-               do (write-char char escaped)
-             finally (write-char #\' escaped)))))
+           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)))))
+
+(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*)))
 
 ;;; number literals
 
@@ -859,9 +940,28 @@ this macro."
 (define-js-single-op delete)
 (define-js-single-op void)
 (define-js-single-op typeof)
-(define-js-single-op instanceof)
 (define-js-single-op new)
 
+;; TODO this may not be the best integrated implementation of
+;; instanceof into the rest of the code
+(defjsclass js-instanceof (expression)
+  ((value)
+   (type :initarg :type)))
+
+(define-js-compiler-macro instanceof (value type)
+  (make-instance 'js-instanceof
+                 :value (js-compile-to-expression value)
+                 :type (js-compile-to-expression type)))
+
+(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)
+   :white-space
+   "  "))
+
 ;;; assignment
 
 (defjsclass js-setf (expression)
@@ -1102,8 +1202,8 @@ this macro."
 
 (define-js-compiler-macro with (statement &rest body)
   (make-instance 'js-with
-                :obj (js-compile-to-expression (first statement))
-                :body (js-compile-to-body (cons 'progn body) :indent "  ")))
+                 :obj (js-compile-to-expression statement)
+                 :body (js-compile-to-body (cons 'progn body) :indent "  ")))
 
 (defmethod js-to-statement-strings ((with js-with) start-pos)
   (nconc (dwim-join (list (js-to-strings (with-obj with) (+ start-pos 2)))
@@ -1213,6 +1313,15 @@ this macro."
 (define-js-compiler-macro regex (regex)
   (make-instance 'regex :value (string 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
 
 (defjsclass cc-if ()
@@ -1234,6 +1343,8 @@ this macro."
   (setf form (js-expand-form form))
   (cond ((stringp form)
         (make-instance 'string-literal :value form))
+        ((characterp form)
+        (make-instance 'string-literal :value (string form)))
        ((numberp form)
         (make-instance 'number-literal :value form))
        ((symbolp form)
@@ -1307,36 +1418,6 @@ this macro."
 (defjsmacro random ()
   `(*Math.random))
 
-;;; helper functions
-
-(defvar *gen-js-name-counter* 0)
-
-(defun gen-js-name-string (&key (prefix "parenscript_"))
-  "Generates a unique valid javascript identifier ()"
-  (concatenate 'string
-               prefix (princ-to-string (incf *gen-js-name-counter*))))
-
-(defun gen-js-name (&key (prefix "parenscript_"))
-  "Generate a new javascript identifier."
-  (intern (gen-js-name-string :prefix prefix)
-          (find-package :js)))
-
-(defmacro with-unique-js-names (symbols &body body)
-  "Evaluate BODY with the variables on SYMBOLS bound to new javascript identifiers.
-
-Each element of SYMBOLS is either a symbol or a list of (symbol
-prefix)."
-  `(let* ,(mapcar (lambda (symbol)
-                    (destructuring-bind (symbol &optional prefix)
-                        (if (consp symbol)
-                            symbol
-                            (list symbol))
-                      (if prefix
-                          `(,symbol (gen-js-name :prefix ,prefix))
-                          `(,symbol (gen-js-name)))))
-                  symbols)
-     ,@body))
-
 ;;; helper macros
 
 (define-js-compiler-macro js (&rest body)