forked from dyoo/whalesong
/
manual.scrbl
1390 lines (987 loc) · 41.8 KB
/
manual.scrbl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#lang scribble/manual
@(require planet/scribble
planet/version
planet/resolver
scribble/eval
scribble/bnf
racket/sandbox
racket/port
racket/list
(only-in racket/contract any/c)
racket/runtime-path
"scribble-helpers.rkt")
@(require racket/runtime-path)
@(define-runtime-path git-head-path "../.git/refs/heads/master")
@(require (for-label (this-package-in js))
(for-label (except-in (this-package-in lang/base)
string?
printf
number->string
void
quasiquote
string=?
string
e
number?
newline
current-output-port
display))
(for-label (this-package-in resource)
(for-label (this-package-in web-world))))
@inject-javascript-inline|{
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-24146890-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
}|
@inject-javascript-src{http://hashcollision.org/whalesong/examples/runtime.js}
@(define-runtime-path whalesong-path "..")
@;; I may need an evaluator for some small examples.
@(define my-evaluator
(call-with-trusted-sandbox-configuration
(lambda ()
(parameterize ([sandbox-output 'string]
[sandbox-error-output 'string])
(make-evaluator 'racket)))))
@title{Whalesong: a Racket to JavaScript compiler}
@author+email["Danny Yoo" "dyoo@hashcollision.org"]
@centered{@smaller{Source code can be found at:
@url{https://github.com/dyoo/whalesong}. The latest version of this
document lives in @url{http://hashcollision.org/whalesong}.}}
@(if (file-exists? git-head-path)
(let ([git-head (call-with-input-file git-head-path port->string)])
@centered{@smaller{Current commit head is @tt{@git-head}.}})
"")
@;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@section{Introduction}
@;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Whalesong is a compiler from Racket to JavaScript; it takes Racket
programs and translates them so that they can run stand-alone on a
user's web browser. It should allow Racket programs to run with
(hopefully!) little modification, and provide access through the
foreign-function interface to native JavaScript APIs. The included
runtime library supports the numeric tower, an image library, and a
framework to program the web in functional event-driven style.
The GitHub source repository to Whalesong can be found at
@url{https://github.com/dyoo/whalesong}.
Prerequisites: at least @link["http://racket-lang.org/"]{Racket
5.1.1}. If you wish to use the JavaScript compression option,
you will need @link["http://www.java.com"]{Java 1.6} SDK.
@; (This might be superfluous information, so commented out
@; for the moment...)
@;The majority of the project is written
@;@link["http://docs.racket-lang.org/ts-guide/index.html"]{Typed
@;Racket}, and Racket 5.1.1 and above provides the support necessary to
@;compile Whalesong; otherwise, compilation may take an unusual amount
@;of time.
@subsection{Examples}
Here are a collection of programs that use the @emph{web-world} library described
later in this document:
@itemize[
@item{@link["http://hashcollision.org/whalesong/examples/attr-animation/attr-animation.html"]{attr-animation.html}
[@link["http://hashcollision.org/whalesong/examples/attr-animation/attr-animation.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/attr-animation/index.html"]{index.html}
@link["http://hashcollision.org/whalesong/examples/attr-animation/style.css"]{style.css}]
Uses @racket[update-view-attr] and @racket[on-tick] to perform a simple color animation.}
@item{@link["http://hashcollision.org/whalesong/examples/boid/boid.html"]{boid.html}
[@link["http://hashcollision.org/whalesong/examples/boid/boid.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/boid/index.html"]{index.html}] Uses @racket[update-view-css] and @racket[on-tick] to perform an animation of a flock of @link["http://en.wikipedia.org/wiki/Boids"]{boids}.}
@item{@link["http://hashcollision.org/whalesong/examples/dwarves/dwarves.html"]{dwarves.html}
[@link["http://hashcollision.org/whalesong/examples/dwarves/dwarves.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/dwarves/index.html"]{index.html}]
Uses @racket[view-show] and @racket[view-hide] to manipulate a view. Click on a dwarf to make them hide.
}
@item{@link["http://hashcollision.org/whalesong/examples/dwarves-with-remove/dwarves-with-remove.html"]{dwarves-with-remove.html}
[@link["http://hashcollision.org/whalesong/examples/dwarves-with-remove/dwarves-with-remove.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/dwarves-with-remove/index.html"]{index.html}]
Uses @racket[view-focus?] and @racket[view-remove] to see if a dwarf should be removed from the view.
}
@item{@link["http://hashcollision.org/whalesong/examples/field/field.html"]{field.html}
[@link["http://hashcollision.org/whalesong/examples/field/field.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/field/index.html"]{index.html}]
Uses @racket[view-bind] to read a text field, and @racket[update-view-text] to change
the text content of an element.
}
@item{@link["http://hashcollision.org/whalesong/examples/phases/phases.html"]{phases.html}
[@link["http://hashcollision.org/whalesong/examples/phases/phases.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/phases/index1.html"]{index1.html}
@link["http://hashcollision.org/whalesong/examples/phases/index2.html"]{index2.html}]
Switches out one view entirely in place of another. Different views can correspond to phases in a program.
}
@item{@link["http://hashcollision.org/whalesong/examples/tick-tock/tick-tock.html"]{tick-tock.html}
[@link["http://hashcollision.org/whalesong/examples/tick-tock/tick-tock.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/tick-tock/index.html"]{index.html}]
Uses @racket[on-tick] to show a timer counting up.
}
@item{@link["http://hashcollision.org/whalesong/examples/redirected/redirected.html"]{redirected.html}
[@link["http://hashcollision.org/whalesong/examples/redirected/redirected.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/redirected/index.html"]{index.html}]
Uses @racket[on-tick] to show a timer counting up, and also uses @racket[open-output-element] to
pipe side-effecting @racket[printf]s to a hidden @tt{div}.
}
@item{@link["http://hashcollision.org/whalesong/examples/todo/todo.html"]{todo.html}
[@link["http://hashcollision.org/whalesong/examples/todo/todo.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/todo/index.html"]{index.html}]
A simple TODO list manager.
}
@item{@link["http://hashcollision.org/whalesong/examples/where-am-i/where-am-i.html"]{where-am-i.html}
[@link["http://hashcollision.org/whalesong/examples/where-am-i/where-am-i.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/where-am-i/index.html"]{index.html}]
Uses @racket[on-location-change] and @racket[on-mock-location-change] to demonstrate location services.
}
@item{@link["http://hashcollision.org/whalesong/examples/hot-cross-buns/hot-cross-buns.html"]{hot-cross-buns.html}
[@link["http://hashcollision.org/whalesong/examples/hot-cross-buns/hot-cross-buns.rkt"]{src}
@link["http://hashcollision.org/whalesong/examples/hot-cross-buns/index.html"]{index.html}]
Demonstrates use of checkboxes. Uses @racket[view-has-attr?] to see if a checkbox has been
checked, and @racket[remove-view-attr] to change the @emph{checked} attribute when the user
wants to reset the page.
}
]
I also gave a
@link["http://hashcollision.org/whalesong/racketcon"]{presentation}
of Whalesong at RacketCon 2011, including examples like:
@itemize[
@item{@link["http://hashcollision.org/whalesong/racketcon/rain.html"]{rain.html}
[@link["http://hashcollision.org/whalesong/racketcon/rain.rkt"]{src}]
Uses the image libraries to show droplets of water falling down.}
@item{@link["http://hashcollision.org/whalesong/racketcon/pacman.html"]{pacman.html}
[@link["http://hashcollision.org/whalesong/racketcon/pacman.rkt"]{src}]
Pacman.}
]
@;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@section{Getting started}
@;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@subsection{Installing Whalesong}
Before you begin, if you are using DrRacket,
@itemize[#:style 'ordered
@item{Please go to the Racket submenu.}
@item{Select the Limit Memory item.}
@item{Change the setting to Unlimited.}]
This is to avoid an installation-time issue that prevents
Whalesong from fully compiling.
If you want to use Whalesong, run the following to create
the @filepath{whalesong} launcher:
@codeblock|{
#lang racket/base
(require (planet dyoo/whalesong:1:14/make-launcher))
}|
This may take a few minutes, as Racket is compiling Whalesong, its
dependencies, and its documentation. When it finally finishes,
you should see a @filepath{whalesong} launcher in the current
directory.
At this point, you should be able to run the @filepath{whalesong} executable from the command line.
@verbatim|{
$ ./whalesong
Usage: whalesong <subcommand> [option ...] <arg ...>
where any unambiguous prefix can be used for a subcommand
The Whalesong command-line tool for compiling Racket to JavaScript
For help on a particular subcommand, use 'whalesong <subcommand> --help'
whalesong build build a standalone html and javascript package
whalesong get-runtime print the runtime library to standard output
whalesong get-javascript Gets just the JavaScript code and prints it to standard output
}|
and if this does appear, then Whalesong should be installed successfully.
To repeat: whenever Whalesong's source code is updated from Github,
please re-run the @tt{raco setup} step. Otherwise, Racket will try to
recompile Whalesong on every single use, which can be very expensive.
@subsection{Making @tt{.html} files with Whalesong}
Let's try making a simple, standalone executable. At the moment, the
program must be written in the base language of @racket[(planet
dyoo/whalesong)]. This restriction unfortunately prevents arbitrary
@racketmodname[racket/base] programs from compiling at the moment;
the developers (namely, dyoo) will be working to remove this
restriction as quickly as possible.
Write a @filepath{hello.rkt} with the following content
@filebox["hello.rkt"]{
@codeblock{
#lang planet dyoo/whalesong
(display "hello world")
(newline)
}}
This program is a regular Racket program, and can be executed normally,
@verbatim|{
$ racket hello.rkt
hello world
$
}|
However, it can also be packaged with @filepath{whalesong}.
@verbatim|{
$ whalesong build hello.rkt
Writing program #<path:/home/dyoo/work/whalesong/examples/hello.js>
Writing html #<path:/home/dyoo/work/whalesong/examples/hello.html>
$ ls -l hello.html
-rw-r--r-- 1 dyoo dyoo 3817 2011-09-10 15:02 hello.html
$ ls -l hello.js
-rw-r--r-- 1 dyoo dyoo 841948 2011-09-10 15:02 hello.js
}|
@margin-note{Visit @link["http://hashcollision.org/whalesong/examples/hello/hello.html"]{hello.html} to execute this program.}
Running @tt{whalesong build} on a Racket program will produce a
@filepath{.html} and @filepath{.js} file. If you open the
@filepath{.html} in your favorite web browser, you should see a
triumphant message show on screen.
We can do something slightly more interesting. Let's write a Whalesong program
that accesses the JavaScript DOM. Call this file @filepath{dom-play.rkt}.
@margin-note{
Visit @link["http://hashcollision.org/whalesong/examples/dom-play/dom-play.html"]{dom-play.html} to execute this program.}
@filebox["dom-play.rkt"]{
@codeblock|{
#lang planet dyoo/whalesong
;; Uses the JavaScript FFI, which provides bindings for:
;; $ and call-method
(require (planet dyoo/whalesong/js))
;; insert-break: -> void
(define (insert-break)
(call-method ($ "<br/>") "appendTo" body)
(void))
;; write-message: any -> void
(define (write-message msg)
(void (call-method (call-method (call-method ($ "<span/>") "text" msg)
"css" "white-space" "pre")
"appendTo"
body)))
;; Set the background green, and show some content
;; on the browser.
(void (call-method body "css" "background-color" "lightgreen"))
(void (call-method ($ "<h1>Hello World</h1>") "appendTo" body))
(write-message "Hello, this is a test!")
(insert-break)
(let loop ([i 0])
(cond
[(= i 10)
(void)]
[else
(write-message "iteration ") (write-message i)
(insert-break)
(loop (add1 i))]))
}|}
This program uses the @link["http:/jquery.com"]{JQuery} API provided by @racketmodname[(planet dyoo/whalesong/js)],
as well as the native JavaScript FFI to produce output on the browser.
If we run Whalesong on this program, and view the resulting @filepath{dom-play.html} in our
web browser, we should see a pale, green page with some output.
@;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@subsection{Using Whalesong functions from JavaScript}
@;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Whalesong also allows functions defined from Racket to be used from
JavaScript. As an example, we can take the boring @emph{factorial}
function and define it in a module called @filepath{fact.rkt}:
@margin-note{
The files can also be downloaded here:
@itemlist[@item{@link["http://hashcollision.org/whalesong/fact-example/fact.rkt"]{fact.rkt}}
@item{@link["http://hashcollision.org/whalesong/fact-example/index.html"]{index.html}}]
with generated JavaScript binaries here:
@itemlist[
@item{@link["http://hashcollision.org/whalesong/fact-example/fact.js"]{fact.js}}
@item{@link["http://hashcollision.org/whalesong/fact-example/runtime.js"]{runtime.js}}
]
}
@filebox["fact.rkt"]{
@codeblock|{
#lang planet dyoo/whalesong
(provide fact)
(define (fact x)
(cond
[(= x 0)
1]
[else
(* x (fact (sub1 x)))]))
}|}
Instead of creating a standalone @tt{.html}, we can use @tt{whalesong} to
get us the module's code. From the command-line:
@verbatim|{
$ whalesong get-javascript fact.rkt > fact.js
$ ls -l fact.js
-rw-r--r-- 1 dyoo dyoo 27421 2011-07-11 22:02 fact.js
}|
This file does require some runtime support not included in
@filepath{fact.js}; let's generate the @tt{runtime.js} and save
it as well. At the command-line:
@verbatim|{
$ whalesong get-runtime > runtime.js
$ ls -l runtime.js
-rw-r--r-- 1 dyoo dyoo 544322 2011-07-11 22:12 runtime.js
}|
Now that we have these, let's write an @filepath{index.html} that uses
the @racket[fact] function that we @racket[provide]ed from
@filepath{fact.rkt}.
@filebox["index.html"]{
@verbatim|{
<!DOCTYPE html>
<html>
<head>
<script src="runtime.js"></script>
<script src="fact.js"></script>
<script>
// Each module compiled with 'whalesong get-runtime' is treated as a
// main module. invokeMains() will invoke them.
plt.runtime.invokeMains();
plt.runtime.ready(function() {
// Grab the definition of 'fact'...
var myFactClosure = plt.runtime.lookupInMains('fact');
// Make it available as a JavaScript function...
var myFact = plt.baselib.functions.asJavaScriptFunction(
myFactClosure);
// And call it!
myFact(function(v) {
$('#answer').text(v.toString());
},
function(err) {
$('#answer').text(err.message).css("color", "red");
},
10000
// "one-billion-dollars"
);
});
</script>
</head>
<body>
The factorial of 10000 is <span id="answer">being computed</span>.
</body>
</html>
}|
}
@margin-note{See: @link["http://hashcollision.org/whalesong/fact-example/bad-index.html"]{bad-index.html}.}
Replacing the @racket[10000] with @racket["one-billion-dollars"] should
reliably produce a proper error message.
@section{Using @tt{whalesong}}
Whalesong provides a command-line utility called @tt{whalesong} for
translating Racket to JavaScript. It can be run in several modes:
@itemize[
@item{To create HTML + js documents}
@item{To output the compiled JavaScript as a single @filepath{.js} file}
]
Using @tt{whalesong} to generate HTML+js documents is
relatively straightforward with the @tt{build} command. To use it,
pass the name of the file to it:
@verbatim|{
$ whalesong build [name-of-racket-file]
}|
A @filepath{.html} and @filepath{.js} will be written to the current directory, as will any external resources that the program uses.
The @tt{whalesong} commands support these command line options:
@itemize[
@item{@verbatim{--compress-javascript} Use Google Closure's JavaScript
compiler to significantly compress the JavaScript. Using this
currently requires a Java 1.6 JDK.}
@item{@verbatim{--verbose} Write verbose debugging information to standard error.}
@item{@verbatim{--dest-dir} Write files to a separate directory, rather than the current directory.}
@item{@verbatim{--split-modules} Write each dependent module as a
separate file, rather than in one large @filepath{.js}. This may be
necessary if your browser environment prohibits large @filepath{.js}
files. The files will be numbered starting from @racket[1].}
]
For more advanced users, @tt{whalesong} can be used to generate
JavaScript in non-standalone mode. This gives the web developer more
fine-grained control over how to control and deploy the outputted
program.
@subsection{@tt{build}}
Given the name of a program, this builds
@filepath{.html} and @filepath{.js} files into the current working directory.
The @filepath{.html} and @filepath{.js} should be self-contained, with an exception: if
the file uses any external @tech{resource}s by using
@racket[define-resource], those resources are written into the current
working directory, if they do not already exist there.
@subsection{@tt{get-javascript}}
Given the name of a program, writes the JavaScript to standard output,
as well as its dependent modules. The outputted file is meant to be
used as a @tt{SCRIPT} source.
By default, the given program will be treated as a @emph{main} module.
All main modules will be executed when the JavaScript function
@tt{plt.runtime.invokeMains()} is called.
@subsection{@tt{get-runtime}}
Prints out the core runtime library that the files generated by
get-javascript depend on.
@section{Including external resources}
@defmodule/this-package[resource]
Programs may need to use external file @deftech{resource}s that aren't
themselves Racket programs, but instead some other kind of data.
Graphical programs will often use @filepath{.png}s, and web-related
programs @filepath{.html}s, for example. Whalesong provides the
@racketmodname/this-package[resource] library to refer and use these
external resources. When Whalesong compiles a program into a package,
these resources will be bundled alongside the JavaScript-compiled
output.
@defform[(define-resource id [path-string])]{
Defines a @tech{resource} with the given path name.
For example,
@codeblock|{
#lang planet dyoo/whalesong
(require (planet dyoo/whalesong/resource))
(define-resource my-whale-image-resource "humpback.png")
}|
}
As a convenience, you can also write
@codeblock|{
#lang planet dyoo/whalesong
(require (planet dyoo/whalesong/resource))
(define-resource humpback.png)
}|
which defines a variable named @racket[humpback.png] whose
@tech{resource} is @filepath{humpback.png}.
If the resource given has an extension one of the following:
@itemize[
@item{@filepath{.png}}
@item{@filepath{.gif}}
@item{@filepath{.jpg}}
@item{@filepath{.jpeg}}]
then it can be treated as an image for which @racket[image?] will be true.
If the resource has the extension @filepath{.html}, then it will be
run through an HTML purifying process to make sure the HTML is
well-formed.
@defproc[(resource? [x any]) boolean]{
Returns @racket[#t] if @racket[x] is a @tech{resource}.}
@defproc[(resource->url [a-resource resource?]) string?]{
Given a @tech{resource}, gets a URL.
For example,
@codeblock|{
#lang planet dyoo/whalesong
(require (planet dyoo/whalesong/resource)
(planet dyoo/whalesong/image))
(define-resource my-whale-image-resource "humpback.png")
(define WHALE-IMAGE
(bitmap/url (resource->url my-whale-image-resource)))
}|
}
@; Not done yet!
@;@defproc[(resource->input-port [a-resource resource?]) string?]{
@;Given a resource, gets an input-port of its contents.
@;}
@;@defform[(define-remote-resource id url-string])]{
@;Given a url, creates a remote resource. At the time of Whalesong compilation,
@;Whalesong will freeze a static copy of the file.
@;}
@section{The web-world API}
@defmodule/this-package[web-world]
The @tt{web-world} library allows you to write functional event-driven
@link["http://world.cs.brown.edu"]{World} programs for the web; the
user defines functional callbacks to handle events, and receive and
consume a world argument.
One difference introduced by the web is the web page itself: because
the page itself is a source of state, it too will be passed to
callbacks. This library presents a functional version of the DOM in
the form of a @tech{view}.
@margin-note{Visit @link["http://hashcollision.org/whalesong/examples/tick-tock/tick-tock.html"]{tick-tock.html} to execute this program.}
Let's demonstrate this by creating a basic ticker that counts on the
screen every second.
The first thing we can do is mock up a web page with a user interface, like this.
@filebox["index.html"]{
@verbatim|{
<html>
<head><title>My simple program</title></head>
<body>
<p>The current counter is: <span id="counter">fill-me-in</span></p>
</body>
</html>
}|
}
We can even look at this in a standard web browser.
Once we're happy with the statics of our program, we can inject dynamic behavior.
Write a file called @filepath{tick-tock.rkt} with the following content.
@filebox["tick-tock.rkt"]{
@codeblock|{
#lang planet dyoo/whalesong
(require (planet dyoo/whalesong/web-world)
(planet dyoo/whalesong/resource))
(define-resource index.html)
;; draw: world view -> view
(define (draw world dom)
(update-view-text (view-focus dom "counter") world))
;; tick: world view -> world
(define (tick world dom)
(add1 world))
;; stop?: world view -> boolean
(define (stop? world dom)
(> world 10))
(big-bang 0
(initial-view index.html)
(to-draw draw)
(on-tick tick 1)
(stop-when stop?))
}|
}
Several things are happening here.
@itemize[
@item{We @racket[require] a few libraries to get us some additional
behavior; in particular, @racketmodname/this-package[web-world] to let
us write event-driven web-based programs, and @racketmodname/this-package[resource]
to give us access to external @tech{resource}s.}
@item{We use @racket[define-resource] to refer to external files, like @filepath{index.html} that
we'd like to include in our program.}
@item{We use @racket[big-bang] to start up a computation that
responses to events. In this example, that's clock ticks introduced
by @racket[on-tick], though because we're on the web, we can
bind to many other kinds of web events (by using @racket[view-bind]).}
]
@subsection{@racket[big-bang] and its options}
@declare-exporting/this-package[web-world]
@defproc[(big-bang [w world]
[h big-bang-handler] ...) world]{
Start a big bang computation. The @racket[big-bang] consumes an initial world,
as well as several handlers to configure it, described next:
}
@defproc[(initial-view [x any]) big-bang-handler]{
Provide an initial view for the big-bang. Normally, @racket[x] will be a @tech{resource}
to a web page.
@codeblock|{
...
(define-resource page1.html)
...
(big-bang ...
(initial-view page1.html))
}|
@racket[initial-view] should only be used in the lexical context of a @racket[big-bang].
}
@defproc[(stop-when [stop? ([w world] [dom view] -> boolean)]) big-bang-handler]{
Tells @racket[big-bang] when to stop.
@codeblock|{
...
(define-struct world (given expected))
...
;; stop?: world view -> boolean
(define (stop? world dom)
(string=? (world-given world) (world-expected world)))
(big-bang ...
(stop-when stop?))
}|
@racket[stop-when] should only be used in the lexical context of a @racket[big-bang].
}
@defproc*[(((on-tick [tick-f ([w world] [v view] [e event]? -> world)] [delay real]) big-bang-handler)
((on-tick [tick-f ([w world] [v view] [e event]? -> world)]) big-bang-handler))]{
Tells @racket[big-bang] to update the world during clock ticks.
By default, this will send a clock tick 28 times a second, but if
given @racket[delay], it will use that instead.
@codeblock|{
...
;; tick: world dom -> world
(define (tick world view)
(add1 world))
(big-bang ...
(on-tick tick 5)) ;; tick every five seconds
}|
@racket[on-tick] should only be used in the lexical context of a @racket[big-bang].
}
@defproc[(on-mock-location-change [location-f ([w world] [v view] [e event]? -> world)]) big-bang-handler]{
Tells @racket[big-bang] to update the world during simulated movement.
During the extent of a big-bang, a form widget will appear in the
@tt{document.body} to allow us to manually send location-changing
events.
The optional @tech{event} argument will contain numbers for
@racket["latitude"] and @racket["longitude"].
@codeblock|{
...
;; move: world view event -> world
(define (move world dom event)
(list (event-ref event "latitude")
(event-ref event "longitude")))
...
(big-bang ...
(on-mock-location-change move))
}|
@racket[on-mock-location-change] should only be used in the lexical context of a @racket[big-bang].
}
@defproc[(on-location-change [location-f ([w world] [v view] [e event]? -> world)]) big-bang-handler]{
Tells @racket[big-bang] to update when the location changes, as
received by the
@link["http://dev.w3.org/geo/api/spec-source.html"]{Geolocation API}.
The optional @tech{event} argument will contain numbers for
@racket["latitude"] and @racket["longitude"].
@codeblock|{
...
;; move: world view event -> world
(define (move world dom event)
(list (event-ref event "latitude")
(event-ref event "longitude")))
...
(big-bang ...
(on-location-change move))
}|
@racket[on-location-change] should only be used in the lexical context of a @racket[big-bang].
}
@defproc[(to-draw [draw-f ([w world] [v view] -> view)]) big-bang-handler]{
Tells @racket[big-bang] how to update the rendering of the world. The draw
function will be called every time an event occurs.
@codeblock|{
...
(define-struct world (name age))
;; draw: world view -> view
(define (draw world dom)
(update-view-text (view-focus dom "name-span")
(world-name world)))
...
(big-bang ...
(to-draw draw))
}|
@racket[to-draw] should only be used in the lexical context of a @racket[big-bang].
}
@subsection{Views}
@declare-exporting/this-package[web-world]
A @deftech{view} is a functional representation of the browser DOM
tree. A view is always focused on an element, and the functions in
this subsection show how to traverse and manipulate the view.
@defproc[(->view [x any]) view]{
Coerse a value into a view whose focus is on the topmost element.
Common values for @racket[x] include @tech{resource}s.
}
@defproc[(view-focus? [v view] [id String]) boolean]{
Return true if the view can be focused using the given id.
}
@defproc[(view-focus [v view] [id String]) view]{
Focuses the view on an element, given the @racket[id]. The view
will be searched starting from the toplevelmost node.
}
@defproc[(view-left? [v view]) boolean]{
See if the view can be moved to the previous sibling.
}
@defproc[(view-left [v view]) view]{
Move the focus to the previous sibling.
}
@defproc[(view-right? [v view]) boolean]{
See if the view can be moved to the next sibling.
}
@defproc[(view-right [v view]) view]{
Move the focus to the next sibling.}
@defproc[(view-up? [v view]) boolean]{
See if the view can be moved to the parent.
}
@defproc[(view-up [v view]) view]{
Move the focus to the parent.}
@defproc[(view-down? [v view]) boolean]{
See if the view can be moved to the first child.
}
@defproc[(view-down [v view]) view]{
Move the view to the first child.}
@defproc[(view-forward? [v view]) boolean]{
See if the view can be moved forward.}
@defproc[(view-forward [v view]) view]{
Move the view forward, assuming a pre-order traversal.
}
@defproc[(view-backward? [v view]) boolean]{
See if the view can be moved backward.}
@defproc[(view-backward [v view]) view]{
Move the view backward, assuming a pre-order traversal.
}
@defproc[(view-text [v view]) string]{
Get the textual content at the focus.
}
@defproc[(update-view-text [v view] [s string]) view]{
Update the textual content at the focus.}
@defproc[(view-bind [v view] [type string] [world-updater ([w world] [v view] [e event]? -> world)]) view]{
Attach a world-updating event to the focus.
Attach a world-updating event to the focus. When the world-updater is
called, the view will be focused on the element that triggered the
event.
Common event types include @racket["click"], @racket["mouseenter"],
@racket["change"].} Note that the name of the event should not
include an @racket["on"] prefix.
A view may have many elements to bind, and it's a common pattern to
focus and view. As a convenience the API provides some syntactic support to
bind multiple handlers at once:
@defform[(view-bind-many a-view [id type world-updater] ...)]{
Composes the use of @racket[view-focus] and @racket[view-bind] to conveniently bind
multiple handlers at once.
Common event types include @racket["click"], @racket["mouseenter"], or
@racket["change"].} Note that the name of each event should not
include an @racket["on"] prefix.
As an example:
@codeblock|{
(define (click-handler w v) ...)
(define (change-handler w v) ...)
(define-resource index.html)
(define my-static-view (->view index.html))
(define connected-view
(view-bind-many my-static-view
["id1" "click" click-handler]
["id2" "click" click-handler]
["id3" "change" change-handler]))
...
}|
}
If the collection of ids, types, and handlers can't be represented as a static list, then
@racket[view-bind-many*] is an alternate helper function that may be helpful to bind
a bulk number of handlers to a view.
@defproc[(view-bind-many* [v view] [id+type+updater-list (listof (list string string world-updater))]) view]{
A functional version of @racket[view-bind-many]. Composes the use of
@racket[view-focus] and @racket[view-bind] to conveniently bind
multiple handlers at once.
Common event types include @racket["click"], @racket["mouseenter"], or
@racket["change"].} Note that the name of each event should not
include an @racket["on"] prefix.
As an example:
@codeblock|{
(define (click-handler w v) ...)
(define (change-handler w v) ...)
(define-resource index.html)
(define my-static-view (->view index.html))
(define connected-view
(view-bind-many* my-static-view
`(["id1" "click" ,click-handler]
["id2" "click" ,click-handler]
["id3" "change" ,change-handler])))
...
}|
}
@defproc[(view-show [v view]) view]{
Show the element at the focus.
}
@defproc[(view-hide [v view]) view]{
Hide the element at the focus.
}
@defproc[(view-attr [v view] [name String]) view]{
Get the attribute @racket[name] at the focus.
}
@defproc[(view-has-attr? [v view] [name String]) boolean]{