Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

wire

import wire

Wire, an HTML template engine.

A Wire template is an HTML document. Not a document with a templating language sprinkled through it, and not a string that happens to end up looking like HTML: the source is parsed by the same WHATWG parser the html module uses, and every valid HTML5 document is already a valid Wire template. What Wire adds are attributes on ordinary elements and values written between {{ and }}.

Parsing rather than substituting is what makes the rest possible. Wire knows that one interpolation sits in a paragraph, another in an href, another in a <script>, and it escapes each of them for where it actually is. A value can never turn into structure.

import wire

echo wire.render_string(
  '<p>Hello {{ name }}</p>',
  { name: '<b>Ada</b>' }
)
# <p>Hello &lt;b&gt;Ada&lt;/b&gt;</p>

Templates from files

render_string() is convenient for a one-off and for reading documentation. Real templates live in files under a root directory, which is templates beside the working directory unless you say otherwise:

import wire

var view = wire.wire()
view.set_root('./views')

echo view.render('pages/home', { user })

The .html extension is added when the path as written names no file, so 'pages/home' finds pages/home.html. set_extension() changes that.

Every path resolves inside the root and nowhere else. One that climbs out with .., or names an absolute path elsewhere, is refused. That matters the moment a path is built from anything a request supplied.


Values

Anything between {{ and }} is an expression. The full language is in wire.expression; in practice it reads the way you would guess:

<h1>{{ post.title }}</h1>
<p>{{ post.author.name|upper }}</p>
<p>{{ post.tags|join(', ') }}</p>
<p>{{ post.views > 1000 ? 'popular' : 'quiet' }}</p>
<p>{{ user.nickname ?? user.name }}</p>

A name that was never supplied is nil, and so is a key read off it, so {{ user.address.city }} on a request with no user renders as nothing rather than failing. Ask with x-if when the difference matters.

