Files
racket-webview/scrbl/js-transform.scrbl
T
2026-05-16 23:17:59 +02:00

340 lines
7.4 KiB
Racket

#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.