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 onlyargs, 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 position | Protected? |
|---|---|
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 |
<a href=${link}>unquoted</a>
<a href="${link}">quoted</a>
<a href="${js}">scheme</a>
<script>var who = "${who}";</script>string link = "/x onmouseover=alert(1)";
string js = "javascript:alert(1)";
string who = "Bo \"B\" </script>";
print(template("contexts.spider", link, js, who));<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 "B" </script>";</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])readstemplate() argument 2 is not a plain variable name; name the value first (auto entry = rows[0];) and pass the name.
<p>Hello, ${name}! You have ${count} new ${count == 1 ? "message" : "messages"} (${ratio:.1f}).</p>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());<p>Hello, Ada <admin>! You have 3 new messages (0.8).</p>
web::html 63What 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
thisare 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 witharray::map, and put anything longer in a function or a smaller template. - Over a passed array write
array::map(xs, f), notxs.map(f): the dot form maps the caller's array in place (Containers & Strings).
<tr><td>${shown += 1}</td><td>${player.name}</td><td>${medal(shown)}</td></tr>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);<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 2Splices and web::html
- A value's text is what
to_stringgives, a class's ownto_string()included, then escaped. - A
web::htmlsplices 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
nullsplice is an error in every template form:a template splice is null: ${title} at page.spider:3, raised at load for a literalnulland at the render otherwise. Write${title ?? ""}for a value that may be missing. A backtick string outside a template still showsnull. - A
web::htmlis a value;to_string()gives its text andprintprints it. It has no constructor from text, so text becomes HTML only through a splice or throughraw.
<li>${item}</li><h2>${title}</h2><ul>${array::map(items, item => template("item.spider", item))}</ul>${items}${raw(footer)}string title = "Tom & Jerry";
array<string> items = ["<script>", "fish & chips"];
string footer = "<hr>";
print(template("list.spider", title, items, footer));<h2>Tom & Jerry</h2><ul><li><script></li><li>fish & chips</li></ul><script>fish & 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.
<h1>${user.name}</h1>
<p>${user.emial}</p>class User { string name = "Ada"; string email = "ada@example.org"; }
User user = User();
print(template("profile.spider", user));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.
<li>${node["label"]}<ul>${array::map(node["children"], node => template("tree.spider", node))}</ul></li>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");<li>root<ul><li>a<ul></ul></li><li>b<ul><li>c<ul></ul></li></ul></li></ul></li>
same| Literal path | Path known at run time | |
|---|---|---|
| Names and functions | checked when the calling script loads | checked at the first render; a failure is the call's catchable error |
| Members of a passed value | checked at load against its declared type | checked when read |
| An edit to the file | reloads 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 running | seen 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
templaterender writes at most the host's template output limit, 10 MiB by default; past it the render raisestemplate()'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()andreflect::scoped_template_output_limit()and cannot change them (Safety Rails). - A
templatefile is read under the filesystem privilege: without it,templateraisesInsufficient 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 raiseInsufficient privilege: scoped_template.. - A scoped file is read only from the host's template folders (
engine::scoped_template_dirs(), by default thetemplatefolder, 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::htmlin 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
templateof the same path kind does.
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); }Hi <ann>: 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.namefor a map key: writeargs["name"];- an in-place method, which returns nothing to splice:
${args["items"].sort()}raises the language's own error, so use the copy twinarray::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.