13.11 The built-in tags
The built-in tags of django are in every Django template without {% load %}: how {% for %} builds its loop variables, {% if %} parses a condition with django/ and {% url %} reverses a name, and how {% cycle %}, {% ifchanged %}, {% regroup %} and {% querystring %} work.
The module is one of the three libraries every Engine gives each parser before the first token. Each tag in it but two is written there as a compile function and a subclass of Node. The function reads the tag’s words at compile time, refuses what it cannot parse, and turns each argument that stands for a value into a FilterExpression; the node resolves those against the context at each render. So a malformed tag fails at compile time, and a bad value at the render that meets it. The two, {% querystring %} and {% csp_nonce_attr %}, are simple tags (Libraries: Library, custom tags and {% load %}, which also has {% load %}). {% autoescape %} and the partial tags have Autoescaping and safe strings and Template partials: {% partialdef %} and {% partial %}.
The loop: {% for %} and ForNode
do_for reads {% for ship in ships %} at compile time: at least four words, an optional last word reversed, loop variables split on commas, the sequence as a filter expression, the body up to {% empty %} or {% endfor %}. ForNode.render then works in this order:
- It reads
"forloop"from the context: inside another loop, the outer loop’s dictionary, which becomes the new"parentloop"(else an empty one). - It pushes a level on the context for the rest, so the loop’s names are gone after
{% endfor %}. - It resolves the sequence with
ignore_failures=True, so a missing variable isNone, not"string_if_invalid", and counts as empty. A sequence without a length, a generator, is made a list, and its length taken before the first item. - An empty sequence renders the
{% empty %}branch, or nothing. - It puts one dictionary in the pushed level as
"forloop", with"parentloop"and"length", and before each item sets"counter","counter0","revcounter","revcounter0","first"and"last"in it. - One loop variable is set in the pushed level. Several are unpacked from the item, which must have as many elements or the render raises
ValueError, and pushed as a level of their own for the body. - It returns the output, joined and marked safe.
One dictionary, changed in place, serves the whole loop, and an inner loop’s "parentloop" is the outer’s dictionary itself.
Conditions: {% if %} and IfParser
do_if parses the condition of {% if %} and of each {% elif %}, and each branch up to the next clause, into an IfNode of pairs, a condition and a node list; {% else %} has None for its condition.
The condition is parsed by django/, a top-down operator-precedence parser. Each token has a binding power, lbp, how tightly it holds what stands on its left; TokenBase.nud is what it does at the start of an expression and TokenBase.led what it does after one, and both raise by default, with the message a misplaced token gets. OPERATORS holds a class for each operator, made by infix or prefix from a power and a function: "or" at 6, "and" at 7, the prefix "not" at 8, "in" and "not in" at 9, and "is", "is not" and the six comparisons at 10.
IfParser joins is not and not in into single tokens and makes each word a fresh operator or, failing that, an operand, which TemplateIfParser makes a TemplateLiteral around a compiled filter expression, so ships|length is one operand. IfParser is the whole algorithm: call the first token’s nud, then, while the next token binds more tightly than the power the call was given, call that token’s led with the expression so far. An operator’s led parses its right side with its own power, so equal powers group to the left. A malformed condition is a TemplateSyntaxError at compile time, such as “Unused ‘port’ at end of if expression.” for {% if ships port %}.
IfNode.render renders the first branch whose condition holds. An operand resolves with ignore_failures=True, so a missing variable is None. An operator evaluates its operands in its function, so and and or stop early, and turns any exception into False; a comment in infix says a template should not raise while rendering. A VariableDoesNotExist reaching IfNode.render, from a filter’s argument, counts as false.
{% url %} and URLNode
{% url %} compiles its first argument, the name, as a filter expression, so it may be quoted or a variable; takes as and a name off the end; and makes each other word a keyword argument, pk=port.pk, or a positional one, by kwarg_re. URLNode.render resolves them and calls reverse (Reversing a name) with a current application from the context’s request: its attribute "current_app", else the namespace of HttpRequest (Namespaces). A name that does not reverse raises NoReverseMatch, unless the tag ends with as, whose name then gets an empty string. The path is escaped when autoescaping is on.
Names a tag sets: {% with %} and as
{% with total=port pushes a level holding its names until {% endwith %}; do_with reads the pairs, or the older port, with token_kwargs. Every value is resolved before the push, so {% with a=1 b=a %} gives b the a from outside, and a missing variable in one becomes "string_if_invalid".
Given as and a name, {% url %}, {% firstof %}, {% now %}, {% widthratio %} and the simple tags assign it in the context’s top level, as {% regroup %} does: set inside {% for %}, {% with %} or {% block %}, it is gone after the end tag, and inside {% if %}, which pushes nothing, it outlasts it. {% cycle %} uses BaseContext, which writes where the name already is, else on top.
{% cycle %} and {% resetcycle %}
CycleNode keeps its place in the render context under the node itself: an iterator cycling through its compiled values, made at its first render. Template.render gives the render context a fresh level, and {% include %} renders its template that way each time (Rendering: nodes and the render context), so {% cycle 'odd' 'even' %} in a template included once for each of three ships renders "odd" all three times.
{% cycle 'odd' 'even' as row %} also sets row to each value, and "silent" after it stops it rendering. Named nodes are kept on the parser, so a name holds within one template; {% cycle row %} returns the same node, and every use advances it. {% resetcycle %} gives a fresh iterator to the named node or, with no name, to the last cycle tag before it that declared values.
{% ifchanged %}
IfChangedNode renders its body when its arguments’ values, or with none its rendered body, differ from last time, and otherwise its {% else %} branch or nothing. It keeps the last values under the node in the loop’s "forloop" dictionary, which is new each time the loop starts, so in a nested loop the comparison starts over on each pass of the outer one; outside a loop, in the render context.
{% regroup %}
{% regroup ships by kind as kinds %} takes exactly those words. The key is compiled as the variable {{ kinds.kind }}, and each ship is put in the context under kinds while its key resolves: a hack, its comment says, for want of a getattr that works through the context. The standard library’s groupby then joins only neighbours: ships of kinds brig, brig, bark, brig make three groups. Each is a GroupedResult of "grouper" and "list"; a missing list gives no groups.
{% querystring %}
querystring is a simple tag that takes the context. Its positional arguments, each a mapping, then its keyword arguments, are applied in order to an empty QueryDict. With no positional argument, the request’s HttpRequest.GET stands in, and a plain Context, which has no request attribute, raises AttributeError. None removes a key, an iterable other than a string sets its values but None, anything else replaces. The result always begins with "?" and is escaped when autoescaping is on: on a request for "/, {% querystring page=3 %} renders "?q=brig&page=3".
The other tags
| tag | renders | where |
|---|---|---|
{% firstof %} |
renders the first true argument, escaped when autoescaping is on | where FirstOfNode django/ |
{% now %} |
renders the time now, through the date filter; its argument is not resolved, only stripped of its first and last characters | where now django/ |
{% spaceless %} |
renders the body with white space between tags, and at its ends, removed | where SpacelessNode django/ |
{% verbatim %} |
renders the body as text, which the lexer left unparsed | where verbatim django/ |
{% templatetag %} |
renders a delimiter of the syntax, by name | where TemplateTagNode django/ |
{% widthratio %} |
renders a value over a maximum, times a width, rounded half to even | where WidthRatioNode django/ |
{% filter %} |
renders the body through filters, refusing the escape and safe filters | where do_filter django/ |
{% lorem %} |
renders placeholder Latin | where LoremNode django/ |
{% debug %} |
renders the context and the loaded modules, when settings.DEBUG is true (Template errors and debug information) |
where DebugNode django/ |
{% comment %} |
renders nothing: the parser skips to {% endcomment %} unparsed |
where comment django/ |
{% csrf_token %} |
renders a hidden input holding the CSRF context processor’s token, or nothing when the context has none | where CsrfTokenNode django/ |
{% csp_ |
renders a simple tag: nonce="…" from the nonce of django, or nothing; given a form’s media, its tags carrying that nonce |
where nonce_attr django/ |