diff --git a/private/js-transform.rkt b/private/js-transform.rkt index 2eb2416..f6ad658 100644 --- a/private/js-transform.rkt +++ b/private/js-transform.rkt @@ -281,6 +281,9 @@ (syntax-case stx () ((_ (a c b1 b2)) (cond + ((eq? (syntax->datum #'a) 'define) #'(js-def c (b1 b2))) + ((eq? (syntax->datum #'a) 'lambda) #'(js-lambda c b1 b2)) + ((eq? (syntax->datum #'a) 'λ) #'(js-lambda c b1 b2)) ((eq? (syntax->datum #'a) 'if) #'(js2-if c b1 b2)) ((memq (syntax->datum #'a) js-ops) #'(js-op a c b1 b2)) ((eq? (syntax->datum #'a) 'let*) #'(js2-let* c b1 b2)) @@ -321,6 +324,7 @@ ((eq? (syntax->datum #'a) 'return) #'(js-return b)) ((eq? (syntax->datum #'a) 'quote) #'(js-quote b)) ((eq? (syntax->datum #'a) 'eval) #'(js-eval b)) + ((eq? (syntax->datum #'a) 'list) #'(js-op a b)) ;string-append ; "\"" (esc-double-quote (format "~a" 'b)) "\"")) (else @@ -334,6 +338,8 @@ (cond ((eq? (syntax->datum #'a) 'let*) #'(js2-let* c b1 ...)) ((eq? (syntax->datum #'a) 'define) #'(js-def c (b1 ...))) + ((eq? (syntax->datum #'a) 'lambda) #'(js-lambda c b1 ...)) + ((eq? (syntax->datum #'a) 'λ) #'(js-lambda c b1 ...)) ((memq (syntax->datum #'a) js-ops) #'(js-op a c b1 ...)) ((eq? (syntax->datum #'a) 'begin) #'(js-begin c b1 ...)) ((eq? (syntax->datum #'a) 'let) #'(error "let is not supported in js context, use let*")) @@ -392,10 +398,11 @@ #| (define t1 (js (set! window.myfunc (λ (x) - (let* ((el (document.getElementById 'hi)) + (let* ((el (send document getElementById 'hi)) (y (* x x))) - (el.setAttribute "x" (+ y "")) + (send el setAttribute "x" (+ y "")) ) + (send console log "dit set attribute x on element hi") ) ))) @@ -408,6 +415,33 @@ ) ) ) + + (define t3 (js (define (f x y z) + (send console log (cons x (cons y (list z)))) + (let* ((l (cons x (cons y (list z))))) + (return (send l map (λ (a) (return (+ a 10))))) + ) + ) + ) + ) + +==> +function f(x, y, z) { +console.log([ x].concat([ y].concat([ z]))); +{ +let l = [ x].concat([ y].concat([ z])); +return (l.map(function (a) { return (a + 10); +; + } +)); +; + +} +; + +} +; + |# diff --git a/scrbl/js-transform.scrbl b/scrbl/js-transform.scrbl new file mode 100644 index 0000000..9478c13 --- /dev/null +++ b/scrbl/js-transform.scrbl @@ -0,0 +1,340 @@ +#lang scribble/manual + +@(require (for-label racket/base + "../private/js-transform.rkt")) + +@title{JavaScript Transformation} +@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] + +@defmodule[racket-webview/private/js-transform] + +This module provides a small compile-time DSL for generating JavaScript source +text from Racket-like forms. It is used internally by the webview +implementation to construct JavaScript snippets in a structured way, without +having to write the complete snippet as one raw string. + +The module exports only @racket[js]. The other forms described in this section +are DSL forms recognized by @racket[js]; they are not exported as separate +bindings. + +@defform[ +(js js-statement ...) +]{ + +Transforms each @racket[js-statement] into JavaScript source text. The result +is a string. The generated string is not evaluated by Racket and no JavaScript +is executed by this module. + +Each top-level statement is emitted with a trailing semicolon and newline. The +resulting string can be passed to the webview layer for injection or execution +in the browser context. + +For example: + +@racketblock[ +(define source + (js + (set! window.myfunc + (λ (x) + (let* ((el (send document getElementById 'hi)) + (y (* x x))) + (send el setAttribute "x" (+ y ""))) + (send console log "did set attribute x on element hi"))))) +] + +This generates JavaScript source that assigns a function to +@tt{window.myfunc}. The function looks up a DOM element, calculates a value, +sets an attribute, and writes a message to the JavaScript console. +} + +@section{Primitive values} + +Numbers are emitted as JavaScript numeric literals. Strings are emitted as +double-quoted JavaScript strings, with embedded double quotes escaped. +Identifiers are emitted as JavaScript names. + +For example: + +@racketblock[ +(js + (send console log "hello") + (send console log 42)) +] + +Symbols can be quoted to produce JavaScript string values: + +@racketblock[ +(js + (send console log 'hello)) +] + +@section{Function calls and method calls} + +A form that is not recognized as a special DSL form is treated as a JavaScript +function call: + +@racketblock[ +(js + (alert "Hello from Racket")) +] + +This generates a JavaScript call to @tt{alert}. + +Method calls can be written with @racket[send]: + +@racketblock[ +(js + (send document getElementById 'hi)) +] + +The @racket[send] form has the shape: + +@racketblock[ +(send object method arg ...) +] + +and generates a JavaScript method call of the form: + +@codeblock|{ +object.method(arg, ...) +}| + +For example, @racket[(send document getElementById 'hi)] generates a call shaped +like: + +@codeblock|{ +document.getElementById("hi") +}| + +Using @racket[send] keeps the method-call structure explicit in the DSL. It is +usually clearer than writing dotted JavaScript names directly as function names. + +@section{Operators} + +The DSL supports a small set of JavaScript infix operators. + +@itemlist[ + #:style 'compact + + @item{@racket[(+ a b ...)], @racket[(- a b ...)], + @racket[(* a b ...)] and @racket[(/ a b ...)] generate arithmetic infix + expressions.} + + @item{@racket[(and a b ...)] and @racket[(or a b ...)] generate JavaScript + @tt{&&} and @tt{||} expressions.} + + @item{@racket[(> a b)], @racket[(< a b)], @racket[(>= a b)], + @racket[(<= a b)], @racket[(== a b)], @racket[(=== a b)] and + @racket[(!= a b)] generate JavaScript comparison expressions.} +] + +For example: + +@racketblock[ +(js + (define (inside-range x) + (if (and (> x 10) (< x 15)) + (return x) + (return (* x x))))) +] + +@section{Definitions and assignments} + +@defform[(define (name arg ...) body ...)]{ + +Generates a JavaScript function declaration. + +@racketblock[ +(js + (define (square x) + (return (* x x)))) +] + +This produces a JavaScript function named @tt{square}. +} + +@defform[(set! name expr)]{ + +Generates a JavaScript assignment. + +@racketblock[ +(js + (set! window.answer 42)) +] + +This produces an assignment to @tt{window.answer}. +} + +@section{Functions} + +@defform[(lambda (arg ...) body ...)]{ +Generates a JavaScript function expression. +} + +@defform[(λ (arg ...) body ...)]{ +Equivalent to @racket[lambda]. +} + +For example: + +@racketblock[ +(js + (set! window.square + (λ (x) + (return (* x x))))) +] + +A function body may contain more than one DSL statement: + +@racketblock[ +(js + (set! window.f + (λ (x) + (send console log x) + (return (* x x))))) +] + +@section{Control flow and statement blocks} + +@defform[(if condition then-expr else-expr)]{ + +Generates a JavaScript @tt{if}/@tt{else} statement. The condition is translated +as a JavaScript expression. The two branches are translated as JavaScript +statements. + +@racketblock[ +(js + (define (f x) + (if (> x 0) + (return x) + (return (- 0 x))))) +] +} + +@defform[(begin body ...)]{ + +Generates a JavaScript block. + +@racketblock[ +(js + (if (> x 10) + (begin + (send console log x) + (return x)) + (return 0))) +] +} + +@defform[(return expr)]{ + +Generates a JavaScript @tt{return} statement. + +@racketblock[ +(js + (define (id x) + (return x))) +] +} + +@section{Sequential bindings} + +@defform[(let* ((id expr) ...) body ...)]{ + +Generates a JavaScript block with sequential @tt{let} declarations, followed by +the generated body statements. + +@racketblock[ +(js + (let* ((x 10) + (y (* x x))) + (send console log y) + (return y))) +] + +The plain @racket[let] form is intentionally not supported in a JavaScript +context. Use @racket[let*] instead, so that the generated JavaScript bindings +remain explicitly sequential. +} + +@section{Lists} + +@defform[(list expr ...)]{ + +Generates a JavaScript array literal. + +@racketblock[ +(js + (send console log (list 1 2 3))) +] +} + +@defform[(cons expr list-expr)]{ + +Generates JavaScript that prepends a value to an array-like expression by +concatenating a one-element array with the translated list expression. + +For example: + +@racketblock[ +(js + (send console log (cons x (cons y (list z))))) +] + +This generates JavaScript shaped like: + +@codeblock|{ +console.log([ x].concat([ y].concat([ z]))); +}| +} + +@section{Embedding Racket values} + +@defform[(eval value)]{ + +Embeds a simple Racket value as a JavaScript literal at expansion time. The +supported values are strings, symbols, numbers and lists containing those same +kinds of values. + +@racketblock[ +(define names '(alice bob charlie)) + +(js + (send console log (eval names))) +] + +The value is converted to JavaScript source text before the generated JavaScript +is returned. +} + +@section{Example} + +The following example combines function definition, method calls, list +construction, sequential bindings and a lambda expression passed to a JavaScript +method: + +@racketblock[ +(define source + (js + (define (f x y z) + (send console log (cons x (cons y (list z)))) + (let* ((l (cons x (cons y (list z))))) + (return + (send l map + (λ (a) + (return (+ a 10))))))))) +] + +This generates a JavaScript function that logs an array, constructs the same +array in a local binding, maps over it, and returns the mapped result. + +@section{Limitations} + +This transformer is intentionally small. It is not a complete JavaScript +parser, not a JavaScript evaluator and not a general Racket-to-JavaScript +compiler. Unrecognized forms are generally treated as JavaScript function +calls. This keeps the implementation compact, but it also means that some +mistakes may produce JavaScript source that only fails when the browser runs the +generated code. + +The generated source is intended for small snippets used by the webview layer, +not for translating arbitrary Racket programs to JavaScript. \ No newline at end of file diff --git a/scrbl/racket-webview-collection.scrbl b/scrbl/racket-webview-collection.scrbl index 33659d4..b23db18 100644 --- a/scrbl/racket-webview-collection.scrbl +++ b/scrbl/racket-webview-collection.scrbl @@ -17,6 +17,7 @@ @include-section["wv-dialog.scrbl"] @include-section["wv-element.scrbl"] @include-section["wv-input.scrbl"] +@include-section["js-transform.scrbl"] @include-section["racket-webview.scrbl"] @include-section["racket-webview-qt.scrbl"] @include-section["rgba.scrbl"]