Search code examples
templatesservletsracketprocedurekeyword-argument

Racket webserver/templates include-template cannot be used with variable


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:

  • Jinja2's 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.


Solution

  • 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: