5 The Imperative Workflow
The imperative workflow has a PHP-like development experience where you can "drop into" Racket code and use any #lang to write content to appear in place. The workflow accepts mixed-mode Markdown as input and writes HTML5 pages to dist-rel as output.
This approach is intuitive and works naturally with #langs that print data as a side-effect. The drawback is that this workflow does not scale well, and requires an increasingly noisy API to handle corner cases. If you want a leaner, more adaptable workflow with less room for error, use The Functional Workflow.
This is the default workflow for backwards-compatibility reasons, meaning that it applies when no workflow is specified when running polyglot commands.
5.1 Imperative App and Library Elements
Imperative app elements use The Printer in write mode to emit tagged X-Expressions. The zero or more written elements will replace the printing script element in the internal txexpr representing the page.
# Hello, world |
|
<script type="application/racket" id="main"> |
#lang racket/base |
(require racket/format racket/date) |
|
(write `(p ,(format "Today is ~a" (date->string (current-date))))) |
</script> |
Don’t use print or display unless you know what will happen. The imperative workflow uses read internally with the intent of building a list of tagged X-expressions.
To avoid confusion or unwanted output, either avoid using the printer in the top-level of library element code or capture any output produced as a side-effect of instantiating a library element’s module.
5.1.1 Setting a Page Layout
Application elements may use (provide layout) to set a layout for a page.
# Hello, world |
|
<script type="application/racket" id="main"> |
#lang racket/base |
|
(provide layout) |
(require racket/format racket/date) |
|
(write `(p ,(format "Today is ~a" (date->string (current-date))))) |
|
(define (layout kids) |
`(html (head (link ((rel "stylesheet") (href "styles.css"))) |
(title "My page")) |
(body . , kids))) |
</script> |
This produces:
<!DOCTYPE html> |
<html> |
<head> |
<link rel="stylesheet" href="styles.css" /> |
<title>My page</title> |
</head> |
<body> |
<h1>Hello, world</h1> |
<p>Today is Friday, September 20th, 2019</p> |
</body> |
</html> |
If multiple application elements in a page each provide a layout, the imperative workflow will use the last layout specified.
5.2 Page Macros and Preprocessing
Racket macros replace Racket code inside of application elements and libraries. However, they do not operate on the page containing the elements themselves.
Consider an application element that only sets the layout of the page.
I omit the <script> markup for brevity.
#lang racket/base (require "project/assets/layouts.rkt") (provide layout) (define layout (lambda (kids) (two-column "My Page Title" (nav-layout) kids)))
This is only 4 lines of code (6 if you count the <script> tags), but it must duplicate for every page. This is especially tedious if all you want to do is change the title.
A macro could expand to this pattern of require, provide, and define, but that macro has to come from somewhere. If you require that macro, the module path would still repeat across pages.
#lang racket/base (require "project/assets/macros.rkt") (layout (two-column "My Page Title" (nav-layout) kids))
If you bundle the macro as a binding in a new language, you still have the surrounding <script> markup eating up bytes and your precious writing time.
<script type="application/racket"> |
#lang layout |
(two-column "My Page Title" (nav-layout) kids) |
</script> |
To get around this, the imperative workflow can match and replace elements before processing script elements.
5.2.1 Replace elements using data-macro
Let’s change the above example to use an element with a data-macro attribute.
<meta itemprop="build-time" |
data-macro="set-layout" |
data-title="My Page Title"> |
Why <meta itemprop>?
HTML5 allows it in a <body>
It does not impact the appearance of a page when viewed by a human, but still adds meaning for a program.
meta is a void element, so you don’t have to type a closing tag.
I use a <meta itemprop> pattern here, but the element does not matter. The imperative workflow responds by trying to load assets/set-layout.rkt and running a provided replace-element procedure.
We’ll assume this implementation is handy:
"set-layout.rkt"
#lang racket/base (provide replace-element) (require txexpr) (define (replace-element target) (define page-title (attr-ref target 'data-title)) ; polyglot will add new line characters for us. ; NOTE: This is a LIST containing a script element! `((script ((type "application/racket")) "#lang racket/base" "(require \"project/assets/layouts.rkt\")" "(provide layout)" "(define (layout kids)" ,(format "(two-column ~e (nav-layout) kids))" page-title))))
Here, target is the entire <meta> element as a Tagged X-expression. replace-element will simply return the layout-defining application element to take its place.
Take careful note that replace-element is returning a list containing only a script element. Like application elements, these procedures can replace one element with many.
If you are dreading writing one file per macro, don’t worry. You can specify an expected provided identifier after the module name. This behaves the same way. replace-element is just a default identifier to seek if none is specified.
<meta itemprop="build-time" |
data-macro="set-layout replace-element" |
data-title="My Page Title"> |
5.2.2 Pre-processing
If you don’t want to use data-macro, you’ll need your own matching procedure.
Subclass polyglot/imperative% and override preprocess-txexprs.
(require polyglot txexpr) (define polyglot+preprocessor% (class* polyglot/imperative% () (super-new) (define/override (preprocess-txexprs txexprs) (for/list ([tx (in-list txexprs)]) (define-values (new-content _) (splitf-txexpr (λ (x) (and (txexpr? x) (equal? 'script) (equal? (attr-ref x 'type #f) "application/racket"))) (λ (x) `(script ((type "application/racket")) "#lang limited" ,@(filter (λ (s) (not (string-contains? s "#lang"))) (get-elements x)))))) new-content))))
This makes the imperative workflow replace all application elements with new application elements under a prescribed limited language. This can be help with untrusted code.
If you want to specialize the imperative workflow’s preprocessing and still leverage data-macro, call (super preprocess-txexprs txexprs) in your overriding method.