scribble/text as a generic templating language

haskal does a racket


side note: for a completely unrelated project i figured out how to create actually static racket executables -- that's here https://git.lain.faith/haskal/racket-static the basics for that is pick a libc that isn't weird about static binaries (musl uwu), build racket without --enable-shared so that it builds static libraries, use raco ctool to create a compiled blob of your application code with all its dependencies, and then create a small C launcher for your racket code that you statically link with libracket. this creates distributable binaries that are slightly more distributable than the stuff output by raco exe/raco dist -- which is still dynamically linked to libffi which isn't always present for example. a racket/base hello world bundled with libracket and all dependencies like this is like 6MB, which isn't terrible for bundling basically the entire state of utah into a single binary


onto the main point of this post

i was wondering if you could use scribble as a generic templating language, along the lines of the stuff people use for web applications. turns out you can

here's an example document using the at-exp syntax

# the @document-name

a @thing is haunting @geographic-region -- the @thing of @ideology
all the powers of old @geographic-region have entered into a holy alliance to exorcise this @thing

etc etc

two things result from this fact

@(define (itemize items)
   (add-between
     (map (lambda (item) @list{* @item}) items) "\n"))

@itemize[results]

the main task is to be able to define the bindings of this template separately. if you simply execute this template directly as scribble/text it'll of course complain that none of these variables are bound. we'd like to be able to provide the bindings in eg a hashmap at runtime, and be able to evaluate templates multiple times with different values for the bindings.

luckily racket provides access to the underlying constructs used to look up bindings. because of course it does. this comes in the form of (namespaces)[https://docs.racket-lang.org/guide/mk-namespace.html]. importantly, we can create new namespaces, alter the bindings of namespaces, and evaluate code under the context of the new namespace. this helps us because we can dynamically create a namespace with all the template variable bindings we want, then evaluate the template, and it will return a rendered result with the values of the variables we installed

the namespace is controlled by the current-namespace parameter, and we can create new namespaces with the (make-*-namespace) functions

(parameterize ([current-namespace (make-base-namespace)])
   now the namespace is a new one we just created)

one important thing to remember is that any custom namespace manipulation happens at runtime, not compile time. at compile time, your code is processed under the default namespace, which means anything that you make available in custom namespaces you create won't actually be available yet. so for example

(define ns (make-base-namespace))
(parameterize ([current-namespace ns])
  (namespace-require 'racket/gui)
  (message-box "hello" "hello world"))

this doesn't work, because the message-box binding doesn't exist at compile time. you need to wrap in eval (or dynamic-require, or anything similar that performs expansion at runtime)

(define ns (make-base-namespace))
(parameterize ([current-namespace ns])
  (namespace-require 'racket/gui)
  (eval '(message-box "hello" "hello world")))

we can also share stuff between the default namespace and the new namespace. this avoids creating duplicate copies of require'd modules, which is the default behavior. namespace-attach-module links a module in one namespace into another (note: you still need to then call namespace-require as well)

(require scribble/text)
(define ns (make-base-namespace))
(namespace-attach-module (current-namespace) 'scribble/text ns)
(parameterize ([current-namespace ns])
  (namespace-require 'scribble/text)
  ...)

now we can assemble a full templating system

(module templater racket/base
  (require (rename-in scribble/text/output [output scribble-output])
           scribble/text)
  (provide eval-template)

  ;; takes a filename and a hash of defined variables, outputs result to port
  (define (eval-template file vars [port (current-output-port)])
    (define cns (current-namespace))
    ;; include/text is like include but for scribble source read with the at-exp reader
    ;; it produces an s-exp that can be rendered to a port with output from scribble/text/output
    ;; first, set up the namespace and eval include/text
    (define output-exp
      (parameterize ([current-namespace (make-base-namespace)])
        (namespace-attach-module cns 'scribble/text)
        (namespace-require 'scribble/text)
        ;; here we update the namespace bindings to add each of the variables in the vars hash
        (hash-for-each vars namespace-set-variable-value!)
        ;; now, this will eval with our bindings present
        (eval '(include/text "template.rkt"))))
    ;; finally, write the result
    (scribble-output output-exp port)))

that's not too bad. i like how concise (hash-for-each vars namespace-set-variable-value!) is tbh (hash-for-each takes every entry of a hash and calls the provided function with the key and the value, so we can set all the namespace bindings in one line)

this can be used as

(require (submod "." templater))
(eval-template "template.rkt"
               (hash 'document-name "communist manifesto"
                     'thing "spectre"
                     'geographic-region "europe"
                     'ideology "communism"
                     'results '("communism is already acknowledged by all european powers to be itself a power"
                                "it is high time that communists should openly, in the face of the whole world, publish their views, their aims, their tendencies, and meet this nursery tale of the spectre of communism with a manifesto of the party itself")))

with the previous template, producing

# the communist manifesto

a spectre is haunting europe -- the spectre of communism
all the powers of old europe have entered into a holy alliance to exorcise this spectre

etc etc

two things result from this fact

* communism is already acknowledged by all european powers to be itself a power
* it is high time that communists should openly, in the face of the whole world, publish their views, their aims, their tendencies, and meet this nursery tale of the spectre of communism with a manifesto of the party itself

you might ask why not just like, use the latest cool curly brace templating framework in node dot js and like,
first of all
at-exp syntax is extremely powerful (i mean you can basically write arbitrary racket, which allows conditionals, loops, everything you might need without the constraints of a more restricted DSL like jekyll liquid or something.
and there's something kinda satisfying about solving this problem in a really abstract way, using the power of basically everything in racket being user-programmable. like we're not reading a template file and regexing for the templates in the file and replacing them with the values by hand, we're creating a custom binding namespace, uploading the template variables in there, and then evaluating the template as racket, letting racket bind and evaluate everything for us, with the full power of racket code available for use in the templates. and i think that's neat