1 (in-package :parenscript.javascript
)
3 (defgeneric js-to-strings
(expression start-pos
)
4 (:documentation
"Transform an enscript-javascript expression to a string"))
6 (defgeneric js-to-statement-strings
(code-fragment start-pos
)
7 (:documentation
"Transform an enscript-javascript code fragment to a string"))
11 (defun special-append-to-last (form elt
)
12 (flet ((special-append (form elt
)
13 (let ((len (length form
)))
15 (string= (char form
(1- len
)) elt
))
17 (concatenate 'string form elt
)))))
19 (special-append form elt
))
21 (let ((last (last form
)))
22 (if (stringp (car last
))
23 (rplaca last
(special-append (car last
) elt
))
24 (append-to-last (car last
) elt
))
26 (t (error "unsupported form ~S" form
)))))
28 (defun dwim-join (value-string-lists max-length
33 (white-space (make-string (length start
) :initial-element
#\Space
))
35 (append-to-last #'append-to-last
)
38 (format t
"value-string-lists: ~S~%" value-string-lists
)
40 ;;; collect single value-string-lists until line full
42 (do* ((string-lists value-string-lists
(cdr string-lists
))
43 (string-list (car string-lists
) (car string-lists
))
52 (list (concatenate 'string start end
))
56 (funcall append-to-last
(first res
) end
)))
59 (format t
"string-list: ~S~%" string-list
)
62 (unless (null (cdr string-lists
))
63 (funcall append-to-last string-list join-after
)))
65 (if (and collect
(= (length string-list
) 1))
68 (format t
"cur-elt: ~S line-length ~D, max-length ~D, string: ~S~%"
70 (+ (length (first string-list
))
75 (< (+ (length (first string-list
))
76 (length cur-elt
)) max-length
))
78 (concatenate 'string cur-elt
79 (if (or is-first
(and cur-empty
(string= join-before
"")))
80 "" (concatenate 'string separator join-before
))
85 (setf cur-elt
(concatenate 'string white-space
86 join-before
(first string-list
))
92 (setf cur-elt white-space
94 (setf res
(nconc (nreverse
95 (cons (concatenate 'string
100 (mapcar #'(lambda (x) (concatenate 'string white-space x
))
103 (setf cur-elt white-space cur-empty t
)))))
105 (defmethod js-to-strings ((expression expression
) start-pos
)
106 (declare (ignore start-pos
))
107 (list (princ-to-string (value expression
))))
109 (defmethod js-to-statement-strings ((expression expression
) start-pos
)
110 (js-to-strings expression start-pos
))
112 (defmethod js-to-statement-strings ((statement statement
) start-pos
)
113 (declare (ignore start-pos
))
114 (list (princ-to-string (value statement
))))
116 (defmethod js-to-strings ((expression script-quote
) start-pos
)
117 (declare (ignore start-pos
))
119 (if (null (value expression
))
121 (case (value expression
)
122 (t (error "Cannot translated quoted value ~s to javascript" (value expression
)))))))
126 (defmethod js-to-strings ((array array-literal
) start-pos
)
127 (let ((value-string-lists
128 (mapcar #'(lambda (x) (js-to-strings x
(+ start-pos
2)))
129 (array-values array
)))
130 (max-length (- 80 start-pos
2)))
131 (dwim-join value-string-lists max-length
132 :start
"[ " :end
" ]"
135 (defmethod js-to-strings ((aref js-aref
) start-pos
)
136 (dwim-join (cons (js-to-strings (aref-array aref
) start-pos
)
137 (mapcar #'(lambda (x) (dwim-join (list (js-to-strings x
(+ start-pos
2)))
139 :start
"[" :end
"]"))
141 (- 80 start-pos
2) :separator
""
144 ;;; object literals (maps and hash-tables)
146 (defmethod js-to-strings ((obj object-literal
) start-pos
)
149 for
(key . value
) in
(object-values obj
)
151 (dwim-join (list (list (format nil
"~A:" (js-translate-symbol key
)))
152 (js-to-strings value
(+ start-pos
2)))
154 :start
"" :end
"" :join-after
"")))
156 :start
"{ " :end
" }"
161 (defvar *js-quote-char
* #\'
162 "Specifies which character JS sholud use for delimiting strings.
164 This variable is usefull when have to embed some javascript code
165 in an html attribute delimited by #\\\" as opposed to #\\', or
168 (defparameter *js-lisp-escaped-chars
*
172 (#\f .
#.
(code-char 12))
177 (defun lisp-special-char-to-js (lisp-char)
178 (car (rassoc lisp-char
*js-lisp-escaped-chars
*)))
180 (defmethod js-to-strings ((string string-literal
) start-pos
)
181 (declare (ignore start-pos
)
182 (inline lisp-special-char-to-js
))
183 (list (with-output-to-string (escaped)
184 (write-char *js-quote-char
* escaped
)
186 for char across
(value string
)
187 for code
= (char-code char
)
188 for special
= (lisp-special-char-to-js char
)
192 (write-char #\\ escaped
)
193 (write-char special escaped
))
194 ((or (<= code
#x1f
) (>= code
#x80
))
195 (format escaped
"\\u~4,'0x" code
))
196 (t (write-char char escaped
)))
197 finally
(write-char *js-quote-char
* escaped
)))))
200 (defgeneric js-translate-symbol-contextually
(symbol package env
)
201 (:documentation
"Translates a symbol to a string in the given environment & package
202 and for the given symbol."))
204 (defparameter *obfuscate-standard-identifiers
* nil
)
206 (defparameter *obfuscation-table
* (make-hash-table))
208 (defun obfuscated-symbol (symbol)
209 (or (gethash symbol
*obfuscation-table
*)
210 (setf (gethash symbol
*obfuscation-table
*) (string (gensym)))))
212 (defmethod js-translate-symbol-contextually ((symbol symbol
)
213 (package ps
::script-package
)
214 (env ps
::compilation-environment
))
216 ((member (ps::script-package-lisp-package package
)
217 (mapcar #'find-package
'(:keyword
:parenscript.global
)))
218 (symbol-to-js symbol
))
219 (*obfuscate-standard-identifiers
*
220 (obfuscated-symbol symbol
))
222 (case *package-prefix-style
*
226 (symbol-to-js (script-package-name package
))
227 (symbol-to-js symbol
)))
229 (symbol-to-js (value symbol
)))))))
231 (defgeneric js-translate-symbol
(var)
232 (:documentation
"Given a JS-VARIABLE returns an output
233 JavaScript version of it as a string."))
235 (defmethod js-translate-symbol ((var js-variable
))
236 (js-translate-symbol (value var
)))
238 (defmethod js-translate-symbol ((var-name symbol
))
239 (if parenscript
::*enable-package-system
*
240 (js-translate-symbol-contextually
242 (ps::symbol-script-package var-name
)
243 ps
::*compilation-environment
*)
244 (symbol-to-js var-name
)))
246 (defmethod js-to-strings ((v js-variable
) start-form
)
247 (declare (ignore start-form
))
248 (list (js-translate-symbol v
)))
250 ;;; arithmetic operators
251 (defun script-convert-op-name (op)
260 (defun op-form-p (form)
262 (not (script-special-form-p form
))
263 (not (null (op-precedence (first form
))))))
265 (defun klammer (string-list)
266 (prepend-to-first string-list
"(")
267 (append-to-last string-list
")")
270 (defmethod expression-precedence ((expression expression
))
273 (defmethod expression-precedence ((form op-form
))
274 (op-precedence (operator form
)))
276 (defmethod js-to-strings ((form op-form
) start-pos
)
277 (let* ((precedence (expression-precedence form
))
279 (mapcar #'(lambda (x)
280 (let ((string-list (js-to-strings x
(+ start-pos
2))))
281 (if (>= (expression-precedence x
) precedence
)
282 (klammer string-list
)
285 (max-length (- 80 start-pos
2))
286 (op-string (format nil
"~A " (operator form
))))
287 (dwim-join value-string-lists max-length
:join-before op-string
)
290 (defmethod js-to-strings ((one-op one-op
) start-pos
)
291 (let* ((value (value one-op
))
292 (value-strings (js-to-strings value start-pos
)))
293 (when (typep value
'op-form
)
294 (setf value-strings
(klammer value-strings
)))
295 (if (one-op-pre-p one-op
)
296 (prepend-to-first value-strings
298 (append-to-last value-strings
303 (defmethod js-to-strings ((form function-call
) start-pos
)
304 (let* ((value-string-lists
305 (mapcar #'(lambda (x) (js-to-strings x
(+ start-pos
2)))
307 (max-length (- 80 start-pos
2))
308 (args (dwim-join value-string-lists max-length
309 :start
"(" :end
")" :join-after
",")))
310 (etypecase (f-function form
)
312 (dwim-join (list (append (dwim-join (list (js-to-strings (f-function form
) (+ start-pos
2)))
314 :start
"(" :end
")" :separator
"")
318 ((or js-variable js-aref js-slot-value
)
319 (dwim-join (list (js-to-strings (f-function form
) (+ start-pos
2))
324 ;; TODO it adds superfluous newlines after each ()
325 ;; and it's nearly the same as the js-lambda case above
326 (dwim-join (list (append (dwim-join (list (js-to-strings (f-function form
) (+ start-pos
2)))
327 max-length
:separator
"")
329 max-length
:separator
"")))))
331 (defmethod js-to-strings ((form method-call
) start-pos
)
332 (let ((object (js-to-strings (m-object form
) (+ start-pos
2))))
333 ;; TODO: this may not be the best way to add ()'s around lambdas
334 ;; probably there is or should be a more general solution working
335 ;; in other situations involving lambda's
336 (when (member (m-object form
) (list 'js-lambda
'number-literal
'js-object
'op-form
)
339 (nconc object
(list ")")))
340 (let* ((fname (dwim-join (list object
341 (list (js-translate-symbol (m-method form
))))
345 (butlast (butlast fname
))
346 (last (car (last fname
)))
347 (method-and-args (dwim-join (mapcar #'(lambda (x) (js-to-strings x
(+ start-pos
2)))
353 (ensure-no-newline-before-dot (concatenate 'string
355 (first method-and-args
))))
356 (nconc (butlast butlast
)
357 (list ensure-no-newline-before-dot
)
358 (rest method-and-args
)))))
360 ;;; optimization that gets rid of nested blocks, which have no meaningful effect
362 (defgeneric expanded-subblocks
(block)
365 (:method
((block js-block
))
366 (mapcan #'expanded-subblocks
(block-statements block
))))
368 (defun consolidate-subblocks (block)
369 (setf (block-statements block
) (expanded-subblocks block
))
373 (defmethod js-to-statement-strings ((body js-block
) start-pos
)
374 (consolidate-subblocks body
)
375 (dwim-join (mapcar #'(lambda (x) (js-to-statement-strings x
(+ start-pos
2)))
376 (block-statements body
))
379 :append-to-last
#'special-append-to-last
380 :start
(block-indent body
) :collect nil
383 (defmethod js-to-strings ((body js-block
) start-pos
)
384 (dwim-join (mapcar #'(lambda (x) (js-to-strings x
(+ start-pos
2)))
385 (block-statements body
))
387 :append-to-last
#'special-append-to-last
389 :start
(block-indent body
)))
392 (defmethod js-to-statement-strings ((body js-sub-block
) start-pos
)
393 (declare (ignore start-pos
))
394 (nconc (list "{") (call-next-method) (list "}")))
396 ;;; function definition
397 (defmethod js-to-strings ((lambda js-lambda
) start-pos
)
398 (let ((fun-header (dwim-join (mapcar #'(lambda (x)
399 (list (js-translate-symbol x
)))
400 (lambda-args lambda
))
402 :start
(function-start-string lambda
)
403 :end
") {" :join-after
","))
404 (fun-body (js-to-statement-strings (lambda-body lambda
) (+ start-pos
2))))
405 (nconc fun-header fun-body
(list "}"))))
407 (defgeneric function-start-string
(function)
408 (:documentation
"Returns the string that starts the function - this varies according to whether
409 this is a lambda or a defun"))
411 (defmethod function-start-string ((lambda js-lambda
))
414 (defmethod js-to-statement-strings ((lambda js-lambda
) start-pos
)
415 (js-to-strings lambda start-pos
))
417 (defmethod function-start-string ((defun js-defun))
418 (format nil
"function ~A(" (js-translate-symbol (defun-name defun
))))
421 (defmethod js-to-strings ((object js-object
) start-pos
)
422 (let ((value-string-lists
423 (mapcar #'(lambda (slot)
424 (let* ((slot-name (first slot
))
426 (if (typep slot-name
'script-quote
)
427 (if (symbolp (value slot-name
))
428 (format nil
"~A" (js-translate-symbol (value slot-name
)))
429 (format nil
"~A" (first (js-to-strings slot-name
0))))
430 (car (js-to-strings slot-name
0)))))
431 (dwim-join (list (js-to-strings (second slot
) (+ start-pos
4)))
433 :start
(concatenate 'string slot-string-name
" : ")
436 (max-length (- 80 start-pos
2)))
437 (dwim-join value-string-lists max-length
444 (defmethod js-to-strings ((sv js-slot-value
) start-pos
)
445 (append-to-last (if (typep (sv-object sv
) 'js-variable
)
446 (js-to-strings (sv-object sv
) start-pos
)
447 (list (format nil
"~A" (js-to-strings (sv-object sv
) start-pos
))))
448 (if (typep (sv-slot sv
) 'script-quote
)
449 (if (symbolp (value (sv-slot sv
)))
450 (format nil
".~A" (js-translate-symbol (value (sv-slot sv
))))
451 (format nil
".~A" (first (js-to-strings (sv-slot sv
) 0))))
452 (format nil
"[~A]" (first (js-to-strings (sv-slot sv
) 0))))))
455 (defmethod js-to-statement-strings ((cond js-cond
) start-pos
)
456 (loop :for body
:on
(cond-bodies cond
)
457 :for first
= (eq body
(cond-bodies cond
))
458 :for last
= (not (cdr body
))
459 :for test
:in
(cond-tests cond
)
460 :append
(if (and last
(not first
) (string= (value test
) "true"))
462 (dwim-join (list (js-to-strings test
0)) (- 80 start-pos
2)
463 :start
(if first
"if (" "else if (") :end
") {"))
464 :append
(js-to-statement-strings (car body
) (+ start-pos
2))
467 (defmethod js-to-statement-strings ((if js-if
) start-pos
)
468 (let ((if-strings (dwim-join (list (js-to-strings (if-test if
) 0))
472 (then-strings (js-to-statement-strings (if-then if
) (+ start-pos
2)))
473 (else-strings (when (if-else if
)
474 (js-to-statement-strings (if-else if
)
476 (nconc if-strings then-strings
(if else-strings
477 (nconc (list "} else {") else-strings
(list "}"))
480 (defmethod js-to-strings ((if js-if
) start-pos
)
481 (assert (typep (if-then if
) 'expression
))
483 (assert (typep (if-else if
) 'expression
)))
484 (dwim-join (list (append-to-last (js-to-strings (if-test if
) start-pos
) " ?")
485 (let* ((new-then (make-instance 'js-block
486 :statements
(block-statements (if-then if
))
488 (res (js-to-strings new-then start-pos
)))
489 (if (>= (expression-precedence (if-then if
))
490 (expression-precedence if
))
495 (let* ((new-else (make-instance 'js-block
496 :statements
(block-statements (if-else if
))
498 (res (js-to-strings new-else start-pos
)))
499 (if (>= (expression-precedence (if-else if
))
500 (expression-precedence if
))
508 (defmethod js-to-strings ((setf js-setf
) start-pos
)
509 (dwim-join (cons (js-to-strings (setf-lhs setf
) start-pos
)
510 (mapcar #'(lambda (x) (js-to-strings x start-pos
)) (setf-rhsides setf
)))
515 (defmethod js-to-statement-strings ((defvar js-defvar
) start-pos
)
516 (dwim-join (nconc (mapcar #'(lambda (x) (list (js-translate-symbol x
))) (var-names defvar
))
517 (when (var-value defvar
)
518 (list (js-to-strings (var-value defvar
) start-pos
))))
521 :start
"var " :end
";"))
524 (defmethod js-to-statement-strings ((for js-for
) start-pos
)
525 (let* ((init (dwim-join (mapcar #'(lambda (x)
526 (dwim-join (list (list (js-translate-symbol (first (var-names x
))))
527 (js-to-strings (var-value x
)
533 :start
"var " :join-after
","))
534 (check (js-to-strings (for-check for
) (+ start-pos
2)))
535 (steps (dwim-join (mapcar #'(lambda (x var
)
537 (list (list (js-translate-symbol (first (var-names var
))))
538 (js-to-strings x
(- start-pos
2)))
545 (header (dwim-join (list init check steps
)
547 :start
"for (" :end
") {"
549 (body (js-to-statement-strings (for-body for
) (+ start-pos
2))))
550 (nconc header body
(list "}"))))
553 (defmethod js-to-statement-strings ((fe for-each
) start-pos
)
554 (let ((header (dwim-join (list (list (js-translate-symbol (fe-name fe
)))
556 (js-to-strings (fe-value fe
) (+ start-pos
2)))
560 (body (js-to-statement-strings (fe-body fe
) (+ start-pos
2))))
561 (nconc header body
(list "}"))))
563 (defmethod js-to-statement-strings ((while js-while
) start-pos
)
564 (let ((header (dwim-join (list (js-to-strings (while-check while
) (+ start-pos
2)))
568 (body (js-to-statement-strings (while-body while
) (+ start-pos
2))))
569 (nconc header body
(list "}"))))
572 (defmethod js-to-statement-strings ((with js-with
) start-pos
)
573 (nconc (dwim-join (list (js-to-strings (with-obj with
) (+ start-pos
2)))
575 :start
"with (" :end
") {")
576 (js-to-statement-strings (with-body with
) (+ start-pos
2))
580 (defmethod js-to-statement-strings ((case js-switch
) start-pos
)
581 (let ((body (mapcan #'(lambda (clause)
582 (let ((val (car clause
))
583 (body (second clause
)))
584 (dwim-join (list (if (eql val
'default
)
586 (js-to-strings val
(+ start-pos
2)))
587 (js-to-statement-strings body
(+ start-pos
2)))
589 :start
(if (eql val
'default
) " default" " case ")
591 :join-after
":"))) (case-clauses case
))))
594 (format t
"body: ~S~%" body
)
595 (nconc (dwim-join (list (js-to-strings (case-value case
) (+ start-pos
2)))
597 :start
"switch (" :end
") {")
602 (defmethod js-to-statement-strings ((try js-try
) start-pos
)
603 (let* ((catch (try-catch try
))
604 (finally (try-finally try
))
605 (catch-list (when catch
607 (dwim-join (list (list (js-translate-symbol (first catch
))))
611 (js-to-statement-strings (second catch
) (+ start-pos
2)))))
612 (finally-list (when finally
613 (nconc (list "} finally {")
614 (js-to-statement-strings finally
(+ start-pos
2))))))
615 (nconc (list "try {")
616 (js-to-statement-strings (try-body try
) (+ start-pos
2))
622 (defun first-slash-p (string)
623 (and (> (length string
) 0)
624 (eq (char string
0) '#\
/)))
626 (defmethod js-to-strings ((regex regex
) start-pos
)
627 (declare (ignore start-pos
))
628 (let ((slash (if (first-slash-p (value regex
)) nil
"/")))
629 (list (format nil
(concatenate 'string slash
"~A" slash
) (value regex
)))))
631 ;;; conditional compilation
632 (defmethod js-to-statement-strings ((cc cc-if
) start-pos
)
633 (nconc (list (format nil
"/*@if ~A" (cc-if-test cc
)))
634 (mapcan #'(lambda (x) (js-to-strings x start-pos
)) (cc-if-body cc
))
639 (defmethod js-to-strings ((instanceof js-instanceof
) start-pos
)
641 (list (js-to-strings (value instanceof
) (+ start-pos
2))
643 (js-to-strings (slot-value instanceof
'type
) (+ start-pos
2)))
650 ;;; single operations
651 (defmacro define-translate-js-single-op
(name &optional
(superclass 'expression
))
652 (let ((script-name (intern (concatenate 'string
"JS-" (symbol-name name
)) #.
*package
*)))
653 `(defmethod ,(if (eql superclass
'expression
)
655 'js-to-statement-strings
)
656 ((,name
,script-name
) start-pos
)
657 (dwim-join (list (js-to-strings (value ,name
) (+ start-pos
2)))
659 :start
,(concatenate 'string
(string-downcase (symbol-name name
)) " ")
662 (define-translate-js-single-op return statement
)
663 (define-translate-js-single-op throw statement
)
664 (define-translate-js-single-op delete
)
665 (define-translate-js-single-op void
)
666 (define-translate-js-single-op typeof
)
667 (define-translate-js-single-op new
)