The SUrflet server enables you to write server side scripted web programs in Scheme. There are lots of example files in scheme/httpd/surflet/webserver/root/surflets from which you can copy freely.
This howto gives a short introduction in how to write a SUrflet. It is concentrated on the practical side rather on describing the SUrflet API in detail to give you instant succes in running your own surflets. See section 6.2 for the (technical) API description.
For those who don't know it already, SUrflets are pieces of code that can be executed interactively through a website. There is a SUrflet handler who administrates their execution and suspension. The SUrflet handler is part of the SUnet webserver. SUrflets ease the implementation of web applications in two ways, compared to other server-side scripting tools like JavaTMServlets or Microsoft®Active Server Pages or PHP:
SUrflets have an automatic program flow control like any other usual program (but unlike usual web programs), i.e.the web designer doesn't have to care about session management at all. The sequence of the web pages result from their appearance in the program like the print statements in any other usual program.
SUrflets come along with a library for robust user interaction. SUrflets represent interaction elements of the web page like text input fields or dropdown lists in the SUrflet program by specific objects. A web designer can plug in these objects into a website and use them to read out the user input.
The following sections probably assume that you have basic knowledge of the SUnet webserver and scsh. The environment variable $sunet refers to the top level directory of your sunet installation. On my system this is /home/andreas/sw/sunet.
The following sections will show pieces of SUrflet code you might want to try out. Therefore you need the SUnet webserver running with the ability to serve SUrflets. This section tells you how to do it.
You need Oleg's SSAX package (for scsh), to be able to use surflets:
Download Oleg's SSAX package from http://prdownloads.sf.net/ssax/ssax-sr5rs-plt200-4.9.tar.gz?download.
Download the SSAX package kit from http://lamp.epfl.ch/~schinz/scsh_packages.
Uncompress and untar both tarballs in the same directory. This will create a directory called SSAX, to which I will refer to as $SSAX. The package kit will add a file pkg-def.scm to the SSAX directory.
Install SSAX as a scsh package by issuing the command scsh-install-pkg -prefix /path/to/your/package/root in the $SSAX directory. If you don't yet have the packaging utility of Michel Schinz, you can obtain it from http://lamp.epfl.ch/~schinz/scsh_packages.
If you don't want to install SSAX with the packaging utility, you can adjust the scripts to directly load the SSAX package definitions from $SSAX/lib/packages.scm. Note that the original file has a typo which you can correct with
cd $SSAX
patch -p1 < $sunet/httpd/surflets/SSAX-goodhtml-patch
You can start the SUnet webserver along with the SUrflet-handler now. The SUnet distribution comes with a script that does this for you:
$ /path/to/your/package/root/sunet/web-server/start-surflet-server
Please be patient, scsh has to load a lot of libraries. If the loading succeeds you will see something like this:
[andreas@hgt web-server]$ ./start-surflet-server Loading... reading options: () Going to run SUrflet server with: htdocs-dir: /home/andreas/bin/lib/scsh/0.6/sunet-2.1/web-server/root/htdocs surflet-dir: /home/andreas/bin/lib/scsh/0.6/sunet-2.1/web-server/root/surflets images-dir: /home/andreas/bin/lib/scsh/0.6/sunet-2.1/web-server/root/img port: 8080 log-file-name: /tmp/httpd.log a maximum of 5 simultaneous requests, syslogging activated, and home-dir-handler (public_html) activated. NOTE: This is the SUrflet server. It does not support cgi.
This means the server is up and running. Try to connect to http://localhost:8080 with your browser and you will see the welcome page of the SUnet server. There's a link to the SUrflets homepage. You can also already try out some of the SUrflets that come with the distribution.
You will probably notice a long response time the first time you load the first SUrflet. This is because the server has to load the SUrflet libraries. The server handles further requests to SUrflets faster.
If the port the SUrflet server tries to use is occupied, you will see an error message similar to this one:
Error: 98
"Address already in use"
#Procedure 11701 (%bind in scsh-level-0)
4
2
(0 . 8080)
In this case, pass another port number to the script, e.g.8000:
start-surflet-server -p 8000
The -help option will show you more parameters that you can adjust, but you won't need them for this howto.
This section will discuss some of the various ways in which you can send a web page to a browser that contacted your SUrflet.
Traditionally, your first program in any programming language prints something like ``Hello, World!''. We follow this tradition:
(define-structure surflet surflet-interface
(open surflets
scheme-with-scsh)
(begin
(define (main req)
(send-html/finish
'(html (body (h1 "Hello, world!")))))
))
You can either save a file with that content in the SUrflets directory the server mentioned at startup or you can use the file howto/hello.scm that comes along with the SUrflets distribution and which is located in the server's standard SUrflets directory. Let's go through the small script step by step:
(define-structure surflet surflet-interface
This defines a module named surflet which implements the interface surflet-interface. surflet-interface just states that the module exports a function named main to which we will come shortly. For those of you who know about the scsh module system: Yes, SUrflets are basically scsh modules that are loaded dynamically during run time.
(open surflets
scheme-with-scsh)
The open form lists all the modules the SUrflet needs. You will probably always need the two modules that are stated here (namely surflets and scheme-with-scsh). If you need other modules, like srfi-13 for string manipulation, this is the place where you want to state it.
(begin
This just opens the body of the SUrflet. All your SUrflet code goes here.5
(define (main req)
Here is the main function that the interface declared this SUrflet will implement. The main function is the entry point to your SUrflet: The server calls this function every time a user browses to your SUrflet the first time. The server calls main with one argument: a representation of the inital request of the browser. We don't have to worry about that at this point.
(send-html/finish
'(html (body (h1 "Hello, world!")))))
))
send-html/finish is one of three function you will regularly use to send web pages to the browser. The other two functions are send-html and send-html/suspend. send-html/finish - as the name already suggests - sends a HTML page to the browser and finishes the SUrflet. send-html just sends the HTML page and does not return. send-html/suspend sends the HTML page and suspends the SUrflet, i.e.it waits until the user continues with the SUrflet, e.g.by submitting a webform. We will discuss send-html and send-html/suspend in detail later. I will refer to these three functions as the sending functions.
In a SUrflet, HTML pages are represented as lists, or, to be more precise, as SXML (S-expression based XML). The first element of a SXML list is a symbol stating the HTML tag. The other elements of a SXML list are the contents that are enclosed by this HTML tag. The contents can be other SXML lists, too. Here are some examples of SXML lists and how they translate to HTML:
| SXML: | '(p "A paragraph.")} |
| HTML: | <p>A paragraph.\htmltag{/p}} |
| SXML: | '(p "A paragraph." (br) "With break line.")} |
| HTML: | <p>A paragraph.<br>With break line.</p>} |
| SXML: | '(p "Nested" (p "paragraphs"))} |
| HTML: | <p>Nested<p>paragraphs</p></p>} |
Attributes are stated by a special list whose first element is the at-symbol. The attribute list must be the second element in the list:
| SXML: | '(a (@ (href "attr.html")) "Attributed HTML tags.") |
| HTML: | <a href="attr.html">Attributed HTML tags.</a> |
| SXML: | '(a (@ (href "attr2.html") (target "\_blank")) "2
attributes.")} |
| HTML: | <a href="attr2.html" target="\_blank">2 attributes.</a>}
|
As you see from the SUrflet example, send-html/finish expects SXML as an argument. In the example, the SXML translates to the following HTML code:
<html><body><h1>Hello, world!</h1> </body> </html>
Please note, that there is no check for valid HTML or even XHTML here. The only thing the translation process takes care of are special characters in strings like the ampersand (&). The translation process replaces them by their HTML representation (e.g., &) so you don't have to worry about that when you use strings. Everything else like using valid HTML tags or valid attributes is your responsibility.
Let's extend our first SUrflet example by some dynamic content, e.g.by displaying the current time using scsh's format-date function. As the HTML page is basically represented as a list, this can be done like this:
(define-structure surflet surflet-interface
(open surflets
scheme-with-scsh)
(begin
(define (main req)
(send-html/finish
`(html (body (h1 "Hello, world!")
(p "The current date and time is "
,(format-date "~H:~M:~S ~p ~m/~d/~Y"
(date)))))))
))
This SUrflet can be found in howto/hello-date.scm. The beginning of this SUrflet is the same as in the previous example. The difference lies in the argument to send-html/finish. Note that the argument starts with a backquote (`) rather than with a regular quote (') as in the previous example.
Instead of passing a ``static'' list, i.e.a list whose contents are given before execution, this SUrflet uses the quasiquote and unquote feature of Scheme to create a ``dynamic'' list, i.e.a list whose contents are given only during execution. A ``dynamic'' list is introduced by a backquote (`) and its dynamic contents are noted by commata (,). Thus, if the SUrflet is executed while I am writing this howto, the argument to send-html/finish above is translated to
'(html (body (h1 "Hello, world!")
(p "The current date and time is "
"13:09:03 PM 11/18/2003")))))
before it is passed to send-html/finish. Thus, using dynamic content can be easily done with Scheme's quasiquote and unquote feature. Of course, you can build your list in any way you want; the quasiquote notation is just a convenient way to do it.
The previous example SUrflets only showed one page and finished afterwards. Here, we want to present two web pages in a row. We use the previously mentioned function send-html/suspend, which suspends after it has sent the page and continues when the user clicked for the next page. In contrast to send-html/finish, that expected SXML, send-html/suspend expects a function that takes an argument and returns SXML. The parameter the function gets (here: k-url) is the URL that points to the next page:6
(define-structure surflet surflet-interface
(open surflets
scheme-with-scsh)
(begin
(define (main req)
(send-html/suspend
(lambda (k-url)
`(html (body (h1 "Hello, world!")
(p (a (@ (href ,k-url)) "Next page -->"))))))
(send-html/finish
'(html (body (h1 "Hello, again!")))))
))
This SUrflet can be found in howto/hello-twice.scm. This example first displays a web page with the message ``Hello, world!'' and a link to the next page labeled with ``Next page ->''. When the user clicks on the provided link, send-html/suspend returns and the next statement after the call to send-html/suspend is executed. Here it is send-html/finish which shows a web page with the message ``Hello, again!''.
When send-html/suspend returns, (almost) the complete context of the running SUrflet is restored. Thus, every variable in the SUrflet will retain its value during suspension. The consequence is that you don't have to worry about sessions, sesssion variables and alike. The user can freely use the back button of her browser or clone a window while the SUrflet will keep on responding in the expected way. This is all automatically managed by the SUrflet-handler.
The only exception are variables whose values are changed by side effects, e.g.if you change a variable via set!. These variables keep their modified values, allowing communication between sessions of the same SUrflet.7
So far I don't have mentioned too much details about sessions. The reason is, as mentioned before, that the SUrflet handler takes care of the session automatically as described in the previous paragraph.
The only thing you have to worry about is when your session ends. As long as your session hasn't been finished by send-html/finish, the user can move freely between the web pages your SUrflet provides. Once you've finished the session via send-html/finish, this freedom ends. As the session is over, the user will get an error message when he tries to recall some web page from the server. The server will tell the user about the possible reasons for the error (namely that most likely the session was finished) and provides a link to the beginning of a new session.
Thus, send-html/suspend suspends the current execution of a SUrflet, returning with the request for the next web page of your SUrflet and send/finish finishes the session. The third sending function is send-html which just sends a web page. send-html does not return and does not touch the session of your SUrflet instance.
The example in subsection ``Several web pages in a row'' wrote down the link to the next web page explicitly via the ``a''-tag. As websites contain a lot of links, the sending functions (like send-html/finish) allow an abbreviation. The following SXML snippets are equivalent:
(a (@ (href ,k-url)) "Next page -->") (url ,k-url "Next page -->")
url expects the target address as the next element and includes every text afterwards as part of the link.
There are also some other abbreviations. (nbsp) inserts ` ' into the HTML, (*COMMENT* ...) inserts a comment, and with (plain-html ...) you can insert arbitrary HTML code (i.e.strings) directly , without any string conversions. The last abbreviation, surflet-form, is discussed in the next section.
The SUrflets come along with a libary for easy user interaction. The following subsections will show how to write web forms and how to get the data the user has entered.
Let's write a SUrflet that reads user input and prints it out on the next page:
(define-structure surflet surflet-interface
(open surflets
scheme-with-scsh)
(begin
(define (main req)
(let* ((text-input (make-text-field))
(submit-button (make-submit-button))
(req (send-html/suspend
(lambda (k-url)
`(html
(body
(h1 "Echo")
(surflet-form ,k-url
(p "Please enter something:"
,text-input
,submit-button)))))))
(bindings (get-bindings req))
(user-input (input-field-value text-input bindings)))
(send-html/finish
`(html (body
(h1 "Echo result")
(p "You've entered: '" ,user-input "'."))))))
))
Here are the details to the code in main:
(define (main req)
(let* ((text-input (make-text-field))
(submit-button (make-submit-button))
make-text-field and make-submit-button define two user interaction elements: a text input field and a submit button. SUrflets represent user interaction elements by Input-field objects. Thus, user interaction elements are first class values in SUrflet, unlike in many other web scripting languages, e.g.Java surflets, PHP or Microsoft Active Server Pages, i.e.you have a representation of a user interaction element in your program that you can pass to functions, receive them as return values, etc. You'll soon see the advantages of this approach.
(req (send-html/suspend
(lambda (k-url)
`(html
(body
(h1 "Echo")
(surflet-form ,k-url
(p "Please enter something:"
,text-input
,submit-button)))))))
Instead of discarding the return value of send-html/suspend as in the examples of the previous section, this time we'll save the return value, as it will contain the data the user has entered in our text input field.
The definition of the website is as described in the previous section except for the new abbreviation surflet-form. surflet-form creates the HTML code for a web form and expects as its next value the URL to the next webpage as provided by send-html/suspend, here named k-url. The remaining arguments constitute the content of the web form. Thus, the code above is equal to the following SXML:
(form (@ (action ,k-url) (method "GET"))
(p "Please enter something:"
,text-input
,submit-button))
If you want to use the POST method instead of the default GET method, add the symbol 'POST after the URL:
(surflet-form ,k-url
POST
(p "Please enter something:"
,text-input
,submit-button))
The web page send-html/suspend sends to the browser looks like in figure [missing]. After the user has entered his data into the web form, send-html/suspend returns with the request object of the browser for the next page. This request object contains the data the user has entered.
(bindings (get-bindings req))
With the function get-bindings we pull out the user data of the request object. Here we save the user data into the variable bindings. get-bindings works for both request methods GET and POST.
(user-input (input-field-value text-input bindings)))
With the function input-field-value and the extracted user data we can read the value for an input-field. Here, we want to know what the user has entered into the text-input-field.
(send-html/finish
`(html (body
(h1 "Echo result")
(p "You've entered: '" ,user-input "'."))))))
After we have extracted what the user has entered into the text field, we can show the final page of our SUrflet and echo her input.
Thus, the scheme for user interaction is about the following:
Create the user interaction elements, input-fields, you want to use in your web page.
Send the web page with send-html/suspend to the browser. Plug in the input-fields in the web page as if they were usual values. Save the return value of send-html/suspend.
Extract the user data from the return value of send-html/suspend.
Read the values of each input-field out of the extracted user data with input-field-value.
The complete list of functions that create input-fields can be found in the API in section 6.2.
As the user interaction elements are first class values in a SUrflet, they can return other types than strings. For example the SUrflets come with a number input field, i.e.an input field that accepts only text that can be interpreted as a number. If the user enters something that is not a number, input-field-value will return #fas the value of the number input field. If you'd rather want an error to be raised, you can use raw-input-field-value instead.
The return value of an input field need not even be a primitive value. The SUrflets library allows you to ``annotate'' your input fields with values which should be returned indicated by the user's input. E.g., consider this SUrflet:
(define-structure surflet surflet-interface
(open surflets
handle-fatal-error
scheme-with-scsh)
(begin
(define (main req)
(let* ((select-input-field
(make-select
(map make-annotated-select-option
'("Icecream" "Chocolate" "Candy")
'(1.5 2.0 0.5))))
(req (send-html/suspend
(lambda (k-url)
`(html
(head (title "Sweet Store"))
(body
(h1 "Your choice")
(surflet-form
,k-url
(p "Select the sweet you want:"
,select-input-field)
,(make-submit-button)))))))
(bindings (get-bindings req))
(price (input-field-value select-input-field
bindings)))
(send-html/finish
`(html (head (title "Receipt"))
(body
(h2 "Your receipt:")
(p "This costs you $" ,price "."))))))
))
Let's go through the important part of this SUrflet:
(let* ((select-input-field
(make-select
(map make-annotated-select-option
'("Icecream" "Chocolate" "Candy")
'(1.5 2.0 0.5))))
Here we define a select input field (a dropdown list). Instead of only providing a list of values that shall show up in the dropdown list and later examining which one was selected and looking up the price for the sweet, we bind the values in the list with the price while we create the select input field. When the select input field is shown in the browser, it will show the names of the sweets. When we lookup the user's input, we will get the associated price for the sweet. Again, this works not only with numbers, but with any arbitrary Scheme value (e.g.functions or records).
If a user tries to forge a SUrflet-URL (e.g.by extracting the continuation URL from the HTML source and editing it), your SUrflet has to deal with unexpected values. Usually, a forged SUrflet-URL will result in an error that is raised in one of the SUrflet library functions. If you don't catch this error, the SUrflet handler will catch it for you, send an error message to the user and terminate the current session as your SUrflet obviously encountered an unexpected error and might be in an invalid state. If you don't want this behavior, you can catch this error (like any other error that is raised by scsh) and send your own error message with send-error which is located in the surflets/error package. The handle-fatal-error package can be useful in this context. Here's an example, that modifies the example from the previous subsection (modifications emphasized):
(define-structure surflet surflet-interface
(open surflets
handle-fatal-error
surflets/error
scheme-with-scsh)
(begin
(define (main req)
(let* ((select-input-field
(make-select
(map make-annotated-select-option
'("Icecream" "Chocolate" "Candy")
'(1.5 2.0 0.5))))
(req (send-html/suspend
(lambda (k-url)
`(html
(head (title "Sweet Store"))
(body
(h1 "Your choice")
(surflet-form
,k-url
(p "Select the sweet you want:"
,select-input-field)
,(make-submit-button)))))))
(bindings (get-bindings req))
(cost (with-fatal-error-handler
(lambda (condition decline)
(send-error (status-code bad-request)
req
"No such option or internal
error. Please try again."))
(raw-input-field-value select-input-field
bindings))))
(send-html/finish
`(html (head (title "Receipt"))
(body
(h2 "Your receipt:")
(p "This costs you $" ,cost "."))))))
))
Let's examine the important part of this example:
(cost (with-fatal-error-handler
(lambda (condition decline)
(send-error (status-code bad-request)
req
"No such option or internal
error. Please try again."))
(raw-input-field-value select-input-field
bindings))))
As mentioned in 6.1.4.2, this SUrflet uses raw-input-field-value instead of input-field-value because the former raises an error while the latter returns #f in case of an error.
If a user forges a continuation URL, raw-input-field-value might not be able to find a valid value for the select-input-field and raises an error. This error is catched by the error handler which was installed by with-fatal-error-handler. The error handler uses send-error to send an error message to the browser. Its first argument is the status code of the error message. See the documentation of the SUnet webserver for different status codes. The second argument is the request which was processed while the error occured. The last argument is a free text message to explain the cause of the error to the user.
While in the original SUrflet the user will still see the resulting receipt web page with an empty dollar amount and has her session finished, this modified version will show an error message and won't finish the session.
It is your choice, which version you choose, i.e.if you let the SUrflet handler handle the occuring error automatically or if you install your own error handlers and use raw-input-field-value. However, be careful if you use raw-input-field-value along with check boxes. The HTML standard dictates that an unchecked check box does not appear in the data the browser sends to the server. Thus, raw-input-field-value won't find the check box in the data and raise an error which is not a ``real'' error as you migh expect it.
The SUrflet library contains constructors for all input fields that are described in the HTML 2.0 standard. See the SUrflet API in section 6.2 for a complete list. The SUrflet library also allows you to create your own input fields, e.g.an input field that only accepts valid dates as its input. This subsection gives you a short overview how to do this. You will find the details in the SUrflet API.
Let's have a look at an SUrflet that uses its own input field. The ``input field'', called nibble input field, consists of four check boxes which represent bits of a nibble (half a byte). The value of the input field is the number that the check boxes represent. E.g., if the user checks the last two checkboxes, the value of the nibble input field is 3.
(define-structure surflet surflet-interface
(open surflets
surflets/my-input-fields
scheme-with-scsh)
(begin
(define (make-nibble-input-fields)
(let ((checkboxes (list (make-annotated-checkbox 8)
(make-annotated-checkbox 4)
(make-annotated-checkbox 2)
(make-annotated-checkbox 1))))
(make-multi-input-field
#f "nibble-input"
(lambda (input-field bindings)
(let loop ((sum 0)
(checkboxes checkboxes))
(if (null? checkboxes)
sum
(loop (+ sum (or (input-field-value (car checkboxes)
bindings)
0))
(cdr checkboxes)))))
'()
(lambda (ignore)
checkboxes))))
(define nibble-input-field (make-nibble-input-fields))
(define (main req)
(let* ((req (send-html/suspend
(lambda (new-url)
`(html (title "Nibble Input Widget")
(body
(h1 "Nibble Input Widget")
(p "Enter your nibble (msb left):")
(surflet-form ,new-url
,nibble-input-field
,(make-submit-button)))))))
(bindings (get-bindings req))
(number (input-field-value nibble-input-field bindings)))
(send-html
`(html (title "Result")
(body
(h2 "Result")
(p "You've entered " ,number "."))))))
))
Let's go through this SUrflet step by step.
(define-structure surflet surflet-interface
(open surflets
surflets/my-input-fields
scheme-with-scsh)
If you want to create your own input fields, you have to open the surflets/my-input-fields package.
(begin
(define (make-nibble-input-fields)
(let ((checkboxes (list (make-annotated-checkbox 8)
(make-annotated-checkbox 4)
(make-annotated-checkbox 2)
(make-annotated-checkbox 1))))
make-nibble-input-fields is the constructor for our new type of input field. As mentioned before, we use check boxes to let the user enter the nibble. We use annotated checkboxes for this purpose whose value is the value in the nibble.
(make-multi-input-field
#f "nibble-input"
The value of our new input field will depend on the value of several real input fields. Thus we create a multi input field. If the value depended only on the browser data that is associated to the name of our input field, we would use make-input-field instead, which creates a usual input field. E.g., if we wanted to create a date input field that accepts only valid dates as input and used a text input field for this purpose, we would use make-input-field.
The first two parameters is the name of the input field and its type. As we use checkboxes to represent our input field, we don't need the name field. The type field is meant for debugging purposes, so you can identify the type of the input field during a debugging session.
(lambda (input-field bindings)
(let loop ((sum 0)
(checkboxes checkboxes))
(if (null? checkboxes)
sum
(loop (+ sum (or (input-field-value (car checkboxes)
bindings)
0))
(cdr checkboxes)))))
The next parameter is the so called transformer function. raw-input-field-value calls the transformer function to determine the value of the input field depending on the given bindings. The transformer function of a multi input field (which our nibble input field is) gets the input field and the bindings as parameters. A usual input field would only get the data that is associated to its name.
The transformer function of our nibble input field goes over each check box, looks it up in the bindings and adds its value to a sum, if input-field-value can find it. If it can't find it, zero is added instead. The value of our nibble input field is the resulting sum.
The rest of the SUrflet is straight forward and not repeated here again. We create, use and evaluate the nibble input field as we do with every other input field.
With the techniques shown so far it is rather difficult to create a web page that has several different successor webpages rather than only one web page. This section will show you how to do this with the SUrflets. Basically, there are two different methods how to perform this task. One method is to mark each link in some way and evaluate the mark after send-html/suspend has returned. The other method is to bind a callback function to each link that is called when the user selects the link. This section shows both methods.
The basic idea of dispatching is to add a mark to a link and evaluate it after the user has clicked on a link and send-html/suspend returned. Let's have a look at an example. It shows an entry page at which the user states the language in which she wants to be greeted:
(define-structure surflet surflet-interface
(open surflets
scheme-with-scsh)
(begin
(define (main req)
(let* ((english (make-address))
(german (make-address))
(req (send-html/suspend
(lambda (k-url)
`(html
(head (title "Multi-lingual"))
(body
(h2 "Select your language:")
(ul
(li (url ,(english k-url) "English")
(li (url ,(german k-url) "Deutsch")))))))))
(bindings (get-bindings req)))
(case-returned-via bindings
((english) (result-page "Hello, how are you?"))
((german) (result-page "Hallo, wie geht es Ihnen?")))))
(define (result-page text)
(send-html/finish
`(html
(head (title "Greeting"))
(body
(h2 ,text)))))
))
Let's see how main presents the different options:
(define (main req)
(let* ((english (make-address))
(german (make-address))
Of course you don't have to worry about adding the mark to the links. Instead, we create the links with make-address.
(req (send-html/suspend
(lambda (k-url)
`(html
(head (title "Multi-lingual"))
(body
(h2 "Select your language:")
(ul
(li (url ,(english k-url) "English")
(li (url ,(german k-url) "Deutsch")))))))))
make-address returns a function you can call to create the link as we did here with
(li (url ,(english k-url) "English")
This creates a list item which contains a hyperlink labeled ``English''. The hyperlink is created by the SXML abbreviation url as shown in 6.1.3.5. Instead of just passing the continuation URL k-url to url, we create the marked link by calling the function make-adddress gave us.
(bindings (get-bindings req)))
(case-returned-via bindings
((english) (result-page "Hello, how are you?"))
((german) (result-page "Hallo, wie geht es Ihnen?")))))
After send-html/suspend has returned, we can evaluate which link the user has clicked by using case-returned-via. case-returned-via works similar to the regular case of Scheme. It evaluates the body of the form whose initial list contains the address that the user used to leave the website. E.g., if the user has selected ``German'' as her preferred language and clicked on the link we have named german in our SUrflet, case-returned-via will evaluate its second form and the SUrflet will display the greeting in German.
case-returned-via is syntactic sugar like the regular case. However, instead of equal? it uses returned-via. returned-via takes the bindings and and an address and returns #t, if the user left the web page via this address (i.e., via the link that is represented by this address) and #fotherwise. returned-via does not end with a question mark as it might return other values as well as we will see shortly. Of course, it is your choice if you want to use case-returned-via or explicitly returned-via.
The approach shown in the previous subsection has one major drawback: the meaning of an address becomes clear only when you look at the dispatching section of case-returned-via. This subsection shows you how to link the meaning and the representation of an address closer together.
We modify the previous code example slightly to this SUrflet (differences emphasized):
(define-structure surflet surflet-interface
(open surflets
scheme-with-scsh)
(begin
(define (main req)
(let* ((language (make-annotated-address))
(req (send-html/suspend
(lambda (k-url)
`(html
(head (title "Multi-lingual"))
(body
(h2 "Select your language:")
(ul
(li (url ,(language k-url
"Hello, how are you?")
"English")
(li (url ,(language k-url
"Hallo, wie geht es Ihnen?")
"Deutsch")))))))))
(bindings (get-bindings req)))
(case-returned-via bindings
((language) => result-page))))
(define (result-page text)
(send-html/finish
`(html
(head (title "Greeting"))
(body
(h2 ,text)))))
))
Let's look at the differing parts:
(let* ((language (make-annotated-address))
To link the meaning with the address itself, we use an annotated address. As we can annotate the address now, we don't need two distinct addresses anymore.
(li (url ,(language k-url
"Hello, how are you?")
"English")
(li (url ,(language k-url
"Hallo, wie geht es Ihnen?")
"Deutsch")))))))))
In addition to the continuation URL k-url we also annotate the address with a value. Here we use the different greetings as the annotation. The address can be annotated with any arbitrary Scheme value, e.g.functions or records.
(case-returned-via bindings
((language) => result-page))))
case-returned-via has an extended syntax similar to cond that it useful with annotated address. The arrow `=>' calls the following function with the annotation of the address via which the user has left the web page. You can extract the annotation yourself with returned-via like this:
(result-page (returned-via language bindings))
This will call result-page with the annotation of the address via which the user has left the web page. returned-via returns #f, if the user didn't leave the web page via one of the links created with this address (which is not really possible in this example).
The other method to lead to different successor web pages is using callbacks. A callback is a function that is called if the user leaves the web page via an associated link. This is different from the dispatch method where send-html/suspend returns. You can create a web page that only uses callbacks to lead to successor web pages and you don't have to use send-html/suspend. Instead, you can use send-html.
Although it is possible to use several different callbacks in a single web page, this is not recommended. The reason lies in the implementation of a callback, which saves the current continuation. Several different callbacks would result in the storage of the several slightly different continuations. This is unnecessary, as you can annotate the callbacks with the arguments for the callback function. Let's see an example which is a variation of the previous examples (important parts / differences emphasized):
(define-structure surflet surflet-interface
(open surflets
surflets/callbacks
scheme-with-scsh)
(begin
(define (main req)
(let ((language (make-annotated-callback result-page)))
(send-html
`(html
(head (title "Multi-lingual"))
(body
(h2 "Select your language:")
(ul
(li (url ,(language "Hello, how are you?")
"English")
(li (url ,(language "Hallo, wie geht es Ihnen?")
"Deutsch")))))))))
(define (result-page req text)
(send-html/finish
`(html
(head (title "Greeting"))
(body
(h2 ,text)))))
))
The differences to the dispatch method are the following: you have to open the surflets/callbacks package to use callbacks, you don't use the continuation URL to create the callback link, and the callbacked function must accept the request from the browser as the first argument. Furthermore, you don't have to use send-html/suspend, if a user can only leave your web page via callbacks. However, it can be sensible to combine the dispatch and the callback method, in which case you have to use send-html/suspend.
Note that is nonsensical to create a callback on top level, i.e.the call to make-annotated-callback must occur every time main is called and not only once when the SUrflet is read into memory. For the same reason it is nonsensical in most cases to reuse a callback.
The SUrflet library provides also a wrapper function with which you can instruct the callback to call different functions instead of a single one. If you create your callback like
(let ((callback (make-annotated-callback callback-function))) ...)
you can instruct the callback to call different functions like this:
(callback function1 arg1 arg2) ...
(callback function2 arg3 arg4 arg5)
Again, it is your choice which option you want to use. Note that calling a function with several arguments and of different amount each time is also possible if you only use a single function for the callback.
When you write web programs, there are usually two kinds of data that you use: data that is local to each instance of a SUrflet, e.g.the user's login, and data that is global to each instance of a SUrflet, e.g.a port to a logfile. Changes to local data is only visible to each session of a SUrflet, while changes to global data is visible to every session of a SUrflet.
The SUrflet library does not really distinguish between these two types of data, but provides ways to realize both of them in a convenient way that is not (really) different from the way you handle these data types in a regular Scheme program.
If a data item is globally used in your SUrflet, define it global (on top level) and change its values with set!. If a data item is locally used, define it locally (in your function) and do not change its value with set!.
If the following sounds too technical to you, you can safely skip this paragraph. The reason why the distinction between global and local data is done via whether you mutate the data's value with set! is that the SUrflets are implemented with continuations. Continuations cannot reflect changes that are done via set! (or side effects in general) and thus such changes are globally visible. On the other hand continuations represent states of a program and a reified continuations reifies also the values of all data.
But what to do if you happen to want to change your local data's value with set!? The SUrflet library provides a place where you store such mutable local data and two functions to access it: set-session-data! sets the mutable local data and get-session-data reads the mutable local data. Here is an example. It uses the callback technique that was presented in the previous section. If you haven't read that section, you only need to know that show-page is called again and again as long as the user keeps on clicking on ``Click''.
(define-structure surflet surflet-interface
(open surflets
surflets/callbacks
scheme-with-scsh)
(begin
(define (main req)
(set-session-data! -1)
(let ((start (make-annotated-callback show-page)))
(show-page req 'click start)))
(define (show-page req what callback)
(if (eq? what 'click)
(click callback)
(cancel)))
(define (click callback)
(set-session-data! (+ 1 (get-session-data)))
(send-html
`(html
(head (title "Click counter"))
(body
(h2 "Click or cancel")
(p "You've already clicked "
,(get-session-data)
" times.")
(p (url ,(callback 'click callback) "Click")
(url ,(callback 'cancel callback) "Cancel"))))))
(define (cancel)
(send-html/finish
`(html
(head (title "Click counter finished"))
(body
(h2 "Finished")
(p "after " ,(get-session-data) " clicks.")))))
))
At the beginning of main we initialize the mutable local data with set-session-data!.
(define (main req)
(set-session-data! -1)
(let ((start (make-annotated-callback show-page)))
(show-page req 'click start)))
Afterwards, we create and save a callback that will be called again and again. We call show-page with the callback to show the first web page.
(define (show-page req what callback)
(if (eq? what 'click)
(click callback)
(cancel)))
show-page evaluates its second argument what to determine what to do next: continue or cancel.
(define (click callback)
(set-session-data! (+ 1 (get-session-data)))
(send-html
`(html
(head (title "Click counter"))
(body
(h2 "Click or cancel")
(p "You've already clicked "
,(get-session-data)
" times.")
(p (url ,(callback 'click callback) "Click")
(url ,(callback 'cancel callback) "Cancel"))))))
If the user continues, click increases the mutable local counter and shows the next page.
Note that we don't use send-html/suspend here because we use the callback to lead to the next web page. If the user clicks on the link labeled with ``Click'' or ``Cancel'', show-page will be called with 'click or 'cancel, respectively, and the callback as parameters. This creates an endless loop without saving endless states of the SUrflet.
cancel shows the final page with the amount of clicks performed.
Section 6.1.3.1 introduced SXML, the way how SUrflets represent HTML. This section will show you, how you can create your own rules to translate from SXML to HTML.
This subsection will introduce the main concepts of the translation process and some necessary terms we will use in the following.
The translation process from SXML to HTML takes two steps. In the first step, SXML is translated to an intermediate form. This is done by the translator. In the second step, the intermediate form is printed into an HTML string. This is done by the printer. The intermediate form looks very much like SXML, but contains only atoms or, recursively, list of atoms. Atoms are numbers, characters, strings, #f, and the empty list. We call the intermediate form an atom tree and the list from which we've started an SXML tree.
The basic unit in the translation process is a conversion rule. A conversion rule consists of a trigger and a conversion function. As its first element, the trigger identifies the list for which the translator shall call the conversion function. The translator calls the conversion function with all list elements as parameters and replaces the whole list by the result of the conversion function. The result of the conversion function is supposed to be an atom tree.
The translator takes the SXML tree and a list of conversion rules as arguments. It then traverses the SXML tree depth first and calls the conversion functions according to the triggers it encounters, replacing the nodes in the SXML tree with the return values of each conversion function called. The result of this translation step will be an atom tree, which the printer will print into a string or port.
The translator calls the conversion function in two different modes, depending on the conversion rule. The regular mode is the preprocess mode: the translator translates every argument of the conversion function before calling it. The other mode is the unprocessed mode: the translator calls the conversion function directly without preprocessing the arguments. This is, the translator stops traversing the SXML tree at nodes that trigger a conversion rule in unprocessed mode.
There are two default triggers which you can't use in your translation rules: *default* and *text*. *default* as the trigger marks the default conversion rule which the translator uses if no other conversion rule triggers. *text* marks the text conversion rule and triggers, if the node in the SXML tree is a string. In the standard conversion rule set the text conversion rule performs HTML escaping, e.g.for the ampersand (&).
More to come soon about SUrflets consisting of different parts and individual SXML.
The SUrflet server comes with an extensive set of modules. This section describes the modules and the programming interfaces. See the howto section 6.1 for a practical guide. Note that most of the procedures mentioned here are meant to be called from within a SUrflet.
The SUrflet server provides basic procedures to send web content to a client. To enable the SUnet webserver to serve SUrflets, you have to add the SUrflet handler to it, which resides in the surflet-handler structure. E.g.:
(httpd (make-httpd-options ...
with-request-handler (alist-path-dispatcher (list (cons "surflet" (surflet-handler (with-surflet-path "web-server/root/surflets")))) (rooted-file-or-directory-handler "web-server/root/htdocs"))))
This will set up the SUrflet handler to handle requests directed to the directory /surflet/. The SUrflet handler can only handle requests directed to SUrflets. Here's the interface description:
This procedures sets up the SUrflet handler and returns the according request handler for the SUnet webserver. The options argument is similar to the one passed to httpd and is exlpained below.The SUrflet handler accepts requests (solely) to SUrflets whose file name must have the extension .scm. The structure of SUrflets is explained below. The SUrflet handler receives the request from httpd, translates it to a surflet-request and passes it to the requested SUrflet. The SUrflet in turn is expected to return a surflet-response, which the SUrflet handler translates to a repsonse for httpd. Thus, the SUrflet deals only with surflet-request and surflet-response objects. The structure of these objects and how they are passed around is explained below.
A SUrflet may also return a redirect response, which the SUrflet handler passes to the httpd untouched. See 2.3 for details.
The SUrflet handler calls the SUrflet wrapped into an error handler that catches any error the SUrflet may yield. In this case, it terminates the SUrflets session (see below for more on sessions) and returns an error response to httpd with the error code 500 "Internal Server Error" and a description of the error.
The options argument can be constructed in a similar way to the options argument of httpd. The procedures' names are of the form with-... and they all either create a new option or add a new parameter to a given option. The new parameter is always the first argument while the (old) option the optional second one. The following procedures reside in the surflet-handler/options structure.
This specifies the path in which the SUrflet handler looks for SUrflets. The surflet-path is a string. This option must be given for the handler to work.
This specifies the initial lifetime of a session. The lifetime of a session is the number of seconds the SUrflet handler waits for a response from a client for that session, before she automatically finishes it. See below for details on sessions. Defaults to 600, i.e.10 minutes.
This specifices whether the SUrflet handler caches SUrflets. The caching of SUrflets is a prerequisite for SUrflet wide global variables. See below for details on the scope of variables in SUrflets. Defaults to #t.
This specifies a procedure that generates the timeout text. The SUrflet handler displays the timeout text when she receives a request for a SUrflet session that does not exist, either because the SUrflet finished its session, the session has timed out, or the URL is illformed. The default is an English text with an explanation of the possible reasons and a link to a new session of the requested SUrflet. timeout-text-procedure accepts the string path to the SUrflet that was requested and returns a string.
Similar to the httpd options there exists a procedure to avoid parenthisis:
This constructs an options value from an argument list of parameter transformers and parameter values. The arguments come in pairs, each an option transformer from the list above, and a value for that parameter. make-surflet-options returns the resulting options value.
For example,
(surflet-handler (make-surflet-options with-surflet-path "root/surflets" with-session-timeout 3600))
defines the SUrflet handler to serve SUrflets from the directory root/surflets and to timeout unused sessions after one hour.
The SUrflet handler allows runtime read and write access to her options:
These procedures return the stored value for the respective option. See above for the description of the options.
These procedures change the respective option value. See above for the description of the arguments. Note that changing the surflet-path within a SUrflet may result in the SUrflets being unreachable. Turning the cache off will not empty the SUrflet cache.
Technically, SUrflets are Scheme48 structures, that have the name surflet and export a main procedure. The file in which their definition reside must have the extension .scm. The main procedure must accept the initial surflet-request as an argument. She may or may not return, but if she does, she must return either a surflet response or a redirect response. For example, this is a valid SUrflet definition:
(define-structure surflet surflet-interface
(open scheme-with-scsh
surflets)
(begin
(define (main req)
(send-html/finish
'(html (body (p "Hello world!")))))
))
surflet-interface is a predefined interface description that exports the main procedure. It is recommended to use this interface. surflets is a structure that combines the most commonly used structures to write SUrflets. send-html/finish is one of the procedures that sends HTML to the client. More on this below.
As SUrflets are Scheme48 structures, you can use all capabilities of the Scheme48 module language. See the documentation of Scheme48 for details.
SUrflets should not use the shift-reset structure, as this might confuse the SUrflet handler. The use of threads within a surflet is currently discouraged, as some procedures might not work, especially procedures dealing with session IDs.
Upon an initial client request, the SUrflet handler looks for the requested SUrflet, loads it dynamically, installs an error handler and calls the main function of the SUrflet with the initial surflet-request. To minimize the time of loading a SUrflet, the SUrflet handler caches the structure of the SUrflet in a cache, the SUrflet cache. As the SUrflet is cached, its global values will remain unchanged even through times when there are no active sessions of the SUrflet. Changing global SUrflet values is a possibility to exchange data between different sessions of the same SUrflet. Note that you have to take care to serialize the access to commonly shared, mutated data.
The SUrflet handler allows access to its cache via the following procedures. The access to these procedures is currently unrestricted but may be restricted in future versions of the SUrflet server.
This returns a list of the file names of the loaded SUrflets.
This removes the surflet from the SUrflet cache. The surflet is identified by its file name, as returned from get-loaded-surflets.
This empties the SUrflet cache.
Of course, when a SUrflet is removed from the cache, the values in its sessions remain untouched. However, if the SUrflet is newly loaded into the SUrflet cache, the SUrflet handler treats it like a new SUrflet, i.e.the sessions of the "old" and the "new" SUrflet (though physically the same) do not share their global data in any way.
SUrflets get their input from surflet-request objects. The relevant procedures are the following. They are all exported by the surflet-handler/requests alias surflet-requests structure.
The procedures inspect surflet-request values. Most of them
return the values of the underlying request object from
httpd. surflet-request? is a predicate for surflet
requests. surflet-request-method extracts the method of the
HTTP request; it's a string and either "GET" or "POST".
SUrflets won't receive requests with other methods.
surflet-request-input-port returns an input-port that contains
data from the client on POST requests and that the SUrflet can
safely read. If the request is no POST request, its value is
undefined. surflet-request-uri returns the escaped URI string
as read from the request line. surflet-request-url returns the
respective HTTP URL value (see the description of the url
structure in chapter 4). surflet-request-version
returns a (major . minor) integer pair representing the
version specified in the HTTP request. surflet-request-headers
returns an association lists of header field names and their values,
each represented by a list of strings, one for each line.
For some unknown weird cases, there are also these two procedures:
surflet-request-socket returns the socket connected to the client. As with requests, SUrflets should not perform I/O on this socket. See section 2.2 for reasoning. surflet-request-requst allows access to the underlying request object from httpd. Both procedures should not be necessary in normal operation and their usage is discouraged.
SUrflets answer to a request by sending a surflet-response to the SUrflet handler. The next section deals with how the surflet responses are sent to the SUrflet handler. The relevant procedures for surflet-response are the following. They are all exported by the surflet-handler/responses alias surflet-response structure.
This creates a surflet-response. status is the status code of the response. See section 2.3 for details on this. content-type is the MIME type of the data, e.g."text/html". headers is an association list of headers to be added to the response, each of which consists of a symbol representing the field name and a string representing the field value. data is the actual data. It must be either a string or a list of values that will be displayed.
This is a predicate on objects that may be surflet data, i.e.a string or a list (of objects that will be displayed).
The procedures return surflet-response values. surflet-response? is a predicate for surflet-responses. surflet-response-status returns the status code of the response. See section 2.3 for details on the status code. surflet-response-content-type returns the MIME type of the response. surflet-response-headers returns the association list of header field names and their values, each represented by a list of strings, one for each line. surflet-response-data returns the data of the surflet-response, the actual answer of the SUrflet.
A session is a set of web pages that logically belong together, e.g.a user surfing through her webmail. A session starts with the initial request for a SUrflet and ends either explicitly by the SUrflet, or implicitly after a timeout. A third, not so common case is its deletion from the session table.
The procedures presented in this subsection are all accessible via the surflets/sessions structure.
The SUrflet handler automatically manages the sessions for each SUrflet, thus the SUrflet does not have to deal with sessions or state control (as it is the case with most other programming interfaces for web applications). In particular, a SUrflet does not have to take care of saving the contents of variables before emitting a web page and restoring the values later upon the next request, or determining how far the user has proceeded in the application. The only thing a SUrflet may want to do is to tell the SUrflet handler when a session finished by calling send/finish or an equivalent procedure (see below).
Although a SUrflet does not have to deal with the management of the sessions, the SUrflet handler allows access to its management structures.
This returns the session ID for the current session. The current session is the session from which the function is called. The SUrflet handler guarantees that there won't be two sessions with the same ID for any given point in time. However, session IDs may be reused.
This returns the session for the given session ID.
This returns the complete list of all active session of the SUrflet handler. The list is an association list with the session-id as key and the session as value.The access to this procedure is currently unrestricted but may be restricted in future versions of the SUrflet server.
This deletes the specified session from the session table. Future requests to the session are answered with the timeout text.
This returns #t, if the specified session is alive, i.e.requests to it will be answered by the appropriate SUrflet. Otherwise, she returns #f.
These procedures inspect values of a session. session-surflet-name returns the name of the SUrflet for which the session was created. session-session-id returns the session ID of the session.
For each session, the SUrflet handler has a counter running. She resets the counter each time she receives a request for the session. When the counter reaches a particular number of seconds, the lifetime of the session, the SUrflet handler deletes the session and removes it from its session table. She will answer all future requests for the session with the timeout text. The following procedures deal with the lifetime of a session.
session-lifetime returns the number of seconds the SUrflet handler will initially wait before she automatically finishes the session. set-session-lifetime! changes the initial lifetime of the session to new-lifetime and also resets the counter for that session.
These reset the counter for the lifetime of either the given session (session-adjust-timeout!) or the current session (adjust-timeout!). Both procedures give the session a lifetime of either lifetime seconds or of the lifetime seconds stored for the according session.
In order to allow easy web programming, the SUrflet handler automatically saves and reifies continuations of a session. This is totally transparent to the web programmer. For adminstration purposes, the SUrflet handler offers access to the continuation table of a session via the following procedures.
These functions return the continuation table, the lock for the continuation table and the counter for the continuations, respectively. The continuation table is a hash table with the continuation ID as key and the continuation as value, based on the tables structure of Scheme48. The lock is based on the locks structure of Scheme48. The thread-safe-counter is based on the thread-safe-counter structure that is part of the SUrflets.The access to these functions is currently unrestricted but may be restricted in future versions of the SUrflet server.
The surflets/continuations structure also offers procedures to access the continuations.
Returns a list of all continuations of the session. The list elements are pairs with the car being the session and the cdr being the continuation.
Removes the specified continuation from the continuation table. session-continuation is a pair as returned from get-continuations. It is no error if the session or the continuation does not exist anymore.The access to this functions is currently unrestricted but may be restricted in future versions of the SUrflet server.
Returns the continuation ID of the continuation specified by session-continuation which is a pair as returned by get-continuations.
The surflets/ids structure provides procedures to determine the session and continuation IDs of the current session. See also the entry for resume-url-ids somewhere else in this document.
These return the session and continuation ID that where used to access the current session. The procedures work for every surflet-request except for the inital one that main gets. The values returned by my-ids are the session and the continuation ID in this order.
This returns the name of the SUrflet of the current session.
The SUrflet handler distinguishes three kinds of session data: session data that is local to a session of a SUrflet and not mutated, session data that is local to a session of a SUrflet and mutated and session data that is global to all sessions of a SUrflet. Every variable value that is never mutated is automatically local to the session of the SUrflet. Variable values that are mutated are automatically global to all sessions of the SUrflet. Values that have to be mutated but should be local to a session of the SUrflet must be stored in a special place, the session data field of the SUrflet handler.8
These procedures allow read/write access to the session data field of the SUrflet handler. The SUrflet handler installs a session data field for each session she creates. get-session-data reads the contents of this field, which is initially #f. set-session-data! sets the contents of the field to new-value. Mutations to the values, no matter when they occur, are local to the session from which the procedures are called, i.e.the changes are only visible within a particular session of the SUrflet. The procedures are exported by the surflet-handler/session-data structure.
The SUrflet communicates with the web client basically with the following send primitives. They are exported by the surflet-handler/primitives structure along with the procedures from surflet-handler/requests, surflet-handler/responses and the status-code syntax.
This procedure sends the data of the surflet-response to the client and does not return.
This procedure does the same as send, but it also finishes the session of the SUrflet. Future requests to the SUrflet will be answered with the timeout text (see above).
This procedure suspends the current computation of the SUrflet and calls surflet-response-maker with the continuation-URL of the current session to create the actual surflet-response. When a client requests the continuation-URL, the computation of the SUrflet will resume with send/suspend returning that request of the client. See 6.2.9 for details on continuation-URLs.
This sends an error response to the client. status-code is the status code of the error, see section 2.3 for details. surflet-request is the last surflet-request the SUrflet received; if unknown this argument may be #f. messages may contain some further information about the cause of the error.
Most of the time, a SUrflet won't send arbitrary data but HTML to the client. For this purpose, the SUrflets provide extensive support. First of all, they provide procedures specially designed for submitting HTML.
These are the equivalent procedures to the send primitives. send-html and send-html/finish accept an SXML object (more on that below), translate it into HTML and send it to the client, the latter finishing the session. send-html/suspend suspends the current computation, calls sxml-maker with the continuation-URL, translates the resulting SXML into HTML and sends it to the client. When the client requests the continuation-URL, the computation is resumed and send-html/suspend returns with the surflet-request.
For easy creation of HTML output, the send-html procedures mentioned above represent the HTML in SXML. SXML is a creation of Oleg Kiselyov. Basically, SXML is a list whose car is an SXML tag, a symbol representing the HTML tag, and the cdr are other SXML elements that will be enclosed by the HTML tag. For example,
'(h2 "Result")represents the tag h2, that encloses the text "Result". The represented HTML is
<h2>Result</h2>.
As in HTML, elements may be nested:
'(body (h2 "Result") (p "Example"))represents
<body><h2>Result</h2><p>Example</p>.
The @ symbol marks HTML attributes. The attributes follow the @ symbol in two element lists, the first element being the name of the attribute and the last its value. For example, this is a link:
'(a (@ (href "add-surflet.scm") (name "linklist")) "Make new calculation.")representing the following HTML
<a href="add-surflet.scm" name="linklist">Make new calculation.</a>.The attributes form will only be recognized as such if it is the second element of a list, right after the SXML tag.
These are procedures on SXML attribute forms. sxml-attribute? is a predicate for SXML attribute forms. It checks if object is a list whose first element is the symbol @. sxml-attribute-attributes returns the list of name-value-lists of the attributes form. Both procedures are exported by the surflets/sxml structure.
The translator translates list elements which are numbers and symbols
to their string representation (except for the first element, of
course). She scans strings for the special characters &,
", > and < and replaces them by their HTML
equivalents, and she ignores #f and the emtpy list. See below
the special SXML tag plain-html to see how to insert HTML code
untranslated. Furthermore, the translator accepts input-fields
as list elements, which are translated to their HTML representation.
See below for details on input fields.
Using lists to represent HTML allows the programmer to define operations on it. Most programmers construct their lists dynamically, often by using quasiquote (the symbol `) and unquote (the symbol ,). E.g.
`(html (title ,my-title)
(body (p "Hello, " ,(get-user-full-name))))
See below for how to create your own SXML.
The SXML to HTML translator accepts some special SXML tags that don't directly translate to an HTML tag.
Inserts a link to URL, named with text. text defaults to URL. Takes at least one argument. E.g.
(url "/" "Main menu") ===> <a href="/">Main menu</a> (url "go.html") ===> <a href="go.html">go.html</a>
Oops: url does not accept extra attributes for the `A' tag of HTML. This should be fixed in a future version.
Inserts the HTML sequence " ". Takes no arguments.
(plain-html html ...) SXML-tag
Inserts html without any changes, thus it works like a quote. Takes any number of arguments.
(*COMMENT* comment ...) SXML-tag
Inserts a comment, i.e.comment enclosed between<!--and-->. Takes any number of arguments.
(surflet-form k-url [method] [attributes] [SXML ...]) SXML-tag
Inserts HTML code for a web form. See below for details. k-url usually is a continuation-URL. method is the method to be used by the client to transfer the webform data. Possible values are the symbols GET, get, POST, post, the first two specifying the GET method, the last two the POST method. method defaults to the GET method. attributes are attributes for the created web form, e.g.(@ (enc-type "text/plain")). The remaining arguments are taken as SXML and translated as usually. Takes at least one argument. Note that the attributes form may come at position three.
The send-html procedures use a standard set of translation rules to translate from SXML to HTML. However, you may define your own set of translation rules or extend the given ones as you see fit. For this, a short introduction to the translation process.
The translation process takes place in two steps. Step one translates the given SXML to low level SXML, essentially a rough form of HTML in list notation. Step two takes this low level SXML and prints it to a port. Step one is performed by sxml->low-level-sxml, step two by display-low-level-sxml. All procedures and rules presented in this subsection are exported from surflets/sxml.
Takes an SXML object (which is essentially a list) and a list of SXML rules (more on this below) and translates it to low level SXML. This procedure is an alias to the pre-post-order procedure of Oleg Kiselyov's SSAX module. It is an error if no rule triggers (see below for when a rule triggers). However, it is no error if multiple rules trigger; the first rule in the rules list wins.
Takes low level SXML and displays it to a port. She traverses the list low-level-sxml depth-first, ignores the empty list and #f, executes thunks and displays all other elements, usually strings and characters, to port. Returns #t if she wrote anything, #f otherwise. This function is basically the SRV:send-reply procedure of Oleg Kiselyov's SSAX module.
Combines step one and two of the translation process and returns the resulting string, i.e.it calls display-low-level-sxml with the result of a call to sxml->low-level-sxml and a string port, returning the content of the string port.
An SXML-rule consists of a trigger, which is a symbol, and the handler, which is a translation procedure.
There are three types of rules, each of which is a dotted list:
There are two special triggers, who may trigger for all elements of the SXML, not only the first element of a list:
*text* triggers for atoms in the SXML list, i.e.usually strings and characters. The handler is called with the symbol *text* and the atom as arguments.
*default* triggers whenever no rule triggered, including *text*. If called for a list whose first element did not trigger a rule, the handler is called with the whole list. If called for an atom, the handler is called with the symbol *text* and the atom as arguments.
The surflets/sxml structure defines some basic rules:
These are the three basic rules exported by the surflets/sxml structure. default-rule creates the leading and trailing HTML tag and encloses the attributes. text-rule just inserts the given text with the special HTML characters&,",>and<escaped. attribute-rule triggers for the attributes form and creates attributes like selected or color="red".
The surflets/surflet-sxml add the rules for the special SXML tags to this list:
These are the rules for the special SXML tags mentioned above, namely url, nbsp, plain-html, *COMMENT* and surflet-form.
These are rule sets. default-rule contains the rulese default-rule, attribute-rule, text-rule, comment-rule, url-rule, plain-html-rule and nbsp-rule. surflet-sxml-rules extends this list by surflet-form-rule and a rule for input fields.
This uses the surflet-sxml-rules to translate sxml to low level SXML, performin step one of the translation process.
The continuation-URL represents the point in the computation of a session of a SUrflet where the computation was halted by the SUrflet handler. When a browser requests a continuation-URL, the SUrflet handler looks up the continuation in its tables and reifies it, allowing the session of the SUrflet to resume its computation.
The procedures to access the continuation-URL are the following. They are exported by the surflet-handler/resume-url structure. Sorry for the double naming resume-url and continuation-URL.
These inspect values of a resume url. resume-url? is a predicate for resume urls (the same as continuation urls). Note that it only operates on strings. resume-url-ids returns the session- and the continuation-id that is stored in the resume-url. resume-url-session-id and resume-url-continuation-id return only the session- or the continuation-id, respectively.
The SUrflets support all input fields defined for HTML 2.0 and allow the creation of own input fields. input-fields are first order values, that represent the actual input field of the web page in the SUrflet. For that, this documentation distinguishes the browser value from the Scheme value of an input field. The browser value is the string representation of the input field data the browser sends. The Scheme value is the value in the SUrflet the input field reports as its value, which may be of any type, not only strings.
Here is a short overview on how to use input fields. See also the howto for more informations. First, you create the input-field that represents the input field you want to use. Then you put this input-field into the SXML of the web page at the place the input field shall appear. After send-html/suspend has returned with the next surflet-request, you call get-bindings with that surlfet-request and collect the resulting bindings. Last, you call input-field-value (or raw-input-field-value) with your input-field and the collected bindings to get the Scheme representation of the value the user has entered. Here is a small example:
(define-structure surflets surflet-interface
(open scheme-with-scsh
surflets)
(begin
(define (main req)
(let* ((text-input (make-text-field))
(req (send-html/suspend
(lambda (k-url)
`(html
(body
(surflet-form
,k-url
(p "Enter some asd text: " ,text-input)
,(make-submit-button)))))))
(bindings (get-bindings req))
(text (input-field-value text-input bindings)))
(send-html/finish
`(html
(body
(p "You've entered `" ,text "'."))))))
))
The surflets/bindings structures exports the necessary functions to create bindings and extract values from them:
This returns an association list representing the data the browser has sent, usually the content of a webform. The name of the input fields are the keys, their browser values the values. The values are already unescaped. get-bindings can (currently) only handle application/x-www-form-urlencoded data. You can call get-bindings on both GET and POST requests, even multiple times (even on POST requests).
These extract values from the bindings as returned by get-bindings. key may be a string or a symbol which will be translated to a string before use. extract-bindings returns a list of all values from bindings whose key is key. extract-single-binding returns the value from the binding whose key is key and raises an error if there is more than one such binding. The two procedures are the same as in PLT's webserver.
get-bindings must acces the "Content-length" header field to handle POST requests. surflets/bindings also exports the procedure that does that job:
Returns the value of the "Content-length" header as a number, as present in headers, e.g.from surflet-request-headers. Will raise an error if there is no "Content-length" header or the header is illformed, e.g.contains no number.
The surflets/input-field-value structure provides the functions necessary to retrieve the Scheme value of input fields.
These extract the Scheme value of an input-field, given the bindings of the last request. Asking for a Scheme value may raise an error. Some error conditions are: the input field was not present in the bindings, the transformer could not generate a Scheme value for the browser value, or some other error occured in a maybe malfunctioning transformer. In any case, raw-input-field-value won't catch that error, while input-field-value will catch it and provide error-value as the input-field's Scheme value, which defaults to #f.
This returns the first binding in bindings that belongs to the given input-field (i.e.has input-field's name as key).
The procedures for the creation of the input fields mentioned in HTML 2.0 are the following. They are exported by the surflets/surflet-input-fields. Note that most of the time, you may omit any of the optional arguments, e.g.you may only specify some further attributes to make-text-field without specifying a default value. Keep in mind that input-field-value catches the error that may occur if an input-field is asked for its Scheme value and may return any (previously chosen) value instead.
These create various input field where the user types something in. default is the text or the number that the browser initially displays in the input field. attributes are some further attributes for the input field in SXML notation. make-text-field creates a regular text input field. Its Scheme value is a string. make-number-field creates a regular text input field, whose Scheme value is a number. It is an error if the input field does not contain a number. make-password-field creates a text input field that will display stars instead of the typed text. Its Scheme value is a string. make-textarea creates a possibly multi line text input field. rows specifies how many rows of the text the browser will display at once and defaults to 5. columns specifies how many columns the browser will display at once and defaults to 20. Note that if you only supply one number, it will be interpreted as the rows argument. readonly? is a boolean that tells the browser whether to disallow changes of the displayed text.
Creates a hidden input field, i.e.a input field that the browser won't display but whose value the browser will send. This input field is provided for completeness; you usually won't need it, as all values in your SUrflet will survive the emission of a web page. default is this value the browser will send. Note that although the argument is marked as optional you usually want to provide it. attributes are some further attributes for the input field in SXML notation.