Alles aangepast.

This commit is contained in:
2026-06-08 12:55:08 +02:00
parent a9610e6e0c
commit 823130e3ac
24 changed files with 418 additions and 2621 deletions
+66 -35
View File
@@ -1,53 +1,84 @@
#lang scribble/manual
@(require (for-label racket/base
js-maker))
@(require (for-label racket/base js-maker))
@title{js-maker}
@author{Hans Dijkema}
@defmodule[js-maker]
@racketmodname[js-maker] provides a deliberately small syntax-driven macro for
making JavaScript strings from a limited Racket-like surface syntax. This is a
clean js-maker 3 restart based on the compact @filepath{js-transform.rkt}
implementation.
@emph{js-maker} is a deliberately small syntax-driven JavaScript string maker.
It provides one public macro, @racket[js]. The helper machinery used to render
subexpressions is private to the package.
@section{Public API}
@defform[(js form ...)]{
Generates JavaScript statements for each @racket[form] and concatenates them.
The generated JavaScript is returned as a string.
Produces a JavaScript string for the supplied forms. Each top-level form is
rendered as a JavaScript statement. The macro is intended for small generated
snippets, demos, and controlled code generation; it is not a complete Racket to
JavaScript compiler.
Examples:
@racketblock[
(js
(define (sum-to n)
(let loop ([i 0] [acc 0])
(if (> i n)
(return acc)
(loop (+ i 1) (+ acc i))))))]
}
@defform[(js1 form)]{
Generates JavaScript for a single expression or syntactic form and returns it as
a string. Use this when you want the expression-level generator directly.
@racketblock[
(js1 (+ 1 2))]
(js (+ 1 2))
(js (define answer 42))
(js (define (square x)
(return (* x x))))
]
}
@section{Supported core forms}
The compact branch supports identifiers, quoted data, primitive literals,
function calls, infix arithmetic and comparison operators, @racket[if],
@racket[begin], @racket[return], @racket[set!], @racket[lambda],
@racket[define], ordinary @racket[let], @racket[let*], and named
@racket[let].
The compact js-maker 3 implementation supports:
Ordinary @racket[let] keeps Racket's parallel binding semantics. All right-hand
sides are generated before the bound names are introduced, and the actual
JavaScript bindings are placed in an inner block so JavaScript temporal dead zone
rules cannot accidentally shadow the initializers.
@itemlist[
@item{@racket[define] for values and functions.}
@item{@racket[lambda] and @racket[lambda]-style generated JavaScript functions.}
@item{@racket[if], @racket[begin], @racket[set!], and explicit @racket[return].}
@item{Ordinary @racket[let] with parallel binding semantics.}
@item{@racket[let*] with sequential binding semantics.}
@item{Named @racket[let] in tail-recursive loop style.}
@item{Common infix operators such as @racket[+], @racket[-], @racket[*],
@racket[/], comparisons, @racket[and], @racket[or], and @racket[not].}
@item{@racket[list], @racket[cons], @racket[send], @racket[js-dot], and
@racket[new].}
]
Named @racket[let] is compiled to a JavaScript @tt{while (true)} loop. A tail
call to the loop name is rewritten to parallel assignment of the loop variables
followed by @tt{continue}. This keeps the important loop semantics without
reintroducing the large js-maker 2 implementation.
@section{Let semantics}
Ordinary @racket[let] evaluates all right-hand sides before introducing the new
bindings. js-maker preserves that behavior by emitting temporary JavaScript
constants and then opening a nested block for the real @tt{let} bindings.
This avoids JavaScript temporal-dead-zone shadowing when a bound name is also
used by a right-hand side.
@racketblock[
(js (define (ordinary-let x)
(let ([x 1] [y x])
(return y))))
]
The generated JavaScript returns the original argument as the value of
@tt{y}, matching Racket's ordinary @racket[let] semantics.
@section{Named let}
Named @racket[let] is emitted as a @tt{while (true)} loop. A tail call to the
loop name is translated to parallel update assignments followed by
@tt{continue}. The intended style is statement-oriented and uses explicit
@racket[return] for terminating branches.
@racketblock[
(js (define (sum-to n)
(let loop ([i 0] [acc 0])
(if (> i n)
(return acc)
(loop (+ i 1) (+ acc i))))))
]
@section{Package layout}
The package includes small demos under @filepath{demo/} and a regression suite
under @filepath{testing/}. The public API remains just @racket[js].
+30 -583
View File
@@ -1,596 +1,43 @@
#lang scribble/manual
@(require scribble/core
(for-label racket/base
"../main.rkt"))
@(require (for-label racket/base js-maker))
@(define (code-box title source)
(tabular #:style 'boxed
(list (list (bold title))
(list (verbatim source)))))
@title{js-maker use cases}
@(define (code-pair racket-source js-source)
(list
(code-box "Racket / js-maker" racket-source)
(code-box "Generated JavaScript" js-source)))
@section{Generating a small function}
@(define side-by-side code-pair)
@racketblock[
(js (define (square x)
(return (* x x))))
]
@(define (tested s)
(nested #:style 'inset (bold "Tested behavior: ") s))
This produces a JavaScript function declaration. Racket identifiers are mapped
to JavaScript-friendly names by replacing unsupported characters with
underscores.
@title{jsmaker JavaScript Use Cases}
@author+email["Hans Dijkema" "hans@dijkewijk.nl"]
@section{Generating a loop}
@defmodule[js-maker/demo/js-usecases]
@racketblock[
(js (define (sum-to n)
(let loop ([i 0] [acc 0])
(if (> i n)
(return acc)
(loop (+ i 1) (+ acc i))))))
]
This document describes the practical JavaScript use cases in
@filepath{demo/js-usecases.rkt}. Each implementation is written as a Racket
snippet using @racket[js] and is tested by compiling it to JavaScript and
executing that JavaScript with the configured test executor. The corresponding
tests are in @filepath{testing/jsmaker-usecases.rkt}.
The named @racket[let] form is useful for simple loops while keeping ordinary
Racket binding semantics for the initial values and loop updates.
The examples are shown vertically: first the Racket/js-maker source, then the
generated JavaScript. Each code fragment is shown in a boxed documentation
cell, preserving the light shaded background of the earlier side-by-side
layout while avoiding narrow, wrapped code columns.
@section{Generating DOM-style calls}
The tests intentionally use @racket[js/expression] for their calls wherever
possible. Raw JavaScript remains only in small harness preambles, such as fake
@tt{setInterval}, fake DOM objects, and fake @tt{fetch}.
@section{Running the examples}
Generate the JavaScript examples with:
@codeblock{
racket demo/js-usecases.rkt
}
Run the use-case regression tests with:
@codeblock{
racket testing/jsmaker-usecases.rkt
}
@section{Use cases}
@subsection{1. Random number between 1 and 5}
@(side-by-side
#<<RKT
@racketblock[
(js
(define (randomBetween1And5)
(return (+ (send Math floor (* (send Math random) 5)) 1))))
RKT
#<<JS
function randomBetween1And5() {
return (Math.floor((Math.random() * 5)) + 1);
}
JS
)
(define title (send document getElementById "title"))
(set! (js-dot title innerHTML) "Hello")
(send title addEventListener "click"
(lambda (evt) (return #t))))
]
@(tested "With Math.random fixed at 0.80, the generated function returns 5.")
@subsection{2. Unique values with Set}
@(side-by-side
#<<RKT
(js
(define (uniqueValues xs)
(return (send Array from (new Set xs)))))
RKT
#<<JS
function uniqueValues(xs) {
return Array.from(new Set(xs));
}
JS
)
@(tested "The array [1, 2, 2, 3, 1] becomes [1, 2, 3].")
@subsection{3. Six falsey JavaScript values}
@(side-by-side
#<<RKT
(js
(define (falseyValues)
(return (array #f 0 "" js-null js-undefined js-NaN))))
RKT
#<<JS
function falseyValues() {
return [false, 0, "", null, undefined, NaN];
}
JS
)
@(tested "Mapping JavaScript Boolean over the returned values yields six false values.")
@subsection{4. Currying}
@(side-by-side
#<<RKT
(js
(define (add x)
(return (lambda (y)
(return (+ x y))))))
RKT
#<<JS
function add(x) {
return function(y) {
return (x + y);
};
}
JS
)
@(tested "The generated call add(2)(3) returns 5; the test call itself is produced with js/expression.")
@subsection{5. Object destructuring}
@(side-by-side
#<<RKT
(js
(define (describePerson person)
(let-object ([name 'name]
[age 'age 0])
person
(return (string-append name ":" (number->string age))))))
RKT
#<<JS
function describePerson(person) {
return (() => {
const { name: name, age: age = 0 } = person;
return (name + ":" + String(age));
})();
}
JS
)
@(tested "The object { name: \"Ada\", age: 37 } is rendered as \"Ada:37\".")
@subsection{6. Escaping a timer interval}
@(side-by-side
#<<RKT
(js
(define (startTimer)
(let* ([ticks 0]
[intervalId #f])
(set! intervalId
(setInterval (lambda ()
(set! ticks (+ ticks 1))
(when (= ticks 3)
(clearInterval intervalId)))
10))
(return (object 'id intervalId
'getTicks (lambda () (return ticks)))))))
RKT
#<<JS
function startTimer() {
{
let ticks = 0;
let intervalId = false;
intervalId = setInterval(function() {
ticks = (ticks + 1);
if ((ticks === 3)) clearInterval(intervalId);
return undefined;
}, 10);
return {id: intervalId, getTicks: function() { return ticks; }};
}
}
JS
)
@(tested "A fake timer runs the callback five times, but clearInterval stops it at tick 3.")
@subsection{7. Get, set, and delete object properties}
@(side-by-side
#<<RKT
(js
(define (objectProps)
(let* ([obj (object 'a 1)]
[a1 obj.a]
[a2 (js-ref obj "a")])
(let-object ([a3 'a]) obj
(set! obj.b 2)
(set-prop! obj "c" 3)
(delete-prop! obj "a")
(return (array a1 a2 a3 obj.b (js-ref obj "c")
(send Object hasOwn obj "a")))))))
RKT
#<<JS
function objectProps() {
let obj = {a: 1};
let a1 = obj.a;
let a2 = obj["a"];
return (() => {
const { a: a3 } = obj;
obj.b = 2;
obj["c"] = 3;
delete obj["a"];
return [a1, a2, a3, obj.b, obj["c"], Object.hasOwn(obj, "a")];
})();
}
JS
)
@(tested "The three reads produce 1, the two writes produce 2 and 3, and the deleted property is absent.")
@subsection{8. String concatenation order}
@(side-by-side
#<<RKT
(js
(define (concatOrder)
(return (array (+ 1 2 "3")
(+ "1" 2 3)))))
RKT
#<<JS
function concatOrder() {
return [(1 + 2 + "3"), ("1" + 2 + 3)];
}
JS
)
@(tested "The generated function returns [\"33\", \"123\"].")
@subsection{9. Object.freeze versus Object.seal}
@(side-by-side
#<<RKT
(js
(define (freezeVsSeal)
(let* ([frozen (send Object freeze (object 'a 1))]
[sealed (send Object seal (object 'a 1))])
(set! frozen.a 9)
(set! sealed.a 9)
(delete-prop! sealed "a")
(return (array frozen.a sealed.a
(send Object isFrozen frozen)
(send Object isSealed sealed)
(send Object hasOwn sealed "a"))))))
RKT
#<<JS
function freezeVsSeal() {
let frozen = Object.freeze({a: 1});
let sealed = Object.seal({a: 1});
frozen.a = 9;
sealed.a = 9;
delete sealed["a"];
return [frozen.a, sealed.a, Object.isFrozen(frozen),
Object.isSealed(sealed), Object.hasOwn(sealed, "a")];
}
JS
)
@(tested "The frozen value stays 1; the sealed value changes to 9 but remains present.")
@subsection{10. Switch example}
@(side-by-side
#<<RKT
(js
(define (switchExample n)
(case n
[(1) (return "one")]
[(2 3) (return "two-or-three")]
[else (return "other")]))))
RKT
#<<JS
function switchExample(n) {
switch (n) {
case 1: return "one";
case 2:
case 3: return "two-or-three";
default: return "other";
}
}
JS
)
@(tested "The generated function maps 1, 2, and 9 to the three expected branches.")
@subsection{11. Class constructor with a default value}
@(side-by-side
#<<RKT
(js
(define-class Greeter
(constructor ([name "world"])
(set! this.name name))
(method greet ()
(return (string-append "Hello " this.name))))
(define (classExample)
(let* ([a (new Greeter)]
[b (new Greeter "Ada")])
(return (array (send a greet) (send b greet))))))
RKT
#<<JS
class Greeter {
constructor(name = "world") {
this.name = name;
}
greet() {
return ("Hello " + this.name);
}
}
function classExample() {
let a = new Greeter();
let b = new Greeter("Ada");
return [a.greet(), b.greet()];
}
JS
)
@(tested "The default constructor value and explicit constructor argument both work.")
@subsection{12. Sort objects by a property}
@(side-by-side
#<<RKT
(js
(define (sortByProperty xs prop)
(return (send (send xs slice)
sort
(lambda (a b)
(return (- (js-ref a prop) (js-ref b prop))))))))
RKT
#<<JS
function sortByProperty(xs, prop) {
return xs.slice().sort(function(a, b) {
return (a[prop] - b[prop]);
});
}
JS
)
@(tested "Sorting age objects by age yields [20, 25, 30].")
@subsection{13. Four ways to delete or remove an array element}
@(side-by-side
#<<RKT
(js
(define (deleteArrayWays xs)
(let* ([a1 (send xs slice)]
[a2 (send xs slice)]
[a3 (send xs slice)]
[a4 (send xs slice)])
(send a1 splice 1 1)
(set! a2 (send a2 filter (lambda (x i) (return (not (= i 1))))))
(set! a3 (send (send a3 slice 0 1) concat (send a3 slice 2)))
(delete-prop! a4 1)
(return (array a1 a2 a3
(array (send Object hasOwn a4 "1") (length a4)))))))
RKT
#<<JS
function deleteArrayWays(xs) {
let a1 = xs.slice(), a2 = xs.slice(), a3 = xs.slice(), a4 = xs.slice();
a1.splice(1, 1);
a2 = a2.filter(function(x, i) { return !(i === 1); });
a3 = a3.slice(0, 1).concat(a3.slice(2));
delete a4[1];
return [a1, a2, a3, [Object.hasOwn(a4, "1"), a4.length]];
}
JS
)
@(tested "splice, filter, and slice/concat remove the item; delete leaves a hole and preserves length.")
@subsection{14. Bubble sort}
@(side-by-side
#<<RKT
(js
(define (bubbleSort xs)
(let* ([a (send xs slice)]
[n (length a)])
(while (> n 1)
(let* ([i 1])
(while (< i n)
(when (> (list-ref a (- i 1)) (list-ref a i))
(let* ([tmp (list-ref a (- i 1))])
(vector-set! a (- i 1) (list-ref a i))
(vector-set! a i tmp)))
(set! i (+ i 1))))
(set! n (- n 1)))
(return a))))
RKT
#<<JS
function bubbleSort(xs) {
let a = xs.slice();
let n = a.length;
while ((n > 1)) {
let i = 1;
while ((i < n)) {
if ((a[(i - 1)] > a[i])) {
let tmp = a[(i - 1)];
a[(i - 1)] = a[i];
a[i] = tmp;
}
i = (i + 1);
}
n = (n - 1);
}
return a;
}
JS
)
@(tested "Sorting [5, 1, 4, 2, 8] yields [1, 2, 4, 5, 8].")
@subsection{15. Recursive binary search}
@(side-by-side
#<<RKT
(js
(define (binarySearch xs target low high)
(if (> low high)
(return -1)
(let* ([mid (send Math floor (/ (+ low high) 2))]
[value (list-ref xs mid)])
(cond
[(= value target) (return mid)]
[(< value target) (return (binarySearch xs target (+ mid 1) high))]
[else (return (binarySearch xs target low (- mid 1)))])))))
RKT
#<<JS
function binarySearch(xs, target, low, high) {
if ((low > high)) return -1;
let mid = Math.floor(((low + high) / 2));
let value = xs[mid];
if ((value === target)) return mid;
if ((value < target)) return binarySearch(xs, target, (mid + 1), high);
return binarySearch(xs, target, low, (mid - 1));
}
JS
)
@(tested "Searching 7 returns index 3; searching 4 returns -1.")
@subsection{16. Count occurrences with Map}
@(side-by-side
#<<RKT
(js
(define (countOccurrences xs)
(let* ([counts (new Map)])
(for ([x (in-list xs)])
(if (send counts has x)
(send counts set x (+ (send counts get x) 1))
(send counts set x 1)))
(return (send Array from (send counts entries))))))
RKT
#<<JS
function countOccurrences(xs) {
let counts = new Map();
for (const x of xs) {
if (counts.has(x)) counts.set(x, (counts.get(x) + 1));
else counts.set(x, 1);
}
return Array.from(counts.entries());
}
JS
)
@(tested "The array [\"a\", \"b\", \"a\", \"c\", \"b\", \"a\"] yields [[\"a\",3],[\"b\",2],[\"c\",1]].")
@subsection{17. Get HTML in three ways}
@(side-by-side
#<<RKT
(js
(define (getHtmlThreeWays)
(return (array document.body.innerHTML
(js-dot (send document querySelector "body") innerHTML)
(js-ref (send document getElementById "root") "innerHTML")))))
RKT
#<<JS
function getHtmlThreeWays() {
return [document.body.innerHTML,
document.querySelector("body").innerHTML,
document.getElementById("root")["innerHTML"]];
}
JS
)
@(tested "A fake DOM returns the same HTML string through all three access forms.")
@subsection{18. Anagram / rearrangement check}
@(side-by-side
#<<RKT
(js
(define (sortChars s)
(return (send (send (send s split "") sort) join "")))
(define (canArrange stringA stringB)
(return (string=? (sortChars stringA) (sortChars stringB)))))
RKT
#<<JS
function sortChars(s) {
return s.split("").sort().join("");
}
function canArrange(stringA, stringB) {
return (sortChars(stringA) === sortChars(stringB));
}
JS
)
@(tested "listen/silent returns true; abc/abd returns false.")
@subsection{19. Pairs equal to a target, without repeats}
@(side-by-side
#<<RKT
(js
(define (pairsEqualTarget xs target)
(let* ([seen (new Set)]
[used (new Set)]
[out (array)])
(for ([x (in-list xs)])
(let* ([y (- target x)])
(if (and (send seen has y)
(not (send used has x))
(not (send used has y)))
(begin
(send out push (array y x))
(send used add x)
(send used add y))
(send seen add x))))
(return out))))
RKT
#<<JS
function pairsEqualTarget(xs, target) {
let seen = new Set();
let used = new Set();
let out = [];
for (const x of xs) {
let y = (target - x);
if (seen.has(y) && !(used.has(x)) && !(used.has(y))) {
out.push([y, x]); used.add(x); used.add(y);
} else {
seen.add(x);
}
}
return out;
}
JS
)
@(tested "For [1, 2, 3, 4, 3, 5] and target 6 the returned pairs are [[2,4],[3,3],[1,5]].")
@subsection{20. Fetch API with result and error handling}
@(side-by-side
#<<RKT
(js
(define (loadTitle url)
(return
(send
(send
(send (fetch url)
then
(lambda (response)
(return (send response json))))
then
(lambda (data)
(return (object 'ok #t 'title data.title))))
catch
(lambda (err)
(return (object 'ok #f 'message err.message)))))))
RKT
#<<JS
function loadTitle(url) {
return fetch(url)
.then(function(response) { return response.json(); })
.then(function(data) { return {ok: true, title: data.title}; })
.catch(function(err) { return {ok: false, message: err.message}; });
}
JS
)
@(tested "A fake fetch resolves /ok to {ok:true,title:\"Done\"} and rejects /fail to {ok:false,message:\"network\"}.")
This is string generation only. The generated JavaScript must still be run in a
JavaScript environment that provides the referenced objects, such as
@tt{document}.