commit 3bf1247: [Minor] Add lua-lupa library for Jinja2 templates

Vsevolod Stakhov vsevolod at highsecure.ru
Tue Mar 26 12:49:05 UTC 2019


Author: Vsevolod Stakhov
Date: 2019-03-26 12:36:41 +0000
URL: https://github.com/rspamd/rspamd/commit/3bf124702cfd42962e321791ba0849c4a5799ecd (HEAD -> master)

[Minor] Add lua-lupa library for Jinja2 templates

---
 CMakeLists.txt                         |    1 +
 contrib/{lua-lpeg => lua-lupa}/LICENSE |   12 +-
 contrib/lua-lupa/README.md             |  179 ++++
 contrib/lua-lupa/lupa.lua              | 1807 ++++++++++++++++++++++++++++++++
 4 files changed, 1994 insertions(+), 5 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index ecd2440ba..b0de490dc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1376,6 +1376,7 @@ ENDFOREACH(LUA_LIB)
 INSTALL(FILES "contrib/lua-fun/fun.lua" DESTINATION ${LUALIBDIR})
 INSTALL(FILES "contrib/lua-argparse/argparse.lua" DESTINATION ${LUALIBDIR})
 INSTALL(FILES "contrib/lua-tableshape/tableshape.lua" DESTINATION ${LUALIBDIR})
+INSTALL(FILES "contrib/lua-lupa/lupa.lua" DESTINATION ${LUALIBDIR})
 
 IF(ENABLE_TORCH MATCHES "ON")
 	INSTALL(FILES "contrib/lua-moses/moses.lua" DESTINATION ${LUALIBDIR})
