I am writing a little blog using the Racket webserver (requiring web-server/templates, web-server/servlet-env, web-server/servlet, web-server/dispatch
). Whenever I want to render a template, I do something this:
(define (render-homeworks-overview-page)
(let
([dates
(sort
(get-all-homework-dates)
#:key my-date->string
string<?)])
(include-template "templates/homework-overview.html")))
Defining a little procedure, to provide the template with all the necessary values, in this case dates
, which is then used inside the template. This works well so far, but I thought maybe I could get rid of the let
in all those render procedures, by putting it once into a more abstract render-template
procedure, which is then called by all the render procedures. Alternatively, the calls to this more abstract procedure could become so simple, that I don't need all the little render procedures anymore. I want to supply the values as keyword arguments and so far I got the following code:
(define render-template
(make-keyword-procedure
(lambda
(keywords keyword-args [content "<p>no content!</p>"] [template-path "template/base.html"])
(let
([content content])
(include-template template-path)))))
This would have a default value for the content displayed in the template and a default path for the template to render and take arbitrary keyword arguments, so that any render procedure could supply whatever is needed by a template by giving it as a keyword.
However, I cannot run this code, because there is an error:
include-at/relative-to/reader: not a pathname string, `file' form, or `lib' form for file
The template-path
in the call (include-template template-path)
is underlined red, to indicate that the error is there. However when I replace the template-path
with an ordinary string like so:
(define render-template
(make-keyword-procedure
(lambda
(keywords keyword-args [content "<p>no content!</p>"] [template-path "template/base.html"])
(let
([content content])
(include-template "templates/base.html")))))
The error does not occur. It seems that Racket somehow wants to ensure, that there is a valid path given to include-template
. But I want that to be a value given to the procedure. Otherwise I cannot write a procedure doing this job.
Also I want the values of the keywords provided to the procedure to be visible to the template. I am not sure, if that is automatically the case, or if I need to put a let
of some kind around the include-template
call, because I could not get the code to run yet, to test this.
How can I write such procedure?
As an example of an ideal procedure I'd like to have:
render_template
I can supply any keyword argument I wish and render any template I wish to render. I also do not really understand, why including something like "rm -rf /"
could damage anything. To me it seems the webserver should simply check if a file exists with that name. Obviously it will not exist, so throw an error. How should this ever lead to any unwanted damage? In consequence I do not understand the reasoning behind limiting what can be used as a path to a template to strings (except for workarounds). However, this might be too much for one SO question and should maybe put into another question about the "why" of things.
If you want to apply include-template
with a variable path argument, you can define a render procedure as:
(define (dynamic-include-template path)
(eval #`(include-template #,path)))
The procedure takes in any template path as argument and includes that template. For example, (dynamic-include-template "static.html")
would render static.html
.
This can be extended to accept any number of keywords and make them available within the template being rendered, as follows:
(define render-template
(make-keyword-procedure
(lambda (kws kw-args
[path "templates/base.html"]
[content "<p>no content!</p>"])
(for ([i kws]
[j kw-args])
(namespace-set-variable-value!
(string->symbol (keyword->string i)) j))
(namespace-set-variable-value! 'content content)
(dynamic-include-template path))))
Here, inside the for
block, keyword values with new identifiers are being set in the top-level environment of namespace using namespace-set-variable-value!, such that for a keyword and value parameter like (render-template ... #:foo 'bar)
, the corresponding identifier that is made available to the template becomes foo
(its @ Syntax
being @foo
), and its value becomes bar
.
For example, to render the homework-overview template, you can do:
(render-template "templates/homework-overview.html"
#:dates (sort (get-all-homework-dates) string<?))
then inside templates/homework-overview.html
you would have:
...
@dates
...
Take caution, however, when using eval
, and consider the following for relevant reads: