4

I'm new to using scribble, but I can't work out how to use its syntax in my own programs, rather than using a scribble language.

> (define ht (make-hash '(("Name" . "Simon"))))
> (define template "Hello @Name")
> (function-i-dont-know ht template)
"Hello Simon"

What is the function that I'm looking for? It must exist, but I can't find it in the documentation.

Richard Parsons
  • 277
  • 1
  • 9
  • I'm unclear on what exactly you want to do. You want to parse a string using Scribble's at-expression reader, and you want to use a hash-table as an environment for variable bindings? – Alexis King May 13 '15 at 06:58
  • Yes and yes. Although I could use something different than a hash-table as the environment if it were easier. Thanks for your reply. – Richard Parsons May 13 '15 at 07:07

3 Answers3

4

Add at-exp to use @-expressions in your language of choice.

#lang at-exp racket

(define x (random 5))
(define y (random 5))

@~a{@x + @y = @(+ x y)}

Output: "3 + 1 = 4"

soegaard
  • 30,661
  • 4
  • 57
  • 106
  • Although the output is correct, aren't I right in thinking that this displays a string on the screen, rather than returning a string? – Richard Parsons May 13 '15 at 09:01
  • 1
    It's probably useful to mention that Racket sees the last expression in exactly the same was as `(~a x " + " y " = " (+ x y))`. Especially since `~a` already looks like some perl-ish-style magice for people who are not used to Lispish identifier names. – Eli Barzilay May 14 '15 at 22:02
3

The answer that @soegaard gave is really complete enough, but for the sake of people who will look for just a more common-looking template system, here's one way to do that.

The main thing is to remember that @-forms are just another way of writing Racket code, so we're really looking for a general way of replacing names based on a given hash-table. (Since Racket has tons of ways to do that, there are tons of ways to do that with @-forms.)

This one uses a lookup function L that looks up values in a hash table that is kept in a paremeter. Since this parameter is "live" only when rendering the text, it actually produces thunks to delay the lookup until the text is rendered. (I modified the hash table a bit to hold symbols for more convenient keys.) It uses the output function from scribble/text to produce the result allowing many kinds of values in the template (like nested lists). For the same reason, there is no need to try and use a string for the result, it's just a list of things. Then, with-output-to-string is used to collect the text into a string.

#lang at-exp racket
(require scribble/text)

(define current-replacements (make-parameter #f))
(define (L key) (λ() (hash-ref (current-replacements) key)))
(define (render-with-hash ht template)
  (parameterize ([current-replacements ht])
    (with-output-to-string (λ() (output template)))))

(define ht (make-hash '([Name . "Simon"])))
(define template @list{Hello @L['Name]})
(render-with-hash ht template) ; => "Hello Simon"

A slightly more convenient variation is to use a macro for L which makes the quoting redundant:

...
(define-syntax-rule (L key) (λ() (hash-ref (current-replacements) 'key)))
...
(define template @list{Hello @L[Name]})
...

... or, since {}s are just @-syntax for strings, go back to using string for the hash keys:

#lang at-exp racket
(require scribble/text)

(define current-replacements (make-parameter #f))
(define (L key) (λ() (hash-ref (current-replacements) key)))
(define (render-with-hash ht template)
  (parameterize ([current-replacements ht])
    (with-output-to-string (λ() (output template)))))

(define ht (make-hash '(["Name" . "Simon"])))
(define template @list{Hello @L{Name}})
(render-with-hash ht template) ; => "Hello Simon"

One caveat to keep in mind here is that the {}s can be a few strings, for example, if there's a newline in the textual content. If you want to deal with that, you can adjust the L function to accept multiple arguments and append them together, then normalize the spaces before the lookup is done:

#lang at-exp racket
(require scribble/text)

(define current-replacements (make-parameter #f))
(define (L . keys)
  (λ() (hash-ref (current-replacements)
                 (regexp-replace #px"\\s+" (string-append* keys) " "))))
(define (render-with-hash ht template)
  (parameterize ([current-replacements ht])
    (with-output-to-string (λ() (output template)))))

(define ht (make-hash '(["First Name" . "Simon"])))
(define template @list{Hello @L{First
                       Name}})
(render-with-hash ht template) ; => "Hello Simon"

One thing that is a bit awkward in all of these is the use of a parameter holding a hash table. Something like this is needed only when you don't know the keys that are used in advance. In most cases you do, and for that you can just use plain variables as arguments to the template that becomes a simple function:

#lang at-exp racket
(require scribble/text)

(define (template Name)
  @list{Hello @Name})
(with-output-to-string (λ() (output (template "Simon"))))
; => "Hello Simon"

One last side note: I used output in all of these things so you can have nested structures of things in the text. If all you need is just a bunch of strings, you can use string-append:

#lang at-exp racket
(define (template Name)
  @string-append{Hello @Name})
(template "Simon") ; => "Hello Simon"

Or, as in @soegaard's answer, use the ~a function, which is kind of a cheap version of output (into a string) that can append a bunch of string values (and displaying non-string values):

#lang at-exp racket
(define (template Name)
  @~a{Hello @Name})
(template "Simon") ; => "Hello Simon"
Eli Barzilay
  • 29,301
  • 3
  • 67
  • 110
1

First of all, understand that Scribble is nothing more than a front-end for Racket code. All that Scribble does is take some input and output executable Racket code.

This means that using the Scribble reader on your template string will just give you this:

"Hello" Name

Imagine that as plain Racket code. It is nothing more than the string literal "Hello" followed by a reference to a variable called Name. This result is then passed to the Racket compiler and compiled into executable code.

So again, Scribble is not a templating engine, it is a programming language. There is no concept of "substitution" like you describe because Scribble just blindly spits out code. You'd need to run this code in order to perform any sort of string substitution.


You can actually do what's described above in Racket by using the racket/sandbox module, which allows you to create self-contained, sandboxed evaluators.

(require racket/sandbox
         scribble/reader)

(define (scribble-eval-string input-str environment)
  (define eval (make-evaluator 'racket))
  (define input (read-inside (open-input-string input-str)))
  (for ([(k v) (in-hash environment)])
    (eval `(define ,(string->symbol k) ,v)))
  (string-append*
   (for/list ([expr (in-list input)])
     (eval `(#%expression ,expr)))))

This function does four things. First, it creates a clean evaluator for the racket language. Next, it reads the input using read-inside from scribble/reader, which reads the input in string-input mode and produces a list. On your input, the produced value would be '("Hello " Name).

Next, it needs to inject the variables from the hash table into the sandbox's environment. This is done by manually evaluating a set of define forms for each key/value pair in the hash table. Finally, it evaluates each element of the input list as an expression, then concatenates the results into a single string.

With all this in place, you can do this:

(define environment (make-hash '(("Name" . "Simon"))))
(define input "Hello @Name")

> (scribble-eval-string input environment)
"Hello Simon"

Is this a good idea? Probably not. Since Scribble is a programming language, you're effectively compiling an entire program on the fly, then executing it. If any of the data comes from a user, you've introduced a huge security hole in your program.

If you just need dumb string replacement, just use format or something similar. However, if you really need the full power of Scribble, you can do something like this to make it available to you.

Alexis King
  • 43,109
  • 15
  • 131
  • 205
  • Thank you so much for your thorough and clear answer. – Richard Parsons May 13 '15 at 07:24
  • "Is this a good idea? Probably not." I think that "at-syntax" is the template engine used by [Racket's own webserver](http://docs.racket-lang.org/web-server/templates.html). You're right though. A template engine is what I'm looking for. It just attracts me having the entirety of Racket available to me within templates. I'm writing my own templates and data, so there's no external security risk from that point of view. – Richard Parsons May 13 '15 at 14:29
  • @Alexis: This is a pretty bad answer for several reasons. First, the Scribble @-form syntax doesn't "produce" any code more than s-expressions do: it's not that kind of a concrete macro too. It's much more accurate, and probably much more helpful to refer to it as just an alternative syntax for expressions. Also, you go on a huge tangent of how to (ab)use a sandbox to implement runtime-defined bindings, which is just begging to confuse newcomers... (Not to mention that if you really want that, then just use namespace functions...) – Eli Barzilay May 14 '15 at 21:58
  • @RichardParsons: You should look at @soegaard's answer, and keep in mind that using the @-forms can be used as a kind of a templating engine, but it's actually much more. Instead of a "stupid" template with holes to fill in with values, the template is actually any piece of code, only using a convenient syntax for strings (which could even be the whole file in the case of a `scribble/text` or `scribble/html` module). The fact that it's just code should help in working with these files, and realize that you have a whole language not just hole-plugging... – Eli Barzilay May 14 '15 at 22:01
  • @EliBarzilay Yes, it's a poor answer if you modify the OP's requirements slightly (which, to be fair, is totally reasonable). This is probably something of an XY problem, but I decided, partially just for fun, to answer precisely the question that was asked, which was to build a string *using a hash table as the environment* for bindings. You can't do that with `#lang at-exp`. But yes, soegaard's answer is probably more useful. – Alexis King May 14 '15 at 23:00
  • @AlexisKing: The thing is that it's yet another place where people can see the `eval` cannon, so yet another chance of blowing foot-holes... I'll add an answer that follows the requirements more closely than @soegaard's answer now. – Eli Barzilay May 15 '15 at 01:51
  • @EliBarzilay Preventing people from blowing holes in their feet isn't my responsibility so long as I provide adequate disclaimer. If people really want to shoot themselves, they'll do it. (That said, having soegaard's answer be the accepted one would probably be better here.) – Alexis King May 15 '15 at 01:54
  • @AlexisKing: I once had that opinion, but I changed into being more active in hiding it. Since Racket is much more popular now, this is even more needed than ever: there's a higher chance of one-foot people giving up on the whole thing. – Eli Barzilay May 15 '15 at 02:34