diff --git a/contrib/lua-lpeg/LICENSE b/contrib/lua-lupa/LICENSE
similarity index 87%
copy from contrib/lua-lpeg/LICENSE
copy to contrib/lua-lupa/LICENSE
index cea2d8b5e..66c114190 100644
--- a/contrib/lua-lpeg/LICENSE
+++ b/contrib/lua-lupa/LICENSE
@@ -1,4 +1,6 @@
-Copyright © 2007-2015 Lua.org, PUC-Rio.
+The MIT License
+
+Copyright (c) 2015-2018 Mitchell
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -7,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE. 
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/contrib/lua-lupa/README.md b/contrib/lua-lupa/README.md
new file mode 100644
index 000000000..edf6dce2a
--- /dev/null
+++ b/contrib/lua-lupa/README.md
@@ -0,0 +1,179 @@
+# Lupa
+
+## Introduction
+
+Lupa is a [Jinja2][] template engine implementation written in Lua and supports
+Lua syntax within tags and variables.
+
+Lupa was sponsored by the [Library of the University of Antwerp][].
+
+[Jinja2]: http://jinja.pocoo.org
+[Library of the University of Antwerp]: http://www.uantwerpen.be/
+
+## Requirements
+
+Lupa has the following requirements:
+
+* [Lua][] 5.1, 5.2, or 5.3.
+* The [LPeg][] library.
+
+[Lua]: http://www.lua.org
+[LPeg]: http://www.inf.puc-rio.br/~roberto/lpeg/
+
+## Download
+
+Download Lupa from the project’s [download page][].
+
+[download page]: download
+
+## Installation
+
+Unzip Lupa and place the "lupa.lua" file in your Lua installation's
+`package.path`. This location depends on your version of Lua. Typical locations
+are listed below.
+
+* Lua 5.1: */usr/local/share/lua/5.1/* or */usr/local/share/lua/5.1/*
+* Lua 5.2: */usr/local/share/lua/5.2/* or */usr/local/share/lua/5.2/*
+* Lua 5.3: */usr/local/share/lua/5.3/* or */usr/local/share/lua/5.3/*
+
+You can also place the "lupa.lua" file wherever you'd like and add it to Lua's
+`package.path` manually in your program. For example, if Lupa was placed in a
+*/home/user/lua/* directory, it can be used as follows:
+
+    package.path = package.path..';/home/user/lua/?.lua'
+
+## Usage
+
+Lupa is simply a Lua library. Its `lupa.expand()` and `lupa.expand_file()`
+functions may called to process templates. For example:
+
+    lupa = require('lupa')
+    lupa.expand("hello {{ s }}!", {s = "world"}) --> "hello world!"
+    lupa.expand("{% for i in {1, 2, 3} %}{{ i }}{% endfor %}") --> 123
+
+By default, Lupa loads templates relative to the current working directory. This
+can be changed by reconfiguring Lupa:
+
+    lupa.expand_file('name') --> expands template "./name"
+    lupa.configure{loader = lupa.loaders.filesystem('path/to/templates')}
+    lupa.expand_file('name') --> expands template "path/to/templates/name"
+
+See Lupa's [API documentation][] for more information.
+
+[API documentation]: api.html
+
+## Syntax
+
+Please refer to Jinja2's extensive [template documentation][]. Any
+incompatibilities are listed in the sections below.
+
+[template documentation]: http://jinja.pocoo.org/docs/dev/templates/
+
+## Comparison with Jinja2
+
+While Lua and Python (Jinja2's implementation language) share some similarities,
+the languages themselves are fundamentally different. Nevertheless, a
+significant effort was made to support a vast majority of Jinja2's Python-style
+syntax. As a result, Lupa passes Jinja2's test suite with only a handful of
+modifications. The comprehensive list of differences between Lupa and Jinja2 is
+described in the following sections.
+
+### Fundamental Differences
+
+* Expressions use Lua's syntax instead of Python's, so many of Python's
+  syntactic constructs are not valid. However, the following constructs
+  *are valid*, despite being invalid in pure Lua:
+
+  + Iterating over table literals or table variables directly in a "for" loop:
+
+        {% for i in {1, 2, 3} %}...{% endfor %}
+
+  + Conditional loops via an "if" expression suffix:
+
+        {% for x in range(10) if is_odd(x) %}...{% endfor %}
+
+  + Table unpacking for list elements when iterating through a list of lists:
+
+        {% for a, b, c in {{1, 2, 3}, {4, 5, 6}} %}...{% endfor %}
+
+  + Default values for macro arguments:
+
+        {% macro m(a, b, c='c', d='d') %}...{% endmacro %}
+
+* Strings do not have unicode escapes nor is unicode interpreted in any way.
+
+### Syntactic Differences
+
+* Line statements are not supported due to parsing complexity.
+* In `{% for ... %}` loops, the `loop.length`, `loop.revindex`,
+  `loop.revindex0`, and `loop.last` variables only apply to sequences, where
+  Lua's `'#'` operator applies.
+* The `{% continue %}` and `{% break %}` loop controls are not supported due to
+  complexity.
+* Loops may be used recursively by default, so the `recursive` loop modifier is
+  not supported.
+* The `is` operator is not supported by Lua, so tests of the form `{{ x is y }}`
+  should be written `{{ is_y(x) }}` (e.g. `{{ is_number(42) }}`).
+* Filters cannot occur after tokens within an expression (e.g.
+  `{{ "foo"|upper .. "bar"|upper }}`), but can only occur at the end of an
+  expression (e.g. `{{ "foo".."bar"|upper }}`).
+* Blocks always have access to scoped variables, so the `scoped` block modifier
+  is not supported.
+* Named block end tags are not supported since the parser cannot easily keep
+  track of that state information.
+* Any `{% block ... %}` tags within a "false" block (e.g. `{% if a %}` where `a`
+  evaluates to `false`) are never read and stored due to the parser
+  implementation.
+* Inline "if" expressions (e.g. `{% extends b if a else c %}`) are not
+  supported. Instead, use a Lua conditional expression
+  (e.g. `{% extends a and b or c %}`).
+* Any `{% extends ... %}` tags within a sub-scope are not effective outside that
+  scope (e.g. `{% if a %}{% extends a %}{% else %}{% extends b %}{% endif %}`).
+  Instead, use a Lua conditional expression (e.g. `{% extends a or b %}`).
+* Macros are simply Lua functions and have no metadata attributes.
+* Macros do not have access to a `kwargs` variable since Lua does not support
+  keyword arguments.
+* `{% from x import y %}` tags are not supported. Instead, you must use either
+  `{% import x %}`, which imports all globals in `x` into the current
+  environment, or use `{% import x as z %}`, which imports all globals in `x`
+  into the variable `z`.
+* `{% set ... %}` does not support multiple assignment. Use `{% do ...%}`
+  instead. The catch is that `{% do ... %}` does not support filters.
+* The `{% trans %}` and `{% endtrans %}` tags, `{% with %}` and `{% endwith %}`
+  tags, and `{% autoescape %}` and `{% endautoescape %}` tags are not supported
+  since they are outside the scope of this implementation.
+
+### Filter Differences
+
+* Only the `batch`, `groupby`, and `slice` filters return generators which
+  produce one item at a time when looping. All other filters that produce
+  iterable results generate all items at once.
+* The `float` filter only works in Lua 5.3 since that version of Lua has a
+  distinction between floats and integers.
+* The `safe` filter must appear at the end of a filter chain since its output
+  cannot be passed to any other filter.
+
+### Function Differences
+
+* The global `range(n)` function returns a sequence from 1 to `n`, inclusive,
+  since lists start at 1 in Lua.
+* No `lipsum()`, `dict()`, or `joiner()` functions for the sake of simplicity.
+
+### API Differences
+
+* Lupa has a much simpler API consisting of just four functions and three
+  fields:
+
+  + `lupa.expand()`: Expands a string template subject to an environment.
+  + `lupa.expand_file()`: Expands a file template subject to an environment.
+  + `lupa.configure()` Configures delimiters and template options.
+  + `lupa.reset()`: Resets delimiters and options to their defaults.
+  + `lupa.env`: The default environment for templates.
+  + `lupa.filters`: The set of available filters (`escape`, `join`, etc.).
+  + `lupa.tests`: The set of available tests (`is_odd`, `is_defined`, etc.).
+
+* There is no bytecode caching.
+* Lupa has no extension mechanism. Instead, modify `lupa.env`, `lupa.filters`,
+  and `lupa.tests` directly. However, the parser cannot be extended.
+* Sandboxing is not supported, although `lupa.env` is safe by default (`io`,
+  `os.execute`, `os.remove`, etc. are not available).
diff --git a/contrib/lua-lupa/lupa.lua b/contrib/lua-lupa/lupa.lua
new file mode 100644
index 000000000..fc49ac258
--- /dev/null
+++ b/contrib/lua-lupa/lupa.lua
@@ -0,0 +1,1807 @@
+-- Copyright 2015-2019 Mitchell mitchell.att.foicica.com. See LICENSE.
+-- Sponsored by the Library of the University of Antwerp.
+-- Contributions from Ana Balan.
+-- Lupa templating engine.
+
+--[[ This comment is for LuaDoc.
+---
+-- Lupa is a Jinja2 template engine implementation written in Lua and supports
+-- Lua syntax within tags and variables.
+module('lupa')]]
+local M = {}
+
+local lpeg = require('lpeg')
+lpeg.locale(lpeg)
+local space, newline = lpeg.space, lpeg.P('\r')^-1 * '\n'
+local P, S, V = lpeg.P, lpeg.S, lpeg.V
+local C, Cc, Cg, Cp, Ct = lpeg.C, lpeg.Cc, lpeg.Cg, lpeg.Cp, lpeg.Ct
+
+---
+-- Lupa's expression filters.
+-- @class table
+-- @name filters
+M.filters = {}
+
+---
+-- Lupa's value tests.
+-- @class table
+-- @name tests
+M.tests = {}
+
+---
+-- Lupa's template loaders.
+-- @class table
+-- @name loaders
+M.loaders = {}
+
+-- Lua version compatibility.
+if _VERSION == 'Lua 5.1' then
+  function load(ld, source, mode, env)
+    local f, err = loadstring(ld)
+    if f and env then return setfenv(f, env) end
+    return f, err
+  end
+  table.unpack = unpack
+end
+
+local newline_sequence, keep_trailing_newline, autoescape = '\n', false, false
+local loader
+
+-- Creates and returns a token pattern with token name *name* and pattern
+-- *patt*.
+-- The returned pattern captures three values: the token's position and name,
+-- and either a string value or table of capture values.
+-- Tokens are used to construct an Abstract Syntax Tree (AST) for a template.
+-- @param name The name of the token.
+-- @param patt The pattern to match. It must contain only one capture: either a
+--   string or table of captures.
+-- @see evaluate
+local function token(name, patt) return Cp() * Cc(name) * patt end
+
+-- Returns an LPeg pattern that immediately raises an error with message
+-- *errmsg* for invalid syntax when parsing a template.
+-- @param errmsg The error message to raise an error with.
+local function lpeg_error(errmsg)
+  return P(function(input, index)
+    input = input:sub(1, index)
+    local _, line_num = input:gsub('\n', '')
+    local col_num = #input:match('[^\n]*$')
+    error(string.format('Parse Error in file "%s" on line %d, column %d: %s',
+                        M._FILENAME, line_num + 1, col_num, errmsg), 0)
+  end)
+end
+
+---
+-- Configures the basic delimiters and options for templates.
+-- This function then regenerates the grammar for parsing templates.
+-- Note: this function cannot be used iteratively to configure Lupa options.
+-- Any options not provided are reset to their default values.
+-- @param ts The tag start delimiter. The default value is '{%'.
+-- @param te The tag end delimiter. The default value is '%}'.
+-- @param vs The variable start delimiter. The default value is '{{'.
+-- @param ve The variable end delimiter. The default value is '}}'.
+-- @param cs The comment start delimiter. The default value is '{#'.
+-- @param ce The comment end delimiter. The default value is '#}'.
+-- @param options Optional set of options for templates:
+--
+--   * `trim_blocks`: Trim the first newline after blocks.
+--   * `lstrip_blocks`: Strip line-leading whitespace in front of tags.
+--   * `newline_sequence`: The end-of-line character to use.
+--   * `keep_trailing_newline`: Whether or not to keep a newline at the end of
+--     a template.
+--   * `autoescape`: Whether or not to autoescape HTML entities. May be a
+--     function that accepts the template's filename as an argument and returns
+--     a boolean.
+--   * `loader`: Function that receives a template name to load and returns the
+--     path to that template.
+-- @name configure
+function M.configure(ts, te, vs, ve, cs, ce, options)
+  if type(ts) == 'table' then options, ts = ts, nil end
+  if not ts then ts = '{%' end
+  if not te then te = '%}' end
+  if not vs then vs = '{{' end
+  if not ve then ve = '}}' end
+  if not cs then cs = '{#' end
+  if not ce then ce = '#}' end
+
+  -- Tokens for whitespace control.
+  local lstrip = token('lstrip', C('-')) + '+' -- '+' is handled by grammar
+  local rstrip = token('rstrip', -(P(te) + ve + ce) * C('-'))
+
+  -- Configure delimiters, including whitespace control.
+  local tag_start = P(ts) * lstrip^-1 * space^0
+  local tag_end = space^0 * rstrip^-1 * P(te)
+  local variable_start = P(vs) * lstrip^-1 * space^0
+  local variable_end = space^0 * rstrip^-1 * P(ve)
+  local comment_start = P(cs) * lstrip^-1 * space^0
+  local comment_end = space^0 * rstrip^-1 * P(ce)
+  if options and options.trim_blocks then
+    -- Consider whitespace, including a newline, immediately following a tag as
+    -- part of that tag so it is not captured as plain text. Basically, strip
+    -- the trailing newline from tags.
+    tag_end = tag_end * S(' \t')^0 * newline^-1
+    comment_end = comment_end * S(' \t')^0 * newline^-1
+  end
+
+  -- Error messages.
+  local variable_end_error = lpeg_error('"'..ve..'" expected')
+  local comment_end_error = lpeg_error('"'..ce..'" expected')
+  local tag_end_error = lpeg_error('"'..te..'" expected')
+  local endraw_error = lpeg_error('additional tag or "'..ts..' endraw '..te..
+                                  '" expected')
+  local expr_error = lpeg_error('expression expected')
+  local endblock_error = lpeg_error('additional tag or "'..ts..' endblock '..
+                                    te..'" expected')
+  local endfor_error = lpeg_error('additional tag or "'..ts..' endfor '..te..
+                                  '" expected')
+  local endif_error = lpeg_error('additional tag or "'..ts..' endif '..te..
+                                 '" expected')
+  local endmacro_error = lpeg_error('additional tag or "'..ts..' endmacro '..
+                                    te..'" expected')
+  local endcall_error = lpeg_error('additional tag or "'..ts..' endcall '..te..
+                                   '" expected')
+  local endfilter_error = lpeg_error('additional tag or "'..ts..' endfilter '..
+                                     te..'" expected')
+  local tag_error = lpeg_error('unknown or unexpected tag')
+  local main_error = lpeg_error('unexpected character; text or tag expected')
+
+  -- Grammar.
+  M.grammar = Ct(P{
+    -- Utility patterns used by tokens.
+    entity_start = tag_start + variable_start + comment_start,
+    any_text = (1 - V('entity_start'))^1,
+    -- Allow '{{' by default in expression text since it is valid in Lua.
+    expr_text = (1 - tag_end - tag_start - comment_start)^1,
+    -- When `options.lstrip_blocks` is enabled, ignore leading whitespace
+    -- immediately followed by a tag (as long as '+' is not present) so that
+    -- whitespace not captured as plain text. Basically, strip leading spaces
+    -- from tags.
+    line_text = (1 - newline - V('entity_start'))^1,
+    lstrip_entity_start = -P(vs) * (P(ts) + cs) * -P('+'),
+    lstrip_space = S(' \t')^1 * #V('lstrip_entity_start'),
+    text_lines = V('line_text') * (newline * -(S(' \t')^0 * V('lstrip_entity_start')) * V('line_text'))^0 * newline^-1 + newline,
+
+    -- Plain text.
+    text = (not options or not options.lstrip_blocks) and
+           token('text', C(V('any_text'))) or
+           V('lstrip_space') + token('text', C(V('text_lines'))),
+
+    -- Variables: {{ expr }}.
+    lua_table = '{' * ((1 - S('{}')) + V('lua_table'))^0 * '}',
+    variable = variable_start *
+               token('variable', C((V('lua_table') + (1 - variable_end))^0)) *
+               (variable_end + variable_end_error),
+
+    -- Filters: handled in variable evaluation.
+
+    -- Tests: handled in control structure expression evaluation.
+
+    -- Comments: {# comment #}.
+    comment = comment_start * (1 - comment_end)^0 * (comment_end + comment_end_error),
+
+    -- Whitespace control: handled in tag/variable/comment start/end.
+
+    -- Escaping: {% raw %} body {% endraw %}.
+    raw_block = tag_start * 'raw' * (tag_end + tag_end_error) *
+                token('text', C((1 - (tag_start * 'endraw' * tag_end))^0)) *
+                (tag_start * 'endraw' * tag_end + endraw_error),
+
+    -- Note: line statements are not supported since this grammer cannot parse
+    -- Lua itself.
+
+    -- Template inheritence.
+    -- {% block ... %} body {% endblock %}
+    block_block = tag_start * 'block' * space^1 * token('block', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) *
+                  V('body')^-1)) *
+                  (tag_start * 'endblock' * tag_end + endblock_error),
+    -- {% extends ... %}
+    extends_tag = tag_start * 'extends' * space^1 * token('extends', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error),
+    -- Super blocks are handled in variables.
+    -- Note: named block end tags are not supported since keeping track of that
+    -- state information is difficult.
+    -- Note: block nesting and scope is not applicable since blocks always have
+    -- access to scoped variables in this implementation.
+
+    -- Control Structures.
+    -- {% for expr %} body {% else %} body {% endfor %}
+    for_block = tag_start * 'for' * space^1 * token('for', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) *
+                V('body')^-1 *
+                Cg(Ct(tag_start * 'else' * tag_end *
+                      V('body')^-1), 'else')^-1)) *
+                (tag_start * 'endfor' * tag_end + endfor_error),
+    -- {% if expr %} body {% elseif expr %} body {% else %} body {% endif %}
+    if_block = tag_start * 'if' * space^1 * token('if', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) *
+               V('body')^-1 *
+               Cg(Ct(Ct(tag_start * 'elseif' * space^1 * (Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) *
+                       V('body')^-1)^1), 'elseif')^-1 *
+               Cg(Ct(tag_start * 'else' * tag_end *
+                       V('body')^-1), 'else')^-1)) *
+               (tag_start * 'endif' * tag_end + endif_error),
+    -- {% macro expr %} body {% endmacro %}
+    macro_block = tag_start * 'macro' * space^1 * token('macro', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) *
+                  V('body')^-1)) *
+                  (tag_start * 'endmacro' * tag_end + endmacro_error),
+    -- {% call expr %} body {% endcall %}
+    call_block = tag_start * 'call' * (space^1 + #P('(')) * token('call', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) *
+                  V('body')^-1)) *
+                  (tag_start * 'endcall' * tag_end + endcall_error),
+    -- {% filter expr %} body {% endfilter %}
+    filter_block = tag_start * 'filter' * space^1 * token('filter', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) *
+                   V('body')^-1)) *
+                   (tag_start * 'endfilter' * tag_end + endfilter_error),
+    -- {% set ... %}
+    set_tag = tag_start * 'set' * space^1 * token('set', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error),
+    -- {% include ... %}
+    include_tag = tag_start * 'include' * space^1 * token('include', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error),
+    -- {% import ... %}
+    import_tag = tag_start * 'import' * space^1 * token('import', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error),
+
+    -- Note: i18n is not supported since it is out of scope for this
+    -- implementation.
+
+    -- Expression statement: {% do ... %}.
+    do_tag = tag_start * 'do' * space^1 * token('do', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error),
+
+    -- Note: loop controls are not supported since that would require jumping
+    -- between "scopes" (e.g. from within an "if" block to outside that "if"
+    -- block's parent "for" block when coming across a {% break %} tag).
+
+    -- Note: with statement is not supported since it is out of scope for this
+    -- implementation.
+
+    -- Note: autoescape is not supported since it is out of scope for this
+    -- implementation.
+
+    -- Any valid blocks of text or tags.
+    body = (V('text') + V('variable') + V('comment') + V('raw_block') +
+            V('block_block') + V('extends_tag') + V('for_block') +
+            V('if_block') + V('macro_block') + V('call_block') +
+            V('filter_block') + V('set_tag') + V('include_tag') +
+            V('import_tag') + V('do_tag'))^0,
+
+    -- Main pattern.
+    V('body') * (-1 + tag_start * tag_error + main_error),
+  })
+
+  -- Other options.
+  if options and options.newline_sequence then
+    assert(options.newline_sequence:find('^\r?\n$'),
+           'options.newline_sequence must be "\r\n" or "\n"')
+    newline_sequence = options.newline_sequence
+  else
+    newline_sequence = '\n'
+  end
+  if options and options.keep_trailing_newline then
+    keep_trailing_newline = options.keep_trailing_newline
+  else
+    keep_trailing_newline = false
+  end
+  if options and options.autoescape then
+    autoescape = options.autoescape
+  else
+    autoescape = false
+  end
+  if options and options.loader then
+    assert(type(options.loader) == 'function',
+           'options.loader must be a function that returns a filename')
+    loader = options.loader
+  else
+    loader = M.loaders.filesystem()
+  end
+end
+
+-- Wraps Lua's `assert()` in template environment *env* such that, when called
+-- in conjunction with another Lua function that produces an error message (e.g.
+-- `load()` and `pcall()`), that error message's context (source and line
+-- number) is replaced by the template's context.
+-- This results in Lua's error messages pointing to a template position rather
+-- than this library's source code.
+-- @param env The environment for the currently running template. It must have
+--   a `_SOURCE` field with the template's source text and a `_POSITION` field
+--   with the current position of expansion.
+-- @param ... Arguments to Lua's `assert()`.
+local function env_assert(env, ...)
+  if not select(1, ...) then
+    local input = env._LUPASOURCE:sub(1, env._LUPAPOSITION)
+    local _, line_num = input:gsub('\n', '')
+    local col_num = #input:match('[^\n]*$')
+    local errmsg = select(2, ...)
+    errmsg = errmsg:match(':%d+: (.*)$') or errmsg -- reformat if necessary
+    error(string.format('Runtime Error in file "%s" on line %d, column %d: %s',
+                        env._LUPAFILENAME, line_num + 1, col_num, errmsg), 0)
+  end
+  return ...
+end
+
+-- Returns a generator that returns the position and filter in a list of
+-- filters, taking into account '|'s that may be within filter arguments.
+-- @usage for pos, filter in each_filter('foo|join("|")|bar') do ... end
+local function each_filter(s)
+  local init = 1
+  return function(s)
+    local pos, filter, e = s:match('^%s*()([^|(]+%b()[^|]*)|?()', init)
+    if not pos then pos, filter, e = s:match('()([^|]+)|?()', init) end
+    init = e
+    return pos, filter
+  end, s
+end
+
+-- Evaluates template variable *expression* subject to template environment
+-- *env*, applying any filters given in *expression*.
+-- @param expression The string expression to evaluate.
+-- @param env The environment to evaluate the expression in.
+local function eval(expression, env)
+  local expr, pos, filters = expression:match('^([^|]*)|?()(.-)$')
+  -- Evaluate base expression.
+  local f = env_assert(env, load('return '..expr, nil, nil, env))
+  local result = select(2, env_assert(env, pcall(f)))
+  -- Apply any filters.
+  local results, multiple_results = nil, false
+  local p = env._LUPAPOSITION + pos - 1 -- mark position at first filter
+  for pos, filter in each_filter(filters) do
+    env._LUPAPOSITION = p + pos - 1 -- update position for error messages
+    local name, params = filter:match('^%s*([%w_]+)%(?(.-)%)?%s*$')
+    f = M.filters[name]
+    env_assert(env, f, 'unknown filter "'..name..'"')
+    local args = env_assert(env, load('return {'..params..'}', nil, nil, env),
+                            'invalid filter parameter(s) for "'..name..'"')()
+    if not multiple_results then
+      results = {select(2,
+                        env_assert(env, pcall(f, result, table.unpack(args))))}
+    else
+      for i = 1, #results do table.insert(args, i, results[i]) end
+      results = {select(2, env_assert(env, pcall(f, table.unpack(args))))}
+    end
+    result, multiple_results = results[1], #results > 1
+  end
+  if multiple_results then return table.unpack(results) end
+  return result
+end
+
+local iterate
+
+-- Iterates over *ast*, a collection of tokens from a portion of a template's
+-- Abstract Syntax Tree (AST), evaluating any expressions in template
+-- environment *env*, and returns a concatenation of the results.
+-- @param ast A template's AST or portion of its AST (e.g. portion inside a
+--   'for' control structure).
+-- @param env Environment to evaluate any expressions in.
+local function evaluate(ast, env)
+  local chunks = {}
+  local extends -- text of a parent template
+  local rstrip -- flag for stripping leading whitespace of next token
+  for i = 1, #ast, 3 do
+    local pos, token, block = ast[i], ast[i + 1], ast[i + 2]
+    env._LUPAPOSITION = pos
+    if token == 'text' then
+      chunks[#chunks + 1] = block
+    elseif token == 'variable' then
+      local value = eval(block, env)
+      if autoescape then
+        local escape = autoescape
+        if type(autoescape) == 'function' then
+          escape = autoescape(env._LUPAFILENAME) -- TODO: test
+        end
+        if escape and type(value) == 'string' then
+          value = M.filters.escape(value)
+        end
+      end
+      chunks[#chunks + 1] = value ~= nil and tostring(value) or ''
+    elseif token == 'extends' then
+      env_assert(env, not extends,
+                 'cannot have multiple "extends" in the same scope')
+      local file = eval(block, env) -- covers strings and variables
+      extends = file
+      env._LUPAEXTENDED = true -- used by parent templates
+    elseif token == 'block' then
+      local name = block.expression:match('^[%w_]+$')
+      env_assert(env, name, 'invalid block name')
+      -- Store the block for potential use by the parent template if this
+      -- template is a child template, or for use by `self`.
+      if not env._LUPABLOCKS then env._LUPABLOCKS = {} end
+      if not env._LUPABLOCKS[name] then env._LUPABLOCKS[name] = {} end
+      table.insert(env._LUPABLOCKS[name], 1, block)
+      -- Handle the block properly.
+      if not extends then
+        if not env._LUPAEXTENDED then
+          -- Evaluate the block normally.
+          chunks[#chunks + 1] = evaluate(block, env)
+        else
+          -- A child template is overriding this parent's named block. Evaluate
+          -- the child's block and use it instead of the parent's.
+          local blocks = env._LUPABLOCKS[name]
+          local super_env = setmetatable({super = function()
+            -- Loop through the chain of defined blocks, evaluating from top to
+            -- bottom, and return the bottom block. In each sub-block, the
+            -- 'super' variable needs to point to the next-highest block's
+            -- evaluated result.
+            local super = evaluate(block, env) -- start with parent block
+            local sub_env = setmetatable({super = function() return super end},
+                                         {__index = env})
+            for i = 1, #blocks - 1 do super = evaluate(blocks[i], sub_env) end
+            return super
+          end}, {__index = env})
+          chunks[#chunks + 1] = evaluate(blocks[#blocks], super_env)
+        end
+      end
+    elseif token == 'for' then
+      local expr = block.expression
+      local p = env._LUPAPOSITION -- mark position at beginning of expression
+      -- Extract variable list and generator.
+      local patt = '^([%w_,%s]+)%s+in%s+()(.+)%s+if%s+(.+)$'
+      local var_list, pos, generator, if_expr = expr:match(patt)
+      if not var_list then
+        var_list, pos, generator = expr:match('^([%w_,%s]+)%s+in%s+()(.+)$')
+      end
+      env_assert(env, var_list and generator, 'invalid for expression')
+      -- Store variable names in a list for loop assignment.
+      local variables = {}
+      for variable, pos in var_list:gmatch('([^,%s]+)()') do
+        env._LUPAPOSITION = p + pos - 1 -- update position for error messages
+        env_assert(env, variable:find('^[%a_]') and variable ~= 'loop',
+                   'invalid variable name')
+        variables[#variables + 1] = variable
+      end
+      -- Evaluate the generator and perform the iteration.
+      env._LUPAPOSITION = p + pos - 1 -- update position to generator
+      if not generator:find('|') then
+        generator = env_assert(env, load('return '..generator, nil, nil, env))
+      else
+        local generator_expr = generator
+        generator = function() return eval(generator_expr, env) end
*** OUTPUT TRUNCATED, 1356 LINES SKIPPED ***


More information about the Commits mailing list