Writing {{ literally is %{{.

Filters

A value passed through | goes through a filter. They chain, and they take arguments:

{{ name|trim|title }}
{{ total|number_format(2) }}
{{ summary|truncate(80, '…') }}

wire.filters lists the forty-odd that ship with Wire, and register_filter() adds your own.


Escaping, and why there is no way to turn it off

Every interpolation is escaped for the place it lands in:

Where it isWhat happens
text between tags&, < and > become references
an attribute value& and " become references
href, src, actionthe URL’s scheme is checked as well
<script>, onclickthe value is encoded as JSON
<style>anything outside a CSS-safe set is dropped

A javascript: URL in an href becomes about:blank. set_url_schemes() changes the allowlist when an application really does need another scheme.

Inside a <script> the value carries its own quotes, so do not add any:

<script>
  var user = {{ name }};    // right, renders as "Ada"
  var user = '{{ name }}';  // wrong, renders as '"Ada"'
</script>

Markup you built yourself and want rendered as markup goes through the raw filter, or arrives already wrapped in wire.safe():

<div class="body">{{ article.rendered_html|raw }}</div>

raw is a promise that the value is safe where it lands. Applying it to anything a user supplied is how a template engine becomes a cross-site scripting hole.


Directives

Wire’s own attributes all begin with x-. Wire owns that prefix outright: an x- attribute it does not recognize is an error rather than something that quietly does nothing, so x-fi is caught the first time the template compiles.

A directive can go on any element, and the element it goes on is part of what it controls. Put it on <template> when you want the control without an element in the output; <template> is the one element HTML lets through untouched anywhere, including inside <head>, a table and a <select>, and Wire removes it.

Choosing

<p x-if="user.is_admin">Admin</p>
<p x-elif="user.is_staff">Staff</p>
<p x-else>Member</p>

x-elif and x-else attach to the element sibling in front of them, with only whitespace and comments allowed in between. x-not is the inverse of x-if and takes no chain.

Repeating

<li x-for="items" x-value="item" x-key="index">
  {{ index }}: {{ item.name }}
</li>

The element repeats, not just its children. Lists, dictionaries, strings and ranges all iterate; a list binds x-key to the position and a dictionary to the key.

Every iteration also publishes a loop variable:

FieldIs
loop.indexthe position counting from 1
loop.index0the position counting from 0
loop.firsttrue on the first pass
loop.lasttrue on the last
loop.lengthhow many entries there are
loop.even, .oddwhether the position is even or odd
loop.key, .valuethis entry, whether or not it is bound
loop.parentthe enclosing loop’s own loop

x-loop="row" renames it, which is how two nested loops can both be read without going through parent.

An x-if on a repeated element is asked once per iteration, so it can read the loop’s variables. It cannot be continued with x-elif there.

Content

<p x-text="summary"></p>
<div x-html="article.body|raw"></div>
<a x-attr="{ href: post.url, rel: post.external ? 'nofollow' : nil }">…</a>

x-text and x-html replace an element’s children. x-attr spreads a dictionary onto it, where true gives a valueless attribute and false or nil leaves the attribute off.


Composition

Including

<include path="partials/header.html" />
<template x-include="partials/header.html"></template>

Those two are the same thing. <include>, <extend>, <declare>, <define> and <super> are sugar, rewritten into the directive form while the source is being tokenized, which is why they work inside a <head> or a table where an unknown element would be moved or dropped.

A path is read as text, not as an expression, so it can be written plainly and computed where it needs to be:

<include path="themes/{{ theme }}/header.html" />

An include sees the variables around it. x-with adds to them and x-only withholds everything else, which is what turns a partial into a component with a real interface:

<include path="components/badge" x-with="{ label: 'New', tone: 'green' }" only />

Anything written inside an include is handed to it as the region called content, so a partial can wrap what it was given:

<!-- components/card.html -->
<div class="card">
  <h3>{{ title }}</h3>
  <declare name="content"></declare>
</div>

<!-- the page -->
<include path="components/card" x-with="{ title: 'Totals' }">
  <p>{{ orders|length }} orders</p>
</include>

Inheriting

A base template marks the regions a page may replace:

<!-- layouts/page.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>{{ title }}</title>
    <declare name="head"></declare>
  </head>
  <body>
    <main>
      <declare name="content"></declare>
    </main>
    <footer>
      <declare name="footer">
        <p>&copy; {{ year }}</p>
      </declare>
    </footer>
  </body>
</html>

A page fills them in:

<!-- pages/home.html -->
<extend base="layouts/page.html">
  <define name="content">
    <h1>Hello {{ user.name }}</h1>
  </define>
  <define name="footer">
    <super />
    <p>All rights reserved.</p>
  </define>
</extend>

A region nobody defines renders whatever the base put inside it. <super /> renders the definition this one replaces, so a page can add to a footer instead of restating it. Inheritance nests as deep as you like, and a middle template can declare regions of its own.

One template extends one base, and everything in an extending template has to be inside a <define>, because the base is what decides the structure. Defining the same region twice needs override said out loud:

<define name="content" override>…</define>

Extending Wire from Zuri

import wire

var view = wire.wire()

# A filter: the value comes first, then whatever the template passed.
view.register_filter('excerpt', @(value, words) {
  var count = words == nil ? 25 : words
  return ' '.join(value.split('/\\s+/').take(count)) + '…'
})

# A value every template can see without being handed it.
view.register_global('site_name', 'Example')

# A function a template calls.
view.register_global('route', @(name) {
  return '/' + name
})

Used as:

<p>{{ post.body|excerpt(40) }}</p>
<a href="{{ route('about') }}">{{ site_name }}</a>

register_element() claims a tag name outright and hands every one of them to a function, which is the escape hatch for anything the directives cannot express.


Compiling and caching

A template is parsed once, not once per render. render() compiles on first use and keeps the result, checking the file’s modification time and size before reusing it.

That check is one stat per template per render, which is what you want while writing templates and not what you want under load. set_auto_reload(false) turns it off, and clear_cache() is how a long-running process picks up a deployment.


Comments

HTML comments are server-side notes and do not reach the page:

<!-- This never ships, and neither does {{ secret }}. -->

Nothing inside one is evaluated. set_comments(true) keeps them when a comment is genuinely meant for the browser, as a conditional comment or a build marker is.


When something is wrong

Everything Wire raises is a WireError and carries the template and the line and column of the tag at fault. TemplateSyntaxError comes from compiling, RenderError from rendering, and TemplateNotFoundError from a path that goes nowhere.

catch {
  echo view.render('pages/home', { user })
} as e {
  if instance_of(e, wire.WireError) {
    echo 'template ${e.location()}: ${e.reason}'
  }
}

The module is callable. wire(...) is the same call as wire.wire(...), documented below.

The wire API

Every public name in wire, wherever it is declared. Each links to the page that documents it.

NameKindSummary
wire.RenderErrorclassRaised while rendering, for anything that depends on the values a template was given: iterating something…
wire.SafeclassA string that is already markup and must be written out as it is.
wire.TemplateNotFoundErrorclassRaised when a template file cannot be found, or when it resolves to somewhere outside the root directory.
wire.TemplateSyntaxErrorclassRaised while compiling a template, for anything Wire can tell is wrong without rendering: an unknown…
wire.WireclassA template engine: a root directory, a set of filters, and a cache of everything compiled so far.
wire.WireErrorclassThe base class of every error Wire raises.
wire.compile.AttributeclassOne attribute of an element.
wire.compile.BranchclassOne arm of a conditional chain.
wire.compile.CommentclassAn HTML comment that survived into the output.
wire.compile.CompilerclassCompiles one template’s source.
wire.compile.ConditionalclassA chain of x-if, x-elif and x-else, or a lone x-if.
wire.compile.ElementclassAn element, with everything about it already worked out.
wire.compile.GroupclassA run of instructions with nothing of its own, which is what a <template> carrying a directive leaves…
wire.compile.IncludeclassAnother template rendered in this instruction’s place.
wire.compile.InstructionclassThe base of every instruction, carrying the kind the renderer switches on and the place in the source it came…
wire.compile.LoopclassA repetition, from x-for.
wire.compile.RawclassCharacters written out exactly as they are, used for the doctype.
wire.compile.SegmentclassOne piece of a run of text or of an attribute value: either literal characters or an expression to evaluate.
wire.compile.SlotclassA region an extending template may replace, from x-slot.
wire.compile.SuperclassThe definition this one replaces, from x-super.
wire.compile.TemplateclassA compiled template, ready to render as many times as you like.
wire.compile.TextclassA run of text, possibly with interpolations in it.
wire.compile.compilefunctionCompiles source into a Template.
wire.constants.ATTR_ATTRconstantSpreads a dictionary of name/value pairs onto the element as attributes.
wire.constants.CALL_CLOSEconstantCloses a template function call.
wire.constants.CALL_OPENconstantOpens a template function call.
wire.constants.CARRIER_TAGconstantThe element Wire uses to carry a directive without emitting anything of its own.
wire.constants.DEFAULT_EXTENSIONconstantThe extension render() appends when the path it was given does not name an existing file and carries no…
wire.constants.DEFAULT_LOOP_NAMEconstantThe variable an x-for publishes its loop metadata under.
wire.constants.DEFAULT_ROOTconstantThe directory render() resolves template paths against until set_root() says otherwise, which is a…
wire.constants.DEFINE_ATTRconstantReplaces the base template’s region of the same name.
wire.constants.DIRECTIVESconstantEvery directive attribute Wire understands.
wire.constants.DIRECTIVE_PREFIXconstantThe prefix every Wire directive attribute carries.
wire.constants.ELIF_ATTRconstantContinues an x-if chain on the next element sibling.
wire.constants.ELSE_ATTRconstantCloses an x-if chain on the next element sibling.
wire.constants.ESCAPE_CHARconstantThe character that escapes an interpolation, making Wire emit the braces literally instead of reading what is…
wire.constants.EXPRESSION_DIRECTIVESconstantThe directives whose value is a Wire expression.
wire.constants.EXTEND_ATTRconstantMarks this template as extending the base template named by the value.
wire.constants.FLAG_DIRECTIVESconstantThe directives that take no value at all.
wire.constants.FOR_ATTRconstantRepeats the element once per entry of the expression’s value.
wire.constants.HTML_ATTRconstantReplaces the element’s children with the expression’s value as markup, without escaping it.
wire.constants.IF_ATTRconstantRenders the element only when the expression is truthy.
wire.constants.INCLUDE_ATTRconstantRenders another template in this element’s place.
wire.constants.KEY_ATTRconstantNames the variable each x-for iteration binds its key or index to.
wire.constants.LOOP_ATTRconstantRenames the loop metadata variable an x-for publishes, which is called loop unless this says otherwise.
wire.constants.MAX_DEPTHconstantHow deep x-include and x-extend may nest before Wire calls it a cycle.
wire.constants.NAME_DIRECTIVESconstantThe directives whose value names a variable or a region, and is read as a bare identifier rather than…
wire.constants.NOT_ATTRconstantRenders the element only when the expression is falsy, the inverse of x-if.
wire.constants.ONLY_ATTRconstantWithholds the surrounding scope from an x-include or a component, leaving it only what x-with passes.
wire.constants.OVERRIDE_ATTRconstantPermits an x-define to replace a definition of the same name made earlier in the same template.
wire.constants.PATH_DIRECTIVESconstantThe directives whose value is a template path, read as literal text with {{ }} interpolation allowed inside…
wire.constants.PSEUDO_ELEMENTSconstantThe pseudo elements Wire accepts as sugar, each mapped to the directive it becomes and the attribute that…
wire.constants.PSEUDO_FLAGSconstantAttributes a pseudo element may carry that become valueless directives rather than the directive its name…
wire.constants.SLOT_ATTRconstantDeclares a region an extending template may replace.
wire.constants.SUPER_ATTRconstantRenders the definition this one replaces.
wire.constants.TEXT_ATTRconstantReplaces the element’s children with the expression’s value as text.
wire.constants.VALUE_ATTRconstantNames the variable each x-for iteration binds its value to.
wire.constants.VAR_CLOSEconstantCloses an interpolation.
wire.constants.VAR_OPENconstantOpens an interpolation.
wire.constants.WITH_ATTRconstantSupplies the variables an x-include or a component is rendered with.
wire.escape.BLOCKED_URLconstantWhat a blocked URL is replaced with.
wire.escape.CONTEXT_ATTRIBUTEconstantAn ordinary attribute value.
wire.escape.CONTEXT_SCRIPTconstantA place the browser reads as JavaScript: the body of a <script>, or the value of an on* handler attribute.
wire.escape.CONTEXT_STYLEconstantThe body of a <style> element.
wire.escape.CONTEXT_TEXTconstantText between tags.
wire.escape.CONTEXT_URLconstantAn attribute the browser resolves as a URL.
wire.escape.DEFAULT_URL_SCHEMESconstantThe URL schemes Wire lets through by default.
wire.escape.STYLE_SAFEconstantThe characters a value may contain inside a <style> body.
wire.escape.URL_ATTRIBUTESconstantThe attributes whose value a browser resolves as a URL, and which therefore have to be checked for a…
wire.escape.URL_LIST_ATTRIBUTESconstantURL attributes holding a list of URLs rather than a single one.
wire.escape.attributefunctionEscapes value for a double quoted attribute value, turning & and " into character references.
wire.escape.attribute_contextfunctionThe escaping context an attribute named name calls for.
wire.escape.escapefunctionEscapes value for the given context.
wire.escape.is_script_attributefunctionWhether name is an event handler attribute, whose value a browser reads as JavaScript.
wire.escape.is_url_attributefunctionWhether name is an attribute the browser resolves as a single URL.
wire.escape.is_url_list_attributefunctionWhether name is an attribute holding a list of URLs.
wire.escape.scriptfunctionEscapes value for a place the browser reads as JavaScript, by encoding it as JSON and then hiding the…
wire.escape.stylefunctionEscapes value for the body of a <style> element by dropping every character outside a conservative…
wire.escape.textfunctionEscapes value for text between tags, turning &, < and > into character references.
wire.escape.text_contextfunctionThe escaping context text inside an element named tag calls for.
wire.escape.urlfunctionEscapes value for an attribute the browser resolves as a URL, replacing it with about:blank when its…
wire.escape.url_listfunctionEscapes value for an attribute holding several URLs, checking each entry’s scheme on its own.
wire.escape.uses_descriptorsfunctionWhether a URL list attribute allows a descriptor after each URL, which srcset does and ping does not.
wire.expression.BinaryclassAn arithmetic or comparison operator.
wire.expression.CallclassA call, as in route('home') or user.display_name().
wire.expression.Conditionalclasscondition ? consequence : alternative.
wire.expression.DictLiteralclassA dictionary literal.
wire.expression.ExpressionclassThe base of every node in a parsed expression.
wire.expression.FilterclassA value passed through a filter, as in name|upper.
wire.expression.IndexclassA bracketed lookup, as in items[index], where the key is itself an expression.
wire.expression.KEYWORDSconstantThe words that are part of the language rather than names a template can bind.
wire.expression.LONG_OPERATORSconstantOperators made of more than one character, longest first so that <= is never read as < followed by =.
wire.expression.ListLiteralclassA list literal.
wire.expression.LiteralclassA number, a string, or one of true, false and nil.
wire.expression.Logicalclassand, or or ??, each of which decides whether to evaluate its right side after looking at its left.
wire.expression.MemberclassA dotted lookup, as in user.name.
wire.expression.ParserclassBuilds a syntax tree from an expression’s tokens.
wire.expression.SHORT_OPERATORSconstantOperators made of a single character.
wire.expression.TokenclassOne piece of an expression’s source: its kind, its value, and where in the expression it started.
wire.expression.Unaryclassnot x, !x or -x.
wire.expression.VariableclassA bare name, looked up in the variables the template was rendered with.
wire.expression.parsefunctionCompiles source into a syntax tree.
wire.expression.tokenizefunctionSplits source into tokens.
wire.filters.BUILTINconstantEvery filter Wire starts with, keyed by the name a template calls it by.
wire.filters.absfunctionThe value without its sign.
wire.filters.capitalizefunctionThe value with its first letter in upper case and the rest left alone.
wire.filters.ceilfunctionThe smallest whole number at or above the value.
wire.filters.datefunctionA date written out with the given format.
wire.filters.default_tofunctionfallback when the value is falsy, otherwise the value.
wire.filters.emptyfunctionWhether the value has nothing in it.
wire.filters.escape_valuefunctionEscapes a value for a context other than the one it is being written into, or escapes a value that was…
wire.filters.filesizefunctionA byte count written the way a person reads it.
wire.filters.firstfunctionThe first entry, or nil when there is none.
wire.filters.floorfunctionThe largest whole number at or below the value.
wire.filters.isfunctionWhether the value equals expected.
wire.filters.joinfunctionThe entries joined into one string with glue between them.
wire.filters.jsonfunctionThe value as JSON.
wire.filters.json_scriptfunctionThe value as JSON wrapped in a <script type="application/json"> element, ready to be read back by a script…
wire.filters.keysfunctionA dictionary’s keys, in insertion order.
wire.filters.lastfunctionThe last entry, or nil when there is none.
wire.filters.lengthfunctionHow many entries the value has.
wire.filters.lowerfunctionThe value in lower case.
wire.filters.lpadfunctionThe value padded on the left with fill until it is width characters long.
wire.filters.nl2brfunctionThe value with its line breaks turned into elements.
wire.filters.notfunctionWhether the value differs from expected.
wire.filters.number_formatfunctionThe value written out with thousands separated and a fixed number of decimal places.
wire.filters.rawfunctionMarks a value as markup so Wire writes it out without escaping it.
wire.filters.repeatfunctionThe value repeated count times.
wire.filters.replacefunctionThe value with every occurrence of search replaced by replacement.
wire.filters.reversefunctionThe entries in the opposite order, or a string backwards.
wire.filters.roundfunctionThe value rounded to places decimal places.
wire.filters.rpadfunctionThe value padded on the right with fill until it is width characters long.
wire.filters.slicefunctionThe entries from start up to but not including end.
wire.filters.slugfunctionThe value as a lowercase, hyphen separated slug.
wire.filters.sortfunctionThe entries in ascending order.
wire.filters.splitfunctionThe value split into a list on separator.
wire.filters.strip_tagsfunctionThe value with every HTML tag removed, leaving only its text.
wire.filters.sumfunctionThe entries added together.
wire.filters.titlefunctionThe value with the first letter of every word in upper case and the rest in lower case.
wire.filters.trimfunctionThe value with leading and trailing whitespace removed.
wire.filters.truncatefunctionThe value cut down to length characters, with suffix put on the end when anything was actually cut.
wire.filters.uniquefunctionThe entries with later duplicates removed, keeping the first of each.
wire.filters.upperfunctionThe value in upper case.
wire.filters.url_encodefunctionThe value percent encoded for use inside a URL.
wire.filters.valuesfunctionA dictionary’s values, in insertion order.
wire.is_safefunctionTrue when value is a Safe.
wire.loader.LoaderclassFinds template files under one root directory.
wire.normalize.NormalizerclassA tokenizer that rewrites Wire’s pseudo elements on the way past.
wire.normalize.parsefunctionParses source into a tree with Wire’s pseudo elements already rewritten.
wire.renderfunctionRenders the template at path using the shared Wire.
wire.render.DefinitionclassOne definition of a region, and what to render it against.
wire.render.FrameclassOne level of variables, pointing at the level around it.
wire.render.RendererclassRenders compiled templates.
wire.render_stringfunctionRenders source using the shared Wire.
wire.safefunctionMarks value as markup that Wire must not escape.
wire.sharedfunctionThe Wire behind the module-level render() and render_string().
wire.stringifyfunctionWhat value looks like once it reaches the page, before escaping.
wire.truthyfunctionWhether value counts as true in an x-if, an x-not, or a boolean operator inside an expression.
wire.values.MAX_ARGUMENTSconstantThe most arguments a filter or a template function can be called with.
wire.values.comparefunctionOrders a before, with or after b, returning -1, 0 or 1.
wire.values.invokefunctionCalls target with arguments spread into its parameters.
wire.values.is_blankfunctionWhether text is empty or is nothing but whitespace.
wire.values.is_emptyfunctionWhether value has nothing in it, for the empty filter and for anything else that wants the question asked…
wire.values.type_namefunctionWire’s name for value’s type, used in error messages so that a complaint reads “expected a list, got a…
wire.values.unwrapfunctionStrips the Safe wrapper off value, leaving anything else alone.
wire.wirefunctionA new Wire.

Submodules

ModuleReached asSummary
wire.compileimport wire.compileTurning a parsed template into the instruction tree the renderer walks.
wire.constantswire.constants.*The names Wire reserves: its directive attributes, the pseudo elements that are sugar for them, and the…
wire.errorswire.errors.*The errors Wire raises, and the location information they carry.
wire.escapewire.escape.*Turning a value into something safe to write at a particular place in a page.
wire.expressionwire.expression.*Wire’s expression language: the thing that sits between {{ and }}, and the thing an x-if or an x-for…
wire.filterswire.filters.*The filters every Wire template starts with.
wire.loaderimport wire.loaderTurning the path written in an x-include into a file on disk, and refusing to when it points somewhere it…
wire.normalizewire.normalize.*Rewriting Wire’s pseudo elements into something HTML’s own tree construction will not move, drop or reshape.
wire.renderimport wire.renderWalking a compiled template and writing the page out.
wire.valueswire.values.*How Wire reads the values a template is given: what counts as true, what a value looks like once it reaches…

Functions

wire()

wire.wire(options: ?dict) -> Wire

A new Wire.

options takes any of root, extension, compact, comments, auto_reload and url_schemes, each the same as the matching set_ method, so a whole configuration can be written in one place.

import wire

var view = wire.wire({
  root: './views',
  auto_reload: false,
})

Parameters

  • options (?dict)

Returns Wire

shared()

wire.shared() -> Wire

The Wire behind the module-level render() and render_string().

Configure this to use those without building your own, which is worth doing for a small program and not worth doing for anything that wants two different roots.

import wire

wire.shared().set_root('./views')
echo wire.render('pages/home', { user })

Returns Wire

render()

wire.render(path: string, variables: ?dict) -> string

Renders the template at path using the shared Wire.

Parameters

  • path (string)
  • variables (?dict)

Returns string

Raises WireError

render_string()

wire.render_string(source: string, variables: ?dict, path: ?string) -> string

Renders source using the shared Wire.

Parameters

  • source (string)
  • variables (?dict)
  • path (?string)

Returns string

Raises WireError

Classes

Wire

class wire.Wire

A template engine: a root directory, a set of filters, and a cache of everything compiled so far.

One of these is normally built at startup, configured once, and used for the life of the process. Rendering does not change it, so it is safe to render from several places at once.

import wire

var view = wire.wire()
view.set_root('./views')
view.register_global('site_name', 'Example')

echo view.render('pages/home', { user })

Fields

FieldTypeDescription
globalsValues every template can read without being handed them.
url_schemesThe URL schemes allowed in an href and its relatives.

Constructor

wire.Wire(options: ?dict)

Parameters

  • options (?dict) — Any of root, extension, compact, comments, auto_reload and url_schemes, each the same as the matching set_ method.

Raises ArgumentError when an option is not of the type it should be.

Wire.set_root()

wire.Wire.set_root(path: string) -> Wire

Sets the directory template paths resolve inside.

A relative path is taken from the working directory. The directory does not have to exist yet; a path under one that does not is reported as not found like any other.

Changing the root empties the cache, since the same path can now mean a different file.

Parameters

  • path (string)

Returns Wire

Raises ArgumentError

Wire.root()

wire.Wire.root() -> string

The directory template paths resolve inside, as an absolute path.

Returns string

Wire.create_root()

wire.Wire.create_root() -> bool

Creates the root directory when it does not exist, and reports whether it had to.

Wire never does this on its own. Creating directories is not a template engine’s business, and a typo in a root should read as a missing template rather than quietly produce an empty directory.

Returns bool

Raises Error when the directory cannot be created.

Wire.set_extension()

wire.Wire.set_extension(extension: string) -> Wire

Sets the extension tried when a path names no file as written.

It has to begin with a dot. render('home') then looks for home and then for home plus this.

Parameters

  • extension (string)

Returns Wire

Raises ArgumentError when it does not begin with a dot.

Wire.set_compact()

wire.Wire.set_compact(compact: bool) -> Wire

Drops whitespace-only text from the output when compact is true.

This is the cheap kind of minification: the indentation between tags goes, and nothing else is touched. Whitespace inside a <pre> is whitespace-only only when the whole run is, so this can still change how preformatted text reads; leave it off where that matters and reach for html.minify() instead, which knows the difference.

Parameters

  • compact (bool)

Returns Wire

Raises ArgumentError

Wire.set_comments()

wire.Wire.set_comments(comments: bool) -> Wire

Keeps HTML comments in the output when comments is true.

Comments are dropped by default, which is what makes an HTML comment a server-side note. Nothing inside one is ever evaluated either way.

Parameters

  • comments (bool)

Returns Wire

Raises ArgumentError

Wire.set_auto_reload()

wire.Wire.set_auto_reload(reload: bool) -> Wire

Checks a cached template against its file before reusing it when reload is true, which is the default.

The check is one stat per template per render. Leave it on while templates are being written and turn it off where throughput matters, remembering that clear_cache() is then the only way a running process notices a deployment.

Parameters

  • reload (bool)

Returns Wire

Raises ArgumentError

Wire.set_url_schemes()

wire.Wire.set_url_schemes(schemes: list) -> Wire

Replaces the URL schemes allowed in an href and its relatives.

A URL with no scheme is always allowed, so every relative link keeps working whatever this is set to. A URL whose scheme is not listed is replaced with about:blank.

The default is in escape.DEFAULT_URL_SCHEMES and deliberately leaves out javascript, vbscript, data and file.

view.set_url_schemes(escape.DEFAULT_URL_SCHEMES + ['app'])

Parameters

  • schemes (list) — Scheme names without the colon, in lower case.

Returns Wire

Raises ArgumentError

Wire.register_filter()

wire.Wire.register_filter(name: string, handler) -> Wire

Registers a filter templates can use after a |.

The value being filtered arrives as the first argument and whatever the template passed follows it. An argument the template left out arrives as nil, so give it a default rather than insisting.

Registering a name that already exists replaces it, which is how an application makes date mean its own thing.

view.register_filter('excerpt', @(value, words) {
  var count = words == nil ? 25 : words
  return ' '.join(value.split('/\\s+/').take(count)) + '…'
})

A filter returning a plain string has its result escaped like any other value. One that genuinely produces markup returns wire.safe(), and is then responsible for what is inside it.

Parameters

  • name (string)
  • handler (callable)

Returns Wire

Raises ArgumentError

Wire.register_element()

wire.Wire.register_element(name: string, handler) -> Wire

Claims a tag name and hands every element of that name to handler instead of writing it out.

The handler is called with the Wire and a dictionary describing the element:

KeyIs
namethe tag name
attributesits attributes, with every value already rendered and escaped
contentits children, already rendered
variablesthe scope it was written in

Return wire.safe(markup) to write markup, any other value to write it as escaped text, or nil to write nothing at all.

view.register_element('icon', @(view, element) {
  var name = element.attributes.get('name', 'dot')
  return wire.safe('<svg class="icon"><use href="#${name}"></use></svg>')
})

Directives still work on a claimed element, so <icon x-if="…" /> behaves the way it reads. Reach for this only where the directives genuinely cannot express something; a partial and x-include is easier to read and does not need Zuri code to follow it.

Parameters

  • name (string)
  • handler (callable)

Returns Wire

Raises ArgumentError

Wire.register_global()

wire.Wire.register_global(name: string, value) -> Wire

Makes value readable from every template by name.

A variable of the same name passed to render() wins, so a global is a default rather than an override. A callable global is a function a template can call.

Parameters

  • name (string)
  • value (any)

Returns Wire

Raises ArgumentError

Wire.render()

wire.Wire.render(path: string, variables: ?dict) -> string

Renders the template at path and returns the page.

path is relative to the root and may leave off the extension. variables is what the template can read, and defaults to nothing at all.

echo view.render('pages/home', { user, posts })

Parameters

  • path (string)
  • variables (?dict)

Returns string

Raises TemplateNotFoundError when the template does not exist or is outside the root.

Raises TemplateSyntaxError when it does not compile.

Raises RenderError when rendering it fails.

Wire.render_string()

wire.Wire.render_string(source: string, variables: ?dict, path: ?string) -> string

Renders source as a template and returns the page.

path names the source in any error raised and defaults to <source>. Includes and extends inside the source still resolve against the root.

The result is not cached, since there is no file to key it on. Use render() for anything rendered more than once.

echo view.render_string('<p>{{ greeting }}</p>', { greeting: 'Hi' })

Parameters

  • source (string)
  • variables (?dict)
  • path (?string)

Returns string

Raises TemplateSyntaxError

Raises RenderError

Wire.compile()

wire.Wire.compile(path: string) -> Template

Compiles the template at path without rendering it, and returns it.

Useful for checking a directory of templates at build or startup time, so that a syntax error is found then rather than on the request that reaches it.

for name in os.list_dir('./views') {
  view.compile(name)
}

Parameters

  • path (string)

Returns Template

Raises TemplateNotFoundError

Raises TemplateSyntaxError

Wire.clear_cache()

wire.Wire.clear_cache() -> Wire

Forgets every compiled template.

A long-running process with set_auto_reload(false) needs this to notice a deployment.

Returns Wire

Wire.load()

wire.Wire.load(path: string, context, from: ?string, line: ?number, column: ?number) -> Template

The compiled template at path, from the cache when it is there and still current.

context names the element the template will sit inside, so that a partial holding table rows is parsed knowing it is going into a table. from, line and column describe where the include was written, for the error when the path goes nowhere.

The renderer calls this; there is rarely a reason to call it directly.

Parameters

  • path (string)
  • context (?string)
  • from (?string)
  • line (?number)
  • column (?number)

Returns Template

Raises TemplateNotFoundError

Raises TemplateSyntaxError

Wire.filter()

wire.Wire.filter(name: string) -> ?callable

The filter called name, or nil.

Parameters

  • name (string)

Returns ?callable

Wire.element()

wire.Wire.element(name: string) -> ?callable

The handler registered for the tag name, or nil.

Parameters

  • name (string)

Returns ?callable


2026, Richard Ore and The Zuri Contributors