HTML Templates​

Part of the engine, in every host. template, scoped_template, scoped_template_text and the web::html value need no registration: the jaiscript runner, jai_spider (which serves the result as a page) and any embedding host have them. A host only sets what they may read and how much they may write (Safety Rails).

  • A template is a file, by convention name.spider. Its text is HTML, and every ${...} in it is one expression, spliced in HTML-escaped.
  • template(path, a, b, ...) renders a template you wrote: it sees the names you pass, by reference, and the program's functions and globals.
  • scoped_template(path, map) renders a template you did not write: it sees only args, a deep copy of the map.
  • Both return a web::html: text that is already HTML, so it splices into another template without being escaped twice.

Where escaping is and is not enough​

A splice escapes & < > " ', as web::html_escape does (The web:: Library). That is the right escaping for two places only:

Splice positionProtected?
element text: <p>${x}</p>yes
quoted attribute: title="${x}"yes
unquoted attribute: href=${x}no: a space ends the value and the rest becomes new attributes. Always quote
URL attribute: href="${x}"no: a javascript: URL has nothing to escape. Check the scheme before the render (web::parse_url), and web::url_encode a value that becomes part of a URL
inside <script> or <style>no: those elements do not decode entities, so the text is wrong, and it is not escaped for the language inside. Never splice there
event attribute: onclick="go('${x}')"no: a browser decodes the entities before running the script, so a quote in the value ends the script's string. Never splice there
template — contexts.spider
<a href=${link}>unquoted</a>
<a href="${link}">quoted</a>
<a href="${js}">scheme</a>
<script>var who = "${who}";</script>
jaiscript
string link = "/x onmouseover=alert(1)";
string js = "javascript:alert(1)";
string who = "Bo \"B\" </script>";
print(template("contexts.spider", link, js, who));
output
<a href=/x onmouseover=alert(1)>unquoted</a>
<a href="/x onmouseover=alert(1)">quoted</a>
<a href="javascript:alert(1)">scheme</a>
<script>var who = "Bo &quot;B&quot; &lt;/script&gt;";</script>

A template file​

  • The whole file is the template. A backtick or a backslash in it is the page's own; the only escape is \${, which writes ${. Line endings read as \n.
  • A splice takes a backtick string's format specs (Template strings & format specs).
  • The path resolves from the calling script's file, as an import's does.
  • Each argument after the path is a plain variable name, and the template sees it by that name. Anything else fails the load: template("row.spider", rows[0]) reads template() argument 2 is not a plain variable name; name the value first (auto entry = rows[0];) and pass the name.
template — greeting.spider
<p>Hello, ${name}! You have ${count} new ${count == 1 ? "message" : "messages"} (${ratio:.1f}).</p>
jaiscript
string name = "Ada <admin>";
int count = 3;
float ratio = 0.75;
web::html page = template("greeting.spider", name, count, ratio);
print(page);
print(type_of(page), " ", page.to_string().size());
output
<p>Hello, Ada &lt;admin&gt;! You have 3 new messages (0.8).</p>
web::html 63

