cwal

whcl: tmpl
Login

whcl: tmpl

(⬑Table of Contents) (⬑API Index)

tmpl: Scriptable Text Templates

Jump to...

whcl.tmpl

The whcl.tmpl function processes "template-ish" text input and generates whcl script code from it. The intention is that it can be used to create text documents which have embedded whcl code, then process them with this function to generate whcl code which can be eval'd to output that processed page.

This API may be installed using:

whcl install-api tmpl

or calling whcl_install_tmpl() from C code.

It adds a whcl.tmpl object with the API described below.

Its usage is trivial:

decl code [whcl.tmpl $templateSource]; # buffer | string
assert 'Script' == [info type-name $code]

Sidebar: the C-level counterparts are whcl_tmpl_to_code() and whcl_cb_tmpl_to_code().

Template text need not come from a file, but here's a simple way to do so:

decl code [whcl.tmpl [whcl.Buffer.read-file $someInputTemplateFile]]

That converts the template text into script code and returns its value as a Script object. If tmpl is passed the -buffer flag as its first argument, it instead returns a buffer object which contains the tmpl-processed script code. The remainder of these docs assume that flag is not in use unless explicitly noted otherwise.

The generated script does not produce any output until it's run: tmpl just converts the template to a script.

tmpl documents are plain text (UTF-8) with two types of embedded tags: <?...?> blocks contain arbitrary script code and <%...%> provides a convenience form which simply passes the expression result of its contents on to the output routine (described later). Both sets of tags are configurable (also described later).

Here is an example document which demonstrates both tag types, <?...?> (a.k.a. "code blocks") and <%...%> (a.k.a. "expression blocks"):

<? /* Starts a "normal" code block. Such blocks get
      output verbatim into the generated script. */
  decl msg 'this is a demo of tmpl: script embedded in text docs';
  decl x 10
  decl y 12

  proc myfunc {} {return [argv.join ' ==> ']}

?>Hi, world! Our message to you is:
<% /* This tag wraps its contents in an expr and output call,
      such that what gets output is the evaluated result of
      this block's body. */
  $msg %>

x=<%$x%>, y=<%$y%>, x+y = <% $x + $y %>!
The tmpl function: <%whcl.tmpl%>
A list of numbers: <% [myfunc 1 2 3] %>
The function which generated that output: <% $myfunc %>

When evaluated, the output to the above input looks like:

Hi, world! Our message to you is:
this is a demo of tmpl: script embedded in text docs

x=10, y=12, x+y = 22!
The tmpl function: function@0x20129e0
A list of numbers: 1 ==> 2 ==> 3
The function which generated that output: function@0x2015450

Note that code blocks almost always need to end with an explicit newline or semicolon to avoid evaluation errors when the template is eval'd, but tmpl does not automatically inject them because doing so feels wrong somehow.

Documents may of course be HTML or some such, as long as (A) they do not use markup which could be confused for template tags (see below for how to use custom tags) and (B) are encoded in UTF-8. Note that whitespace outside of tags is retained when the tags are replaced, with one exception: if a template tag is the first non-space content of the document then the leading space before that tag is elided.

It is legal for a <? tag to be missing a closing ?> tag, in which case the rest of the document is considered to be inside that tag (this is how PHP does it, incidentally, and we do it for the same reason1). <% %> tags, on the other hand, require matching open/close tags, else an exception is thrown.

Evaling tmpl Scripts

There are two options for evaluating scripts:

eval -> $theScript
theScript run

They're almost equivalent: eval runs the code in the current scope, whereas the second approach requires a method call, which inherently brings with it a new scope (and eval -scope can be used to force a new scope). Even so, that particular method is configured to not block symbol lookup, so scripts can resolve symbols via the calling scope.

Templates can be embedded in script code - simply wrap them in a heredoc:

decl myTemplate {{{
<? decl z 42; ?><% [info type-name $z] %> z = <% $z %>
}}}

Note that no code in the template is actually evaluated until the tmpl-processed result is eval'd. tmpl simply compiles the template to a whcl script.

Customizing tmpl Compilation

tmpl accepts an Object as an optional second parameter, which can be used to customize the open/close tags and the output command. The properties supported are:

The rules for custom tags are:

tmpl will throw an exception if any of these rules are violated (or, perhaps, perceives itself as having been violated). Note that it is legal to specify only one of the tag pairs, e.g. the value-block tags and not the code-block tags, or vice versa, in which case the default will be used for the other. This is particularly useful in mini-templates, where it is convenient to specify the value-block tags when embedding template snippets in an outer (page-level) template.

Customizing the tmpl Output Destination

Where does evaluated template output go? When compiled, tmpl checks its optional 2nd argument for an object containing customization properties, as described above. The output-command option is the string tmpl will prepend to all output, effectively sending the output to that command. The string must be syntactically legal for use at the start of a whcl command line.

The requirements for the command (which we assume will output the results somewhere, though it may instead capture/redirect them):

As mentioned above, a default command is used if output-command is not defined beforehand, meaning that the template's output will go to the standard whcl-defined output channel. The default can be used, e.g., in conjunction with the output buffering API to capture processed output instead of outputting it.

Here's an example which reads in a template file, compiles it, runs it, capturing its tmpl output in a buffer:

whcl.install-api tmpl
decl tmplFile 'xyz.tmpl'
decl s [whcl.tmpl
        [whcl.Buffer.read-file $tmplFile]
        [object output-command out]
       ]
#echo [s.source-code]
proc out {} {
    out.buf append argv[0]
}
set out.buf new whcl.Buffer
s run
echo __FLC "Captured" [out.buf.length] "bytes:"
echo [out.buf.take-string]

The Obligatory Rant

It is, of course, not considered "best practice" to "mix presentation and logic" in software (noting that that's exactly what tmpl does). Bah, humbug! When writing scripts, it's sometimes exactly what's needed. When writing business-grade software, keep them separated, but when writing a quick script to solve a small problem, there is little reason not to mix the two.

Footnotes


  1. ^ Short version: to avoid difficult questions about how to handle trailing whitespace, seeing as they are significant and potentially (but not necessarily) desired. Remember that text files normally have a trailing newline (which is whitespace and can, e.g. mangle HTTP header output).
  2. ^ Potential TODO (20191215): allow one, but not both, set of tags to be empty/falsy, in order to disable the use of that tag type within a given template.
  3. ^ Potential TODO (20191217): Allow opening/closing tags to be the same byte sequence, like how many lightweight markups using matching pairs of * for italics or bold. That "should" work as-is right now (for well-formed inputs) if that part of the open/closing tags validation is simply removed, but that's untested.