What a template sees​

  • Each name is the caller's variable itself, bound by reference with its declared type, so a store through it lands in the caller's variable.
  • The program's functions and globals are visible; a passed name hides a global of the same name.
  • The caller's other locals, its class's fields and this are not. A name the call does not pass is looked up as a global and fails the load if there is none (use of undeclared identifier 'secret', at the template's line), even when the caller has a local of that name.
  • A template is one expression with no statements. Choose with ?:, loop with array::map, and put anything longer in a function or a smaller template.
  • Over a passed array write array::map(xs, f), not xs.map(f): the dot form maps the caller's array in place (Containers & Strings).
template — row.spider
<tr><td>${shown += 1}</td><td>${player.name}</td><td>${medal(shown)}</td></tr>
jaiscript
class Player { string name; int score; Player(string n, int s) { name = n; score = s; } }
string medal(int rank) { return rank == 1 ? "gold" : "silver"; }
array<Player> players = [Player("Vex", 90), Player("Ada", 75)];
int shown = 0;
for (auto& player : players) { print(template("row.spider", shown, player)); }
print("shown is now ", shown);
output
<tr><td>1</td><td>Vex</td><td>gold</td></tr>
<tr><td>2</td><td>Ada</td><td>silver</td></tr>
shown is now 2

Splices and web::html​

  • A value's text is what to_string gives, a class's own to_string() included, then escaped.
  • A web::html splices as it is. An array splices its elements one after another, each by its own rule.
  • raw(text), inside a template you wrote, is the opt-out: its text splices unescaped.
  • A null splice is an error in every template form: a template splice is null: ${title} at page.spider:3, raised at load for a literal null and at the render otherwise. Write ${title ?? ""} for a value that may be missing. A backtick string outside a template still shows null.
  • A web::html is a value; to_string() gives its text and print prints it. It has no constructor from text, so text becomes HTML only through a splice or through raw.
template — item.spider
<li>${item}</li>
template — list.spider
<h2>${title}</h2><ul>${array::map(items, item => template("item.spider", item))}</ul>${items}${raw(footer)}
jaiscript
string title = "Tom & Jerry";
array<string> items = ["<script>", "fish & chips"];
string footer = "<hr>";
print(template("list.spider", title, items, footer));
output
<h2>Tom &amp; Jerry</h2><ul><li>&lt;script&gt;</li><li>fish &amp; chips</li></ul>&lt;script&gt;fish &amp; chips<hr>

Checked at load​

A template with a literal path is compiled when the calling script loads and checked against the names and types its call passes, like a C++ template where it is instantiated. A failure carries the template's own file, line and column.

template — profile.spider
<h1>${user.name}</h1>
<p>${user.emial}</p>
jaiscript
class User { string name = "Ada"; string email = "ada@example.org"; }
User user = User();
print(template("profile.spider", user));
output
jaiscript: static check failed:
profile.spider:2:11: error: no member 'emial' in class 'User'

Paths known at run time, limits and reload​

A path that is not a literal is compiled and checked the first time its call renders, then reused by each later render with the same names. A template that renders itself works the same way: pass the child under the template's own name.

template — tree.spider
<li>${node["label"]}<ul>${array::map(node["children"], node => template("tree.spider", node))}</ul></li>
jaiscript
var node = {"label": "root", "children": [{"label": "a", "children": []}, {"label": "b", "children": [{"label": "c", "children": []}]}]};
print(template("tree.spider", node));
string theme = "tree.spider";
print(template(theme, node).to_string() == template("tree.spider", node).to_string() ? "same" : "different");
output
<li>root<ul><li>a<ul></ul></li><li>b<ul><li>c<ul></ul></li></ul></li></ul></li>
same
Literal pathPath known at run time
Names and functionschecked when the calling script loadschecked at the first render; a failure is the call's catchable error
Members of a passed valuechecked at load against its declared typechecked when read
An edit to the filereloads the calling script where an import of it is reached (Modules); an edit that does not check is reported there and the old version keeps runningseen at a render once the import window (import_recheck_interval) has passed; an edit that does not check raises once where it renders, then the last good version renders until the file changes again

Nothing needs a restart, and a template a template calls is tracked the same way. Limits and privilege:

  • A template render writes at most the host's template output limit, 10 MiB by default; past it the render raises template()'s output passed its 10485760-byte limit: the host raises it with engine::template_output_limit.
  • A script reads the limits with reflect::template_output_limit() and reflect::scoped_template_output_limit() and cannot change them (Safety Rails).
  • A template file is read under the filesystem privilege: without it, template raises Insufficient privilege: filesystem. where it renders.

Templates you did not write: scoped_template​

For a source you do not trust (a user's theme, a page a customer edits). The map is taken by value, and the only name inside is args, the template's own deep copy.

  • scoped_template_text(text, map) is the same over a string; the two names keep a path from being mistaken for template text.
  • Their privilege is jai::scoped_template, on by default and separate from filesystem. Disabled, both raise Insufficient privilege: scoped_template..
  • A scoped file is read only from the host's template folders (engine::scoped_template_dirs(), by default the template folder, found as an import's file is). Its path is relative, with no .., tried in each folder in order, and a file whose canonical path leaves its folder is refused (Safety Rails).
  • Output is always escaped, a web::html in the map included.
  • A render writes at most the host's scoped template output limit, 1 MiB by default (a scoped template's output passed its 1048576-byte limit: ...); a nested template's text counts each time it is spliced.
  • The execution budget applies, and a scoped file reloads as a template of the same path kind does.
jaiscript
map<string, var> data = {"user": {"name": "<ann>"}, "items": ["tea", "cake"], "total": 7.5};
print(scoped_template_text("Hi ${args[\"user\"][\"name\"]}: ${args[\"items\"].size()} items, ${array::map(args[\"items\"], i => string::to_upper(i)).join(\", \")}, ${args[\"total\"]:.2f}", data));
string untrusted = "${secret}";
try { scoped_template_text(untrusted, data); } catch (e) { print("caught: ", e); }
output
Hi &lt;ann&gt;: 2 items, TEA, CAKE, 7.50
caught: parse failed:
<scoped_template_text>:1:3: error: a scoped template sees only args: 'secret' is not available
    ${secret}

Allowed: indexing into args; the plain fields of a script class instance, through a shared_ptr too; format specs; built-in methods of built-in values (a string, an array, a map, a math type); the string::, array:: and map:: functions, which return copies; and x => expr lambdas for them, under the same rules.

Refused, each with an error naming what the template tried (the reference lists the texts):

  • a global (a scoped template sees only args: 'secret' is not available), a free function or one in any other namespace, a script's own included, and a function value;
  • raw, an assignment, this, new, a cast, a statement;
  • anything that would run code of a class instance or a host object: a method, an operator overload, a built-in method over a value holding one, and splicing one, which would call its to_string() (a scoped template does not splice a class instance, a host object or a function: its code could run);
  • a host property, or a field with a user accessor;
  • args.name for a map key: write args["name"];
  • an in-place method, which returns nothing to splice: ${args["items"].sort()} raises the language's own error, so use the copy twin array::sort.

What the template's text breaks (a name, a call, raw, a construct) fails when it is compiled: at load for a literal text or path, at the first render otherwise. What depends on a value inside args is checked as the template runs.