├── tests ├── data │ ├── imports │ │ ├── item │ │ ├── o_printer │ │ ├── header │ │ ├── module │ │ ├── main │ │ └── exports │ ├── debug │ │ ├── broken.html │ │ └── syntaxerror.html │ ├── other │ │ └── parent.html │ ├── test_macro │ └── inheritence │ │ ├── dynamic │ │ ├── master1 │ │ ├── master2 │ │ └── child │ │ ├── multi │ │ ├── master1 │ │ ├── master2 │ │ └── child │ │ ├── preserve │ │ ├── b │ │ └── a │ │ ├── scoped │ │ ├── layout.html │ │ ├── helpers.html │ │ ├── master.html │ │ └── index.html │ │ ├── super │ │ ├── b │ │ ├── a │ │ └── c │ │ ├── macro_scoping │ │ ├── standard.html │ │ ├── test.html │ │ └── details.html │ │ ├── level1 │ │ ├── level4 │ │ ├── super_scoped │ │ └── master.html │ │ ├── level2 │ │ ├── level3 │ │ ├── working │ │ ├── doublee │ │ └── layout └── suite.lua ├── .hgtags ├── docs ├── changelog.md ├── fill_layout.lua ├── _layouts │ └── default.html ├── style.css ├── markdowndoc.lua ├── index.md └── api.md ├── Makefile ├── LICENSE ├── README.md └── lupa.lua /tests/data/imports/item: -------------------------------------------------------------------------------- 1 | {{ item }} 2 | -------------------------------------------------------------------------------- /tests/data/imports/o_printer: -------------------------------------------------------------------------------- 1 | ({{ o }}) 2 | -------------------------------------------------------------------------------- /tests/data/imports/header: -------------------------------------------------------------------------------- 1 | [{{ foo }}|{{ 23 }}] 2 | -------------------------------------------------------------------------------- /tests/data/debug/broken.html: -------------------------------------------------------------------------------- 1 | Before 2 | {{ fail() }} 3 | After 4 | -------------------------------------------------------------------------------- /tests/data/other/parent.html: -------------------------------------------------------------------------------- 1 | (({% block title %}{% endblock %})) 2 | -------------------------------------------------------------------------------- /tests/data/test_macro: -------------------------------------------------------------------------------- 1 | {% macro test(foo) %}[{{ foo }}]{% endmacro %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/dynamic/master1: -------------------------------------------------------------------------------- 1 | MASTER1{% block x %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/dynamic/master2: -------------------------------------------------------------------------------- 1 | MASTER2{% block x %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/multi/master1: -------------------------------------------------------------------------------- 1 | MASTER1{% block x %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/multi/master2: -------------------------------------------------------------------------------- 1 | MASTER2{% block x %}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/data/imports/module: -------------------------------------------------------------------------------- 1 | {% macro test() %}[{{ foo }}|{{ bar }}]{% endmacro %} 2 | -------------------------------------------------------------------------------- /tests/data/imports/main: -------------------------------------------------------------------------------- 1 | {% for item in {1, 2, 3} %}{% include 'item' %}{% endfor %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/dynamic/child: -------------------------------------------------------------------------------- 1 | {% extends master %}{% block x %}CHILD{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/data/debug/syntaxerror.html: -------------------------------------------------------------------------------- 1 | Foo 2 | {% for item in broken %} 3 | ... 4 | {% endif %} 5 | -------------------------------------------------------------------------------- /tests/data/inheritence/preserve/b: -------------------------------------------------------------------------------- 1 | {% extends "a" %}{% block x %}B{{ super() }}{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/scoped/layout.html: -------------------------------------------------------------------------------- 1 | 2 | {% block useless %}{% endblock %} 3 | -------------------------------------------------------------------------------- /tests/data/inheritence/super/b: -------------------------------------------------------------------------------- 1 | {% extends "a" %}{% block data %}({{ super() }}){% endblock %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/macro_scoping/standard.html: -------------------------------------------------------------------------------- 1 | 2 | {% block content %} {% endblock %} 3 | -------------------------------------------------------------------------------- /tests/data/inheritence/multi/child: -------------------------------------------------------------------------------- 1 | {% extends master or 'master1' %}{% block x %}CHILD{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/preserve/a: -------------------------------------------------------------------------------- 1 | {% if false %}{% block x %}A{% endblock %}{% endif %}{{ self.x() }} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/level1: -------------------------------------------------------------------------------- 1 | {% extends "layout" %} 2 | {% block block1 %}block 1 from level1{% endblock %} 3 | -------------------------------------------------------------------------------- /tests/data/inheritence/level4: -------------------------------------------------------------------------------- 1 | {% extends "level3" %} 2 | {% block block3 %}block 3 from level4{% endblock %} 3 | -------------------------------------------------------------------------------- /tests/data/inheritence/scoped/helpers.html: -------------------------------------------------------------------------------- 1 | 2 | {% macro foo(x) %}{{ the_foo + x }}{% endmacro %} 3 | -------------------------------------------------------------------------------- /tests/data/inheritence/scoped/master.html: -------------------------------------------------------------------------------- 1 | {% for item in seq %}[{% block item %}{% endblock %}]{% endfor %} 2 | -------------------------------------------------------------------------------- /.hgtags: -------------------------------------------------------------------------------- 1 | 8e10c31c3b1980bebed089bf0825e59329822627 lupa_0.1_alpha 2 | 5c8613bf064a93dc1617403ea9de4a42f322bb68 lupa_1.0 3 | -------------------------------------------------------------------------------- /tests/data/inheritence/super/a: -------------------------------------------------------------------------------- 1 | {% block intro %}INTRO{% endblock %}|BEFORE|{% block data %}INNER{% endblock %}|AFTER 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/super_scoped/master.html: -------------------------------------------------------------------------------- 1 | {% for item in seq %}[{% block item %}{{ item }}{% endblock %}]{% endfor %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/super/c: -------------------------------------------------------------------------------- 1 | {% extends "b" %}{% block intro %}--{{ super() }}--{% endblock %}{% block data %}[{{ super() }}]{% endblock %} 2 | -------------------------------------------------------------------------------- /tests/data/inheritence/level2: -------------------------------------------------------------------------------- 1 | {% extends "level1" %} 2 | {% block block2 %}{% block block5 %}nested block 5 from level2{% 3 | endblock %}{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/data/inheritence/level3: -------------------------------------------------------------------------------- 1 | {% extends "level2" %} 2 | {% block block5 %}block 5 from level3{% endblock %} 3 | {% block block4 %}block 4 from level3{% endblock %} 4 | -------------------------------------------------------------------------------- /tests/data/inheritence/working: -------------------------------------------------------------------------------- 1 | {% extends "layout" %} 2 | {% block block1 %} 3 | {% if false %} 4 | {% block block2 %} 5 | this should workd 6 | {% endblock %} 7 | {% endif %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /tests/data/inheritence/doublee: -------------------------------------------------------------------------------- 1 | {% extends "layout" %} 2 | {% extends "layout" %} 3 | {% block block1 %} 4 | {% if false %} 5 | {% block block2 %} 6 | this should workd 7 | {% endblock %} 8 | {% endif %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /tests/data/inheritence/layout: -------------------------------------------------------------------------------- 1 | |{% block block1 %}block 1 from layout{% endblock %} 2 | |{% block block2 %}block 2 from layout{% endblock %} 3 | |{% block block3 %} 4 | {% block block4 %}nested block 4 from layout{% endblock %} 5 | {% endblock %}| 6 | -------------------------------------------------------------------------------- /tests/data/inheritence/macro_scoping/test.html: -------------------------------------------------------------------------------- 1 | {% extends 'details.html' %} 2 | 3 | {% macro my_macro() %} 4 | my_macro 5 | {% endmacro %} 6 | 7 | {% block inner_box %} 8 | {{ my_macro() }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /tests/data/imports/exports: -------------------------------------------------------------------------------- 1 | 2 | {% macro toplevel() %}...{% endmacro %} 3 | {% macro __private() %}...{% endmacro %} 4 | {% set variable = 42 %} 5 | {% for item in {1} %} 6 | {% macro notthere() %}{% endmacro %} 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /tests/data/inheritence/scoped/index.html: -------------------------------------------------------------------------------- 1 | 2 | {%- extends 'layout.html' %} 3 | {% include 'helpers.html' %} 4 | {% block useless %} 5 | {% for x in {1, 2, 3} %} 6 | {% block testing %} 7 | {{ foo(x) }} 8 | {% endblock %} 9 | {% endfor %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /tests/data/inheritence/macro_scoping/details.html: -------------------------------------------------------------------------------- 1 | {% extends 'standard.html' %} 2 | 3 | {% macro my_macro() %} 4 | my_macro 5 | {% endmacro %} 6 | 7 | {% block content %} 8 | {% block outer_box %} 9 | outer_box 10 | {% block inner_box %} 11 | inner_box 12 | {% endblock %} 13 | {% endblock %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0 (11 Nov 2015) 4 | 5 | Download: 6 | 7 | * [Lupa 1.0][] 8 | 9 | Bugfixes: 10 | 11 | * Fixed some warnings issued by Luacheck, a Lua linter. 12 | 13 | Changes: 14 | 15 | * None. 16 | 17 | [Lupa 1.0]: download/lupa_1.0.zip 18 | 19 | ## 0.1_alpha (18 Mar 2015) 20 | 21 | Download: 22 | 23 | * [Lupa 0.1 alpha][] 24 | 25 | Initial release. 26 | 27 | [Lupa 0.1 alpha]: download/lupa_0.1_alpha.zip 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2020 Mitchell. See LICENSE. 2 | 3 | # Run test suite. 4 | 5 | .PHONY: tests 6 | tests: ; cd tests && lua5.1 suite.lua 7 | 8 | # Documentation. 9 | 10 | docs: docs/index.md docs/api.md $(wildcard docs/*.md) | \ 11 | docs/_layouts/default.html 12 | for file in $(basename $^); do \ 13 | cat $| | docs/fill_layout.lua $$file.md > $$file.html; \ 14 | done 15 | docs/index.md: README.md 16 | sed 's/^\# [[:alpha:]]\+/## Introduction/;' $< > $@ 17 | sed -i 's|https://[[:alpha:]]\+\.github\.io/[[:alpha:]]\+/||;' $@ 18 | sed -i '1 i {% raw %}' $@ && echo "{% endraw %}" >> $@ 19 | docs/api.md: lupa.lua 20 | luadoc --doclet docs/markdowndoc $^ > $@ 21 | sed -i '1 i {% raw %}' $@ && echo "{% endraw %}" >> $@ 22 | cleandocs: ; rm -f docs/*.html docs/index.md docs/api.html 23 | -------------------------------------------------------------------------------- /docs/fill_layout.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | -- Filters the given file through markdown, inserts it into the template 3 | -- specified by stdin by replacing simple {{ variable }} tags, and outputs the 4 | -- result to stdout. 5 | 6 | -- Filter the file through markdown using TOC generation in order to get header 7 | -- anchors, but ignore the actual TOC. 8 | local p = io.popen('markdown -f toc -T ' .. arg[1]) 9 | local html = p:read('*a'):match('^.-\n\n(.+)$') 10 | html = html:gsub('([^<]+)'), 19 | content = html:gsub('%%', '%%%%') 20 | } 21 | io.write(io.stdin:read('*a'):gsub('{{ (%S+) }}', tags)) 22 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ page.title }} 5 | 6 | 7 | 8 | 9 |
10 | 19 |
20 | {{ content }} 21 |
22 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015-2020 Mitchell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | /* Copyright 2012-2020 Mitchell. See LICENSE. */ 2 | 3 | * { 4 | border: 0 solid #a6a6a6; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | a { 10 | color: #1a66b3; 11 | text-decoration: none; 12 | } 13 | a:hover { text-decoration: underline; } 14 | a:visited { color: #661a66; } 15 | 16 | body { 17 | background-color: #f2f2f2; 18 | color: #333333; 19 | } 20 | 21 | code { font-size: 1.2em; } 22 | 23 | del { color: #994d4d; } 24 | 25 | h1 { margin: 0 0 1em 0; } 26 | h2, h3, h4, h5, h6 { margin: 1em 0 1em 0; } 27 | h1 { font-size: 1.3em; } 28 | h2 { font-size: 1.1em; } 29 | h3 { font-size: 1em; } 30 | h4 { font-size: 0.9em; } 31 | h5 { font-size: 0.8em; } 32 | 33 | hr { 34 | border: 1px solid #d9d9d9; 35 | border-width: 1px 0 0 0; 36 | margin: 1em 0 1em 0; 37 | } 38 | 39 | input, textarea { 40 | border-width: 1px; 41 | font-size: 1em; 42 | } 43 | 44 | ins { color: #4d994d; } 45 | 46 | li > code, p > code, em > code, td > code { color: #808080; } 47 | 48 | pre { 49 | color: #808080; 50 | margin: 0 2.5em 0 2.5em; 51 | white-space: pre-wrap; 52 | } 53 | 54 | table, th, td { 55 | border-width: 1px; 56 | border-collapse: collapse; 57 | margin-left: 1em; 58 | padding: 0.25em; 59 | } 60 | 61 | #content { 62 | border-width: 0 1px 0 1px; 63 | font-size: 1.2em; 64 | margin: 0 auto 0 auto; 65 | max-width: 1000px; 66 | } 67 | 68 | #header h1 { 69 | background-color: #d9d9d9; 70 | border-width: 0 0 1px 0; 71 | margin: 0; 72 | padding: 0.25em; 73 | } 74 | #header img { vertical-align: text-bottom; } 75 | #header ul, #header p { 76 | border-width: 0 0 1px 0; 77 | margin-bottom: 1.5em; 78 | padding: 0.25em; 79 | } 80 | #header ul { list-style: none; } 81 | #header li { 82 | color: #808080; 83 | display: inline; 84 | } 85 | #header li form { display: inline; } 86 | 87 | #main { margin-left: 1em; } 88 | #main dl, #main p { margin: 1em; } 89 | #main dd, #main ol, #main ul { margin-left: 2.5em; } 90 | #main ol p, #main ul p { margin-left: 0; } 91 | 92 | #footer { 93 | background-color: #d9d9d9; 94 | border-width: 1px 0 1px 0; 95 | clear: both; 96 | padding: 0.25em; 97 | margin-top: 1.5em; 98 | } 99 | -------------------------------------------------------------------------------- /docs/markdowndoc.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2015-2020 Mitchell. See LICENSE. 2 | 3 | -- Markdown doclet for Luadoc. 4 | -- @usage luadoc -d [output_path] -doclet path/to/markdowndoc [file(s)] 5 | local M = {} 6 | 7 | local MODULE = '\n## The `%s` Module\n' 8 | local FIELD = '\n#### `%s` %s\n\n' 9 | local FUNCTION = '\n#### `%s`(*%s*)\n\n' 10 | local FUNCTION_NO_PARAMS = '\n#### `%s`()\n\n' 11 | local DESCRIPTION = '%s\n\n' 12 | local LIST_TITLE = '%s:\n\n' 13 | local PARAM = '* *`%s`*: %s\n' 14 | local USAGE = '* `%s`\n' 15 | local RETURN = '* %s\n' 16 | local SEE = '* [`%s`](#%s)\n' 17 | local TABLE = '\n#### `%s`\n\n' 18 | local TFIELD = '* `%s`: %s\n' 19 | local titles = { 20 | [PARAM] = 'Parameters', [USAGE] = 'Usage', [RETURN] = 'Return', 21 | [SEE] = 'See also', [TFIELD] = 'Fields' 22 | } 23 | 24 | -- Writes a LuaDoc description to the given file. 25 | -- @param f The markdown file being written to. 26 | -- @param description The description. 27 | -- @param name The name of the module the description belongs to. Used for 28 | -- headers in module descriptions. 29 | local function write_description(f, description, name) 30 | -- Substitute custom [`code`]() link convention with [`code`](#code) links. 31 | local self_link = '(%[`([^`(]+)%(?%)?`%])%(%)' 32 | description = description:gsub(self_link, function(link, id) 33 | return string.format("%s(#%s)", link, id:gsub(':', '.')) 34 | end) 35 | f:write(string.format(DESCRIPTION, description)) 36 | end 37 | 38 | -- Writes a LuaDoc list to the given file. 39 | -- @param f The markdown file being written to. 40 | -- @param fmt The format of a list item. 41 | -- @param list The LuaDoc list. 42 | -- @param name The name of the module the list belongs to. Used for @see. 43 | local function write_list(f, fmt, list, name) 44 | if not list or #list == 0 then return end 45 | if type(list) == 'string' then list = {list} end 46 | f:write(string.format(LIST_TITLE, titles[fmt])) 47 | for _, value in ipairs(list) do 48 | if fmt == SEE and name ~= '_G' then 49 | if not value:find('%.') then 50 | -- Prepend module name to identifier if necessary. 51 | value = name .. '.' .. value 52 | else 53 | -- TODO: cannot link to fields, functions, or tables in `_G`? 54 | value = value:gsub('^_G%.', '') 55 | end 56 | end 57 | f:write(string.format(fmt, value, value)) 58 | end 59 | f:write('\n') 60 | end 61 | 62 | -- Writes a LuaDoc hashmap to the given file. 63 | -- @param f The markdown file being written to. 64 | -- @param fmt The format of a hashmap item. 65 | -- @param list The LuaDoc hashmap. 66 | local function write_hashmap(f, fmt, hashmap) 67 | if not hashmap or #hashmap == 0 then return end 68 | f:write(string.format(LIST_TITLE, titles[fmt])) 69 | for _, name in ipairs(hashmap) do 70 | f:write(string.format(fmt, name, hashmap[name] or '')) 71 | end 72 | f:write('\n') 73 | end 74 | 75 | -- Called by LuaDoc to process a doc object. 76 | -- @param doc The LuaDoc doc object. 77 | function M.start(doc) 78 | local modules, files = doc.modules, doc.files 79 | local f = io.stdout 80 | f:write('## Lupa API Documentation\n\n') 81 | 82 | -- Create a map of doc objects to file names so their Markdown doc comments 83 | -- can be extracted. 84 | local filedocs = {} 85 | for _, name in ipairs(files) do filedocs[files[name].doc] = name end 86 | 87 | -- Loop over modules, writing the Markdown document to stdout. 88 | for _, name in ipairs(modules) do 89 | local module = modules[name] 90 | 91 | -- Write the header and description. 92 | f:write(string.format(MODULE, name, name)) 93 | f:write('---\n\n') 94 | write_description(f, module.description, name) 95 | 96 | -- Write fields. 97 | if module.doc[1].class == 'module' then 98 | local fields = module.doc[1].field 99 | if fields and #fields > 0 then 100 | table.sort(fields) 101 | f:write('### Fields defined by `', name, '`\n\n') 102 | for _, field in ipairs(fields) do 103 | local type, description = fields[field]:match('^(%b())%s*(.+)$') 104 | if not field:find('%.') and name ~= '_G' then 105 | field = name .. '.' .. field -- absolute name 106 | else 107 | field = field:gsub('^_G%.', '') -- strip _G required for Luadoc 108 | end 109 | f:write(string.format(FIELD, field, field, type or '')) 110 | write_description(f, description or fields[field]) 111 | end 112 | f:write('\n') 113 | end 114 | end 115 | 116 | -- Write functions. 117 | local funcs = module.functions 118 | if #funcs > 0 then 119 | f:write('### Functions defined by `', name, '`\n\n') 120 | for _, fname in ipairs(funcs) do 121 | local func = funcs[fname] 122 | local params = table.concat(func.param, ', '):gsub('_', '\\_') 123 | if not func.name:find('[%.:]') and name ~= '_G' then 124 | func.name = name .. '.' .. func.name -- absolute name 125 | end 126 | if params ~= '' then 127 | f:write(string.format(FUNCTION, func.name, func.name, params)) 128 | else 129 | f:write(string.format(FUNCTION_NO_PARAMS, func.name, func.name)) 130 | end 131 | write_description(f, func.description) 132 | write_hashmap(f, PARAM, func.param) 133 | write_list(f, USAGE, func.usage) 134 | write_list(f, RETURN, func.ret) 135 | write_list(f, SEE, func.see, name) 136 | end 137 | f:write('\n') 138 | end 139 | 140 | -- Write tables. 141 | local tables = module.tables 142 | if #tables > 0 then 143 | f:write('### Tables defined by `', name, '`\n\n') 144 | for _, tname in ipairs(tables) do 145 | local tbl = tables[tname] 146 | if not tbl.name:find('%.') and 147 | (name ~= '_G' or tbl.name == 'buffer' or tbl.name == 'view') then 148 | tbl.name = name .. '.' .. tbl.name -- absolute name 149 | elseif tbl.name ~= '_G.keys' and tbl.name ~= '_G.snippets' then 150 | tbl.name = tbl.name:gsub('^_G%.', '') -- strip _G required for Luadoc 151 | end 152 | f:write(string.format(TABLE, tbl.name, tbl.name)) 153 | write_description(f, tbl.description) 154 | write_hashmap(f, TFIELD, tbl.field) 155 | write_list(f, USAGE, tbl.usage) 156 | write_list(f, SEE, tbl.see, name) 157 | end 158 | end 159 | f:write('---\n') 160 | end 161 | end 162 | 163 | return M 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lupa 2 | 3 | Lupa is a [Jinja2][] template engine implementation written in Lua and supports 4 | Lua syntax within tags and variables. 5 | 6 | Lupa was sponsored by the [Library of the University of Antwerp][]. 7 | 8 | [Jinja2]: http://jinja.pocoo.org 9 | [Library of the University of Antwerp]: http://www.uantwerpen.be/ 10 | 11 | ## Requirements 12 | 13 | Lupa has the following requirements: 14 | 15 | * [Lua][] 5.1, 5.2, or 5.3. 16 | * The [LPeg][] library. 17 | 18 | [Lua]: https://www.lua.org 19 | [LPeg]: http://www.inf.puc-rio.br/~roberto/lpeg/ 20 | 21 | ## Download 22 | 23 | Lupa releases can be found [here][]. 24 | 25 | [here]: https://github.com/orbitalquark/lupa/releases 26 | 27 | ## Installation 28 | 29 | Unzip Lupa and place the "lupa.lua" file in your Lua installation's 30 | `package.path`. This location depends on your version of Lua. Typical locations 31 | are listed below. 32 | 33 | * Lua 5.1: */usr/local/share/lua/5.1/* or */usr/local/share/lua/5.1/* 34 | * Lua 5.2: */usr/local/share/lua/5.2/* or */usr/local/share/lua/5.2/* 35 | * Lua 5.3: */usr/local/share/lua/5.3/* or */usr/local/share/lua/5.3/* 36 | 37 | You can also place the *lupa.lua* file wherever you'd like and add it to Lua's 38 | `package.path` manually in your program. For example, if Lupa was placed in a 39 | */home/user/lua/* directory, it can be used as follows: 40 | 41 | package.path = package.path .. ';/home/user/lua/?.lua' 42 | 43 | ## Usage 44 | 45 | Lupa is simply a Lua library. Its `lupa.expand()` and `lupa.expand_file()` 46 | functions may called to process templates. For example: 47 | 48 | lupa = require('lupa') 49 | lupa.expand("hello {{ s }}!", {s = "world"}) --> "hello world!" 50 | lupa.expand("{% for i in {1, 2, 3} %}{{ i }}{% endfor %}") --> 123 51 | 52 | By default, Lupa loads templates relative to the current working directory. This 53 | can be changed by reconfiguring Lupa: 54 | 55 | lupa.expand_file('name') --> expands template "./name" 56 | lupa.configure{loader = lupa.loaders.filesystem('path/to/templates')} 57 | lupa.expand_file('name') --> expands template "path/to/templates/name" 58 | 59 | See Lupa's [API documentation][] for more information. 60 | 61 | [API documentation]: https://orbitalquark.github.io/lupa/api.html 62 | 63 | ## Syntax 64 | 65 | Please refer to Jinja2's extensive [template documentation][]. Any 66 | incompatibilities are listed in the sections below. 67 | 68 | [template documentation]: http://jinja.pocoo.org/docs/dev/templates/ 69 | 70 | ## Comparison with Jinja2 71 | 72 | While Lua and Python (Jinja2's implementation language) share some similarities, 73 | the languages themselves are fundamentally different. Nevertheless, a 74 | significant effort was made to support a vast majority of Jinja2's Python-style 75 | syntax. As a result, Lupa passes Jinja2's test suite with only a handful of 76 | modifications. The comprehensive list of differences between Lupa and Jinja2 is 77 | described in the following sections. 78 | 79 | ### Fundamental Differences 80 | 81 | * Expressions use Lua's syntax instead of Python's, so many of Python's 82 | syntactic constructs are not valid. However, the following constructs 83 | *are valid*, despite being invalid in pure Lua: 84 | 85 | + Iterating over table literals or table variables directly in a "for" loop: 86 | 87 | {% for i in {1, 2, 3} %}...{% endfor %} 88 | 89 | + Conditional loops via an "if" expression suffix: 90 | 91 | {% for x in range(10) if is_odd(x) %}...{% endfor %} 92 | 93 | + Table unpacking for list elements when iterating through a list of lists: 94 | 95 | {% for a, b, c in {{1, 2, 3}, {4, 5, 6}} %}...{% endfor %} 96 | 97 | + Default values for macro arguments: 98 | 99 | {% macro m(a, b, c='c', d='d') %}...{% endmacro %} 100 | 101 | * Strings do not have unicode escapes nor is unicode interpreted in any way. 102 | 103 | ### Syntactic Differences 104 | 105 | * Line statements are not supported due to parsing complexity. 106 | * In `{% for ... %}` loops, the `loop.length`, `loop.revindex`, 107 | `loop.revindex0`, and `loop.last` variables only apply to sequences, where 108 | Lua's `'#'` operator applies. 109 | * The `{% continue %}` and `{% break %}` loop controls are not supported due to 110 | complexity. 111 | * Loops may be used recursively by default, so the `recursive` loop modifier is 112 | not supported. 113 | * The `is` operator is not supported by Lua, so tests of the form `{{ x is y }}` 114 | should be written `{{ is_y(x) }}` (e.g. `{{ is_number(42) }}`). 115 | * Filters cannot occur after tokens within an expression (e.g. 116 | `{{ "foo"|upper .. "bar"|upper }}`), but can only occur at the end of an 117 | expression (e.g. `{{ "foo".."bar"|upper }}`). 118 | * Blocks always have access to scoped variables, so the `scoped` block modifier 119 | is not supported. 120 | * Named block end tags are not supported since the parser cannot easily keep 121 | track of that state information. 122 | * Any `{% block ... %}` tags within a "false" block (e.g. `{% if a %}` where `a` 123 | evaluates to `false`) are never read and stored due to the parser 124 | implementation. 125 | * Inline "if" expressions (e.g. `{% extends b if a else c %}`) are not 126 | supported. Instead, use a Lua conditional expression 127 | (e.g. `{% extends a and b or c %}`). 128 | * Any `{% extends ... %}` tags within a sub-scope are not effective outside that 129 | scope (e.g. `{% if a %}{% extends a %}{% else %}{% extends b %}{% endif %}`). 130 | Instead, use a Lua conditional expression (e.g. `{% extends a or b %}`). 131 | * Macros are simply Lua functions and have no metadata attributes. 132 | * Macros do not have access to a `kwargs` variable since Lua does not support 133 | keyword arguments. 134 | * `{% from x import y %}` tags are not supported. Instead, you must use either 135 | `{% import x %}`, which imports all globals in `x` into the current 136 | environment, or use `{% import x as z %}`, which imports all globals in `x` 137 | into the variable `z`. 138 | * `{% set ... %}` does not support multiple assignment. Use `{% do ...%}` 139 | instead. The catch is that `{% do ... %}` does not support filters. 140 | * The `{% trans %}` and `{% endtrans %}` tags, `{% with %}` and `{% endwith %}` 141 | tags, and `{% autoescape %}` and `{% endautoescape %}` tags are not supported 142 | since they are outside the scope of this implementation. 143 | 144 | ### Filter Differences 145 | 146 | * Only the `batch`, `groupby`, and `slice` filters return generators which 147 | produce one item at a time when looping. All other filters that produce 148 | iterable results generate all items at once. 149 | * The `float` filter only works in Lua 5.3 since that version of Lua has a 150 | distinction between floats and integers. 151 | * The `safe` filter must appear at the end of a filter chain since its output 152 | cannot be passed to any other filter. 153 | 154 | ### Function Differences 155 | 156 | * The global `range(n)` function returns a sequence from 1 to `n`, inclusive, 157 | since lists start at 1 in Lua. 158 | * No `lipsum()`, `dict()`, or `joiner()` functions for the sake of simplicity. 159 | 160 | ### API Differences 161 | 162 | * Lupa has a much simpler API consisting of just four functions and three 163 | fields: 164 | 165 | + `lupa.expand()`: Expands a string template subject to an environment. 166 | + `lupa.expand_file()`: Expands a file template subject to an environment. 167 | + `lupa.configure()` Configures delimiters and template options. 168 | + `lupa.reset()`: Resets delimiters and options to their defaults. 169 | + `lupa.env`: The default environment for templates. 170 | + `lupa.filters`: The set of available filters (`escape`, `join`, etc.). 171 | + `lupa.tests`: The set of available tests (`is_odd`, `is_defined`, etc.). 172 | 173 | * There is no bytecode caching. 174 | * Lupa has no extension mechanism. Instead, modify `lupa.env`, `lupa.filters`, 175 | and `lupa.tests` directly. However, the parser cannot be extended. 176 | * Sandboxing is not supported, although `lupa.env` is safe by default (`io`, 177 | `os.execute`, `os.remove`, etc. are not available). 178 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | ## Introduction 3 | 4 | Lupa is a [Jinja2][] template engine implementation written in Lua and supports 5 | Lua syntax within tags and variables. 6 | 7 | Lupa was sponsored by the [Library of the University of Antwerp][]. 8 | 9 | [Jinja2]: http://jinja.pocoo.org 10 | [Library of the University of Antwerp]: http://www.uantwerpen.be/ 11 | 12 | ## Requirements 13 | 14 | Lupa has the following requirements: 15 | 16 | * [Lua][] 5.1, 5.2, or 5.3. 17 | * The [LPeg][] library. 18 | 19 | [Lua]: https://www.lua.org 20 | [LPeg]: http://www.inf.puc-rio.br/~roberto/lpeg/ 21 | 22 | ## Download 23 | 24 | Lupa releases can be found [here][]. 25 | 26 | [here]: https://github.com/orbitalquark/lupa/releases 27 | 28 | ## Installation 29 | 30 | Unzip Lupa and place the "lupa.lua" file in your Lua installation's 31 | `package.path`. This location depends on your version of Lua. Typical locations 32 | are listed below. 33 | 34 | * Lua 5.1: */usr/local/share/lua/5.1/* or */usr/local/share/lua/5.1/* 35 | * Lua 5.2: */usr/local/share/lua/5.2/* or */usr/local/share/lua/5.2/* 36 | * Lua 5.3: */usr/local/share/lua/5.3/* or */usr/local/share/lua/5.3/* 37 | 38 | You can also place the *lupa.lua* file wherever you'd like and add it to Lua's 39 | `package.path` manually in your program. For example, if Lupa was placed in a 40 | */home/user/lua/* directory, it can be used as follows: 41 | 42 | package.path = package.path .. ';/home/user/lua/?.lua' 43 | 44 | ## Usage 45 | 46 | Lupa is simply a Lua library. Its `lupa.expand()` and `lupa.expand_file()` 47 | functions may called to process templates. For example: 48 | 49 | lupa = require('lupa') 50 | lupa.expand("hello {{ s }}!", {s = "world"}) --> "hello world!" 51 | lupa.expand("{% for i in {1, 2, 3} %}{{ i }}{% endfor %}") --> 123 52 | 53 | By default, Lupa loads templates relative to the current working directory. This 54 | can be changed by reconfiguring Lupa: 55 | 56 | lupa.expand_file('name') --> expands template "./name" 57 | lupa.configure{loader = lupa.loaders.filesystem('path/to/templates')} 58 | lupa.expand_file('name') --> expands template "path/to/templates/name" 59 | 60 | See Lupa's [API documentation][] for more information. 61 | 62 | [API documentation]: api.html 63 | 64 | ## Syntax 65 | 66 | Please refer to Jinja2's extensive [template documentation][]. Any 67 | incompatibilities are listed in the sections below. 68 | 69 | [template documentation]: http://jinja.pocoo.org/docs/dev/templates/ 70 | 71 | ## Comparison with Jinja2 72 | 73 | While Lua and Python (Jinja2's implementation language) share some similarities, 74 | the languages themselves are fundamentally different. Nevertheless, a 75 | significant effort was made to support a vast majority of Jinja2's Python-style 76 | syntax. As a result, Lupa passes Jinja2's test suite with only a handful of 77 | modifications. The comprehensive list of differences between Lupa and Jinja2 is 78 | described in the following sections. 79 | 80 | ### Fundamental Differences 81 | 82 | * Expressions use Lua's syntax instead of Python's, so many of Python's 83 | syntactic constructs are not valid. However, the following constructs 84 | *are valid*, despite being invalid in pure Lua: 85 | 86 | + Iterating over table literals or table variables directly in a "for" loop: 87 | 88 | {% for i in {1, 2, 3} %}...{% endfor %} 89 | 90 | + Conditional loops via an "if" expression suffix: 91 | 92 | {% for x in range(10) if is_odd(x) %}...{% endfor %} 93 | 94 | + Table unpacking for list elements when iterating through a list of lists: 95 | 96 | {% for a, b, c in {{1, 2, 3}, {4, 5, 6}} %}...{% endfor %} 97 | 98 | + Default values for macro arguments: 99 | 100 | {% macro m(a, b, c='c', d='d') %}...{% endmacro %} 101 | 102 | * Strings do not have unicode escapes nor is unicode interpreted in any way. 103 | 104 | ### Syntactic Differences 105 | 106 | * Line statements are not supported due to parsing complexity. 107 | * In `{% for ... %}` loops, the `loop.length`, `loop.revindex`, 108 | `loop.revindex0`, and `loop.last` variables only apply to sequences, where 109 | Lua's `'#'` operator applies. 110 | * The `{% continue %}` and `{% break %}` loop controls are not supported due to 111 | complexity. 112 | * Loops may be used recursively by default, so the `recursive` loop modifier is 113 | not supported. 114 | * The `is` operator is not supported by Lua, so tests of the form `{{ x is y }}` 115 | should be written `{{ is_y(x) }}` (e.g. `{{ is_number(42) }}`). 116 | * Filters cannot occur after tokens within an expression (e.g. 117 | `{{ "foo"|upper .. "bar"|upper }}`), but can only occur at the end of an 118 | expression (e.g. `{{ "foo".."bar"|upper }}`). 119 | * Blocks always have access to scoped variables, so the `scoped` block modifier 120 | is not supported. 121 | * Named block end tags are not supported since the parser cannot easily keep 122 | track of that state information. 123 | * Any `{% block ... %}` tags within a "false" block (e.g. `{% if a %}` where `a` 124 | evaluates to `false`) are never read and stored due to the parser 125 | implementation. 126 | * Inline "if" expressions (e.g. `{% extends b if a else c %}`) are not 127 | supported. Instead, use a Lua conditional expression 128 | (e.g. `{% extends a and b or c %}`). 129 | * Any `{% extends ... %}` tags within a sub-scope are not effective outside that 130 | scope (e.g. `{% if a %}{% extends a %}{% else %}{% extends b %}{% endif %}`). 131 | Instead, use a Lua conditional expression (e.g. `{% extends a or b %}`). 132 | * Macros are simply Lua functions and have no metadata attributes. 133 | * Macros do not have access to a `kwargs` variable since Lua does not support 134 | keyword arguments. 135 | * `{% from x import y %}` tags are not supported. Instead, you must use either 136 | `{% import x %}`, which imports all globals in `x` into the current 137 | environment, or use `{% import x as z %}`, which imports all globals in `x` 138 | into the variable `z`. 139 | * `{% set ... %}` does not support multiple assignment. Use `{% do ...%}` 140 | instead. The catch is that `{% do ... %}` does not support filters. 141 | * The `{% trans %}` and `{% endtrans %}` tags, `{% with %}` and `{% endwith %}` 142 | tags, and `{% autoescape %}` and `{% endautoescape %}` tags are not supported 143 | since they are outside the scope of this implementation. 144 | 145 | ### Filter Differences 146 | 147 | * Only the `batch`, `groupby`, and `slice` filters return generators which 148 | produce one item at a time when looping. All other filters that produce 149 | iterable results generate all items at once. 150 | * The `float` filter only works in Lua 5.3 since that version of Lua has a 151 | distinction between floats and integers. 152 | * The `safe` filter must appear at the end of a filter chain since its output 153 | cannot be passed to any other filter. 154 | 155 | ### Function Differences 156 | 157 | * The global `range(n)` function returns a sequence from 1 to `n`, inclusive, 158 | since lists start at 1 in Lua. 159 | * No `lipsum()`, `dict()`, or `joiner()` functions for the sake of simplicity. 160 | 161 | ### API Differences 162 | 163 | * Lupa has a much simpler API consisting of just four functions and three 164 | fields: 165 | 166 | + `lupa.expand()`: Expands a string template subject to an environment. 167 | + `lupa.expand_file()`: Expands a file template subject to an environment. 168 | + `lupa.configure()` Configures delimiters and template options. 169 | + `lupa.reset()`: Resets delimiters and options to their defaults. 170 | + `lupa.env`: The default environment for templates. 171 | + `lupa.filters`: The set of available filters (`escape`, `join`, etc.). 172 | + `lupa.tests`: The set of available tests (`is_odd`, `is_defined`, etc.). 173 | 174 | * There is no bytecode caching. 175 | * Lupa has no extension mechanism. Instead, modify `lupa.env`, `lupa.filters`, 176 | and `lupa.tests` directly. However, the parser cannot be extended. 177 | * Sandboxing is not supported, although `lupa.env` is safe by default (`io`, 178 | `os.execute`, `os.remove`, etc. are not available). 179 | {% endraw %} 180 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | ## Lupa API Documentation 3 | 4 | 5 | ## The `lupa` Module 6 | --- 7 | 8 | Lupa is a Jinja2 template engine implementation written in Lua and supports 9 | Lua syntax within tags and variables. 10 | 11 | ### Functions defined by `lupa` 12 | 13 | 14 | #### `_G.cycler`(*...*) 15 | 16 | Returns an object that cycles through the given values by calls to its 17 | `next()` function. 18 | A `current` field contains the cycler's current value and a `reset()` 19 | function resets the cycler to its beginning. 20 | 21 | Parameters: 22 | 23 | * *`...`*: Values to cycle through. 24 | 25 | Usage: 26 | 27 | * `c = cycler(1, 2, 3)` 28 | * `c:next(), c:next() --> 1, 2` 29 | * `c:reset() --> c.current == 1` 30 | 31 | 32 | #### `_G.range`(*start, stop, step*) 33 | 34 | Returns a sequence of integers from *start* to *stop*, inclusive, in 35 | increments of *step*. 36 | The complete sequence is generated at once -- no generator is returned. 37 | 38 | Parameters: 39 | 40 | * *`start`*: Optional number to start at. The default value is `1`. 41 | * *`stop`*: Number to stop at. 42 | * *`step`*: Optional increment between sequence elements. The default value 43 | is `1`. 44 | 45 | 46 | #### `lupa.configure`(*ts, te, vs, ve, cs, ce, options*) 47 | 48 | Configures the basic delimiters and options for templates. 49 | This function then regenerates the grammar for parsing templates. 50 | Note: this function cannot be used iteratively to configure Lupa options. 51 | Any options not provided are reset to their default values. 52 | 53 | Parameters: 54 | 55 | * *`ts`*: The tag start delimiter. The default value is '{%'. 56 | * *`te`*: The tag end delimiter. The default value is '%}'. 57 | * *`vs`*: The variable start delimiter. The default value is '{{'. 58 | * *`ve`*: The variable end delimiter. The default value is '}}'. 59 | * *`cs`*: The comment start delimiter. The default value is '{#'. 60 | * *`ce`*: The comment end delimiter. The default value is '#}'. 61 | * *`options`*: Optional set of options for templates: 62 | 63 | * `trim_blocks`: Trim the first newline after blocks. 64 | * `lstrip_blocks`: Strip line-leading whitespace in front of tags. 65 | * `newline_sequence`: The end-of-line character to use. 66 | * `keep_trailing_newline`: Whether or not to keep a newline at the end of 67 | a template. 68 | * `autoescape`: Whether or not to autoescape HTML entities. May be a 69 | function that accepts the template's filename as an argument and returns 70 | a boolean. 71 | * `loader`: Function that receives a template name to load and returns the 72 | path to that template. 73 | 74 | 75 | #### `lupa.expand`(*template, env*) 76 | 77 | Expands the string template *template*, subject to template environment 78 | *env*, and returns the result. 79 | 80 | Parameters: 81 | 82 | * *`template`*: String template to expand. 83 | * *`env`*: Optional environment for the given template. 84 | 85 | 86 | #### `lupa.expand_file`(*filename, env*) 87 | 88 | Expands the template within file *filename*, subject to template environment 89 | *env*, and returns the result. 90 | 91 | Parameters: 92 | 93 | * *`filename`*: Filename containing the template to expand. 94 | * *`env`*: Optional environment for the template to expand. 95 | 96 | 97 | #### `filters.batch`(*t, size, fill*) 98 | 99 | Returns a generator that produces all of the items in table *t* in batches 100 | of size *size*, filling any empty spaces with value *fill*. 101 | Combine this with the "list" filter to produce a list. 102 | 103 | Parameters: 104 | 105 | * *`t`*: The table to split into batches. 106 | * *`size`*: The batch size. 107 | * *`fill`*: The value to use when filling in any empty space in the last 108 | batch. 109 | 110 | Usage: 111 | 112 | * `expand('{% for i in {1, 2, 3}|batch(2, 0) %}{{ i|string }} 113 | {% endfor %}') --> {1, 2} {3, 0}` 114 | 115 | See also: 116 | 117 | * [`filters.list`](#filters.list) 118 | 119 | 120 | #### `filters.capitalize`(*s*) 121 | 122 | Capitalizes string *s*. 123 | The first character will be uppercased, the others lowercased. 124 | 125 | Parameters: 126 | 127 | * *`s`*: The string to capitalize. 128 | 129 | Usage: 130 | 131 | * `expand('{{ "foo bar"|capitalize }}') --> Foo bar` 132 | 133 | 134 | #### `filters.center`(*s, width*) 135 | 136 | Centers string *s* within a string of length *width*. 137 | 138 | Parameters: 139 | 140 | * *`s`*: The string to center. 141 | * *`width`*: The length of the centered string. 142 | 143 | Usage: 144 | 145 | * `expand('{{ "foo"|center(9) }}') --> " foo "` 146 | 147 | 148 | #### `filters.default`(*value, default, false\_defaults*) 149 | 150 | Returns value *value* or value *default*, depending on whether or not *value* 151 | is "true" and whether or not boolean *false_defaults* is `true`. 152 | 153 | Parameters: 154 | 155 | * *`value`*: The value return if "true" or if `false` and *false_defaults* 156 | is `true`. 157 | * *`default`*: The value to return if *value* is `nil` or `false` (the latter 158 | applies only if *false_defaults* is `true`). 159 | * *`false_defaults`*: Optional flag indicating whether or not to return 160 | *default* if *value* is `false`. The default value is `false`. 161 | 162 | Usage: 163 | 164 | * `expand('{{ false|default("no") }}') --> false` 165 | * `expand('{{ false|default("no", true) }') --> no` 166 | 167 | 168 | #### `filters.dictsort`(*t, case\_sensitive, by, value*) 169 | 170 | Returns a table constructed from table *t* such that each element is a list 171 | that contains a single key-value pair and all elements are sorted according 172 | to string *by* (which is either "key" or "value") and boolean 173 | *case_sensitive*. 174 | 175 | Parameters: 176 | 177 | * *`t`*: 178 | * *`case_sensitive`*: Optional flag indicating whether or not to consider 179 | case when sorting string values. The default value is `false`. 180 | * *`by`*: Optional string that specifies which of the key-value to sort by, 181 | either "key" or "value". The default value is `"key"`. 182 | * *`value`*: The table to sort. 183 | 184 | Usage: 185 | 186 | * `expand('{{ {b = 1, a = 2}|dictsort|string }}') --> {{"a", 2}, 187 | {"b", 1}}` 188 | 189 | 190 | #### `filters.e`(*s*) 191 | 192 | Returns an HTML-safe copy of string *s*. 193 | 194 | Parameters: 195 | 196 | * *`s`*: String to ensure is HTML-safe. 197 | 198 | Usage: 199 | 200 | * `expand([[{{ '<">&'|escape}}]]) --> <">&` 201 | 202 | 203 | #### `filters.escape`(*s*) 204 | 205 | Returns an HTML-safe copy of string *s*. 206 | 207 | Parameters: 208 | 209 | * *`s`*: String to ensure is HTML-safe. 210 | 211 | Usage: 212 | 213 | * `expand([[{{ '<">&'|e}}]]) --> <">&` 214 | 215 | 216 | #### `filters.filesizeformat`(*bytes, binary*) 217 | 218 | Returns a human-readable, decimal (or binary, depending on boolean *binary*) 219 | file size for *bytes* number of bytes. 220 | 221 | Parameters: 222 | 223 | * *`bytes`*: The number of bytes to return the size for. 224 | * *`binary`*: Flag indicating whether or not to report binary file size 225 | as opposed to decimal file size. The default value is `false`. 226 | 227 | Usage: 228 | 229 | * `expand('{{ 1000|filesizeformat }}') --> 1.0 kB` 230 | 231 | 232 | #### `filters.first`(*t*) 233 | 234 | Returns the first element in table *t*. 235 | 236 | Parameters: 237 | 238 | * *`t`*: The table to get the first element of. 239 | 240 | Usage: 241 | 242 | * `expand('{{ range(10)|first }}') --> 1` 243 | 244 | 245 | #### `filters.float`(*value*) 246 | 247 | Returns value *value* as a float. 248 | This filter only works in Lua 5.3, which has a distinction between floats and 249 | integers. 250 | 251 | Parameters: 252 | 253 | * *`value`*: The value to interpret as a float. 254 | 255 | Usage: 256 | 257 | * `expand('{{ 42|float }}') --> 42.0` 258 | 259 | 260 | #### `filters.forceescape`(*value*) 261 | 262 | Returns an HTML-safe copy of value *value*, even if *value* was returned by 263 | the "safe" filter. 264 | 265 | Parameters: 266 | 267 | * *`value`*: Value to ensure is HTML-safe. 268 | 269 | Usage: 270 | 271 | * `expand('{% set x = "
"|safe %}{{ x|forceescape }}') --> 272 | <div />` 273 | 274 | 275 | #### `filters.format`(*s, ...*) 276 | 277 | Returns the given arguments formatted according to string *s*. 278 | See Lua's `string.format()` for more information. 279 | 280 | Parameters: 281 | 282 | * *`s`*: The string to format subsequent arguments according to. 283 | * *`...`*: Arguments to format. 284 | 285 | Usage: 286 | 287 | * `expand('{{ "%s,%s"|format("a", "b") }}') --> a,b` 288 | 289 | 290 | #### `filters.groupby`(*t, attribute*) 291 | 292 | Returns a generator that produces lists of items in table *t* grouped by 293 | string attribute *attribute*. 294 | 295 | Parameters: 296 | 297 | * *`t`*: The table to group items from. 298 | * *`attribute`*: The attribute of items in the table to group by. This may 299 | be nested (e.g. "foo.bar" groups by t[i].foo.bar for all i). 300 | 301 | Usage: 302 | 303 | * `expand('{% for age, group in people|groupby("age") %}...{% endfor %}')` 304 | 305 | 306 | #### `filters.indent`(*s, width, first\_line*) 307 | 308 | Returns a copy of string *s* with all lines after the first indented by 309 | *width* number of spaces. 310 | If boolean *first_line* is `true`, indents the first line as well. 311 | 312 | Parameters: 313 | 314 | * *`s`*: The string to indent lines of. 315 | * *`width`*: The number of spaces to indent lines with. 316 | * *`first_line`*: Optional flag indicating whether or not to indent the 317 | first line of text. The default value is `false`. 318 | 319 | Usage: 320 | 321 | * `expand('{{ "foo\nbar"|indent(2) }}') --> "foo\n bar"` 322 | 323 | 324 | #### `filters.int`(*value*) 325 | 326 | Returns value *value* as an integer. 327 | 328 | Parameters: 329 | 330 | * *`value`*: The value to interpret as an integer. 331 | 332 | Usage: 333 | 334 | * `expand('{{ 32.32|int }}') --> 32` 335 | 336 | 337 | #### `filters.join`(*t, sep, attribute*) 338 | 339 | Returns a string that contains all the elements in table *t* (or all the 340 | attributes named *attribute* in *t*) separated by string *sep*. 341 | 342 | Parameters: 343 | 344 | * *`t`*: The table to join. 345 | * *`sep`*: The string to separate table elements with. 346 | * *`attribute`*: Optional attribute of elements to use for joining instead 347 | of the elements themselves. This may be nested (e.g. "foo.bar" joins 348 | `t[i].foo.bar` for all i). 349 | 350 | Usage: 351 | 352 | * `expand('{{ {1, 2, 3}|join("|") }}') --> 1|2|3` 353 | 354 | 355 | #### `filters.last`(*t*) 356 | 357 | Returns the last element in table *t*. 358 | 359 | Parameters: 360 | 361 | * *`t`*: The table to get the last element of. 362 | 363 | Usage: 364 | 365 | * `expand('{{ range(10)|last }}') --> 10` 366 | 367 | 368 | #### `filters.length`(*value*) 369 | 370 | Returns the length of string or table *value*. 371 | 372 | Parameters: 373 | 374 | * *`value`*: The value to get the length of. 375 | 376 | Usage: 377 | 378 | * `expand('{{ "hello world"|length }}') --> 11` 379 | 380 | 381 | #### `filters.list`(*generator, s, i*) 382 | 383 | Returns the list of items produced by generator *generator*, subject to 384 | initial state *s* and initial iterator variable *i*. 385 | This filter should only be used after a filter that returns a generator. 386 | 387 | Parameters: 388 | 389 | * *`generator`*: Generator function that produces an item. 390 | * *`s`*: Initial state for the generator. 391 | * *`i`*: Initial iterator variable for the generator. 392 | 393 | Usage: 394 | 395 | * `expand('{{ range(4)|batch(2)|list|string }}') --> {{1, 2}, {3, 4}}` 396 | 397 | See also: 398 | 399 | * [`filters.batch`](#filters.batch) 400 | * [`filters.groupby`](#filters.groupby) 401 | * [`filters.slice`](#filters.slice) 402 | 403 | 404 | #### `filters.lower`(*s*) 405 | 406 | Returns a copy of string *s* with all lowercase characters. 407 | 408 | Parameters: 409 | 410 | * *`s`*: The string to lowercase. 411 | 412 | Usage: 413 | 414 | * `expand('{{ "FOO"|lower }}') --> foo` 415 | 416 | 417 | #### `filters.map`(*t, filter, ...*) 418 | 419 | Maps each element of table *t* to a value produced by filter name *filter* 420 | and returns the resultant table. 421 | 422 | Parameters: 423 | 424 | * *`t`*: The table of elements to map. 425 | * *`filter`*: The name of the filter to pass table elements through. 426 | * *`...`*: Any arguments for the filter. 427 | 428 | Usage: 429 | 430 | * `expand('{{ {"1", "2", "3"}|map("int")|sum }}') --> 6` 431 | 432 | 433 | #### `filters.mapattr`(*t, attribute, filter, ...*) 434 | 435 | Maps the value of each element's string *attribute* in table *t* to the 436 | value produced by filter name *filter* and returns the resultant table. 437 | 438 | Parameters: 439 | 440 | * *`t`*: The table of elements with attributes to map. 441 | * *`attribute`*: The attribute of elements in the table to filter. This may 442 | be nested (e.g. "foo.bar" maps t[i].foo.bar for all i). 443 | * *`filter`*: The name of the filter to pass table elements through. 444 | * *`...`*: Any arguments for the filter. 445 | 446 | Usage: 447 | 448 | * `expand('{{ users|mapattr("name")|join("|") }}')` 449 | 450 | 451 | #### `filters.random`(*t*) 452 | 453 | Returns a random element from table *t*. 454 | 455 | Parameters: 456 | 457 | * *`t`*: The table to get a random element from. 458 | 459 | Usage: 460 | 461 | * `expand('{{ range(100)|random }}')` 462 | 463 | 464 | #### `filters.reject`(*t, test, ...*) 465 | 466 | Returns a list of elements in table *t* that fail test name *test*. 467 | 468 | Parameters: 469 | 470 | * *`t`*: The table of elements to reject from. 471 | * *`test`*: The name of the test to use on table elements. 472 | * *`...`*: Any arguments for the test. 473 | 474 | Usage: 475 | 476 | * `expand('{{ range(5)|reject(is_odd)|join("|") }}') --> 2|4` 477 | 478 | 479 | #### `filters.rejectattr`(*t, attribute, test, ...*) 480 | 481 | Returns a list of elements in table *t* whose string attribute *attribute* 482 | fails test name *test*. 483 | 484 | Parameters: 485 | 486 | * *`t`*: The table of elements to reject from. 487 | * *`attribute`*: The attribute of items in the table to reject from. This 488 | may be nested (e.g. "foo.bar" tests t[i].foo.bar for all i). 489 | * *`test`*: The name of the test to use on table elements. 490 | * *`...`*: Any arguments for the test. 491 | 492 | Usage: 493 | 494 | * `expand('{{ users|rejectattr("offline")|mapattr("name")|join(",") }}')` 495 | 496 | 497 | #### `filters.replace`(*s, pattern, repl, n*) 498 | 499 | Returns a copy of string *s* with all (or up to *n*) occurrences of string 500 | *old* replaced by string *new*. 501 | Identical to Lua's `string.gsub()` and handles Lua patterns. 502 | 503 | Parameters: 504 | 505 | * *`s`*: The subject string. 506 | * *`pattern`*: The string or Lua pattern to replace. 507 | * *`repl`*: The replacement text (may contain Lua captures). 508 | * *`n`*: Optional number indicating the maximum number of replacements to 509 | make. The default value is `nil`, which is unlimited. 510 | 511 | Usage: 512 | 513 | * `expand('{% filter upper|replace("FOO", "foo") %}foobar 514 | {% endfilter %}') --> fooBAR` 515 | 516 | 517 | #### `filters.reverse`(*value*) 518 | 519 | Returns a copy of the given string or table *value* in reverse order. 520 | 521 | Parameters: 522 | 523 | * *`value`*: The value to reverse. 524 | 525 | Usage: 526 | 527 | * `expand('{{ {1, 2, 3}|reverse|string }}') --> {3, 2, 1}` 528 | 529 | 530 | #### `filters.round`(*value, precision, method*) 531 | 532 | Returns number *value* rounded to *precision* decimal places based on string 533 | *method* (if given). 534 | 535 | Parameters: 536 | 537 | * *`value`*: The number to round. 538 | * *`precision`*: Optional precision to round the number to. The default 539 | value is `0`. 540 | * *`method`*: Optional string rounding method, either `"ceil"` or 541 | `"floor"`. The default value is `nil`, which uses the common rounding 542 | method (if a number's fractional part is 0.5 or greater, rounds up; 543 | otherwise rounds down). 544 | 545 | Usage: 546 | 547 | * `expand('{{ 2.1236|round(3, "floor") }}') --> 2.123` 548 | 549 | 550 | #### `filters.safe`(*s*) 551 | 552 | Marks string *s* as HTML-safe, preventing Lupa from modifying it when 553 | configured to autoescape HTML entities. 554 | This filter must be used at the end of a filter chain unless it is 555 | immediately proceeded by the "forceescape" filter. 556 | 557 | Parameters: 558 | 559 | * *`s`*: The string to mark as HTML-safe. 560 | 561 | Usage: 562 | 563 | * `lupa.configure{autoescape = true}` 564 | * `expand('{{ "
foo
"|safe }}') -->
foo
` 565 | 566 | 567 | #### `filters.select`(*t, test, ...*) 568 | 569 | Returns a list of the elements in table *t* that pass test name *test*. 570 | 571 | Parameters: 572 | 573 | * *`t`*: The table of elements to select from. 574 | * *`test`*: The name of the test to use on table elements. 575 | * *`...`*: Any arguments for the test. 576 | 577 | Usage: 578 | 579 | * `expand('{{ range(5)|select(is_odd)|join("|") }}') --> 1|3|5` 580 | 581 | 582 | #### `filters.selectattr`(*t, attribute, test, ...*) 583 | 584 | Returns a list of elements in table *t* whose string attribute *attribute* 585 | passes test name *test*. 586 | 587 | Parameters: 588 | 589 | * *`t`*: The table of elements to select from. 590 | * *`attribute`*: The attribute of items in the table to select from. This 591 | may be nested (e.g. "foo.bar" tests t[i].foo.bar for all i). 592 | * *`test`*: The name of the test to use on table elements. 593 | * *`...`*: Any arguments for the test. 594 | 595 | Usage: 596 | 597 | * `expand('{{ users|selectattr("online")|mapattr("name")|join("|") }}')` 598 | 599 | 600 | #### `filters.slice`(*t, slices, fill*) 601 | 602 | Returns a generator that produces all of the items in table *t* in *slices* 603 | number of iterations, filling any empty spaces with value *fill*. 604 | Combine this with the "list" filter to produce a list. 605 | 606 | Parameters: 607 | 608 | * *`t`*: The table to slice. 609 | * *`slices`*: The number of slices to produce. 610 | * *`fill`*: The value to use when filling in any empty space in the last 611 | slice. 612 | 613 | Usage: 614 | 615 | * `expand('{% for i in {1, 2, 3}|slice(2, 0) %}{{ i|string }} 616 | {% endfor %}') --> {1, 2} {3, 0}` 617 | 618 | See also: 619 | 620 | * [`filters.list`](#filters.list) 621 | 622 | 623 | #### `filters.sort`(*value, reverse, case\_sensitive, attribute*) 624 | 625 | Returns a copy of table or string *value* in sorted order by value (or by 626 | an attribute named *attribute*), depending on booleans *reverse* and 627 | *case_sensitive*. 628 | 629 | Parameters: 630 | 631 | * *`value`*: The table or string to sort. 632 | * *`reverse`*: Optional flag indicating whether or not to sort in reverse 633 | (descending) order. The default value is `false`, which sorts in ascending 634 | order. 635 | * *`case_sensitive`*: Optional flag indicating whether or not to consider 636 | case when sorting string values. The default value is `false`. 637 | * *`attribute`*: Optional attribute of elements to sort by instead of the 638 | elements themselves. 639 | 640 | Usage: 641 | 642 | * `expand('{{ {2, 3, 1}|sort|string }}') --> {1, 2, 3}` 643 | 644 | 645 | #### `filters.string`(*value*) 646 | 647 | Returns the string representation of value *value*, handling lists properly. 648 | 649 | Parameters: 650 | 651 | * *`value`*: Value to return the string representation of. 652 | 653 | Usage: 654 | 655 | * `expand('{{ {1 * 1, 2 * 2, 3 * 3}|string }}') --> {1, 4, 9}` 656 | 657 | 658 | #### `filters.striptags`(*s*) 659 | 660 | Returns a copy of string *s* with any HTML tags stripped. 661 | Also cleans up whitespace. 662 | 663 | Parameters: 664 | 665 | * *`s`*: String to strip HTML tags from. 666 | 667 | Usage: 668 | 669 | * `expand('{{ "
foo
"|striptags }}') --> foo` 670 | 671 | 672 | #### `filters.sum`(*t, attribute*) 673 | 674 | Returns the numeric sum of the elements in table *t* or the sum of all 675 | attributes named *attribute* in *t*. 676 | 677 | Parameters: 678 | 679 | * *`t`*: The table to calculate the sum of. 680 | * *`attribute`*: Optional attribute of elements to use for summing instead 681 | of the elements themselves. This may be nested (e.g. "foo.bar" sums 682 | `t[i].foo.bar` for all i). 683 | 684 | Usage: 685 | 686 | * `expand('{{ range(6)|sum }}') --> 21` 687 | 688 | 689 | #### `filters.title`(*s*) 690 | 691 | Returns a copy of all words in string *s* in titlecase. 692 | 693 | Parameters: 694 | 695 | * *`s`*: The string to titlecase. 696 | 697 | Usage: 698 | 699 | * `expand('{{ "foo bar"|title }}') --> Foo Bar` 700 | 701 | 702 | #### `filters.truncate`(*s, length, partial\_words, delimiter*) 703 | 704 | Returns a copy of string *s* truncated to *length* number of characters. 705 | Truncated strings end with '...' or string *delimiter*. If boolean 706 | *partial_words* is `false`, truncation will only happen at word boundaries. 707 | 708 | Parameters: 709 | 710 | * *`s`*: The string to truncate. 711 | * *`length`*: The length to truncate the string to. 712 | * *`partial_words`*: Optional flag indicating whether or not to allow 713 | truncation within word boundaries. The default value is `false`. 714 | * *`delimiter`*: Optional delimiter text. The default value is '...'. 715 | 716 | Usage: 717 | 718 | * `expand('{{ "foo bar"|truncate(4) }}') --> "foo ..."` 719 | 720 | 721 | #### `filters.upper`(*s*) 722 | 723 | Returns a copy of string *s* with all uppercase characters. 724 | 725 | Parameters: 726 | 727 | * *`s`*: The string to uppercase. 728 | 729 | Usage: 730 | 731 | * `expand('{{ "foo"|upper }}') --> FOO` 732 | 733 | 734 | #### `filters.urlencode`(*value*) 735 | 736 | Returns a string suitably encoded to be used in a URL from value *value*. 737 | *value* may be a string, table of key-value query parameters, or table of 738 | lists of key-value query parameters (for order). 739 | 740 | Parameters: 741 | 742 | * *`value`*: Value to URL-encode. 743 | 744 | Usage: 745 | 746 | * `expand('{{ {{'f', 1}, {'z', 2}}|urlencode }}') --> f=1&z=2` 747 | 748 | 749 | #### `filters.urlize`(*s, length, nofollow*) 750 | 751 | Replaces any URLs in string *s* with HTML links, limiting link text to 752 | *length* characters. 753 | 754 | Parameters: 755 | 756 | * *`s`*: The string to replace URLs with HTML links in. 757 | * *`length`*: Optional maximum number of characters to include in link text. 758 | The default value is `nil`, which imposes no limit. 759 | * *`nofollow`*: Optional flag indicating whether or not HTML links will get a 760 | "nofollow" attribute. 761 | 762 | Usage: 763 | 764 | * `expand('{{ "example.com"|urlize }}') --> 765 | example.com` 766 | 767 | 768 | #### `filters.wordcount`(*s*) 769 | 770 | Returns the number of words in string *s*. 771 | A word is a sequence of non-space characters. 772 | 773 | Parameters: 774 | 775 | * *`s`*: The string to count words in. 776 | 777 | Usage: 778 | 779 | * `expand('{{ "foo bar baz"|wordcount }}') --> 3` 780 | 781 | 782 | #### `filters.xmlattr`(*t*) 783 | 784 | Interprets table *t* as a list of XML attribute-value pairs, returning them 785 | as a properly formatted, space-separated string. 786 | 787 | Parameters: 788 | 789 | * *`t`*: The table of XML attribute-value pairs. 790 | 791 | Usage: 792 | 793 | * `expand('')` 794 | 795 | 796 | #### `loaders.filesystem`(*directory*) 797 | 798 | Returns a loader for templates that uses the filesystem starting at directory 799 | *directory*. 800 | When looking up the template for a given filename, the loader considers the 801 | following: if no template is being expanded, the loader assumes the given 802 | filename is relative to *directory* and returns the full path; otherwise the 803 | loader assumes the given filename is relative to the current template's 804 | directory and returns the full path. 805 | The returned path may be passed to `io.open()`. 806 | 807 | Parameters: 808 | 809 | * *`directory`*: Optional the template root directory. The default value is 810 | ".", which is the current working directory. 811 | 812 | See also: 813 | 814 | * [`lupa.configure`](#lupa.configure) 815 | 816 | 817 | #### `lupa.reset`() 818 | 819 | Resets Lupa's default delimiters, options, and environments to their 820 | original default values. 821 | 822 | 823 | #### `tests.is_callable`(*value*) 824 | 825 | Returns whether or not value *value* is a function. 826 | 827 | Parameters: 828 | 829 | * *`value`*: The value to test. 830 | 831 | Usage: 832 | 833 | * `expand('{% if is_callable(x) %}...{% endif %}')` 834 | 835 | 836 | #### `tests.is_defined`(*value*) 837 | 838 | Returns whether or not value *value* is non-nil, and thus defined. 839 | 840 | Parameters: 841 | 842 | * *`value`*: The value to test. 843 | 844 | Usage: 845 | 846 | * `expand('{% if is_defined(x) %}...{% endif %}')` 847 | 848 | 849 | #### `tests.is_divisibleby`(*n, num*) 850 | 851 | Returns whether or not number *n* is evenly divisible by number *num*. 852 | 853 | Parameters: 854 | 855 | * *`n`*: The dividend to test. 856 | * *`num`*: The divisor to use. 857 | 858 | Usage: 859 | 860 | * `expand('{% if is_divisibleby(x, y) %}...{% endif %}')` 861 | 862 | 863 | #### `tests.is_escaped`(*value*) 864 | 865 | Returns whether or not value *value* is HTML-safe. 866 | 867 | Parameters: 868 | 869 | * *`value`*: The value to test. 870 | 871 | Usage: 872 | 873 | * `expand('{% if is_escaped(x) %}...{% endif %}')` 874 | 875 | 876 | #### `tests.is_even`(*n*) 877 | 878 | Returns whether or not number *n* is even. 879 | 880 | Parameters: 881 | 882 | * *`n`*: The number to test. 883 | 884 | Usage: 885 | 886 | * `expand('{% for x in range(10) if is_even(x) %}...{% endif %}')` 887 | 888 | 889 | #### `tests.is_iterable`(*value*) 890 | 891 | Returns whether or not value *value* is a sequence (a table with non-zero 892 | length) or a generator. 893 | At the moment, all functions are considered generators. 894 | 895 | Parameters: 896 | 897 | * *`value`*: The value to test. 898 | 899 | Usage: 900 | 901 | * `expand('{% if is_iterable(x) %}...{% endif %}')` 902 | 903 | 904 | #### `tests.is_lower`(*s*) 905 | 906 | Returns whether or not string *s* is in all lower-case characters. 907 | 908 | Parameters: 909 | 910 | * *`s`*: The string to test. 911 | 912 | Usage: 913 | 914 | * `expand('{% if is_lower(s) %}...{% endif %}')` 915 | 916 | 917 | #### `tests.is_mapping`(*value*) 918 | 919 | Returns whether or not value *value* is a table. 920 | 921 | Parameters: 922 | 923 | * *`value`*: The value to test. 924 | 925 | Usage: 926 | 927 | * `expand('{% if is_mapping(x) %}...{% endif %}')` 928 | 929 | 930 | #### `tests.is_nil`(*value*) 931 | 932 | Returns whether or not value *value* is nil. 933 | 934 | Parameters: 935 | 936 | * *`value`*: The value to test. 937 | 938 | Usage: 939 | 940 | * `expand('{% if is_nil(x) %}...{% endif %}')` 941 | 942 | 943 | #### `tests.is_none`(*value*) 944 | 945 | Returns whether or not value *value* is nil. 946 | 947 | Parameters: 948 | 949 | * *`value`*: The value to test. 950 | 951 | Usage: 952 | 953 | * `expand('{% if is_none(x) %}...{% endif %}')` 954 | 955 | 956 | #### `tests.is_number`(*value*) 957 | 958 | Returns whether or not value *value* is a number. 959 | 960 | Parameters: 961 | 962 | * *`value`*: The value to test. 963 | 964 | Usage: 965 | 966 | * `expand('{% if is_number(x) %}...{% endif %}')` 967 | 968 | 969 | #### `tests.is_odd`(*n*) 970 | 971 | Returns whether or not number *n* is odd. 972 | 973 | Parameters: 974 | 975 | * *`n`*: The number to test. 976 | 977 | Usage: 978 | 979 | * `expand('{% for x in range(10) if is_odd(x) %}...{% endif %}')` 980 | 981 | 982 | #### `tests.is_sameas`(*value, other*) 983 | 984 | Returns whether or not value *value* is the same as value *other*. 985 | 986 | Parameters: 987 | 988 | * *`value`*: The value to test. 989 | * *`other`*: The value to compare with. 990 | 991 | Usage: 992 | 993 | * `expand('{% if is_sameas(x, y) %}...{% endif %}')` 994 | 995 | 996 | #### `tests.is_sequence`(*value*) 997 | 998 | Returns whether or not value *value* is a sequence, namely a table with 999 | non-zero length. 1000 | 1001 | Parameters: 1002 | 1003 | * *`value`*: The value to test. 1004 | 1005 | Usage: 1006 | 1007 | * `expand('{% if is_sequence(x) %}...{% endif %}')` 1008 | 1009 | 1010 | #### `tests.is_string`(*value*) 1011 | 1012 | Returns whether or not value *value* is a string. 1013 | 1014 | Parameters: 1015 | 1016 | * *`value`*: The value to test. 1017 | 1018 | Usage: 1019 | 1020 | * `expand('{% if is_string(x) %}...{% endif %}')` 1021 | 1022 | 1023 | #### `tests.is_table`(*value*) 1024 | 1025 | Returns whether or not value *value* is a table. 1026 | 1027 | Parameters: 1028 | 1029 | * *`value`*: The value to test. 1030 | 1031 | Usage: 1032 | 1033 | * `expand('{% if is_table(x) %}...{% endif %}')` 1034 | 1035 | 1036 | #### `tests.is_undefined`(*value*) 1037 | 1038 | Returns whether or not value *value* is nil, and thus effectively undefined. 1039 | 1040 | Parameters: 1041 | 1042 | * *`value`*: The value to test. 1043 | 1044 | Usage: 1045 | 1046 | * `expand('{% if is_undefined(x) %}...{% endif %}')` 1047 | 1048 | 1049 | #### `tests.is_upper`(*s*) 1050 | 1051 | Returns whether or not string *s* is in all upper-case characters. 1052 | 1053 | Parameters: 1054 | 1055 | * *`s`*: The string to test. 1056 | 1057 | Usage: 1058 | 1059 | * `expand('{% if is_upper(s) %}...{% endif %}')` 1060 | 1061 | 1062 | ### Tables defined by `lupa` 1063 | 1064 | 1065 | #### `lupa.env` 1066 | 1067 | The default template environment. 1068 | 1069 | 1070 | #### `lupa.filters` 1071 | 1072 | Lupa's expression filters. 1073 | 1074 | 1075 | #### `lupa.loaders` 1076 | 1077 | Lupa's template loaders. 1078 | 1079 | 1080 | #### `lupa.tests` 1081 | 1082 | Lupa's value tests. 1083 | 1084 | --- 1085 | {% endraw %} 1086 | -------------------------------------------------------------------------------- /tests/suite.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2015-2020 Mitchell. See LICENSE. 2 | -- Contributions from Ana Balan. 3 | -- Contains Lupa's copy of Jinja2's test suite. 4 | -- Any descrepancies are noted and/or described. 5 | -- Note: Lupa's range(n) behaves differently than Jinja2's in that it produces 6 | -- sequences from 1 to n. All tests that utilize range() reflect this. 7 | -- Also, Lua tables are 1-indexed, not 0-indexed, so the tests reflect that. 8 | 9 | local lupa = dofile('../lupa.lua') 10 | local expand, expand_file = lupa.expand, lupa.expand_file 11 | 12 | -- Asserts that value *value* is equal to value *expected*. 13 | -- @param value Resultant value. 14 | -- @param expected Expected value. 15 | local function assert_equal(value, expected) 16 | assert(expected ~= nil, 'expected argument not given to assert_equal') 17 | assert(value == expected, 18 | 'assertion failed! "'..expected..'" expected, got "'..value..'"') 19 | end 20 | 21 | -- Asserts that function *f* raises an error that contains string or pattern 22 | -- *message*. 23 | -- @param message String or pattern that matches the error raised by *f*. 24 | -- @param f The function to call. 25 | -- @param ... Any arguments to *f*. 26 | local function assert_raises(message, f, ...) 27 | local ok, errmsg = pcall(f, ...) 28 | assert(not ok, 'no error raised') 29 | assert(errmsg:find(message), 30 | 'raised error was "'..errmsg..'" and did not contain "'..message..'"') 31 | end 32 | 33 | -- Returns string *s* with any leading or trailing whitespace removed. 34 | -- @param s The string to trim. 35 | function string.trim(s) return s:gsub('^%s*(.-)%s*$', '%1') end 36 | 37 | local test_suite = { 38 | api = { 39 | -- Note: Nearly all Jinja2 API tests are not applicable since Lupa's API is 40 | -- completely different. 41 | api_tests = { 42 | test_cycler = function() 43 | local c = cycler(1, 2, 3) 44 | for _, item in ipairs{1, 2, 3, 1, 2, 3} do 45 | assert_equal(c.current, item) 46 | assert_equal(c:next(), item) 47 | end 48 | c:next() 49 | assert_equal(c.current, 2) 50 | c:reset() 51 | assert_equal(c.current, 1) 52 | end, 53 | } 54 | }, 55 | core_tags = { 56 | for_tests = { 57 | test_simple = function() 58 | local tmpl = '{% for item in seq %}{{ item }}{% endfor %}' 59 | local env = {seq = range(10)} 60 | assert_equal(expand(tmpl, env), '12345678910') 61 | end, 62 | test_else = function() 63 | local tmpl = '{% for item in seq %}XXX{% else %}...{% endfor %}' 64 | assert_equal(expand(tmpl), '...') 65 | end, 66 | test_empty_blocks = function() 67 | local tmpl = '<{% for item in seq %}{% else %}{% endfor %}>' 68 | assert_equal(expand(tmpl), '<>') 69 | end, 70 | test_context_vars = function() 71 | local tmpl = [[{% for item in seq -%} 72 | {{ loop.index }}|{{ loop.index0 }}|{{ loop.revindex }}|{{ 73 | loop.revindex0}}|{{ loop.first }}||{{ loop.last }}|{{ 74 | loop.length}}###{% endfor %}]] 75 | local env = {seq = {0, 1}} 76 | local one, two = expand(tmpl, env):match('^(.+)###(.+)###$') 77 | local one_values, two_values = {}, {} 78 | for v in one:gmatch('[^|]+') do one_values[#one_values + 1] = v end 79 | for v in two:gmatch('[^|]+') do two_values[#two_values + 1] = v end 80 | assert_equal(tonumber(one_values[1]), 1) 81 | assert_equal(tonumber(two_values[1]), 2) 82 | assert_equal(tonumber(one_values[2]), 0) 83 | assert_equal(tonumber(two_values[2]), 1) 84 | assert_equal(tonumber(one_values[3]), 2) 85 | assert_equal(tonumber(two_values[3]), 1) 86 | assert_equal(tonumber(one_values[4]), 1) 87 | assert_equal(tonumber(two_values[4]), 0) 88 | assert_equal(one_values[5], 'true') 89 | assert_equal(two_values[5], 'false') 90 | assert_equal(one_values[6], 'false') 91 | assert_equal(two_values[6], 'true') 92 | assert_equal(one_values[7], '2') 93 | assert_equal(two_values[7], '2') 94 | end, 95 | test_cycling = function() 96 | local tmpl = [[{% for item in seq %}{{ 97 | loop.cycle('<1>', '<2>') }}{% endfor %}{% 98 | for item in seq %}{{ loop.cycle(table.unpack(through)) }}{% endfor %}]] 99 | local env = {seq = range(4), through = {'<1>', '<2>'}} 100 | assert_equal(expand(tmpl, env), string.rep('<1><2>', 4)) 101 | end, 102 | test_scope = function() 103 | local tmpl = '{% for item in seq %}{% endfor %}{{ item }}' 104 | local env = {seq = range(10)} 105 | assert_equal(expand(tmpl, env), '') 106 | end, 107 | test_varlen = function() 108 | local t = range(5) 109 | local function iter() 110 | return function(_, i) 111 | if i > #t then return nil end 112 | return i + 1, t[i] 113 | end, t, 1 114 | end 115 | local tmpl = '{% for item in iter() %}{{ item }}{% endfor %}' 116 | local env = {iter = iter} 117 | assert_equal(expand(tmpl, env), '12345') 118 | tmpl = '{% for item in iter %}{{ item }}{% endfor %}' 119 | assert_raises('invalid generator', expand, tmpl, env) 120 | end, 121 | test_noniter = function() 122 | local tmpl = '{% for item in seq() %}...{% endfor %}' 123 | assert_raises('attempt to call.+nil value', expand, tmpl) 124 | end, 125 | test_recursive = function() 126 | -- Note: no need for 'recursive' keyword, unlike Jinja2. 127 | local tmpl = [[{% for item in seq -%} 128 | [{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] 129 | {%- endfor %}]] 130 | local env = {seq = {{a = 1, b = {{a = 1}, {a = 2}}}, 131 | {a = 2, b = {{a = 1}, {a = 2}}}, 132 | {a = 3, b = {{a = 'a'}}}}} 133 | assert_equal(expand(tmpl, env), '[1<[1][2]>][2<[1][2]>][3<[a]>]') 134 | end, 135 | test_recursive_depth0 = function() 136 | local tmpl = [[{% for item in seq -%} 137 | [{{ loop.depth0 }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] 138 | {%- endfor %}]] 139 | local env = {seq = {{a = 1, b = {{a = 1}, {a = 2}}}, 140 | {a = 2, b = {{a = 1}, {a = 2}}}, 141 | {a = 3, b = {{a = 'a'}}}}} 142 | assert_equal(expand(tmpl, env), '[0:1<[1:1][1:2]>][0:2<[1:1][1:2]>][0:3<[1:a]>]') 143 | end, 144 | test_recursive_depth = function() 145 | local tmpl = [[{% for item in seq -%} 146 | [{{ loop.depth }}:{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] 147 | {%- endfor %}]] 148 | local env = {seq = {{a = 1, b = {{a = 1}, {a = 2}}}, 149 | {a = 2, b = {{a = 1}, {a = 2}}}, 150 | {a = 3, b = {{a = 'a'}}}}} 151 | assert_equal(expand(tmpl, env), '[1:1<[2:1][2:2]>][1:2<[2:1][2:2]>][1:3<[2:a]>]') 152 | end, 153 | test_looploop = function() 154 | local tmpl = [[{% for row in table %} 155 | {%- set rowloop = loop -%} 156 | {% for cell in row:gmatch('.') -%} 157 | [{{ rowloop.index }}|{{ loop.index }}] 158 | {%- endfor %} 159 | {%- endfor %}]] 160 | local env = {table = {'ab', 'cd'}} 161 | assert_equal(expand(tmpl, env), '[1|1][1|2][2|1][2|2]') 162 | end, 163 | test_loop_last = function() 164 | local tmpl = '{% for i in items %}{{ i }}'.. 165 | '{% if not loop.last %}'.. 166 | ',{% endif %}{% endfor %}' 167 | local env = {items={1, 2, 3}} 168 | assert_equal(expand(tmpl, env), '1,2,3') 169 | end, 170 | test_loop_errors = function() 171 | local tmpl = [[{% for item in {1} if loop.index 172 | == 0 %}...{% endfor %}]] 173 | assert_raises('attempt to index.+nil value', expand, tmpl) 174 | tmpl = [[{% for item in {} %}...{% else 175 | %}{{ loop }}{% endfor %}]] 176 | assert_equal(expand(tmpl), '') 177 | end, 178 | test_loop_filter = function() 179 | local tmpl = '{% for item in range(10) if '.. 180 | 'is_even(item) %}[{{ item }}]{% endfor %}' 181 | assert_equal(expand(tmpl), '[2][4][6][8][10]') 182 | tmpl = [[ 183 | {%- for item in range(10) if is_even(item) %}[{{ 184 | loop.index }}:{{ item }}]{% endfor %}]] 185 | assert_equal(expand(tmpl), '[1:2][2:4][3:6][4:8][5:10]') 186 | end, 187 | test_loop_unassignable = function() 188 | local tmpl = '{% for loop in seq %}...{% endfor %}' 189 | local env = {0} 190 | assert_raises('invalid variable name', expand, tmpl, env) 191 | end, 192 | test_scoped_special_var = function() 193 | local tmpl = '{% for s in seq %}[{{ loop.first }}{% for c in s:gmatch(".") %}'.. 194 | '|{{ loop.first }}{% endfor %}]{% endfor %}' 195 | local env = {seq = {'ab', 'cd'}} 196 | assert_equal(expand(tmpl, env), '[true|true|false][false|true|false]') 197 | end, 198 | test_scoped_loop_var = function() 199 | local tmpl = '{% for x in seq %}{{ loop.first }}'.. 200 | '{% for y in seq %}{% endfor %}{% endfor %}' 201 | local env = {seq = {'a', 'b'}} 202 | assert_equal(expand(tmpl, env), 'truefalse') 203 | tmpl = '{% for x in seq %}{% for y in seq %}'.. 204 | '{{ loop.first }}{% endfor %}{% endfor %}' 205 | assert_equal(expand(tmpl, env), 'truefalsetruefalse') 206 | end, 207 | test_recursive_empty_loop_iter = function() 208 | local tmpl = [[ 209 | {%- for item in foo -%}{%- endfor -%} 210 | ]] 211 | local env = {foo = {}} 212 | assert_equal(expand(tmpl, env), '') 213 | end, 214 | test_call_in_loop = function() 215 | local tmpl = [[ 216 | {%- macro do_something() -%} 217 | [{{ caller() }}] 218 | {%- endmacro %} 219 | 220 | {%- for i in {1,2,3} %} 221 | {%- call do_something() -%} 222 | {{ i }} 223 | {%- endcall %} 224 | {%- endfor -%} 225 | ]] 226 | assert_equal(expand(tmpl), '[1][2][3]') 227 | end, 228 | test_scoping = function() 229 | local tmpl = [[ 230 | {%- for item in foo %}...{{ item }}...{% endfor %} 231 | {%- macro item(a) %}...{{ a }}...{% endmacro %} 232 | {{- item(2) -}} 233 | ]] 234 | local env = {foo = {1}} 235 | assert_equal(expand(tmpl, env), '...1......2...') 236 | end, 237 | test_unpacking = function() 238 | local tmpl = '{% for a, b, c in {{1, 2, 3}} %}'.. 239 | '{{ a }}|{{ b }}|{{ c }}{% endfor %}' 240 | assert_equal(expand(tmpl), '1|2|3') 241 | end 242 | }, 243 | if_tests = { 244 | test_simple = function() 245 | local tmpl = '{% if true %}...{% endif %}' 246 | assert_equal(expand(tmpl), '...') 247 | end, 248 | test_elif = function() 249 | local tmpl = '{% if false %}XXX{% elseif true'.. 250 | '%}...{% else %}XXX{% endif %}' 251 | assert_equal(expand(tmpl), '...') 252 | end, 253 | test_else = function() 254 | local tmpl = '{% if false %}XXX{% else %}...{% endif %}' 255 | assert_equal(expand(tmpl), '...') 256 | end, 257 | test_empty = function() 258 | local tmpl = '[{% if true %}{% else %}{% endif %}]' 259 | assert_equal(expand(tmpl), '[]') 260 | end, 261 | test_complete = function() 262 | local tmpl = '{% if a %}A{% elseif b %}B{% elseif c == d %}'.. 263 | 'C{% else %}D{% endif %}' 264 | local env = {a = false, b = false, c = 42, d = 42.0} 265 | assert_equal(expand(tmpl, env), 'C') 266 | end, 267 | test_no_scope = function() 268 | local tmpl = '{% if a %}{% set foo = 1 %}{% endif %}{{ foo }}' 269 | local env = {a = true} 270 | assert_equal(expand(tmpl, env), '1') 271 | tmpl = '{% if true %}{% set foo = 1 %}{% endif %}{{ foo }}' 272 | assert_equal(expand(tmpl), '1') 273 | end, 274 | -- Note: this test does not exist in Jinja2's suite, but is needed for 275 | -- completeness. 276 | test_elif_else = function() 277 | local tmpl = '{% if false %}XXX{% elseif false %}XXX{% else'.. 278 | '%}...{% endif %}' 279 | assert_equal(expand(tmpl), '...') 280 | end 281 | }, 282 | macro_tests = { 283 | setup = function() lupa.configure{trim_blocks = true} end, 284 | teardown = lupa.reset, 285 | test_simple = function() 286 | local tmpl = [[ 287 | {%macro say_hello(name) %}Hello {{ name }}!{% endmacro %} 288 | {{ say_hello('Peter') }}]] 289 | assert_equal(expand(tmpl), 'Hello Peter!') 290 | end, 291 | test_scoping = function() 292 | local tmpl = [[ 293 | {% macro level1(data1) %} 294 | {% macro level2(data2) %}{{ data1 }}|{{ data2 }}{% endmacro %} 295 | {{ level2('bar') }}{% endmacro %} 296 | {{ level1('foo') }}]] 297 | assert_equal(expand(tmpl), 'foo|bar') 298 | end, 299 | test_arguments = function() 300 | local tmpl = [[ 301 | {% macro m(a, b, c='c', d='d') %}{{ a }}|{{ b }}|{{ c }}|{{ d }}{% endmacro %} 302 | {{ m() }}|{{ m('a') }}|{{ m('a', 'b') }}|{{ m(1, 2, 3) }}]] 303 | assert_equal(expand(tmpl), '||c|d|a||c|d|a|b|c|d|1|2|3|d') 304 | end, 305 | test_varargs = function() 306 | local tmpl = [[ 307 | {% macro test() %}{{ varargs|join('|') }}{% endmacro %} 308 | {{ test(1, 2, 3) }}]] 309 | assert_equal(expand(tmpl), '1|2|3') 310 | end, 311 | test_simple_call = function() 312 | local tmpl = [=[ 313 | {% macro test() %}[[{{ caller() }}{% endmacro %} 314 | {% call test() %}data{% endcall %}]]]=] 315 | assert_equal(expand(tmpl), '[[data]]') 316 | end, 317 | test_complex_call = function() 318 | local tmpl = [=[ 319 | {% macro test() %}[[{{ caller('data') }}]]{% endmacro %} 320 | {% call(data) test() %}{{ data }}{% endcall %}]=] 321 | assert_equal(expand(tmpl), '[[data]]') 322 | end, 323 | test_caller_undefined = function() 324 | local tmpl = [[ 325 | {% set caller = 42 %} 326 | {% macro test() %}{{ not caller }}{% endmacro %} 327 | {{ test() }}]] 328 | assert_equal(expand(tmpl), 'true') 329 | end, 330 | test_include = function() 331 | local tmpl = '{% include "data/test_macro" %}{{ test("foo") }}' 332 | assert_equal(expand(tmpl), '[foo]') 333 | end, 334 | -- Note: test_macro_api is not applicable since this implementation stores 335 | -- macros as Lua functions with no metadata. 336 | test_callself = function() 337 | local tmpl = '{% macro foo(x) %}{{ x }}{% if x > 1 %}|'.. 338 | '{{ foo(x - 1) }}{% endif %}{% endmacro %}'.. 339 | '{{ foo(5) }}' 340 | assert_equal(expand(tmpl), '5|4|3|2|1') 341 | end 342 | } 343 | }, 344 | debug = { 345 | debug_tests = { 346 | test_runtime_error = function() 347 | local tmpl = 'data/debug/broken.html' 348 | local env = {fail = function() next() end} 349 | assert_raises('^Runtime Error.+broken%.html.+'.. 350 | 'on line %d, column %d: bad argument', expand_file, tmpl, 351 | env) 352 | end, 353 | test_syntax_error = function() 354 | local tmpl = 'data/debug/syntaxerror.html' 355 | assert_raises('^Parse Error.+syntaxerror%.html.+'.. 356 | 'on line %d, column %d:.+endfor.+expected', expand_file, 357 | tmpl) 358 | end, 359 | test_regular_error = function() 360 | local tmpl = '{{ test() }}' 361 | local env = {test = function() error('wtf') end} 362 | assert_raises('^Runtime Error.+.+wtf', expand, tmpl, env) 363 | end, 364 | } 365 | }, 366 | filters = { 367 | filter_tests = { 368 | test_filter_calling = function() 369 | local result = lupa.filters.sum{1, 2, 3} 370 | assert_equal(result, 6) 371 | end, 372 | test_capitalize = function() 373 | local tmpl = '{{ "foo bar"|capitalize }}' 374 | assert_equal(expand(tmpl), 'Foo bar') 375 | end, 376 | test_center = function() 377 | local tmpl = '{{ "foo"|center(9) }}' 378 | assert_equal(expand(tmpl), ' foo ') 379 | end, 380 | test_default = function() 381 | local tmpl = '{{ missing|default("no") }}|{{ false|default("no") }}|'.. 382 | '{{ false|default("no", true) }}|{{ given|default("no") }}' 383 | local env = {given = 'yes'} 384 | assert_equal(expand(tmpl, env), 'no|false|no|yes') 385 | end, 386 | test_dictsort = function() 387 | local tmpl = '{{ foo|dictsort|string }}|'.. 388 | '{{ foo|dictsort(true)|string }}|'.. 389 | '{{ foo|dictsort(false, "value")|string }}' 390 | local env = {foo={aa = 0, b = 1, c = 2, AB = 3}} 391 | assert_equal(expand(tmpl, env), 392 | '{{"aa", 0}, {"AB", 3}, {"b", 1}, {"c", 2}}|'.. 393 | '{{"AB", 3}, {"aa", 0}, {"b", 1}, {"c", 2}}|'.. 394 | '{{"aa", 0}, {"b", 1}, {"c", 2}, {"AB", 3}}') 395 | end, 396 | test_batch = function() 397 | local tmpl = '{{ foo|batch(3)|list|string }}|'.. 398 | '{{ foo|batch(3, "X")|list|string }}' 399 | local env = {foo = range(10)} 400 | assert_equal(expand(tmpl, env), 401 | '{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10}}|'.. 402 | '{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, "X", "X"}}') 403 | end, 404 | test_slice = function() 405 | local tmpl = '{{ foo|slice(3)|list|string }}|'.. 406 | '{{ foo|slice(3, "X")|list|string }}' 407 | local env = {foo = range(10)} 408 | assert_equal(expand(tmpl, env), 409 | '{{1, 2, 3, 4}, {5, 6, 7}, {8, 9, 10}}|'.. 410 | '{{1, 2, 3, 4}, {5, 6, 7, "X"}, {8, 9, 10, "X"}}') 411 | end, 412 | test_escape = function() 413 | local tmpl = [[{{ '<">&'|escape}}]] 414 | assert_equal(expand(tmpl), '<">&') 415 | end, 416 | test_striptags = function() 417 | local tmpl = '{{ foo|striptags }}' 418 | local env = {foo = '

just a small \n '.. 419 | 'example link

\n

to a webpage

'.. 420 | ''} 421 | assert_equal(expand(tmpl, env), 'just a small example link to a webpage') 422 | end, 423 | test_filesizeformat = function() 424 | local tmpl = '{{ 100|filesizeformat }}|'.. 425 | '{{ 1000|filesizeformat }}|'.. 426 | '{{ 1000000|filesizeformat }}|'.. 427 | '{{ 1000000000|filesizeformat }}|'.. 428 | '{{ 1000000000000|filesizeformat }}|'.. 429 | '{{ 100|filesizeformat(true) }}|'.. 430 | '{{ 1000|filesizeformat(true) }}|'.. 431 | '{{ 1000000|filesizeformat(true) }}|'.. 432 | '{{ 1000000000|filesizeformat(true) }}|'.. 433 | '{{ 1000000000000|filesizeformat(true) }}' 434 | assert_equal(expand(tmpl), 435 | '100 Bytes|1.0 kB|1.0 MB|1.0 GB|1.0 TB|100 Bytes|'.. 436 | '1000 Bytes|976.6 KiB|953.7 MiB|931.3 GiB') 437 | tmpl = '{{ 300|filesizeformat }}|'.. 438 | '{{ 3000|filesizeformat }}|'.. 439 | '{{ 3000000|filesizeformat }}|'.. 440 | '{{ 3000000000|filesizeformat }}|'.. 441 | '{{ 3000000000000|filesizeformat }}|'.. 442 | '{{ 300|filesizeformat(true) }}|'.. 443 | '{{ 3000|filesizeformat(true) }}|'.. 444 | '{{ 3000000|filesizeformat(true) }}' 445 | assert_equal(expand(tmpl), 446 | '300 Bytes|3.0 kB|3.0 MB|3.0 GB|3.0 TB|300 Bytes|'.. 447 | '2.9 KiB|2.9 MiB') 448 | end, 449 | test_first = function() 450 | local tmpl = '{{ foo|first }}' 451 | local env = {foo = range(10)} 452 | assert_equal(expand(tmpl, env), '1') 453 | end, 454 | test_float = function() 455 | local tmpl = '{{ "42"|float }}|'.. 456 | '{{ "ajsghasjgd"|float }}|'.. 457 | '{{ "32.32"|float }}' 458 | if _VERSION >= 'Lua 5.3' then 459 | assert_equal(expand(tmpl), '42.0|0.0|32.32') 460 | end 461 | end, 462 | test_format = function() 463 | local tmpl = '{{ "%s,%s"|format("a", "b") }}' 464 | assert_equal(expand(tmpl), 'a,b') 465 | end, 466 | test_indent = function() 467 | local tmpl = '{{ foo|indent(2) }}|{{ foo|indent(2, true) }}' 468 | local env = {foo = 'foo bar foo bar\nfoo bar foo bar'} 469 | assert_equal(expand(tmpl, env), 470 | 'foo bar foo bar\n foo bar foo bar| '.. 471 | 'foo bar foo bar\n foo bar foo bar') 472 | end, 473 | test_int = function() 474 | local tmpl = '{{ "42"|int }}|{{ "ajsghasjgd"|int }}|{{ "32.32"|int }}' 475 | assert_equal(expand(tmpl), '42|0|32') 476 | end, 477 | test_join = function() 478 | local tmpl = '{{ {1, 2, 3}|join("|") }}' 479 | assert_equal(expand(tmpl), '1|2|3') 480 | -- Note: '|' cannot occur within an expression, only at the end, so this 481 | -- test input is slightly different. 482 | lupa.configure{autoescape = true} 483 | tmpl = '{{ {"", "foo"}|join }}' 484 | assert_equal(expand(tmpl), '<foo><span>foo</span>') 485 | lupa.reset() 486 | end, 487 | test_join_attribute = function() 488 | local tmpl = '{{ users|join(", ", "username") }}' 489 | local env = {users = {{username = 'foo'}, {username = 'bar'}}} 490 | assert_equal(expand(tmpl, env), 'foo, bar') 491 | end, 492 | test_last = function() 493 | local tmpl = '{{ foo|last }}' 494 | local env = {foo = range(10)} 495 | assert_equal(expand(tmpl, env), '10') 496 | end, 497 | test_length = function() 498 | local tmpl = '{{ "hello world"|length }}' 499 | assert_equal(expand(tmpl), '11') 500 | end, 501 | test_lower = function() 502 | local tmpl = '{{ "FOO"|lower }}' 503 | assert_equal(expand(tmpl), 'foo') 504 | end, 505 | -- Note: pprint filter is not applicable since Lua does not have a data 506 | -- pretty-printer. 507 | test_random = function() 508 | local tmpl = '{{ seq|random }}' 509 | local env = {seq = range(100)} 510 | for i = 1, 10 do 511 | local j = tonumber(expand(tmpl, env)) 512 | assert(j >= 1 and j <= 100) 513 | end 514 | end, 515 | test_reverse = function() 516 | local tmpl = '{{ "foobar"|reverse }}|'.. 517 | '{{ {1, 2, 3}|reverse|string }}' 518 | assert_equal(expand(tmpl), 'raboof|{3, 2, 1}') 519 | end, 520 | test_string = function() 521 | local tmpl = '{{ obj|string }}' 522 | local env = {obj = {1, 2, 3, 4, 5}} 523 | assert_equal(expand(tmpl, env), '{1, 2, 3, 4, 5}') 524 | end, 525 | test_title = function() 526 | local tmpl = '{{ "foo bar"|title }}' 527 | assert_equal(expand(tmpl), 'Foo Bar') 528 | tmpl = [[{{ "foo\'s bar"|title }}]] 529 | assert_equal(expand(tmpl), "Foo's Bar") 530 | tmpl = '{{ "foo bar"|title }}' 531 | assert_equal(expand(tmpl), 'Foo Bar') 532 | tmpl = '{{ "f bar f"|title }}' 533 | assert_equal(expand(tmpl), 'F Bar F') 534 | tmpl = '{{ "foo-bar"|title }}' 535 | assert_equal(expand(tmpl), 'Foo-Bar') 536 | tmpl = '{{ "foo\tbar"|title }}' 537 | assert_equal(expand(tmpl), 'Foo\tBar') 538 | tmpl = '{{ "FOO\tBAR"|title }}' 539 | assert_equal(expand(tmpl), 'Foo\tBar') 540 | end, 541 | test_truncate = function() 542 | local tmpl = '{{ data|truncate(15, true, ">>>") }}|'.. 543 | '{{ data|truncate(15, false, ">>>") }}|'.. 544 | '{{ smalldata|truncate(15) }}' 545 | local env = { 546 | data = string.rep('foobar baz bar', 1000), 547 | smalldata = 'foobar baz bar' 548 | } 549 | assert_equal(expand(tmpl, env), 'foobar baz barf>>>|foobar baz >>>|foobar baz bar') 550 | end, 551 | test_upper = function() 552 | local tmpl = '{{ "foo"|upper }}' 553 | assert_equal(expand(tmpl), 'FOO') 554 | end, 555 | test_urlize = function() 556 | local tmpl = '{{ "foo http://www.example.com/ bar"|urlize }}' 557 | assert_equal(expand(tmpl), 558 | 'foo '.. 559 | 'http://www.example.com/ bar') 560 | end, 561 | test_wordcount = function() 562 | local tmpl = '{{ "foo bar baz"|wordcount }}' 563 | assert_equal(expand(tmpl), '3') 564 | end, 565 | test_block = function() 566 | local tmpl = '{% filter lower|escape %}{% endfilter %}' 567 | assert_equal(expand(tmpl), '<hehe>') 568 | end, 569 | test_chaining = function() 570 | local tmpl = '{{ {"", ""}|first|upper|escape}}' 571 | assert_equal(expand(tmpl), '<FOO>') 572 | end, 573 | test_sum = function() 574 | local tmpl = '{{ {1, 2, 3, 4, 5, 6}|sum }}' 575 | assert_equal(expand(tmpl), '21') 576 | end, 577 | test_sum_attributes = function() 578 | local tmpl = '{{ values|sum("value") }}' 579 | local env = {values = {{value = 23}, {value = 1}, {value = 18}}} 580 | assert_equal(expand(tmpl, env), '42') 581 | end, 582 | test_sum_attributes_nested = function() 583 | local tmpl = '{{ values|sum("real.value") }}' 584 | local env = {values = {{real = {value = 23}}, 585 | {real = {value = 1}}, 586 | {real = {value = 18}}}} 587 | assert_equal(expand(tmpl, env), '42') 588 | end, 589 | test_sum_attributes = function() 590 | local tmpl = [[{{ values|sum('2') }}]] 591 | local env = {values = {{'foo', 23}, {'bar', 1}, {'baz', 18}}} 592 | assert_equal(expand(tmpl, env), '42') 593 | tmpl = [[{{ values|sum(2) }}]] 594 | assert_equal(expand(tmpl, env), '42') 595 | end, 596 | test_abs = function() 597 | local tmpl = '{{ -1|abs }}|{{ 1|abs }}' 598 | assert_equal(expand(tmpl), '1|1') 599 | end, 600 | test_round_positive = function() 601 | local tmpl = '{{ 2.7|round }}|{{ 2.1|round }}|'.. 602 | '{{ 2.1234|round(3, "floor") }}|'.. 603 | '{{ 2.1|round(0, "ceil") }}' 604 | -- Note: Lua's results drop the fractional part if it is 0. 605 | assert_equal(expand(tmpl), '3|2|2.123|3') 606 | end, 607 | test_round_negative = function() 608 | local tmpl = '{{ 21.3|round(-1)}}|'.. 609 | '{{ 21.3|round(-1, "ceil")}}|'.. 610 | '{{ 21.3|round(-1, "floor")}}' 611 | assert_equal(expand(tmpl), '20|30|20') 612 | end, 613 | test_xmlattr = function() 614 | local tmpl = '{{ {foo = 42, bar = 23, ["blub:blub"] = ""}|xmlattr }}' 615 | local s = expand(tmpl) 616 | assert(select(2, s:gsub(' ', '')) == 2) 617 | assert(s:find('foo="42"')) 618 | assert(s:find('bar="23"')) 619 | assert(s:find('blub:blub="<?>"', 1, true)) 620 | end, 621 | test_sort = function() 622 | local tmpl = '{{ {2, 3, 1}|sort|string }}|{{ {2, 3, 1}|sort(true)|string }}' 623 | assert_equal(expand(tmpl), '{1, 2, 3}|{3, 2, 1}') 624 | tmpl = '{{ {"c", "A", "b", "D"}|sort|join }}' 625 | assert_equal(expand(tmpl), 'AbcD') 626 | tmpl = '{{ {"foo", "Bar", "blah"}|sort|string }}' 627 | assert_equal(expand(tmpl), '{"Bar", "blah", "foo"}') 628 | tmpl = '{{ items|sort(nil, nil, "value")|join("", "value") }}' 629 | local env = { 630 | items = {{value = 3}, {value = 2}, {value = 4}, {value = 1}} 631 | } 632 | assert_equal(expand(tmpl, env), '1234') 633 | end, 634 | test_groupby = function() 635 | local tmpl = [[ 636 | {%- for grouper, list in {{foo = 1, bar = 2}, 637 | {foo = 2, bar = 3}, 638 | {foo = 1, bar = 1}, 639 | {foo = 3, bar = 4}}|groupby("foo") -%} 640 | {{ grouper }}{% for x in list %}: {{ x.foo }}, {{ x.bar }}{% endfor %}| 641 | {%- endfor %}]] 642 | assert_equal(expand(tmpl), '1: 1, 2: 1, 1|2: 2, 3|3: 3, 4|') 643 | end, 644 | test_grouby_index = function() 645 | local tmpl = [[ 646 | {%- for grouper, list in {{"a", 1}, 647 | {"a", 2}, 648 | {"b", 1}}|groupby(1) -%} 649 | {{ grouper }}{% for x in list %}:{{ x[2] }}{% endfor %}| 650 | {%- endfor %}]] 651 | assert_equal(expand(tmpl), 'a:1:2|b:1|') 652 | end, 653 | test_groupby_multidot = function() 654 | local tmpl = [[ 655 | {%- for year, list in articles|groupby("date.year") -%} 656 | {{ year }}{% for x in list %}[{{ x.title }}]{% endfor %}| 657 | {%- endfor %}]] 658 | local env = { 659 | articles = { 660 | {title = 'aha', date = {day = 1, month = 1, year = 1970}}, 661 | {title = 'interesting', date = {day = 2, month = 1, year = 1970}}, 662 | {title = 'really?', date = {day = 3, month = 1, year = 1970}}, 663 | {title = 'totally not', date = {day = 1, month = 1, year = 1971}}, 664 | } 665 | } 666 | assert_equal(expand(tmpl, env), '1970[aha][interesting][really?]|1971[totally not]|') 667 | end, 668 | test_filtertag = function() 669 | local tmpl = '{% filter upper|replace("FOO", "foo") %}'.. 670 | 'foobar{% endfilter %}' 671 | assert_equal(expand(tmpl), 'fooBAR') 672 | end, 673 | test_replace = function() 674 | local tmpl = '{{ string|replace("o", 42) }}' 675 | local env = {string = ''} 676 | assert_equal(expand(tmpl, env), '') 677 | lupa.configure{autoescape = true} 678 | tmpl = '{{ string|replace("o", 42) }}' 679 | env = {string = ''} 680 | assert_equal(expand(tmpl, env), '<f4242>') 681 | tmpl = '{{ string|replace("<", 42) }}' 682 | env = {string = ''} 683 | assert_equal(expand(tmpl, env), '42foo>') 684 | tmpl = '{{ string|replace("o", ">x<") }}' 685 | env = {string = 'foo'} 686 | assert_equal(expand(tmpl, env), 'f>x<>x<') 687 | lupa.reset() 688 | end, 689 | test_forceescape = function() 690 | -- Note: This implementation does not support markup, so this test input 691 | -- is slightly different. 692 | local tmpl = '{% set x = "
"|safe %}{{ x|forceescape }}' 693 | assert_equal(expand(tmpl), '<div />') 694 | end, 695 | test_safe = function() 696 | lupa.configure{autoescape = true} 697 | local tmpl = '{{ "
foo
"|safe }}' 698 | assert_equal(expand(tmpl), '
foo
') 699 | tmpl = '{{ "
foo
" }}' 700 | assert_equal(expand(tmpl), '<div>foo</div>') 701 | lupa.reset() 702 | end, 703 | test_urlencode = function() 704 | lupa.configure{autoescape = true} 705 | local tmpl = '{{ "Hello, world!"|urlencode }}' 706 | assert_equal(expand(tmpl), 'Hello%2C%20world%21') 707 | -- Note: Lua does not support unicode escape sequences in strings so 708 | -- some unicode tests are left out. 709 | tmpl = '{{ o|urlencode }}' 710 | local env = {o = {{'f', 1}}} 711 | assert_equal(expand(tmpl, env), 'f=1') 712 | env = {o = {{'f', 1}, {'z', 2}}} 713 | assert_equal(expand(tmpl, env), 'f=1&z=2') 714 | env = {o = {[0] = 1}} 715 | assert_equal(expand(tmpl, env), '0=1') 716 | lupa.reset() 717 | end, 718 | test_simple_map = function() 719 | local tmpl = '{{ {"1", "2", "3"}|map("int")|sum }}' 720 | assert_equal(expand(tmpl), '6') 721 | end, 722 | test_attribute_map = function() 723 | local tmpl = '{{ users|mapattr("name")|join("|") }}' 724 | local env = { 725 | users = {{name = 'john'}, {name = 'jane'}, {name = 'mike'}} 726 | } 727 | assert_equal(expand(tmpl, env), 'john|jane|mike') 728 | end, 729 | test_empty_map = function() 730 | local tmpl = '{{ {}|map("upper")|string }}' 731 | assert_equal(expand(tmpl), '{}') 732 | end, 733 | test_simple_select = function() 734 | local tmpl = '{{ {1, 2, 3, 4, 5}|select(is_odd)|join("|") }}' 735 | assert_equal(expand(tmpl), '1|3|5') 736 | end, 737 | test_bool_select = function() 738 | local tmpl = '{{ {false, 0, 1, 2, 3, 4, 5}|select|join("|") }}' 739 | assert_equal(expand(tmpl), '0|1|2|3|4|5') 740 | end, 741 | test_simple_reject = function() 742 | local tmpl = '{{ {1, 2, 3, 4, 5}|reject(is_odd)|join("|") }}' 743 | assert_equal(expand(tmpl), '2|4') 744 | end, 745 | test_bool_reject = function() 746 | local tmpl = '{{ {false, 0, 1, 2, 3, 4, 5}|reject|join("|") }}' 747 | assert_equal(expand(tmpl), 'false') 748 | end, 749 | test_simple_select_attr = function() 750 | local tmpl = '{{ users|selectattr("is_active")|'.. 751 | 'mapattr("name")|join("|") }}' 752 | local env = {users = {{name = 'john', is_active = true}, 753 | {name = 'jane', is_active = true}, 754 | {name = 'mike', is_active = false}}} 755 | assert_equal(expand(tmpl, env), 'john|jane') 756 | end, 757 | test_simple_reject_attr = function() 758 | local tmpl = '{{ users|rejectattr("is_active")|'.. 759 | 'mapattr("name")|join(",") }}' 760 | local env = {users = {{name = 'john', is_active = true}, 761 | {name = 'jane', is_active = true}, 762 | {name = 'mike', is_active = false}}} 763 | assert_equal(expand(tmpl, env), 'mike') 764 | end, 765 | test_func_select_attr = function() 766 | local tmpl = '{{ users|selectattr("id", is_odd)|'.. 767 | 'mapattr("name")|join("|") }}' 768 | local env = {users = {{id = 1, name = 'john'}, 769 | {id = 2, name = 'jane'}, 770 | {id = 3, name = 'mike'}}} 771 | assert_equal(expand(tmpl, env), 'john|mike') 772 | end, 773 | test_func_reject_attr = function() 774 | local tmpl = '{{ users|rejectattr("id", is_odd)|'.. 775 | 'mapattr("name")|join(",") }}' 776 | local env = {users = {{id = 1, name = 'john'}, 777 | {id = 2, name = 'jane'}, 778 | {id = 3, name = 'mike'}}} 779 | assert_equal(expand(tmpl, env), 'jane') 780 | end, 781 | } 782 | }, 783 | imports = { 784 | import_tests = { 785 | setup = function() 786 | lupa.configure{loader = lupa.loaders.filesystem('data/imports')} 787 | end, 788 | teardown = lupa.reset, 789 | test_context_imports = function() 790 | lupa.env.bar = 23 791 | local tmpl = '{% import "module" as m %}{{ m.test() }}' 792 | local env = {foo = 42} 793 | assert_equal(expand(tmpl, env), '[|23]') 794 | tmpl = '{% import "module" as m without context %}{{ m.test() }}' 795 | assert_equal(expand(tmpl, env), '[|23]') 796 | tmpl = '{% import "module" as m with context %}{{ m.test() }}' 797 | assert_equal(expand(tmpl, env), '[42|23]') 798 | -- Note: "from x import y" is not supported by this implementation. 799 | lupa.env.bar = nil 800 | end, 801 | -- Note: test_trailing_comma is not applicable since this implementation 802 | -- does not support "from x import y". 803 | test_exports = function() 804 | local tmpl = '{% import "exports" %}' 805 | local env = {} 806 | expand(tmpl, env) 807 | assert_equal(env.toplevel(), '...') 808 | assert(not env.__missing) 809 | assert_equal(env.variable, 42) 810 | assert(not env.nothere) 811 | end, 812 | }, 813 | include_tests = { 814 | setup = function() 815 | lupa.configure{loader = lupa.loaders.filesystem('data/imports')} 816 | end, 817 | teardown = lupa.reset, 818 | test_context_include = function() 819 | local tmpl = '{% include "header" %}' 820 | local env = {foo = 42} 821 | assert_equal(expand(tmpl, env), '[42|23]') 822 | tmpl = '{% include "header" with context %}' 823 | assert_equal(expand(tmpl, env), '[42|23]') 824 | tmpl = '{% include "header" without context %}' 825 | assert_equal(expand(tmpl, env), '[|23]') 826 | end, 827 | test_choice_includes = function() 828 | local tmpl = '{% include {"missing", "header"} %}' 829 | local env = {foo = 42} 830 | assert_equal(expand(tmpl, env), '[42|23]') 831 | tmpl = '{% include {"missing", "missing2"} ignore missing %}' 832 | assert_equal(expand(tmpl, env), '') 833 | tmpl = '{% include {"missing", "missing2"} %}' 834 | assert_raises('no file.-found', expand, tmpl, env) 835 | tmpl = '{% include x %}' 836 | env.x = {'missing', 'header'} 837 | assert_equal(expand(tmpl, env), '[42|23]') 838 | env.x = 'header' 839 | assert_equal(expand(tmpl, env), '[42|23]') 840 | tmpl = '{% include {x} %}' 841 | assert_equal(expand(tmpl, env), '[42|23]') 842 | end, 843 | test_include_ignoring_missing = function() 844 | local tmpl = '{% include "missing" %}' 845 | assert_raises('no file.-found', expand, tmpl) 846 | tmpl = '{% include "missing" ignore missing %}' 847 | assert_equal(expand(tmpl), '') 848 | tmpl = '{% include "missing" ignore missing with context %}' 849 | assert_equal(expand(tmpl), '') 850 | tmpl = '{% include "missing" ignore missing without context %}' 851 | assert_equal(expand(tmpl), '') 852 | end, 853 | test_context_include_with_override = function() 854 | local tmpl = 'main' 855 | assert_equal(expand_file(tmpl), '123') 856 | end, 857 | test_unoptimized_scopes = function() 858 | local tmpl = [[ 859 | {% macro outer(o) %} 860 | {% macro inner() %} 861 | {% include "o_printer" %} 862 | {% endmacro %} 863 | {{ inner() }} 864 | {% endmacro %} 865 | {{ outer("FOO") }} 866 | ]] 867 | assert_equal(expand(tmpl):trim(), '(FOO)') 868 | end, 869 | } 870 | }, 871 | inheritence = { 872 | inheritence_tests = { 873 | setup = function() 874 | lupa.configure{ 875 | trim_blocks = true, 876 | loader = lupa.loaders.filesystem('data/inheritence') 877 | } 878 | end, 879 | teardown = lupa.reset, 880 | test_layout = function() 881 | local tmpl = 'layout' 882 | assert_equal(expand_file(tmpl), 883 | '|block 1 from layout|block 2 from '.. 884 | 'layout|nested block 4 from layout|') 885 | end, 886 | test_level1 = function() 887 | local tmpl = 'level1' 888 | assert_equal(expand_file(tmpl), 889 | '|block 1 from level1|block 2 from '.. 890 | 'layout|nested block 4 from layout|') 891 | end, 892 | test_level2 = function() 893 | local tmpl = 'level2' 894 | assert_equal(expand_file(tmpl), 895 | '|block 1 from level1|nested block 5 from '.. 896 | 'level2|nested block 4 from layout|') 897 | end, 898 | test_level3 = function() 899 | local tmpl = 'level3' 900 | assert_equal(expand_file(tmpl), 901 | '|block 1 from level1|block 5 from level3|'.. 902 | 'block 4 from level3|') 903 | end, 904 | test_level4 = function() 905 | local tmpl = 'level4' 906 | assert_equal(expand_file(tmpl), 907 | '|block 1 from level1|block 5 from '.. 908 | 'level3|block 3 from level4|') 909 | end, 910 | test_super = function() 911 | local tmpl = 'super/c' 912 | assert_equal(expand_file(tmpl), '--INTRO--|BEFORE|[(INNER)]|AFTER') 913 | end, 914 | -- Note: test_working is not applicable since it is incomplete. 915 | test_reuse_blocks = function() 916 | local tmpl = '{% block foo %}42{% endblock %}|'.. 917 | '{{ self.foo() }}|{{ self.foo() }}' 918 | assert_equal(expand(tmpl), '42|42|42') 919 | end, 920 | -- Note: test_preserve_blocks is not applicable since false blocks are 921 | -- never loaded. 922 | test_dynamic_inheritence = function() 923 | for i = 1, 2 do 924 | local tmpl = 'dynamic/child' 925 | local env = {master = 'master'..i} 926 | assert_equal(expand_file(tmpl, env), 'MASTER'..i..'CHILD') 927 | end 928 | end, 929 | test_multi_inheritence = function() 930 | -- Note: cannot have 931 | -- {% if master %}{% extends master %}{% else %} 932 | -- {% extends 'master1' %}{% endif %}{% block x %}CHILD{% endblock %} 933 | -- since the extends within 'if' is local to that block. 934 | -- Must use {% extends master or 'master1' %} instead. 935 | local tmpl = 'multi/child' 936 | local env = {master = 'master2'} 937 | assert_equal(expand_file(tmpl, env), 'MASTER2CHILD') 938 | local env = {master = 'master1'} 939 | assert_equal(expand_file(tmpl, env), 'MASTER1CHILD') 940 | assert_equal(expand_file(tmpl), 'MASTER1CHILD') 941 | end, 942 | test_scoped_block = function() 943 | local tmpl = '{% extends "scoped/master.html" %}{% block item %}'.. 944 | '{{ item }}{% endblock %}' 945 | local env = {seq = range(5)} 946 | assert_equal(expand(tmpl, env), '[1][2][3][4][5]') 947 | end, 948 | test_super_in_scoped_block = function() 949 | local tmpl = '{% extends "super_scoped/master.html" %}{% block item %}'.. 950 | '{{ super() }}|{{ item * 2 }}{% endblock %}' 951 | local env = {seq = range(5)} 952 | assert_equal(expand(tmpl, env), '[1|2][2|4][3|6][4|8][5|10]') 953 | end, 954 | test_scoped_block_after_inheritence = function() 955 | local tmpl = 'scoped/index.html' 956 | local env = {the_foo = 42} 957 | assert_equal(expand_file(tmpl, env):trim():gsub('%s+', '|'), '43|44|45') 958 | end, 959 | test_fixed_macro_scoping = function() 960 | local tmpl = 'macro_scoping/test.html' 961 | assert_equal(expand_file(tmpl, env):trim():gsub('%s+', '|'), 962 | 'outer_box|my_macro') 963 | end, 964 | test_double_extends = function() 965 | local tmpl = 'doublee' 966 | assert_raises('multiple.+extends', expand_file, tmpl) 967 | end, 968 | } 969 | }, 970 | lexer = { 971 | -- Note: token_stream_tests are not applicable since this implementation 972 | -- does not have a similar tokenizer. 973 | lexer_tests = { 974 | test_raw1 = function() 975 | local tmpl = '{% raw %}foo{% endraw %}|'.. 976 | '{%raw%}{{ bar }}|{% baz %}{% endraw %}' 977 | assert_equal(expand(tmpl), 'foo|{{ bar }}|{% baz %}') 978 | end, 979 | test_raw2 = function() 980 | local tmpl = '1 {%- raw -%} 2 {%- endraw -%} 3' 981 | assert_equal(expand(tmpl), '123') 982 | end, 983 | test_raw3 = function() 984 | local tmpl = '{% raw %}{{ FOO }} and {% BAR %}{% endraw %}' 985 | assert_equal(expand(tmpl), '{{ FOO }} and {% BAR %}') 986 | end, 987 | test_balancing = function() 988 | lupa.configure('{%', '%}', '${', '}') 989 | local tmpl = [[{% for item in seq 990 | %}${{item..' foo'}|string|upper}{% endfor %}]] 991 | local env = {seq = range(3)} 992 | assert_equal(expand(tmpl, env), '{"1 FOO"}{"2 FOO"}{"3 FOO"}') 993 | lupa.reset() 994 | end, 995 | test_comments = function() 996 | lupa.configure('', '{', '}') 997 | local tmpl = [[ 998 |
    999 | 1000 |
  • {item}
  • 1001 | 1002 |
]] 1003 | local env = {seq = range(3)} 1004 | assert_equal(expand(tmpl, env), '
    \n
  • 1
  • \n '.. 1005 | '
  • 2
  • \n
  • 3
  • \n
') 1006 | lupa.reset() 1007 | end, 1008 | -- Note: test_string_escapes is not applicable since Lua does not handle 1009 | -- unicode well enough (even with Lua 5.3). 1010 | -- Note: test_bytefallback is not applicable since Lua does not have a 1011 | -- data pretty-printer. 1012 | -- Note: test_operators is not applicable since this implementation 1013 | -- does not have a similar tokenizer. 1014 | test_normalizing = function() 1015 | local tmpl = '1\n2\r\n3\n4\n' 1016 | for _, seq in ipairs{'\r\n', '\n'} do 1017 | lupa.configure{newline_sequence = seq} 1018 | assert_equal(expand(tmpl):gsub(seq, 'X'), '1X2X3X4') 1019 | end 1020 | lupa.reset() 1021 | end, 1022 | test_trailing_newline = function() 1023 | for _, keep in ipairs{true, false} do 1024 | lupa.configure{keep_trailing_newline = keep} 1025 | local tmpl = '' 1026 | assert_equal(expand(tmpl), '') 1027 | tmpl = 'no\nnewline' 1028 | assert_equal(expand(tmpl), tmpl) 1029 | tmpl = 'with\nnewline\n' 1030 | assert_equal(expand(tmpl), keep and tmpl or 'with\nnewline') 1031 | tmpl = 'with\nseveral\n\n\n' 1032 | assert_equal(expand(tmpl), keep and tmpl or 'with\nseveral\n\n') 1033 | end 1034 | lupa.reset() 1035 | end, 1036 | }, 1037 | parser_tests = { 1038 | test_php_syntax = function() 1039 | lupa.configure('', '', '') 1040 | local tmpl = [[ 1041 | 1042 | 1043 | 1044 | ]] 1045 | local env = {seq = range(5)} 1046 | assert_equal(expand(tmpl, env), '\n12345') 1047 | lupa.reset() 1048 | end, 1049 | test_erb_syntax = function() 1050 | lupa.configure('<%', '%>', '<%=', '%>', '<%#', '%>') 1051 | local tmpl = [[ 1052 | <%# I'm a comment, I'm not interesting %> 1053 | <% for item in seq -%> 1054 | <%= item %> 1055 | <%- endfor %>]] 1056 | local env = {seq = range(5)} 1057 | assert_equal(expand(tmpl, env), '\n12345') 1058 | lupa.reset() 1059 | end, 1060 | test_comment_syntax = function() 1061 | lupa.configure('', '${', '}', '') 1062 | local tmpl = [[ 1063 | 1064 | 1065 | ${item} 1066 | ]] 1067 | local env = {seq = range(5)} 1068 | assert_equal(expand(tmpl, env), '\n12345') 1069 | lupa.reset() 1070 | end, 1071 | test_balancing = function() 1072 | local tmpl = [[{{{1, 2, 3}|length}}]] 1073 | assert_equal(expand(tmpl), '3') 1074 | end, 1075 | test_start_comment = function() 1076 | local tmpl = [[{# foo comment 1077 | and bar comment #} 1078 | {% macro blub() %}foo{% endmacro %} 1079 | {{ blub() }}]] 1080 | assert_equal(expand(tmpl):trim(), 'foo') 1081 | end, 1082 | -- Note: test_line_syntax is not applicable since line statements are not 1083 | -- supported. 1084 | -- Note: test_line_syntax_priority is not applicable since line statements 1085 | -- are not supported. 1086 | test_error_messages = function() 1087 | local tmpl = '{% for item in seq %}...{% endif %}' 1088 | assert_raises('endfor.+expected', expand, tmpl) 1089 | tmpl = '{% if foo %}{% for item in seq %}...{% endfor %}{% endfor %}' 1090 | assert_raises('endif.+expected', expand, tmpl) 1091 | tmpl = '{% if foo %}' 1092 | assert_raises('endif.+expected', expand, tmpl) 1093 | tmpl = '{% for item in seq %}' 1094 | assert_raises('endfor.+expected', expand, tmpl) 1095 | tmpl = '{% block foo-bar-baz %}{% endblock %}' 1096 | assert_raises('invalid block name', expand, tmpl) 1097 | tmpl = '{% unknown_tag %}' 1098 | assert_raises('unknown or unexpected tag', expand, tmpl) 1099 | end, 1100 | }, 1101 | -- Note: syntax_tests are not applicable since this implementation uses 1102 | -- Lua's parser. 1103 | lstrip_blocks_tests = { 1104 | setup = function() lupa.configure{lstrip_blocks = true} end, 1105 | teardown = lupa.reset, 1106 | test_lstrip = function() 1107 | local tmpl = ' {% if true %}\n {% endif %}' 1108 | assert_equal(expand(tmpl), '\n') 1109 | end, 1110 | test_lstrip_trim = function() 1111 | lupa.configure{lstrip_blocks = true, trim_blocks = true} 1112 | local tmpl = ' {% if true %}\n {% endif %}' 1113 | assert_equal(expand(tmpl), '') 1114 | lupa.configure{lstrip_blocks = true} 1115 | end, 1116 | test_no_lstrip = function() 1117 | local tmpl = ' {%+ if true %}\n {%+ endif %}' 1118 | assert_equal(expand(tmpl), ' \n ') 1119 | end, 1120 | test_lstrip_endline = function() 1121 | local tmpl = ' hello{% if true %}\n goodbye{% endif %}' 1122 | assert_equal(expand(tmpl), ' hello\n goodbye') 1123 | end, 1124 | test_lstrip_inline = function() 1125 | local tmpl = ' {% if true %}hello {% endif %}' 1126 | assert_equal(expand(tmpl), 'hello ') 1127 | end, 1128 | test_lstrip_nested = function() 1129 | local tmpl = ' {% if true %}a {% if true %}b {% endif %}c {% endif %}' 1130 | assert_equal(expand(tmpl), 'a b c ') 1131 | end, 1132 | test_lstrip_left_chars = function() 1133 | local tmpl = [[ abc {% if true %} 1134 | hello{% endif %}]] 1135 | assert_equal(expand(tmpl), ' abc \n hello') 1136 | end, 1137 | -- Note: test_lstrip_embedded_strings is not applicable since this 1138 | -- implementation's grammar cannot parse Lua itself in order to handle 1139 | -- embedded tags. 1140 | test_lstrip_preserve_leading_newlines = function() 1141 | local tmpl = '\n\n\n{% set hello = 1 %}' 1142 | assert_equal(expand(tmpl), '\n\n\n') 1143 | end, 1144 | test_lstrip_comment = function() 1145 | local tmpl = [[ {# if true #} 1146 | hello 1147 | {#endif#}]] 1148 | assert_equal(expand(tmpl), '\nhello\n') 1149 | end 1150 | }, 1151 | lstrip_blocks_angle_bracket_tests = { 1152 | setup = function() 1153 | lupa.configure('<%', '%>', '${', '}', '<%#', '%>', 1154 | {lstrip_blocks = true, trim_blocks = true}) 1155 | end, 1156 | teardown = lupa.reset, 1157 | test_lstrip_angle_bracket_simple = function() 1158 | local tmpl = ' <% if true %>hello <% endif %>' 1159 | assert_equal(expand(tmpl), 'hello ') 1160 | end, 1161 | test_lstrip_angle_bracket_comment = function() 1162 | local tmpl = ' <%# if true %>hello <%# endif %>' 1163 | assert_equal(expand(tmpl), 'hello ') 1164 | end, 1165 | test_lstrip_angle_bracket = function() 1166 | -- Note: this implementation does not support line statements, so this 1167 | -- test input is slightly different. 1168 | local tmpl = [[ 1169 | <%# regular comment %> 1170 | <% for item in seq %> 1171 | ${item} 1172 | <% endfor %>]] 1173 | local env = {seq = range(5)} 1174 | assert_equal(expand(tmpl, env), '1\n2\n3\n4\n5\n') 1175 | end, 1176 | test_lstrip_angle_bracket_compact = function() 1177 | -- Note: this implementation does not support line statements, so this 1178 | -- test input is slightly different. 1179 | local tmpl = [[ 1180 | <%#regular comment%> 1181 | <%for item in seq%> 1182 | ${item} 1183 | <%endfor%>]] 1184 | local env = {seq = range(5)} 1185 | assert_equal(expand(tmpl, env), '1\n2\n3\n4\n5\n') 1186 | end, 1187 | }, 1188 | lstrip_blocks_php_syntax_tests = { 1189 | setup = function() 1190 | lupa.configure('', '', '', 1191 | {lstrip_blocks = true, trim_blocks = true}) 1192 | end, 1193 | teardown = lupa.reset, 1194 | test_php_syntax_with_manual = function() 1195 | local tmpl = [[ 1196 | 1197 | 1198 | 1199 | ]] 1200 | local env = {seq = range(5)} 1201 | assert_equal(expand(tmpl, env), '12345') 1202 | end, 1203 | test_php_syntax = function() 1204 | local tmpl = [[ 1205 | 1206 | 1207 | 1208 | ]] 1209 | local env = {seq = range(5)} 1210 | assert_equal(expand(tmpl, env), 1211 | ' 1\n 2\n 3\n 4\n 5\n') 1212 | end, 1213 | test_php_syntax_compact = function() 1214 | local tmpl = [[ 1215 | 1216 | 1217 | 1218 | ]] 1219 | local env = {seq = range(5)} 1220 | assert_equal(expand(tmpl, env), 1221 | ' 1\n 2\n 3\n 4\n 5\n') 1222 | end, 1223 | }, 1224 | lstrip_blocks_erb_syntax_tests = { 1225 | setup = function() 1226 | lupa.configure('<%', '%>', '<%=', '%>', '<%#', '%>', 1227 | {lstrip_blocks = true, trim_blocks = true}) 1228 | end, 1229 | teardown = lupa.reset, 1230 | test_erb_syntax = function() 1231 | local tmpl = [[ 1232 | <%# I'm a comment, I'm not interesting %> 1233 | <% for item in seq %> 1234 | <%= item %> 1235 | <% endfor %> 1236 | ]] 1237 | local env = {seq = range(5)} 1238 | assert_equal(expand(tmpl, env), ' 1\n 2\n 3\n 4\n 5\n') 1239 | end, 1240 | test_erb_syntax_with_manual = function() 1241 | local tmpl = [[ 1242 | <%# I'm a comment, I'm not interesting %> 1243 | <% for item in seq -%> 1244 | <%= item %> 1245 | <%- endfor %>]] 1246 | local env = {seq = range(5)} 1247 | assert_equal(expand(tmpl, env), '12345') 1248 | end, 1249 | test_erb_syntax_no_lstrip = function() 1250 | local tmpl = [[ 1251 | <%# I'm a comment, I'm not interesting %> 1252 | <%+ for item in seq -%> 1253 | <%= item %> 1254 | <%- endfor %>]] 1255 | local env = {seq = range(5)} 1256 | assert_equal(expand(tmpl, env), ' 12345') 1257 | end, 1258 | }, 1259 | lstrip_blocks_comment_tests = { 1260 | test_comment_syntax = function() 1261 | lupa.configure('', '${', '}', '', 1262 | {lstrip_blocks = true, trim_blocks = true}) 1263 | local tmpl = [[ 1264 | 1265 | 1266 | ${item} 1267 | ]] 1268 | local env = {seq = range(5)} 1269 | assert_equal(expand(tmpl, env), '12345') 1270 | lupa.reset() 1271 | end, 1272 | } 1273 | }, 1274 | regression = { 1275 | corner_case_tests = { 1276 | test_assigned_scoping = function() 1277 | local tmpl = [[ 1278 | {%- for item in {1, 2, 3, 4} -%} 1279 | [{{ item }}] 1280 | {%- endfor %} 1281 | {{- item -}} 1282 | ]] 1283 | local env = {item = 42} 1284 | assert_equal(expand(tmpl, env), '[1][2][3][4]42') 1285 | tmpl = [[ 1286 | {%- for item in {1, 2, 3, 4} -%} 1287 | [{{ item }}] 1288 | {%- endfor %} 1289 | {%- set item = 42 %} 1290 | {{- item -}} 1291 | ]] 1292 | assert_equal(expand(tmpl), '[1][2][3][4]42') 1293 | tmpl = [[ 1294 | {%- set item = 42 %} 1295 | {%- for item in {1, 2, 3, 4} -%} 1296 | [{{ item }}] 1297 | {%- endfor %} 1298 | {{- item -}} 1299 | ]] 1300 | assert_equal(expand(tmpl), '[1][2][3][4]42') 1301 | end, 1302 | test_closure_scoping = function() 1303 | local tmpl = [[ 1304 | {%- set wrapper = "" %} 1305 | {%- for item in {1, 2, 3, 4} %} 1306 | {%- macro wrapper() %}[{{ item }}]{% endmacro %} 1307 | {{- wrapper() }} 1308 | {%- endfor %} 1309 | {{- wrapper -}} 1310 | ]] 1311 | assert_equal(expand(tmpl), '[1][2][3][4]') 1312 | end, 1313 | }, 1314 | other_tests = { 1315 | test_keyword_folding = function() 1316 | lupa.filters.testing = function(v, s) return v..s end 1317 | local tmpl = [[{{ 'test'|testing('stuff') }}]] 1318 | assert_equal(expand(tmpl), 'teststuff') 1319 | lupa.filters.testing = nil 1320 | end, 1321 | test_extends_output = function() 1322 | -- Note: "extends" cannot be within an "if" so use conditional expr. 1323 | local tmpl = '{% extends expr and "data/other/parent.html" %}'.. 1324 | '[[{% block title %}title{% endblock %}]]'.. 1325 | '{% for item in {1, 2, 3} %}({{ item }}){% endfor %}' 1326 | local env = {expr = false} 1327 | assert_equal(expand(tmpl, env):gsub('\n', ''), '[[title]](1)(2)(3)') 1328 | env = {expr = true} 1329 | assert_equal(expand(tmpl, env):gsub('\n', ''), '((title))') 1330 | end, 1331 | test_urlize_filter_escaping = function() 1332 | local tmpl = '{{ "http://www.example.org/http://www.example.org/<foo') 1334 | end, 1335 | test_loop_call_loop = function() 1336 | local tmpl = [[ 1337 | 1338 | {% macro test() %} 1339 | {{ caller() }} 1340 | {% endmacro %} 1341 | 1342 | {% for num1 in range(5) %} 1343 | {% call test() %} 1344 | {% for num2 in range(10) %} 1345 | {{ loop.index }} 1346 | {% endfor %} 1347 | {% endcall %} 1348 | {% endfor %} 1349 | 1350 | ]] 1351 | assert_equal(expand(tmpl):trim():gsub('%s+', ''), 1352 | string.rep('12345678910', 5)) 1353 | end, 1354 | test_weird_inline_comment = function() 1355 | local tmpl = '{% for item in seq {# missing #}%}...{% endfor %}' 1356 | assert_raises('%%}.+expected', expand, tmpl) 1357 | end, 1358 | test_old_macro_loop_scoping = function() 1359 | local tmpl = '{% for i in {1, 2} %}{{ i }}{% endfor %}'.. 1360 | '{% macro i() %}3{% endmacro %}{{ i() }}' 1361 | assert_equal(expand(tmpl), '123') 1362 | end, 1363 | test_partial_conditional_assignments = function() 1364 | local tmpl = '{% if b %}{% set a = 42 %}{% endif %}{{ a }}' 1365 | local env = {a = 23} 1366 | assert_equal(expand(tmpl, env), '23') 1367 | env = {b = true} 1368 | assert_equal(expand(tmpl, env), '42') 1369 | end, 1370 | test_stacked_locals_scoping = function() 1371 | -- Note: this implementation does not support line statements, so this 1372 | -- test input is slightly different. 1373 | local tmpl = [[ 1374 | {% for j in {1, 2} -%} 1375 | {% set x = 1 -%} 1376 | {% for i in {1, 2} -%} 1377 | {{ x -}} 1378 | {% if i % 2 == 0 -%} 1379 | {% set x = x + 1 -%} 1380 | {% endif -%} 1381 | {% endfor -%} 1382 | {% endfor -%} 1383 | {% if a -%} 1384 | {{ 'A' -}} 1385 | {% elseif b -%} 1386 | {{ 'B' -}} 1387 | {% elseif c == d -%} 1388 | {{ 'C' -}} 1389 | {% else -%} 1390 | {{ 'D' -}} 1391 | {% endif -%} 1392 | ]] 1393 | local env = {a = nil, b = false, c = 42, d = 42.0} 1394 | assert_equal(expand(tmpl, env), '1111C') 1395 | end, 1396 | test_stacked_locals_scoping_twoframe = function() 1397 | local tmpl = [[ 1398 | {% set x = 1 %} 1399 | {% for item in foo %} 1400 | {% if item == 1 %} 1401 | {% set x = 2 %} 1402 | {% endif %} 1403 | {% endfor %} 1404 | {{ x }} 1405 | ]] 1406 | local env = {foo = {1}} 1407 | assert_equal(expand(tmpl, env):gsub('%s+', ''), '1') 1408 | end, 1409 | test_call_with_args = function() 1410 | local tmpl = [[{% macro dump_users(users) -%} 1411 |
    1412 | {%- for user in users -%} 1413 |
  • {{ user.username|e }}

    {{ caller(user) }}
  • 1414 | {%- endfor -%} 1415 |
1416 | {%- endmacro -%} 1417 | 1418 | {% call(user) dump_users(list_of_user) -%} 1419 |
1420 |
Realname
1421 |
{{ user.realname|e }}
1422 |
Description
1423 |
{{ user.description }}
1424 |
1425 | {% endcall %}]] 1426 | local env = { 1427 | list_of_user = { 1428 | {username = 'apo', realname = 'something else', 1429 | description = 'test'} 1430 | } 1431 | } 1432 | local lines = {} 1433 | for line in expand(tmpl, env):gmatch('[^\n]+') do 1434 | lines[#lines + 1] = line:trim() 1435 | end 1436 | assert_equal(lines[1], '
  • apo

    ') 1437 | assert_equal(lines[2], '
    Realname
    ') 1438 | assert_equal(lines[3], '
    something else
    ') 1439 | assert_equal(lines[4], '
    Description
    ') 1440 | assert_equal(lines[5], '
    test
    ') 1441 | assert_equal(lines[6], '
    ') 1442 | assert_equal(lines[7], '
') 1443 | end, 1444 | test_empty_if_condition_fails = function() 1445 | local tmpl = '{% if %}....{% endif %}' 1446 | assert_raises('expression expected', expand, tmpl) 1447 | tmpl = '{% if foo %}...{% elif %}...{% endif %}' 1448 | assert_raises('additional tag or.+endif.+expected', expand, tmpl) 1449 | tmpl = '{% for x in %}..{% endfor %}' 1450 | assert_raises('invalid for expression', expand, tmpl) 1451 | end, 1452 | -- Note: test_recursive_loop is not applicable since it is incomplete. 1453 | test_else_loop = function() 1454 | local tmpl = [[ 1455 | {% for x in y %} 1456 | {{ loop.index0 }} 1457 | {% else %} 1458 | {% for i in range(3) %}{{ i }}{% endfor %} 1459 | {% endfor %} 1460 | ]] 1461 | local env = {y = {}} 1462 | assert_equal(expand(tmpl, env):trim(), '123') 1463 | end, 1464 | -- Note: test_correct_prefix_loader is not applicable since this 1465 | -- implementation does not use a prefix loader. 1466 | -- TODO: this isn't exactly practical... 1467 | test_contextfunction_callable_classes = function() 1468 | local tmpl = '{{ callableclass() }}' 1469 | local env = {hello = 'TEST'} 1470 | env.callableclass = setmetatable({env = env}, {__call = function(t) 1471 | return t.env.hello 1472 | end}) 1473 | assert_equal(expand(tmpl, env), 'TEST') 1474 | end, 1475 | } 1476 | }, 1477 | -- Note: security tests are not applicable since Lua has no security 1478 | -- mechanisms. 1479 | tests = { 1480 | tests_tests = { 1481 | test_defined = function() 1482 | local tmpl = '{{ is_defined(nil) }}|{{ is_defined(true) }}' 1483 | assert_equal(expand(tmpl), 'false|true') 1484 | end, 1485 | test_even = function() 1486 | local tmpl = '{{ is_even(1) }}|{{ is_even(2) }}' 1487 | assert_equal(expand(tmpl), 'false|true') 1488 | end, 1489 | test_odd = function() 1490 | local tmpl = '{{ is_odd(1) }}|{{ is_odd(2) }}' 1491 | assert_equal(expand(tmpl), 'true|false') 1492 | end, 1493 | test_lower = function() 1494 | local tmpl = '{{ is_lower("foo") }}|{{ is_lower("FOO") }}' 1495 | assert_equal(expand(tmpl), 'true|false') 1496 | end, 1497 | test_typechecks = function() 1498 | local tmpl = [[ 1499 | {{ is_undefined(42) }} 1500 | {{ is_defined(42) }} 1501 | {{ is_nil(42) }} 1502 | {{ is_nil(nil) }} 1503 | {{ is_number(42) }} 1504 | {{ is_string(42) }} 1505 | {{ is_string("foo") }} 1506 | {{ is_sequence("foo") }} 1507 | {{ is_sequence({1}) }} 1508 | {{ is_callable(range) }} 1509 | {{ is_callable(42) }} 1510 | {{ is_iterable(range(5)) }} 1511 | {{ is_mapping({}) }} 1512 | {{ is_mapping(mydict) }} 1513 | {{ is_mapping("foo") }} 1514 | ]] 1515 | local env = {mydict = {}} 1516 | local results = {} 1517 | for result in expand(tmpl, env):gmatch('%S+') do 1518 | results[#results + 1] = result 1519 | end 1520 | local expected = { 1521 | 'false', 'true', 'false', 'true', 'true', 'false', 1522 | 'true', 'false', 'true', 'true', 'false', 'true', 1523 | 'true', 'true', 'false' 1524 | } 1525 | for i = 1, #results do assert_equal(results[i], expected[i]) end 1526 | end, 1527 | test_sequence = function() 1528 | local tmpl = '{{ is_sequence({1, 2, 3}) }}|'.. 1529 | '{{ is_sequence("foo") }}|'.. 1530 | '{{ is_sequence(42) }}' 1531 | assert_equal(expand(tmpl), 'true|false|false') 1532 | end, 1533 | test_upper = function() 1534 | local tmpl = '{{ is_upper("FOO") }}|{{ is_upper("foo") }}' 1535 | assert_equal(expand(tmpl), 'true|false') 1536 | end, 1537 | test_sameas = function() 1538 | local tmpl = '{{ is_sameas(foo, false) }}|'.. 1539 | '{{ is_sameas(nil, false) }}' 1540 | local env = {foo = false} 1541 | assert_equal(expand(tmpl, env), 'true|false') 1542 | end, 1543 | test_nil_is_nil = function() 1544 | local tmpl = '{{ is_sameas(foo, nil) }}' 1545 | local env = {foo = nil} 1546 | assert_equal(expand(tmpl, env), 'true') 1547 | end, 1548 | test_escaped = function() 1549 | lupa.configure{autoescape = true} 1550 | -- Note: This implementation does not support markup, so this test input 1551 | -- is slightly different. 1552 | local tmpl = '{% set y = "foo"|safe %}{{ is_escaped(x) }}|{{ is_escaped(y) }}' 1553 | local env = {x = 'foo'} 1554 | assert_equal(expand(tmpl, env), 'false|true') 1555 | lupa.reset() 1556 | end, 1557 | } 1558 | } 1559 | } 1560 | 1561 | local num_tests, failures = 0, {} 1562 | print('Starting test suite.') 1563 | for test_group, types in pairs(test_suite) do 1564 | for test_type, tests in pairs(types) do 1565 | print("Running all of "..test_type.."'s tests.") 1566 | if tests.setup then tests.setup() end 1567 | for test_name, test in pairs(tests) do 1568 | if test_name ~= 'setup' and test_name ~= 'teardown' then 1569 | local pass, message = pcall(test) 1570 | if pass then 1571 | io.output():write('.') 1572 | else 1573 | io.output():write('E') 1574 | failures[#failures + 1] = { 1575 | test_type, test_name, message:match('^[^:]*:?%d*:?%s*(.+)$') 1576 | } 1577 | end 1578 | io.output():flush() 1579 | num_tests = num_tests + 1 1580 | end 1581 | end 1582 | if tests.teardown then tests.teardown() end 1583 | print('') -- newline 1584 | end 1585 | end 1586 | print('\nSummary:') 1587 | if #failures == 0 then 1588 | print('All '..num_tests..' tests passed!') 1589 | else 1590 | local line = string.rep('-', 72) 1591 | print(#failures..' of '..num_tests..' tests failed!') 1592 | print(line) 1593 | for i = 1, #failures do 1594 | print("Failure in "..failures[i][1].."'s "..failures[i][2]..':') 1595 | print(failures[i][3]) 1596 | print(line) 1597 | end 1598 | end 1599 | -------------------------------------------------------------------------------- /lupa.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2015-2020 Mitchell. See LICENSE. 2 | -- Sponsored by the Library of the University of Antwerp. 3 | -- Contributions from Ana Balan. 4 | -- Lupa templating engine. 5 | 6 | --[[ This comment is for LuaDoc. 7 | --- 8 | -- Lupa is a Jinja2 template engine implementation written in Lua and supports 9 | -- Lua syntax within tags and variables. 10 | module('lupa')]] 11 | local M = {} 12 | 13 | local lpeg = require('lpeg') 14 | lpeg.locale(lpeg) 15 | local space, newline = lpeg.space, lpeg.P('\r')^-1 * '\n' 16 | local P, S, V = lpeg.P, lpeg.S, lpeg.V 17 | local C, Cc, Cg, Cp, Ct = lpeg.C, lpeg.Cc, lpeg.Cg, lpeg.Cp, lpeg.Ct 18 | 19 | --- 20 | -- Lupa's expression filters. 21 | -- @class table 22 | -- @name filters 23 | M.filters = {} 24 | 25 | --- 26 | -- Lupa's value tests. 27 | -- @class table 28 | -- @name tests 29 | M.tests = {} 30 | 31 | --- 32 | -- Lupa's template loaders. 33 | -- @class table 34 | -- @name loaders 35 | M.loaders = {} 36 | 37 | -- Lua version compatibility. 38 | if _VERSION == 'Lua 5.1' then 39 | function load(ld, source, mode, env) 40 | local f, err = loadstring(ld) 41 | if f and env then return setfenv(f, env) end 42 | return f, err 43 | end 44 | table.unpack = unpack 45 | end 46 | 47 | local newline_sequence, keep_trailing_newline, autoescape = '\n', false, false 48 | local loader 49 | 50 | -- Creates and returns a token pattern with token name *name* and pattern 51 | -- *patt*. 52 | -- The returned pattern captures three values: the token's position and name, 53 | -- and either a string value or table of capture values. 54 | -- Tokens are used to construct an Abstract Syntax Tree (AST) for a template. 55 | -- @param name The name of the token. 56 | -- @param patt The pattern to match. It must contain only one capture: either a 57 | -- string or table of captures. 58 | -- @see evaluate 59 | local function token(name, patt) return Cp() * Cc(name) * patt end 60 | 61 | -- Returns an LPeg pattern that immediately raises an error with message 62 | -- *errmsg* for invalid syntax when parsing a template. 63 | -- @param errmsg The error message to raise an error with. 64 | local function lpeg_error(errmsg) 65 | return P(function(input, index) 66 | input = input:sub(1, index) 67 | local _, line_num = input:gsub('\n', '') 68 | local col_num = #input:match('[^\n]*$') 69 | error(string.format('Parse Error in file "%s" on line %d, column %d: %s', 70 | M._FILENAME, line_num + 1, col_num, errmsg), 0) 71 | end) 72 | end 73 | 74 | --- 75 | -- Configures the basic delimiters and options for templates. 76 | -- This function then regenerates the grammar for parsing templates. 77 | -- Note: this function cannot be used iteratively to configure Lupa options. 78 | -- Any options not provided are reset to their default values. 79 | -- @param ts The tag start delimiter. The default value is '{%'. 80 | -- @param te The tag end delimiter. The default value is '%}'. 81 | -- @param vs The variable start delimiter. The default value is '{{'. 82 | -- @param ve The variable end delimiter. The default value is '}}'. 83 | -- @param cs The comment start delimiter. The default value is '{#'. 84 | -- @param ce The comment end delimiter. The default value is '#}'. 85 | -- @param options Optional set of options for templates: 86 | -- 87 | -- * `trim_blocks`: Trim the first newline after blocks. 88 | -- * `lstrip_blocks`: Strip line-leading whitespace in front of tags. 89 | -- * `newline_sequence`: The end-of-line character to use. 90 | -- * `keep_trailing_newline`: Whether or not to keep a newline at the end of 91 | -- a template. 92 | -- * `autoescape`: Whether or not to autoescape HTML entities. May be a 93 | -- function that accepts the template's filename as an argument and returns 94 | -- a boolean. 95 | -- * `loader`: Function that receives a template name to load and returns the 96 | -- path to that template. 97 | -- @name configure 98 | function M.configure(ts, te, vs, ve, cs, ce, options) 99 | if type(ts) == 'table' then options, ts = ts, nil end 100 | if not ts then ts = '{%' end 101 | if not te then te = '%}' end 102 | if not vs then vs = '{{' end 103 | if not ve then ve = '}}' end 104 | if not cs then cs = '{#' end 105 | if not ce then ce = '#}' end 106 | 107 | -- Tokens for whitespace control. 108 | local lstrip = token('lstrip', C('-')) + '+' -- '+' is handled by grammar 109 | local rstrip = token('rstrip', -(P(te) + ve + ce) * C('-')) 110 | 111 | -- Configure delimiters, including whitespace control. 112 | local tag_start = P(ts) * lstrip^-1 * space^0 113 | local tag_end = space^0 * rstrip^-1 * P(te) 114 | local variable_start = P(vs) * lstrip^-1 * space^0 115 | local variable_end = space^0 * rstrip^-1 * P(ve) 116 | local comment_start = P(cs) * lstrip^-1 * space^0 117 | local comment_end = space^0 * rstrip^-1 * P(ce) 118 | if options and options.trim_blocks then 119 | -- Consider whitespace, including a newline, immediately following a tag as 120 | -- part of that tag so it is not captured as plain text. Basically, strip 121 | -- the trailing newline from tags. 122 | tag_end = tag_end * S(' \t')^0 * newline^-1 123 | comment_end = comment_end * S(' \t')^0 * newline^-1 124 | end 125 | 126 | -- Error messages. 127 | local variable_end_error = lpeg_error('"'..ve..'" expected') 128 | local comment_end_error = lpeg_error('"'..ce..'" expected') 129 | local tag_end_error = lpeg_error('"'..te..'" expected') 130 | local endraw_error = lpeg_error('additional tag or "'..ts..' endraw '..te.. 131 | '" expected') 132 | local expr_error = lpeg_error('expression expected') 133 | local endblock_error = lpeg_error('additional tag or "'..ts..' endblock '.. 134 | te..'" expected') 135 | local endfor_error = lpeg_error('additional tag or "'..ts..' endfor '..te.. 136 | '" expected') 137 | local endif_error = lpeg_error('additional tag or "'..ts..' endif '..te.. 138 | '" expected') 139 | local endmacro_error = lpeg_error('additional tag or "'..ts..' endmacro '.. 140 | te..'" expected') 141 | local endcall_error = lpeg_error('additional tag or "'..ts..' endcall '..te.. 142 | '" expected') 143 | local endfilter_error = lpeg_error('additional tag or "'..ts..' endfilter '.. 144 | te..'" expected') 145 | local tag_error = lpeg_error('unknown or unexpected tag') 146 | local main_error = lpeg_error('unexpected character; text or tag expected') 147 | 148 | -- Grammar. 149 | M.grammar = Ct(P{ 150 | -- Utility patterns used by tokens. 151 | entity_start = tag_start + variable_start + comment_start, 152 | any_text = (1 - V('entity_start'))^1, 153 | -- Allow '{{' by default in expression text since it is valid in Lua. 154 | expr_text = (1 - tag_end - tag_start - comment_start)^1, 155 | -- When `options.lstrip_blocks` is enabled, ignore leading whitespace 156 | -- immediately followed by a tag (as long as '+' is not present) so that 157 | -- whitespace not captured as plain text. Basically, strip leading spaces 158 | -- from tags. 159 | line_text = (1 - newline - V('entity_start'))^1, 160 | lstrip_entity_start = -P(vs) * (P(ts) + cs) * -P('+'), 161 | lstrip_space = S(' \t')^1 * #V('lstrip_entity_start'), 162 | text_lines = V('line_text') * (newline * -(S(' \t')^0 * V('lstrip_entity_start')) * V('line_text'))^0 * newline^-1 + newline, 163 | 164 | -- Plain text. 165 | text = (not options or not options.lstrip_blocks) and 166 | token('text', C(V('any_text'))) or 167 | V('lstrip_space') + token('text', C(V('text_lines'))), 168 | 169 | -- Variables: {{ expr }}. 170 | lua_table = '{' * ((1 - S('{}')) + V('lua_table'))^0 * '}', 171 | variable = variable_start * 172 | token('variable', C((V('lua_table') + (1 - variable_end))^0)) * 173 | (variable_end + variable_end_error), 174 | 175 | -- Filters: handled in variable evaluation. 176 | 177 | -- Tests: handled in control structure expression evaluation. 178 | 179 | -- Comments: {# comment #}. 180 | comment = comment_start * (1 - comment_end)^0 * (comment_end + comment_end_error), 181 | 182 | -- Whitespace control: handled in tag/variable/comment start/end. 183 | 184 | -- Escaping: {% raw %} body {% endraw %}. 185 | raw_block = tag_start * 'raw' * (tag_end + tag_end_error) * 186 | token('text', C((1 - (tag_start * 'endraw' * tag_end))^0)) * 187 | (tag_start * 'endraw' * tag_end + endraw_error), 188 | 189 | -- Note: line statements are not supported since this grammer cannot parse 190 | -- Lua itself. 191 | 192 | -- Template inheritence. 193 | -- {% block ... %} body {% endblock %} 194 | block_block = tag_start * 'block' * space^1 * token('block', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) * 195 | V('body')^-1)) * 196 | (tag_start * 'endblock' * tag_end + endblock_error), 197 | -- {% extends ... %} 198 | extends_tag = tag_start * 'extends' * space^1 * token('extends', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error), 199 | -- Super blocks are handled in variables. 200 | -- Note: named block end tags are not supported since keeping track of that 201 | -- state information is difficult. 202 | -- Note: block nesting and scope is not applicable since blocks always have 203 | -- access to scoped variables in this implementation. 204 | 205 | -- Control Structures. 206 | -- {% for expr %} body {% else %} body {% endfor %} 207 | for_block = tag_start * 'for' * space^1 * token('for', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) * 208 | V('body')^-1 * 209 | Cg(Ct(tag_start * 'else' * tag_end * 210 | V('body')^-1), 'else')^-1)) * 211 | (tag_start * 'endfor' * tag_end + endfor_error), 212 | -- {% if expr %} body {% elseif expr %} body {% else %} body {% endif %} 213 | if_block = tag_start * 'if' * space^1 * token('if', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) * 214 | V('body')^-1 * 215 | Cg(Ct(Ct(tag_start * 'elseif' * space^1 * (Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) * 216 | V('body')^-1)^1), 'elseif')^-1 * 217 | Cg(Ct(tag_start * 'else' * tag_end * 218 | V('body')^-1), 'else')^-1)) * 219 | (tag_start * 'endif' * tag_end + endif_error), 220 | -- {% macro expr %} body {% endmacro %} 221 | macro_block = tag_start * 'macro' * space^1 * token('macro', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) * 222 | V('body')^-1)) * 223 | (tag_start * 'endmacro' * tag_end + endmacro_error), 224 | -- {% call expr %} body {% endcall %} 225 | call_block = tag_start * 'call' * (space^1 + #P('(')) * token('call', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) * 226 | V('body')^-1)) * 227 | (tag_start * 'endcall' * tag_end + endcall_error), 228 | -- {% filter expr %} body {% endfilter %} 229 | filter_block = tag_start * 'filter' * space^1 * token('filter', Ct((Cg(V('expr_text'), 'expression') + expr_error) * (tag_end + tag_end_error) * 230 | V('body')^-1)) * 231 | (tag_start * 'endfilter' * tag_end + endfilter_error), 232 | -- {% set ... %} 233 | set_tag = tag_start * 'set' * space^1 * token('set', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error), 234 | -- {% include ... %} 235 | include_tag = tag_start * 'include' * space^1 * token('include', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error), 236 | -- {% import ... %} 237 | import_tag = tag_start * 'import' * space^1 * token('import', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error), 238 | 239 | -- Note: i18n is not supported since it is out of scope for this 240 | -- implementation. 241 | 242 | -- Expression statement: {% do ... %}. 243 | do_tag = tag_start * 'do' * space^1 * token('do', C(V('expr_text')) + expr_error) * (tag_end + tag_end_error), 244 | 245 | -- Note: loop controls are not supported since that would require jumping 246 | -- between "scopes" (e.g. from within an "if" block to outside that "if" 247 | -- block's parent "for" block when coming across a {% break %} tag). 248 | 249 | -- Note: with statement is not supported since it is out of scope for this 250 | -- implementation. 251 | 252 | -- Note: autoescape is not supported since it is out of scope for this 253 | -- implementation. 254 | 255 | -- Any valid blocks of text or tags. 256 | body = (V('text') + V('variable') + V('comment') + V('raw_block') + 257 | V('block_block') + V('extends_tag') + V('for_block') + 258 | V('if_block') + V('macro_block') + V('call_block') + 259 | V('filter_block') + V('set_tag') + V('include_tag') + 260 | V('import_tag') + V('do_tag'))^0, 261 | 262 | -- Main pattern. 263 | V('body') * (-1 + tag_start * tag_error + main_error), 264 | }) 265 | 266 | -- Other options. 267 | if options and options.newline_sequence then 268 | assert(options.newline_sequence:find('^\r?\n$'), 269 | 'options.newline_sequence must be "\r\n" or "\n"') 270 | newline_sequence = options.newline_sequence 271 | else 272 | newline_sequence = '\n' 273 | end 274 | if options and options.keep_trailing_newline then 275 | keep_trailing_newline = options.keep_trailing_newline 276 | else 277 | keep_trailing_newline = false 278 | end 279 | if options and options.autoescape then 280 | autoescape = options.autoescape 281 | else 282 | autoescape = false 283 | end 284 | if options and options.loader then 285 | assert(type(options.loader) == 'function', 286 | 'options.loader must be a function that returns a filename') 287 | loader = options.loader 288 | else 289 | loader = M.loaders.filesystem() 290 | end 291 | end 292 | 293 | -- Wraps Lua's `assert()` in template environment *env* such that, when called 294 | -- in conjunction with another Lua function that produces an error message (e.g. 295 | -- `load()` and `pcall()`), that error message's context (source and line 296 | -- number) is replaced by the template's context. 297 | -- This results in Lua's error messages pointing to a template position rather 298 | -- than this library's source code. 299 | -- @param env The environment for the currently running template. It must have 300 | -- a `_SOURCE` field with the template's source text and a `_POSITION` field 301 | -- with the current position of expansion. 302 | -- @param ... Arguments to Lua's `assert()`. 303 | local function env_assert(env, ...) 304 | if not select(1, ...) then 305 | local input = env._LUPASOURCE:sub(1, env._LUPAPOSITION) 306 | local _, line_num = input:gsub('\n', '') 307 | local col_num = #input:match('[^\n]*$') 308 | local errmsg = select(2, ...) 309 | errmsg = errmsg:match(':%d+: (.*)$') or errmsg -- reformat if necessary 310 | error(string.format('Runtime Error in file "%s" on line %d, column %d: %s', 311 | env._LUPAFILENAME, line_num + 1, col_num, errmsg), 0) 312 | end 313 | return ... 314 | end 315 | 316 | -- Returns a generator that returns the position and filter in a list of 317 | -- filters, taking into account '|'s that may be within filter arguments. 318 | -- @usage for pos, filter in each_filter('foo|join("|")|bar') do ... end 319 | local function each_filter(s) 320 | local init = 1 321 | return function(s) 322 | local pos, filter, e = s:match('^%s*()([^|(]+%b()[^|]*)|?()', init) 323 | if not pos then pos, filter, e = s:match('()([^|]+)|?()', init) end 324 | init = e 325 | return pos, filter 326 | end, s 327 | end 328 | 329 | -- Evaluates template variable *expression* subject to template environment 330 | -- *env*, applying any filters given in *expression*. 331 | -- @param expression The string expression to evaluate. 332 | -- @param env The environment to evaluate the expression in. 333 | local function eval(expression, env) 334 | local expr, pos, filters = expression:match('^([^|]*)|?()(.-)$') 335 | -- Evaluate base expression. 336 | local f = env_assert(env, load('return '..expr, nil, nil, env)) 337 | local result = select(2, env_assert(env, pcall(f))) 338 | -- Apply any filters. 339 | local results, multiple_results = nil, false 340 | local p = env._LUPAPOSITION + pos - 1 -- mark position at first filter 341 | for pos, filter in each_filter(filters) do 342 | env._LUPAPOSITION = p + pos - 1 -- update position for error messages 343 | local name, params = filter:match('^%s*([%w_]+)%(?(.-)%)?%s*$') 344 | f = M.filters[name] 345 | env_assert(env, f, 'unknown filter "'..name..'"') 346 | local args = env_assert(env, load('return {'..params..'}', nil, nil, env), 347 | 'invalid filter parameter(s) for "'..name..'"')() 348 | if not multiple_results then 349 | results = {select(2, 350 | env_assert(env, pcall(f, result, table.unpack(args))))} 351 | else 352 | for i = 1, #results do table.insert(args, i, results[i]) end 353 | results = {select(2, env_assert(env, pcall(f, table.unpack(args))))} 354 | end 355 | result, multiple_results = results[1], #results > 1 356 | end 357 | if multiple_results then return table.unpack(results) end 358 | return result 359 | end 360 | 361 | local iterate 362 | 363 | -- Iterates over *ast*, a collection of tokens from a portion of a template's 364 | -- Abstract Syntax Tree (AST), evaluating any expressions in template 365 | -- environment *env*, and returns a concatenation of the results. 366 | -- @param ast A template's AST or portion of its AST (e.g. portion inside a 367 | -- 'for' control structure). 368 | -- @param env Environment to evaluate any expressions in. 369 | local function evaluate(ast, env) 370 | local chunks = {} 371 | local extends -- text of a parent template 372 | local rstrip -- flag for stripping leading whitespace of next token 373 | for i = 1, #ast, 3 do 374 | local pos, token, block = ast[i], ast[i + 1], ast[i + 2] 375 | env._LUPAPOSITION = pos 376 | if token == 'text' then 377 | chunks[#chunks + 1] = block 378 | elseif token == 'variable' then 379 | local value = eval(block, env) 380 | if autoescape then 381 | local escape = autoescape 382 | if type(autoescape) == 'function' then 383 | escape = autoescape(env._LUPAFILENAME) -- TODO: test 384 | end 385 | if escape and type(value) == 'string' then 386 | value = M.filters.escape(value) 387 | end 388 | end 389 | chunks[#chunks + 1] = value ~= nil and tostring(value) or '' 390 | elseif token == 'extends' then 391 | env_assert(env, not extends, 392 | 'cannot have multiple "extends" in the same scope') 393 | local file = eval(block, env) -- covers strings and variables 394 | extends = file 395 | env._LUPAEXTENDED = true -- used by parent templates 396 | elseif token == 'block' then 397 | local name = block.expression:match('^[%w_]+$') 398 | env_assert(env, name, 'invalid block name') 399 | -- Store the block for potential use by the parent template if this 400 | -- template is a child template, or for use by `self`. 401 | if not env._LUPABLOCKS then env._LUPABLOCKS = {} end 402 | if not env._LUPABLOCKS[name] then env._LUPABLOCKS[name] = {} end 403 | table.insert(env._LUPABLOCKS[name], 1, block) 404 | -- Handle the block properly. 405 | if not extends then 406 | if not env._LUPAEXTENDED then 407 | -- Evaluate the block normally. 408 | chunks[#chunks + 1] = evaluate(block, env) 409 | else 410 | -- A child template is overriding this parent's named block. Evaluate 411 | -- the child's block and use it instead of the parent's. 412 | local blocks = env._LUPABLOCKS[name] 413 | local super_env = setmetatable({super = function() 414 | -- Loop through the chain of defined blocks, evaluating from top to 415 | -- bottom, and return the bottom block. In each sub-block, the 416 | -- 'super' variable needs to point to the next-highest block's 417 | -- evaluated result. 418 | local super = evaluate(block, env) -- start with parent block 419 | local sub_env = setmetatable({super = function() return super end}, 420 | {__index = env}) 421 | for i = 1, #blocks - 1 do super = evaluate(blocks[i], sub_env) end 422 | return super 423 | end}, {__index = env}) 424 | chunks[#chunks + 1] = evaluate(blocks[#blocks], super_env) 425 | end 426 | end 427 | elseif token == 'for' then 428 | local expr = block.expression 429 | local p = env._LUPAPOSITION -- mark position at beginning of expression 430 | -- Extract variable list and generator. 431 | local patt = '^([%w_,%s]+)%s+in%s+()(.+)%s+if%s+(.+)$' 432 | local var_list, pos, generator, if_expr = expr:match(patt) 433 | if not var_list then 434 | var_list, pos, generator = expr:match('^([%w_,%s]+)%s+in%s+()(.+)$') 435 | end 436 | env_assert(env, var_list and generator, 'invalid for expression') 437 | -- Store variable names in a list for loop assignment. 438 | local variables = {} 439 | for variable, pos in var_list:gmatch('([^,%s]+)()') do 440 | env._LUPAPOSITION = p + pos - 1 -- update position for error messages 441 | env_assert(env, variable:find('^[%a_]') and variable ~= 'loop', 442 | 'invalid variable name') 443 | variables[#variables + 1] = variable 444 | end 445 | -- Evaluate the generator and perform the iteration. 446 | env._LUPAPOSITION = p + pos - 1 -- update position to generator 447 | if not generator:find('|') then 448 | generator = env_assert(env, load('return '..generator, nil, nil, env)) 449 | else 450 | local generator_expr = generator 451 | generator = function() return eval(generator_expr, env) end 452 | end 453 | local new_env = setmetatable({}, {__index = env}) 454 | chunks[#chunks + 1] = iterate(generator, variables, if_expr, block, 455 | new_env, 1, ast[i + 4] == 'lstrip') 456 | elseif token == 'if' then 457 | if eval(block.expression, env) then 458 | chunks[#chunks + 1] = evaluate(block, env) 459 | else 460 | local evaluate_else = true 461 | local elseifs = block['elseif'] 462 | if elseifs then 463 | for j = 1, #elseifs do 464 | if eval(elseifs[j].expression, env) then 465 | chunks[#chunks + 1] = evaluate(elseifs[j], env) 466 | evaluate_else = false 467 | break 468 | end 469 | end 470 | end 471 | if evaluate_else and block['else'] then 472 | chunks[#chunks + 1] = evaluate(block['else'], env) 473 | end 474 | end 475 | elseif token == 'macro' then 476 | -- Parse the macro's name and parameter list. 477 | local signature = block.expression 478 | local name, param_list = signature:match('^([%w_]+)(%b())') 479 | env_assert(env, name and param_list, 'invalid macro expression') 480 | param_list = param_list:sub(2, -2) 481 | local p = env._LUPAPOSITION + #name + 1 -- mark pos at beginning of args 482 | local params, defaults = {}, {} 483 | for param, pos, default in param_list:gmatch('([%w_]+)=?()([^,]*)') do 484 | params[#params + 1] = param 485 | if default ~= '' then 486 | env._LUPAPOSITION = p + pos - 1 -- update position for error messages 487 | local f = env_assert(env, load('return '..default)) 488 | defaults[param] = select(2, env_assert(env, pcall(f))) 489 | end 490 | end 491 | -- Create the function associated with the macro such that when the 492 | -- function is called (from within {{ ... }}), the macro's body is 493 | -- evaluated subject to an environment where parameter names are variables 494 | -- whose values are the ones passed to the macro itself. 495 | env[name] = function(...) 496 | local new_env = setmetatable({}, {__index = function(_, k) 497 | if k == 'caller' and type(env[k]) ~= 'function' then return nil end 498 | return env[k] 499 | end}) 500 | local args = {...} 501 | -- Assign the given parameter values. 502 | for i = 1, #args do 503 | if i > #params then break end 504 | new_env[params[i]] = args[i] 505 | end 506 | -- Clear all other unspecified parameter values or set them to their 507 | -- defined defaults. 508 | for i = #args + 1, #params do 509 | new_env[params[i]] = defaults[params[i]] 510 | end 511 | -- Store extra parameters in "varargs" variable. 512 | new_env.varargs = {} 513 | for i = #params + 1, #args do 514 | new_env.varargs[#new_env.varargs + 1] = args[i] 515 | end 516 | local chunk = evaluate(block, new_env) 517 | if ast[i + 4] == 'lstrip' then chunk = chunk:gsub('%s*$', '') end 518 | return chunk 519 | end 520 | elseif token == 'call' then 521 | -- Parse the call block's parameter list (if any) and determine the macro 522 | -- to call. 523 | local param_list = block.expression:match('^(%b())') 524 | local params = {} 525 | if param_list then 526 | for param in param_list:gmatch('[%w_]+') do 527 | params[#params + 1] = param 528 | end 529 | end 530 | local macro = block.expression:match('^%b()(.+)$') or block.expression 531 | -- Evaluate the given macro, subject to a "caller" function that returns 532 | -- the contents of this call block. Any arguments passed to the caller 533 | -- function are used as values of this parameters parsed earlier. 534 | local old_caller = M.env.caller -- save 535 | M.env.caller = function(...) 536 | local new_env = setmetatable({}, {__index = env}) 537 | local args = {...} 538 | -- Assign the given parameter values (if any). 539 | for i = 1, #args do new_env[params[i]] = args[i] end 540 | local chunk = evaluate(block, new_env) 541 | if ast[i + 4] == 'lstrip' then chunk = chunk:gsub('%s*$', '') end 542 | return chunk 543 | end 544 | chunks[#chunks + 1] = eval(macro, env) 545 | M.env.caller = old_caller -- restore 546 | elseif token == 'filter' then 547 | local text = evaluate(block, env) 548 | local p = env._LUPAPOSITION -- mark position at beginning of expression 549 | for pos, filter in each_filter(block.expression) do 550 | env._LUPAPOSITION = p + pos - 1 -- update position for error messages 551 | local name, params = filter:match('^%s*([%w_]+)%(?(.-)%)?%s*$') 552 | local f = M.filters[name] 553 | env_assert(env, f, 'unknown filter "'..name..'"') 554 | local args = env_assert(env, load('return {'..params..'}'), 555 | 'invalid filter parameter(s) for "'..name.. 556 | '"')() 557 | text = select(2, env_assert(env, pcall(f, text, table.unpack(args)))) 558 | end 559 | chunks[#chunks + 1] = text 560 | elseif token == 'set' then 561 | local var, expr = block:match('^([%a_][%w_]*)%s*=%s*(.+)$') 562 | env_assert(env, var and expr, 'invalid variable name or expression') 563 | env[var] = eval(expr, env) 564 | elseif token == 'do' then 565 | env_assert(env, pcall(env_assert(env, load(block, nil, nil, env)))) 566 | elseif token == 'include' then 567 | -- Parse the include block for flags. 568 | local without_context = block:find('without%s+context%s*') 569 | local ignore_missing = block:find('ignore%s+missing%s*') 570 | block = block:gsub('witho?u?t?%s+context%s*', '') 571 | :gsub('ignore%s+missing%s*', '') 572 | -- Evaluate the include expression in order to determine the file to 573 | -- include. If the result is a table, use the first file that exists. 574 | local file = eval(block, env) -- covers strings and variables 575 | if type(file) == 'table' then 576 | local files = file 577 | for i = 1, #files do 578 | file = loader(files[i], env) 579 | if file then break end 580 | end 581 | if type(file) == 'table' then file = nil end 582 | elseif type(file) == 'string' then 583 | file = loader(file, env) 584 | else 585 | error('"include" requires a string or table of files') 586 | end 587 | -- If the file exists, include it. Otherwise throw an error unless the 588 | -- "ignore missing" flag was given. 589 | env_assert(env, file or ignore_missing, 'no file(s) found to include') 590 | if file then 591 | chunks[#chunks + 1] = M.expand_file(file, not without_context and env or 592 | M.env) 593 | end 594 | elseif token == 'import' then 595 | local file, global = block:match('^%s*(.+)%s+as%s+([%a][%w_]*)%s*') 596 | local new_env = setmetatable({}, { 597 | __index = block:find('with%s+context%s*$') and env or M.env 598 | }) 599 | M.expand_file(eval(file or block, env), new_env) 600 | -- Copy any defined macros and variables over into the proper namespace. 601 | if global then env[global] = {} end 602 | local namespace = global and env[global] or env 603 | for k, v in pairs(new_env) do if not env[k] then namespace[k] = v end end 604 | elseif token == 'lstrip' and chunks[#chunks] then 605 | chunks[#chunks] = chunks[#chunks]:gsub('%s*$', '') 606 | elseif token == 'rstrip' then 607 | rstrip = true -- can only strip after determining the next chunk 608 | end 609 | if rstrip and token ~= 'rstrip' then 610 | chunks[#chunks] = chunks[#chunks]:gsub('^%s*', '') 611 | rstrip = false 612 | end 613 | end 614 | return not extends and table.concat(chunks) or M.expand_file(extends, env) 615 | end 616 | 617 | local pairs_gen, ipairs_gen = pairs({}), ipairs({}) 618 | 619 | -- Iterates over the generator *generator* subject to string "if" expression 620 | -- *if_expr*, assigns that generator's returned values to the variable names 621 | -- listed in *variables* within template environment *env*, evaluates any 622 | -- expressions in *block* (a portion of a template's AST), and returns a 623 | -- concatenation of the results. 624 | -- @param generator Either a function that returns a generator function, or a 625 | -- table to iterate over. In the latter case, `ipairs()` is used as the 626 | -- generator function. 627 | -- @param variables List of variable names to assign values returned by 628 | -- *generator* to. 629 | -- @param if_expr A conditional expression that when `false`, skips the current 630 | -- loop item. 631 | -- @param block The portion inside the 'for' structure of a template's AST to 632 | -- iterate with. 633 | -- @param env The environment iteration variables are defined in and where 634 | -- expressions are evaluated in. 635 | -- @param depth The current recursion depth. Recursion is performed by calling 636 | -- `loop(t)` with a table to iterate over. 637 | -- @param lstrip Whether or not the "endfor" block strips whitespace on the 638 | -- left. When `true`, all blocks produced by iteration are left-stripped. 639 | iterate = function(generator, variables, if_expr, block, env, depth, lstrip) 640 | local chunks = {} 641 | local orig_variables = {} -- used to store original loop variables' values 642 | for i = 1, #variables do orig_variables[variables[i]] = env[variables[i]] end 643 | local i, n = 1 -- used for loop variables 644 | local _, s, v -- state variables 645 | if type(generator) == 'function' then 646 | _, generator, s, v = env_assert(env, pcall(generator)) 647 | -- In practice, a generator's state variable is normally unused and hidden. 648 | -- This is not the case for 'pairs()' and 'ipairs', though. 649 | if variables[1] ~= '_index' and generator ~= pairs_gen and 650 | generator ~= ipairs_gen then 651 | table.insert(variables, 1, '_index') 652 | end 653 | end 654 | if type(generator) == 'table' then 655 | n = #generator 656 | generator, s, v = ipairs(generator) 657 | -- "for x in y" translates to "for _, x in ipairs(y)"; hide _ state variable 658 | if variables[1] ~= '_index' then table.insert(variables, 1, '_index') end 659 | end 660 | if generator then 661 | local first_results -- for preventing infinite loop from invalid generator 662 | while true do 663 | local results = {generator(s, v)} 664 | if results[1] == nil then break end 665 | -- If the results from the generator look like results returned by a 666 | -- generator itself (function, state, initial variable), verify last two 667 | -- results are different. If they are the same, then the original 668 | -- generator is invalid and will loop infinitely. 669 | if first_results == nil then 670 | first_results = #results == 3 and type(results[1]) == 'function' and 671 | results 672 | elseif first_results then 673 | env_assert(env, results[3] ~= first_results[3] or 674 | results[2] ~= first_results[2], 675 | 'invalid generator (infinite loop)') 676 | end 677 | -- Assign context variables and evaluate the body of the loop. 678 | -- As long as the result (ignoring the _index variable) is not a single 679 | -- table and there is only one loop variable defined (again, ignoring 680 | -- _index variable), assignment occurs as normal in Lua. Otherwise, 681 | -- unpacking on the table is done (like assignment to ...). 682 | if not (type(results[2]) == 'table' and #results == 2 and 683 | #variables > 2) then 684 | for j = 1, #variables do env[variables[j]] = results[j] end 685 | else 686 | for j = 2, #variables do env[variables[j]] = results[2][j - 1] end 687 | end 688 | if not if_expr or eval(if_expr, env) then 689 | env.loop = setmetatable({ 690 | index = i, index0 = i - 1, 691 | revindex = n and n - (i - 1), revindex0 = n and n - i, 692 | first = i == 1, last = i == n, length = n, 693 | cycle = function(...) 694 | return select((i - 1) % select('#', ...) + 1, ...) 695 | end, 696 | depth = depth, depth0 = depth - 1 697 | }, {__call = function(_, t) 698 | return iterate(t, variables, if_expr, block, env, depth + 1, lstrip) 699 | end}) 700 | chunks[#chunks + 1] = evaluate(block, env) 701 | if lstrip then chunks[#chunks] = chunks[#chunks]:gsub('%s*$', '') end 702 | i = i + 1 703 | end 704 | -- Prepare for next iteration. 705 | v = results[1] 706 | end 707 | end 708 | if i == 1 and block['else'] then 709 | chunks[#chunks + 1] = evaluate(block['else'], env) 710 | end 711 | for i = 1, #variables do env[variables[i]] = orig_variables[variables[i]] end 712 | return table.concat(chunks) 713 | end 714 | 715 | -- Expands string template *template* from source *source*, subject to template 716 | -- environment *env*, and returns the result. 717 | -- @param template String template to expand. 718 | -- @param env Environment for the given template. 719 | -- @param source Filename or identifier the template comes from for error 720 | -- messages and debugging. 721 | local function expand(template, env, source) 722 | template = template:gsub('\r?\n', newline_sequence) -- normalize 723 | if not keep_trailing_newline then template = template:gsub('\r?\n$', '') end 724 | -- Set up environment. 725 | if not env then env = {} end 726 | if not getmetatable(env) then env = setmetatable(env, {__index = M.env}) end 727 | env.self = setmetatable({}, {__index = function(_, k) 728 | env_assert(env, env._LUPABLOCKS and env._LUPABLOCKS[k], 729 | 'undefined block "'..k..'"') 730 | return function() return evaluate(env._LUPABLOCKS[k][1], env) end 731 | end}) 732 | -- Set context variables and expand the template. 733 | env._LUPASOURCE, env._LUPAFILENAME = template, source 734 | M._FILENAME = source -- for lpeg errors only 735 | local ast = assert(lpeg.match(M.grammar, template), "internal error") 736 | local result = evaluate(ast, env) 737 | return result 738 | end 739 | 740 | --- 741 | -- Expands the string template *template*, subject to template environment 742 | -- *env*, and returns the result. 743 | -- @param template String template to expand. 744 | -- @param env Optional environment for the given template. 745 | -- @name expand 746 | function M.expand(template, env) return expand(template, env, '') end 747 | 748 | --- 749 | -- Expands the template within file *filename*, subject to template environment 750 | -- *env*, and returns the result. 751 | -- @param filename Filename containing the template to expand. 752 | -- @param env Optional environment for the template to expand. 753 | -- @name expand_file 754 | function M.expand_file(filename, env) 755 | filename = loader(filename, env) or filename 756 | local f = (not env or not env._LUPASOURCE) and assert(io.open(filename)) or 757 | env_assert(env, io.open(filename)) 758 | local template = f:read('*a') 759 | f:close() 760 | return expand(template, env, filename) 761 | end 762 | 763 | --- 764 | -- Returns a loader for templates that uses the filesystem starting at directory 765 | -- *directory*. 766 | -- When looking up the template for a given filename, the loader considers the 767 | -- following: if no template is being expanded, the loader assumes the given 768 | -- filename is relative to *directory* and returns the full path; otherwise the 769 | -- loader assumes the given filename is relative to the current template's 770 | -- directory and returns the full path. 771 | -- The returned path may be passed to `io.open()`. 772 | -- @param directory Optional the template root directory. The default value is 773 | -- ".", which is the current working directory. 774 | -- @name loaders.filesystem 775 | -- @see configure 776 | function M.loaders.filesystem(directory) 777 | return function(filename, env) 778 | if not filename then return nil end 779 | local current_dir = env and env._LUPAFILENAME and 780 | env._LUPAFILENAME:match('^(.+)[/\\]') 781 | if not filename:find('^/') and not filename:find('^%a:[/\\]') then 782 | filename = (current_dir or directory or '.')..'/'..filename 783 | end 784 | local f = io.open(filename) 785 | if not f then return nil end 786 | f:close() 787 | return filename 788 | end 789 | end 790 | 791 | -- Globally defined functions. 792 | 793 | --- 794 | -- Returns a sequence of integers from *start* to *stop*, inclusive, in 795 | -- increments of *step*. 796 | -- The complete sequence is generated at once -- no generator is returned. 797 | -- @param start Optional number to start at. The default value is `1`. 798 | -- @param stop Number to stop at. 799 | -- @param step Optional increment between sequence elements. The default value 800 | -- is `1`. 801 | -- @name _G.range 802 | function range(start, stop, step) 803 | if not stop and not step then stop, start = start, 1 end 804 | if not step then step = 1 end 805 | local t = {} 806 | for i = start, stop, step do t[#t + 1] = i end 807 | return t 808 | end 809 | 810 | --- 811 | -- Returns an object that cycles through the given values by calls to its 812 | -- `next()` function. 813 | -- A `current` field contains the cycler's current value and a `reset()` 814 | -- function resets the cycler to its beginning. 815 | -- @param ... Values to cycle through. 816 | -- @usage c = cycler(1, 2, 3) 817 | -- @usage c:next(), c:next() --> 1, 2 818 | -- @usage c:reset() --> c.current == 1 819 | -- @name _G.cycler 820 | function cycler(...) 821 | local c = {...} 822 | c.n, c.i, c.current = #c, 1, c[1] 823 | function c:next() 824 | local current = self.current 825 | self.i = self.i + 1 826 | if self.i > self.n then self.i = 1 end 827 | self.current = self[self.i] 828 | return current 829 | end 830 | function c:reset() self.i, self.current = 1, self[1] end 831 | return c 832 | end 833 | 834 | -- Create the default sandbox environment for templates. 835 | local safe = { 836 | -- Lua globals. 837 | '_VERSION', 'ipairs', 'math', 'pairs', 'select', 'tonumber', 'tostring', 838 | 'type', 'bit32', 'os.date', 'os.time', 'string', 'table', 'utf8', 839 | -- Lupa globals. 840 | 'range', 'cycler' 841 | } 842 | local sandbox_env = setmetatable({}, {__index = M.tests}) 843 | for i = 1, #safe do 844 | local v = safe[i] 845 | if not v:find('%.') then 846 | sandbox_env[v] = _G[v] 847 | else 848 | local mod, func = v:match('^([^.]+)%.(.+)$') 849 | if not sandbox_env[mod] then sandbox_env[mod] = {} end 850 | sandbox_env[mod][func] = _G[mod][func] 851 | end 852 | end 853 | sandbox_env._G = sandbox_env 854 | 855 | --- 856 | -- Resets Lupa's default delimiters, options, and environments to their 857 | -- original default values. 858 | -- @name reset 859 | function M.reset() 860 | M.configure('{%', '%}', '{{', '}}', '{#', '#}') 861 | M.env = setmetatable({}, {__index = sandbox_env}) 862 | end 863 | M.reset() 864 | 865 | --- 866 | -- The default template environment. 867 | -- @class table 868 | -- @name env 869 | local env 870 | 871 | -- Lupa filters. 872 | 873 | --- 874 | -- Returns the absolute value of number *n*. 875 | -- @param n The number to compute the absolute value of. 876 | -- @name filters.abs 877 | M.filters.abs = math.abs 878 | 879 | -- Returns a table that, when indexed with an integer, indexes table *t* with 880 | -- that integer along with string *attribute*. 881 | -- This is used by filters that operate on particular attributes of table 882 | -- elements. 883 | -- @param t The table to index. 884 | -- @param attribute The additional attribute to index with. 885 | local function attr_accessor(t, attribute) 886 | return setmetatable({}, {__index = function(_, i) 887 | local value = t[i] 888 | attribute = tonumber(attribute) or attribute 889 | if type(attribute) == 'number' then return value[attribute] end 890 | for k in attribute:gmatch('[^.]+') do value = value[k] end 891 | return value 892 | end}) 893 | end 894 | 895 | --- 896 | -- Returns a generator that produces all of the items in table *t* in batches 897 | -- of size *size*, filling any empty spaces with value *fill*. 898 | -- Combine this with the "list" filter to produce a list. 899 | -- @param t The table to split into batches. 900 | -- @param size The batch size. 901 | -- @param fill The value to use when filling in any empty space in the last 902 | -- batch. 903 | -- @usage expand('{% for i in {1, 2, 3}|batch(2, 0) %}{{ i|string }} 904 | -- {% endfor %}') --> {1, 2} {3, 0} 905 | -- @see filters.list 906 | -- @name filters.batch 907 | function M.filters.batch(t, size, fill) 908 | assert(t, 'input to filter "batch" was nil instead of a table') 909 | local n = #t 910 | return function(t, i) 911 | if i > n then return nil end 912 | local batch = {} 913 | for j = i, i + size - 1 do batch[j - i + 1] = t[j] end 914 | if i + size > n and fill then 915 | for j = n + 1, i + size - 1 do batch[#batch + 1] = fill end 916 | end 917 | return i + size, batch 918 | end, t, 1 919 | end 920 | 921 | --- 922 | -- Capitalizes string *s*. 923 | -- The first character will be uppercased, the others lowercased. 924 | -- @param s The string to capitalize. 925 | -- @usage expand('{{ "foo bar"|capitalize }}') --> Foo bar 926 | -- @name filters.capitalize 927 | function M.filters.capitalize(s) 928 | assert(s, 'input to filter "capitalize" was nil instead of a string') 929 | local first, rest = s:match('^(.)(.*)$') 930 | return first and first:upper()..rest:lower() or s 931 | end 932 | 933 | --- 934 | -- Centers string *s* within a string of length *width*. 935 | -- @param s The string to center. 936 | -- @param width The length of the centered string. 937 | -- @usage expand('{{ "foo"|center(9) }}') --> " foo " 938 | -- @name filters.center 939 | function M.filters.center(s, width) 940 | assert(s, 'input to filter "center" was nil instead of a string') 941 | local padding = (width or 80) - #s 942 | local left, right = math.ceil(padding / 2), math.floor(padding / 2) 943 | return ("%s%s%s"):format((' '):rep(left), s, (' '):rep(right)) 944 | end 945 | 946 | --- 947 | -- Returns value *value* or value *default*, depending on whether or not *value* 948 | -- is "true" and whether or not boolean *false_defaults* is `true`. 949 | -- @param value The value return if "true" or if `false` and *false_defaults* 950 | -- is `true`. 951 | -- @param default The value to return if *value* is `nil` or `false` (the latter 952 | -- applies only if *false_defaults* is `true`). 953 | -- @param false_defaults Optional flag indicating whether or not to return 954 | -- *default* if *value* is `false`. The default value is `false`. 955 | -- @usage expand('{{ false|default("no") }}') --> false 956 | -- @usage expand('{{ false|default("no", true) }') --> no 957 | -- @name filters.default 958 | function M.filters.default(value, default, false_defaults) 959 | if value == nil or false_defaults and not value then return default end 960 | return value 961 | end 962 | 963 | --- 964 | -- Returns a table constructed from table *t* such that each element is a list 965 | -- that contains a single key-value pair and all elements are sorted according 966 | -- to string *by* (which is either "key" or "value") and boolean 967 | -- *case_sensitive*. 968 | -- @param value The table to sort. 969 | -- @param case_sensitive Optional flag indicating whether or not to consider 970 | -- case when sorting string values. The default value is `false`. 971 | -- @param by Optional string that specifies which of the key-value to sort by, 972 | -- either "key" or "value". The default value is `"key"`. 973 | -- @usage expand('{{ {b = 1, a = 2}|dictsort|string }}') --> {{"a", 2}, 974 | -- {"b", 1}} 975 | -- @name filters.dictsort 976 | function M.filters.dictsort(t, case_sensitive, by) 977 | assert(t, 'input to filter "dictsort" was nil instead of a table') 978 | assert(not by or by == 'key' or by == 'value', 979 | 'filter "dictsort" can only sort tables by "key" or "value"') 980 | local i = (not by or by == 'key') and 1 or 2 981 | local items = {} 982 | for k, v in pairs(t) do items[#items + 1] = {k, v} end 983 | table.sort(items, function(a, b) 984 | a, b = a[i], b[i] 985 | if not case_sensitive then 986 | if type(a) == 'string' then a = a:lower() end 987 | if type(b) == 'string' then b = b:lower() end 988 | end 989 | return a < b 990 | end) 991 | return items 992 | end 993 | 994 | --- 995 | -- Returns an HTML-safe copy of string *s*. 996 | -- @param s String to ensure is HTML-safe. 997 | -- @usage expand([[{{ '<">&'|e}}]]) --> <">& 998 | -- @name filters.escape 999 | function M.filters.escape(s) 1000 | assert(s, 'input to filter "escape" was nil instead of a string') 1001 | return s:gsub('[<>"\'&]', { 1002 | ['<'] = '<', ['>'] = '>', ['"'] = '"', ["'"] = ''', 1003 | ['&'] = '&' 1004 | }) 1005 | end 1006 | 1007 | --- 1008 | -- Returns an HTML-safe copy of string *s*. 1009 | -- @param s String to ensure is HTML-safe. 1010 | -- @usage expand([[{{ '<">&'|escape}}]]) --> <">& 1011 | -- @name filters.e 1012 | function M.filters.e(s) 1013 | assert(s, 'input to filter "e" was nil instead of a string') 1014 | return M.filters.escape(s) 1015 | end 1016 | 1017 | --- 1018 | -- Returns a human-readable, decimal (or binary, depending on boolean *binary*) 1019 | -- file size for *bytes* number of bytes. 1020 | -- @param bytes The number of bytes to return the size for. 1021 | -- @param binary Flag indicating whether or not to report binary file size 1022 | -- as opposed to decimal file size. The default value is `false`. 1023 | -- @usage expand('{{ 1000|filesizeformat }}') --> 1.0 kB 1024 | -- @name filters.filesizeformat 1025 | function M.filters.filesizeformat(bytes, binary) 1026 | assert(bytes, 'input to filter "filesizeformat" was nil instead of a number') 1027 | local base = binary and 1024 or 1000 1028 | local units = { 1029 | binary and 'KiB' or 'kB', binary and 'MiB' or 'MB', 1030 | binary and 'GiB' or 'GB', binary and 'TiB' or 'TB', 1031 | binary and 'PiB' or 'PB', binary and 'EiB' or 'EB', 1032 | binary and 'ZiB' or 'ZB', binary and 'YiB' or 'YB' 1033 | } 1034 | if bytes < base then 1035 | return string.format('%d Byte%s', bytes, bytes > 1 and 's' or '') 1036 | else 1037 | local limit, unit 1038 | for i = 1, #units do 1039 | limit, unit = base^(i + 1), units[i] 1040 | if bytes < limit then break end 1041 | end 1042 | return string.format('%.1f %s', (base * bytes / limit), unit) 1043 | end 1044 | end 1045 | 1046 | --- 1047 | -- Returns the first element in table *t*. 1048 | -- @param t The table to get the first element of. 1049 | -- @usage expand('{{ range(10)|first }}') --> 1 1050 | -- @name filters.first 1051 | function M.filters.first(t) 1052 | assert(t, 'input to filter "first" was nil instead of a table') 1053 | return t[1] 1054 | end 1055 | 1056 | --- 1057 | -- Returns value *value* as a float. 1058 | -- This filter only works in Lua 5.3, which has a distinction between floats and 1059 | -- integers. 1060 | -- @param value The value to interpret as a float. 1061 | -- @usage expand('{{ 42|float }}') --> 42.0 1062 | -- @name filters.float 1063 | function M.filters.float(value) 1064 | assert(value, 'input to filter "float" was nil instead of a number') 1065 | return (tonumber(value) or 0) * 1.0 1066 | end 1067 | 1068 | --- 1069 | -- Returns an HTML-safe copy of value *value*, even if *value* was returned by 1070 | -- the "safe" filter. 1071 | -- @param value Value to ensure is HTML-safe. 1072 | -- @usage expand('{% set x = "
"|safe %}{{ x|forceescape }}') --> 1073 | -- <div /> 1074 | -- @name filters.forceescape 1075 | function M.filters.forceescape(value) 1076 | assert(value, 'input to filter "forceescape" was nil instead of a string') 1077 | return M.filters.escape(tostring(value)) 1078 | end 1079 | 1080 | --- 1081 | -- Returns the given arguments formatted according to string *s*. 1082 | -- See Lua's `string.format()` for more information. 1083 | -- @param s The string to format subsequent arguments according to. 1084 | -- @param ... Arguments to format. 1085 | -- @usage expand('{{ "%s,%s"|format("a", "b") }}') --> a,b 1086 | -- @name filters.format 1087 | function M.filters.format(s, ...) 1088 | assert(s, 'input to filter "format" was nil instead of a string') 1089 | return string.format(s, ...) 1090 | end 1091 | 1092 | --- 1093 | -- Returns a generator that produces lists of items in table *t* grouped by 1094 | -- string attribute *attribute*. 1095 | -- @param t The table to group items from. 1096 | -- @param attribute The attribute of items in the table to group by. This may 1097 | -- be nested (e.g. "foo.bar" groups by t[i].foo.bar for all i). 1098 | -- @usage expand('{% for age, group in people|groupby("age") %}...{% endfor %}') 1099 | -- @name filters.groupby 1100 | function M.filters.groupby(t, attribute) 1101 | assert(t, 'input to filter "groupby" was nil instead of a table') 1102 | local n = #t 1103 | local seen = {} -- keep track of groupers in order to avoid duplicates 1104 | return function(t, i) 1105 | if i > n then return nil end 1106 | local ta = attr_accessor(t, attribute) 1107 | -- Determine the next grouper. 1108 | local grouper = ta[i] 1109 | while seen[grouper] do 1110 | i = i + 1 1111 | if i > n then return nil end 1112 | grouper = ta[i] 1113 | end 1114 | seen[grouper] = true 1115 | -- Create and return the group. 1116 | local group = {} 1117 | for j = i, #t do if ta[j] == grouper then group[#group + 1] = t[j] end end 1118 | return i + 1, grouper, group 1119 | end, t, 1 1120 | end 1121 | 1122 | --- 1123 | -- Returns a copy of string *s* with all lines after the first indented by 1124 | -- *width* number of spaces. 1125 | -- If boolean *first_line* is `true`, indents the first line as well. 1126 | -- @param s The string to indent lines of. 1127 | -- @param width The number of spaces to indent lines with. 1128 | -- @param first_line Optional flag indicating whether or not to indent the 1129 | -- first line of text. The default value is `false`. 1130 | -- @usage expand('{{ "foo\nbar"|indent(2) }}') --> "foo\n bar" 1131 | -- @name filters.indent 1132 | function M.filters.indent(s, width, first_line) 1133 | assert(s, 'input to filter "indent" was nil instead of a string') 1134 | local indent = (' '):rep(width) 1135 | return (first_line and indent or '')..s:gsub('([\r\n]+)', '%1'..indent) 1136 | end 1137 | 1138 | --- 1139 | -- Returns value *value* as an integer. 1140 | -- @param value The value to interpret as an integer. 1141 | -- @usage expand('{{ 32.32|int }}') --> 32 1142 | -- @name filters.int 1143 | function M.filters.int(value) 1144 | assert(value, 'input to filter "int" was nil instead of a number') 1145 | return math.floor(tonumber(value) or 0) 1146 | end 1147 | 1148 | --- 1149 | -- Returns a string that contains all the elements in table *t* (or all the 1150 | -- attributes named *attribute* in *t*) separated by string *sep*. 1151 | -- @param t The table to join. 1152 | -- @param sep The string to separate table elements with. 1153 | -- @param attribute Optional attribute of elements to use for joining instead 1154 | -- of the elements themselves. This may be nested (e.g. "foo.bar" joins 1155 | -- `t[i].foo.bar` for all i). 1156 | -- @usage expand('{{ {1, 2, 3}|join("|") }}') --> 1|2|3 1157 | -- @name filters.join 1158 | function M.filters.join(t, sep, attribute) 1159 | assert(t, 'input to filter "join" was nil instead of a table') 1160 | if not attribute then 1161 | local strings = {} 1162 | for i = 1, #t do strings[#strings + 1] = tostring(t[i]) end 1163 | return table.concat(strings, sep) 1164 | end 1165 | local ta = attr_accessor(t, attribute) 1166 | local attributes = {} 1167 | for i = 1, #t do attributes[#attributes + 1] = ta[i] end 1168 | return table.concat(attributes, sep) 1169 | end 1170 | 1171 | --- 1172 | -- Returns the last element in table *t*. 1173 | -- @param t The table to get the last element of. 1174 | -- @usage expand('{{ range(10)|last }}') --> 10 1175 | -- @name filters.last 1176 | function M.filters.last(t) 1177 | assert(t, 'input to filter "last" was nil instead of a table') 1178 | return t[#t] 1179 | end 1180 | 1181 | --- 1182 | -- Returns the length of string or table *value*. 1183 | -- @param value The value to get the length of. 1184 | -- @usage expand('{{ "hello world"|length }}') --> 11 1185 | -- @name filters.length 1186 | function M.filters.length(value) 1187 | assert(value, 'input to filter "length" was nil instead of a table or string') 1188 | return #value 1189 | end 1190 | 1191 | --- 1192 | -- Returns the list of items produced by generator *generator*, subject to 1193 | -- initial state *s* and initial iterator variable *i*. 1194 | -- This filter should only be used after a filter that returns a generator. 1195 | -- @param generator Generator function that produces an item. 1196 | -- @param s Initial state for the generator. 1197 | -- @param i Initial iterator variable for the generator. 1198 | -- @usage expand('{{ range(4)|batch(2)|list|string }}') --> {{1, 2}, {3, 4}} 1199 | -- @see filters.batch 1200 | -- @see filters.groupby 1201 | -- @see filters.slice 1202 | -- @name filters.list 1203 | function M.filters.list(generator, s, i) 1204 | assert(type(generator) == 'function', 1205 | 'input to filter "list" must be a generator') 1206 | local list = {} 1207 | for _, v in generator, s, i do list[#list + 1] = v end 1208 | return list 1209 | end 1210 | 1211 | --- 1212 | -- Returns a copy of string *s* with all lowercase characters. 1213 | -- @param s The string to lowercase. 1214 | -- @usage expand('{{ "FOO"|lower }}') --> foo 1215 | -- @name filters.lower 1216 | function M.filters.lower(s) 1217 | assert(s, 'input to filter "lower" was nil instead of a string') 1218 | return string.lower(s) 1219 | end 1220 | 1221 | --- 1222 | -- Maps each element of table *t* to a value produced by filter name *filter* 1223 | -- and returns the resultant table. 1224 | -- @param t The table of elements to map. 1225 | -- @param filter The name of the filter to pass table elements through. 1226 | -- @param ... Any arguments for the filter. 1227 | -- @usage expand('{{ {"1", "2", "3"}|map("int")|sum }}') --> 6 1228 | -- @name filters.map 1229 | function M.filters.map(t, filter, ...) 1230 | assert(t, 'input to filter "map" was nil instead of a table') 1231 | local f = M.filters[filter] 1232 | assert(f, 'unknown filter "'..filter..'"') 1233 | local map = {} 1234 | for i = 1, #t do map[i] = f(t[i], ...) end 1235 | return map 1236 | end 1237 | 1238 | --- 1239 | -- Maps the value of each element's string *attribute* in table *t* to the 1240 | -- value produced by filter name *filter* and returns the resultant table. 1241 | -- @param t The table of elements with attributes to map. 1242 | -- @param attribute The attribute of elements in the table to filter. This may 1243 | -- be nested (e.g. "foo.bar" maps t[i].foo.bar for all i). 1244 | -- @param filter The name of the filter to pass table elements through. 1245 | -- @param ... Any arguments for the filter. 1246 | -- @usage expand('{{ users|mapattr("name")|join("|") }}') 1247 | -- @name filters.mapattr 1248 | function M.filters.mapattr(t, attribute, filter, ...) 1249 | assert(t, 'input to filter "mapattr" was nil instead of a table') 1250 | local ta = attr_accessor(t, attribute) 1251 | local f = M.filters[filter] 1252 | if filter then 1253 | assert(f, 'unknown filter "'..filter..'" given to filter "mapattr"') 1254 | end 1255 | local map = {} 1256 | for i = 1, #t do map[i] = filter and f(ta[i], ...) or ta[i] end 1257 | return map 1258 | end 1259 | 1260 | --- 1261 | -- Returns a random element from table *t*. 1262 | -- @param t The table to get a random element from. 1263 | -- @usage expand('{{ range(100)|random }}') 1264 | -- @name filters.random 1265 | function M.filters.random(t) 1266 | assert(t, 'input to filter "random" was nil instead of a table') 1267 | math.randomseed(os.time()) 1268 | return t[math.random(#t)] 1269 | end 1270 | 1271 | --- 1272 | -- Returns a list of elements in table *t* that fail test name *test*. 1273 | -- @param t The table of elements to reject from. 1274 | -- @param test The name of the test to use on table elements. 1275 | -- @param ... Any arguments for the test. 1276 | -- @usage expand('{{ range(5)|reject(is_odd)|join("|") }}') --> 2|4 1277 | -- @name filters.reject 1278 | function M.filters.reject(t, test, ...) 1279 | assert(t, 'input to filter "reject" was nil instead of a table') 1280 | local f = test or function(value) return not not value end 1281 | local items = {} 1282 | for i = 1, #t do if not f(t[i], ...) then items[#items + 1] = t[i] end end 1283 | return items 1284 | end 1285 | 1286 | --- 1287 | -- Returns a list of elements in table *t* whose string attribute *attribute* 1288 | -- fails test name *test*. 1289 | -- @param t The table of elements to reject from. 1290 | -- @param attribute The attribute of items in the table to reject from. This 1291 | -- may be nested (e.g. "foo.bar" tests t[i].foo.bar for all i). 1292 | -- @param test The name of the test to use on table elements. 1293 | -- @param ... Any arguments for the test. 1294 | -- @usage expand('{{ users|rejectattr("offline")|mapattr("name")|join(",") }}') 1295 | -- @name filters.rejectattr 1296 | function M.filters.rejectattr(t, attribute, test, ...) 1297 | assert(t, 'input to filter "rejectattr" was nil instead of a table') 1298 | local ta = attr_accessor(t, attribute) 1299 | local f = test or function(value) return not not value end 1300 | local items = {} 1301 | for i = 1, #t do if not f(ta[i], ...) then items[#items + 1] = t[i] end end 1302 | return items 1303 | end 1304 | 1305 | --- 1306 | -- Returns a copy of string *s* with all (or up to *n*) occurrences of string 1307 | -- *old* replaced by string *new*. 1308 | -- Identical to Lua's `string.gsub()` and handles Lua patterns. 1309 | -- @param s The subject string. 1310 | -- @param pattern The string or Lua pattern to replace. 1311 | -- @param repl The replacement text (may contain Lua captures). 1312 | -- @param n Optional number indicating the maximum number of replacements to 1313 | -- make. The default value is `nil`, which is unlimited. 1314 | -- @usage expand('{% filter upper|replace("FOO", "foo") %}foobar 1315 | -- {% endfilter %}') --> fooBAR 1316 | -- @name filters.replace 1317 | function M.filters.replace(s, pattern, repl, n) 1318 | assert(s, 'input to filter "replace" was nil instead of a string') 1319 | return string.gsub(s, pattern, repl, n) 1320 | end 1321 | 1322 | --- 1323 | -- Returns a copy of the given string or table *value* in reverse order. 1324 | -- @param value The value to reverse. 1325 | -- @usage expand('{{ {1, 2, 3}|reverse|string }}') --> {3, 2, 1} 1326 | -- @name filters.reverse 1327 | function M.filters.reverse(value) 1328 | assert(type(value) == 'table' or type(value) == 'string', 1329 | 'input to filter "reverse" was nil instead of a table or string') 1330 | if type(value) == 'string' then return value:reverse() end 1331 | local t = {} 1332 | for i = 1, #value do t[i] = value[#value - i + 1] end 1333 | return t 1334 | end 1335 | 1336 | --- 1337 | -- Returns number *value* rounded to *precision* decimal places based on string 1338 | -- *method* (if given). 1339 | -- @param value The number to round. 1340 | -- @param precision Optional precision to round the number to. The default 1341 | -- value is `0`. 1342 | -- @param method Optional string rounding method, either `"ceil"` or 1343 | -- `"floor"`. The default value is `nil`, which uses the common rounding 1344 | -- method (if a number's fractional part is 0.5 or greater, rounds up; 1345 | -- otherwise rounds down). 1346 | -- @usage expand('{{ 2.1236|round(3, "floor") }}') --> 2.123 1347 | -- @name filters.round 1348 | function M.filters.round(value, precision, method) 1349 | assert(value, 'input to filter "round" was nil instead of a number') 1350 | assert(not method or method == 'ceil' or method == 'floor', 1351 | 'rounding method given to filter "round" must be "ceil" or "floor"') 1352 | precision = precision or 0 1353 | method = method or (select(2, math.modf(value)) >= 0.5 and 'ceil' or 'floor') 1354 | local s = string.format('%.'..(precision >= 0 and precision or 0)..'f', 1355 | math[method](value * 10^precision) / 10^precision) 1356 | return tonumber(s) 1357 | end 1358 | 1359 | --- 1360 | -- Marks string *s* as HTML-safe, preventing Lupa from modifying it when 1361 | -- configured to autoescape HTML entities. 1362 | -- This filter must be used at the end of a filter chain unless it is 1363 | -- immediately proceeded by the "forceescape" filter. 1364 | -- @param s The string to mark as HTML-safe. 1365 | -- @usage lupa.configure{autoescape = true} 1366 | -- @usage expand('{{ "
foo
"|safe }}') -->
foo
1367 | -- @name filters.safe 1368 | function M.filters.safe(s) 1369 | assert(s, 'input to filter "safe" was nil instead of a string') 1370 | return setmetatable({}, {__tostring = function() return s end}) 1371 | end 1372 | 1373 | --- 1374 | -- Returns a list of the elements in table *t* that pass test name *test*. 1375 | -- @param t The table of elements to select from. 1376 | -- @param test The name of the test to use on table elements. 1377 | -- @param ... Any arguments for the test. 1378 | -- @usage expand('{{ range(5)|select(is_odd)|join("|") }}') --> 1|3|5 1379 | -- @name filters.select 1380 | function M.filters.select(t, test, ...) 1381 | assert(t, 'input to filter "select" was nil instead of a table') 1382 | local f = test or function(value) return not not value end 1383 | local items = {} 1384 | for i = 1, #t do if f(t[i], ...) then items[#items + 1] = t[i] end end 1385 | return items 1386 | end 1387 | 1388 | --- 1389 | -- Returns a list of elements in table *t* whose string attribute *attribute* 1390 | -- passes test name *test*. 1391 | -- @param t The table of elements to select from. 1392 | -- @param attribute The attribute of items in the table to select from. This 1393 | -- may be nested (e.g. "foo.bar" tests t[i].foo.bar for all i). 1394 | -- @param test The name of the test to use on table elements. 1395 | -- @param ... Any arguments for the test. 1396 | -- @usage expand('{{ users|selectattr("online")|mapattr("name")|join("|") }}') 1397 | -- @name filters.selectattr 1398 | function M.filters.selectattr(t, attribute, test, ...) 1399 | assert(t, 'input to filter "selectattr" was nil instead of a table') 1400 | local ta = attr_accessor(t, attribute) 1401 | local f = test or function(value) return not not value end 1402 | local items = {} 1403 | for i = 1, #t do if f(ta[i], ...) then items[#items + 1] = t[i] end end 1404 | return items 1405 | end 1406 | 1407 | --- 1408 | -- Returns a generator that produces all of the items in table *t* in *slices* 1409 | -- number of iterations, filling any empty spaces with value *fill*. 1410 | -- Combine this with the "list" filter to produce a list. 1411 | -- @param t The table to slice. 1412 | -- @param slices The number of slices to produce. 1413 | -- @param fill The value to use when filling in any empty space in the last 1414 | -- slice. 1415 | -- @usage expand('{% for i in {1, 2, 3}|slice(2, 0) %}{{ i|string }} 1416 | -- {% endfor %}') --> {1, 2} {3, 0} 1417 | -- @see filters.list 1418 | -- @name filters.slice 1419 | function M.filters.slice(t, slices, fill) 1420 | assert(t, 'input to filter "slice" was nil instead of a table') 1421 | local size, slices_with_extra = math.floor(#t / slices), #t % slices 1422 | return function(t, i) 1423 | if i > slices then return nil end 1424 | local slice = {} 1425 | local s = (i - 1) * size + math.min(i, slices_with_extra + 1) 1426 | local e = i * size + math.min(i, slices_with_extra) 1427 | for j = s, e do slice[j - s + 1] = t[j] end 1428 | if slices_with_extra > 0 and i > slices_with_extra and fill then 1429 | slice[#slice + 1] = fill 1430 | end 1431 | return i + 1, slice 1432 | end, t, 1 1433 | end 1434 | 1435 | --- 1436 | -- Returns a copy of table or string *value* in sorted order by value (or by 1437 | -- an attribute named *attribute*), depending on booleans *reverse* and 1438 | -- *case_sensitive*. 1439 | -- @param value The table or string to sort. 1440 | -- @param reverse Optional flag indicating whether or not to sort in reverse 1441 | -- (descending) order. The default value is `false`, which sorts in ascending 1442 | -- order. 1443 | -- @param case_sensitive Optional flag indicating whether or not to consider 1444 | -- case when sorting string values. The default value is `false`. 1445 | -- @param attribute Optional attribute of elements to sort by instead of the 1446 | -- elements themselves. 1447 | -- @usage expand('{{ {2, 3, 1}|sort|string }}') --> {1, 2, 3} 1448 | -- @name filters.sort 1449 | function M.filters.sort(value, reverse, case_sensitive, attribute) 1450 | assert(value, 'input to filter "sort" was nil instead of a table or string') 1451 | assert(not attribute or type(attribute) == 'string' or 1452 | type(attribute) == 'number', 1453 | 'attribute to filter "sort" must be a string or number') 1454 | local t = {} 1455 | local sort_string = type(value) == 'string' 1456 | if not sort_string then 1457 | for i = 1, #value do t[#t + 1] = value[i] end 1458 | else 1459 | for char in value:gmatch('.') do t[#t + 1] = char end -- chars in string 1460 | end 1461 | table.sort(t, function(a, b) 1462 | if attribute then 1463 | if type(attribute) == 'number' then 1464 | a, b = a[attribute], b[attribute] 1465 | else 1466 | for k in attribute:gmatch('[^.]+') do a, b = a[k], b[k] end 1467 | end 1468 | end 1469 | if not case_sensitive then 1470 | if type(a) == 'string' then a = a:lower() end 1471 | if type(b) == 'string' then b = b:lower() end 1472 | end 1473 | if not reverse then 1474 | return a < b 1475 | else 1476 | return a > b 1477 | end 1478 | end) 1479 | return not sort_string and t or table.concat(t) 1480 | end 1481 | 1482 | --- 1483 | -- Returns the string representation of value *value*, handling lists properly. 1484 | -- @param value Value to return the string representation of. 1485 | -- @usage expand('{{ {1 * 1, 2 * 2, 3 * 3}|string }}') --> {1, 4, 9} 1486 | -- @name filters.string 1487 | function M.filters.string(value) 1488 | if type(value) ~= 'table' then return tostring(value) end 1489 | local t = {} 1490 | for i = 1, #value do 1491 | local item = value[i] 1492 | t[i] = type(item) == 'string' and '"'..item..'"' or M.filters.string(item) 1493 | end 1494 | return '{'..table.concat(t, ', ')..'}' 1495 | end 1496 | 1497 | --- 1498 | -- Returns a copy of string *s* with any HTML tags stripped. 1499 | -- Also cleans up whitespace. 1500 | -- @param s String to strip HTML tags from. 1501 | -- @usage expand('{{ "
foo
"|striptags }}') --> foo 1502 | -- @name filters.striptags 1503 | function M.filters.striptags(s) 1504 | assert(s, 'input to filter "striptags" was nil instead of a string') 1505 | return s:gsub('%b<>', ''):gsub('%s+', ' '):match('^%s*(.-)%s*$') 1506 | end 1507 | 1508 | --- 1509 | -- Returns the numeric sum of the elements in table *t* or the sum of all 1510 | -- attributes named *attribute* in *t*. 1511 | -- @param t The table to calculate the sum of. 1512 | -- @param attribute Optional attribute of elements to use for summing instead 1513 | -- of the elements themselves. This may be nested (e.g. "foo.bar" sums 1514 | -- `t[i].foo.bar` for all i). 1515 | -- @usage expand('{{ range(6)|sum }}') --> 21 1516 | -- @name filters.sum 1517 | function M.filters.sum(t, attribute) 1518 | assert(t, 'input to filter "sum" was nil instead of a table') 1519 | local ta = attribute and attr_accessor(t, attribute) or t 1520 | local sum = 0 1521 | for i = 1, #t do sum = sum + ta[i] end 1522 | return sum 1523 | end 1524 | 1525 | --- 1526 | -- Returns a copy of all words in string *s* in titlecase. 1527 | -- @param s The string to titlecase. 1528 | -- @usage expand('{{ "foo bar"|title }}') --> Foo Bar 1529 | -- @name filters.title 1530 | function M.filters.title(s) 1531 | assert(s, 'input to filter "title" was nil instead of a string') 1532 | return s:gsub('[^-%s]+', M.filters.capitalize) 1533 | end 1534 | 1535 | --- 1536 | -- Returns a copy of string *s* truncated to *length* number of characters. 1537 | -- Truncated strings end with '...' or string *delimiter*. If boolean 1538 | -- *partial_words* is `false`, truncation will only happen at word boundaries. 1539 | -- @param s The string to truncate. 1540 | -- @param length The length to truncate the string to. 1541 | -- @param partial_words Optional flag indicating whether or not to allow 1542 | -- truncation within word boundaries. The default value is `false`. 1543 | -- @param delimiter Optional delimiter text. The default value is '...'. 1544 | -- @usage expand('{{ "foo bar"|truncate(4) }}') --> "foo ..." 1545 | -- @name filters.truncate 1546 | function M.filters.truncate(s, length, partial_words, delimiter) 1547 | assert(s, 'input to filter "truncate" was nil instead of a string') 1548 | if #s <= length then return s end 1549 | local truncated = s:sub(1, length) 1550 | if s:find('[%w_]', length) and not partial_words then 1551 | truncated = truncated:match('^(.-)[%w_]*$') -- drop partial word 1552 | end 1553 | return truncated..(delimiter or '...') 1554 | end 1555 | 1556 | --- 1557 | -- Returns a copy of string *s* with all uppercase characters. 1558 | -- @param s The string to uppercase. 1559 | -- @usage expand('{{ "foo"|upper }}') --> FOO 1560 | -- @name filters.upper 1561 | function M.filters.upper(s) 1562 | assert(s, 'input to filter "upper" was nil instead of a string') 1563 | return string.upper(s) 1564 | end 1565 | 1566 | --- 1567 | -- Returns a string suitably encoded to be used in a URL from value *value*. 1568 | -- *value* may be a string, table of key-value query parameters, or table of 1569 | -- lists of key-value query parameters (for order). 1570 | -- @param value Value to URL-encode. 1571 | -- @usage expand('{{ {{'f', 1}, {'z', 2}}|urlencode }}') --> f=1&z=2 1572 | -- @name filters.urlencode 1573 | function M.filters.urlencode(value) 1574 | assert(value, 1575 | 'input to filter "urlencode" was nil instead of a string or table') 1576 | if type(value) ~= 'table' then 1577 | return tostring(value):gsub('[^%w.-]', function(c) 1578 | return string.format('%%%X', string.byte(c)) 1579 | end) 1580 | end 1581 | local params = {} 1582 | if #value > 0 then 1583 | for i = 1, #value do 1584 | local k = M.filters.urlencode(value[i][1]) 1585 | local v = M.filters.urlencode(value[i][2]) 1586 | params[#params + 1] = k..'='..v 1587 | end 1588 | else 1589 | for k, v in pairs(value) do 1590 | params[#params + 1] = M.filters.urlencode(k)..'='..M.filters.urlencode(v) 1591 | end 1592 | end 1593 | return table.concat(params, '&') 1594 | end 1595 | 1596 | --- 1597 | -- Replaces any URLs in string *s* with HTML links, limiting link text to 1598 | -- *length* characters. 1599 | -- @param s The string to replace URLs with HTML links in. 1600 | -- @param length Optional maximum number of characters to include in link text. 1601 | -- The default value is `nil`, which imposes no limit. 1602 | -- @param nofollow Optional flag indicating whether or not HTML links will get a 1603 | -- "nofollow" attribute. 1604 | -- @usage expand('{{ "example.com"|urlize }}') --> 1605 | -- example.com 1606 | -- @name filters.urlize 1607 | function M.filters.urlize(s, length, nofollow) 1608 | assert(s, 'input to filter "urlize" was nil instead of a string') 1609 | -- Trims the given url. 1610 | local function trim_url(url) 1611 | return length and s:sub(1, length)..(#s > length and '...' or '') or url 1612 | end 1613 | local nofollow_attr = nofollow and ' rel="nofollow"' or '' 1614 | local lead, trail = C((S('(<') + '<')^0), C((S('.,)>\n') + '>')^0) * -1 1615 | local middle = C((1 - trail)^0) 1616 | local patt = lpeg.Cs(lead * middle * trail / function(lead, middle, trail) 1617 | local linked 1618 | if middle:find('^www%.') or (not middle:find('@') and 1619 | not middle:find('^https?://') and 1620 | #middle > 0 and middle:find('^%w') and ( 1621 | middle:find('%.com$') or 1622 | middle:find('%.net$') or 1623 | middle:find('%.org$') 1624 | )) then 1625 | middle, linked = string.format('%s', middle, 1626 | nofollow_attr, trim_url(middle)), true 1627 | end 1628 | if middle:find('^https?://') then 1629 | middle, linked = string.format('%s', middle, 1630 | nofollow_attr, trim_url(middle)), true 1631 | end 1632 | if middle:find('@') and not middle:find('^www%.') and 1633 | not middle:find(':') and middle:find('^%S+@[%w._-]+%.[%w._-]+$') then 1634 | middle, linked = string.format('%s', middle, 1635 | middle), true 1636 | end 1637 | if linked then return lead..middle..trail end 1638 | end) 1639 | return M.filters.escape(s):gsub('%S+', function(word) 1640 | return lpeg.match(patt, word) 1641 | end) 1642 | end 1643 | 1644 | --- 1645 | -- Returns the number of words in string *s*. 1646 | -- A word is a sequence of non-space characters. 1647 | -- @param s The string to count words in. 1648 | -- @usage expand('{{ "foo bar baz"|wordcount }}') --> 3 1649 | -- @name filters.wordcount 1650 | function M.filters.wordcount(s) 1651 | assert(s, 'input to filter "wordcount" was nil instead of a string') 1652 | return select(2, s:gsub('%S+', '')) 1653 | end 1654 | 1655 | --- 1656 | -- Interprets table *t* as a list of XML attribute-value pairs, returning them 1657 | -- as a properly formatted, space-separated string. 1658 | -- @param t The table of XML attribute-value pairs. 1659 | -- @usage expand('') 1660 | -- @name filters.xmlattr 1661 | function M.filters.xmlattr(t) 1662 | assert(t, 'input to filter "xmlattr" was nil instead of a table') 1663 | local attributes = {} 1664 | for k, v in pairs(t) do 1665 | attributes[#attributes + 1] = string.format('%s="%s"', k, 1666 | M.filters.escape(tostring(v))) 1667 | end 1668 | return table.concat(attributes, ' ') 1669 | end 1670 | 1671 | -- Lupa tests. 1672 | 1673 | --- 1674 | -- Returns whether or not number *n* is odd. 1675 | -- @param n The number to test. 1676 | -- @usage expand('{% for x in range(10) if is_odd(x) %}...{% endif %}') 1677 | -- @name tests.is_odd 1678 | function M.tests.is_odd(n) return n % 2 == 1 end 1679 | 1680 | --- 1681 | -- Returns whether or not number *n* is even. 1682 | -- @param n The number to test. 1683 | -- @usage expand('{% for x in range(10) if is_even(x) %}...{% endif %}') 1684 | -- @name tests.is_even 1685 | function M.tests.is_even(n) return n % 2 == 0 end 1686 | 1687 | --- 1688 | -- Returns whether or not number *n* is evenly divisible by number *num*. 1689 | -- @param n The dividend to test. 1690 | -- @param num The divisor to use. 1691 | -- @usage expand('{% if is_divisibleby(x, y) %}...{% endif %}') 1692 | -- @name tests.is_divisibleby 1693 | function M.tests.is_divisibleby(n, num) return n % num == 0 end 1694 | 1695 | --- 1696 | -- Returns whether or not value *value* is non-nil, and thus defined. 1697 | -- @param value The value to test. 1698 | -- @usage expand('{% if is_defined(x) %}...{% endif %}') 1699 | -- @name tests.is_defined 1700 | function M.tests.is_defined(value) return value ~= nil end 1701 | 1702 | --- 1703 | -- Returns whether or not value *value* is nil, and thus effectively undefined. 1704 | -- @param value The value to test. 1705 | -- @usage expand('{% if is_undefined(x) %}...{% endif %}') 1706 | -- @name tests.is_undefined 1707 | function M.tests.is_undefined(value) return value == nil end 1708 | 1709 | --- 1710 | -- Returns whether or not value *value* is nil. 1711 | -- @param value The value to test. 1712 | -- @usage expand('{% if is_none(x) %}...{% endif %}') 1713 | -- @name tests.is_none 1714 | function M.tests.is_none(value) return value == nil end 1715 | 1716 | --- 1717 | -- Returns whether or not value *value* is nil. 1718 | -- @param value The value to test. 1719 | -- @usage expand('{% if is_nil(x) %}...{% endif %}') 1720 | -- @name tests.is_nil 1721 | function M.tests.is_nil(value) return value == nil end 1722 | 1723 | --- 1724 | -- Returns whether or not string *s* is in all lower-case characters. 1725 | -- @param s The string to test. 1726 | -- @usage expand('{% if is_lower(s) %}...{% endif %}') 1727 | -- @name tests.is_lower 1728 | function M.tests.is_lower(s) return s:lower() == s end 1729 | 1730 | --- 1731 | -- Returns whether or not string *s* is in all upper-case characters. 1732 | -- @param s The string to test. 1733 | -- @usage expand('{% if is_upper(s) %}...{% endif %}') 1734 | -- @name tests.is_upper 1735 | function M.tests.is_upper(s) return s:upper() == s end 1736 | 1737 | --- 1738 | -- Returns whether or not value *value* is a string. 1739 | -- @param value The value to test. 1740 | -- @usage expand('{% if is_string(x) %}...{% endif %}') 1741 | -- @name tests.is_string 1742 | function M.tests.is_string(value) return type(value) == 'string' end 1743 | 1744 | --- 1745 | -- Returns whether or not value *value* is a table. 1746 | -- @param value The value to test. 1747 | -- @usage expand('{% if is_mapping(x) %}...{% endif %}') 1748 | -- @name tests.is_mapping 1749 | function M.tests.is_mapping(value) return type(value) == 'table' end 1750 | 1751 | --- 1752 | -- Returns whether or not value *value* is a table. 1753 | -- @param value The value to test. 1754 | -- @usage expand('{% if is_table(x) %}...{% endif %}') 1755 | -- @name tests.is_table 1756 | function M.tests.is_table(value) return type(value) == 'table' end 1757 | 1758 | --- 1759 | -- Returns whether or not value *value* is a number. 1760 | -- @param value The value to test. 1761 | -- @usage expand('{% if is_number(x) %}...{% endif %}') 1762 | -- @name tests.is_number 1763 | function M.tests.is_number(value) return type(value) == 'number' end 1764 | 1765 | --- 1766 | -- Returns whether or not value *value* is a sequence, namely a table with 1767 | -- non-zero length. 1768 | -- @param value The value to test. 1769 | -- @usage expand('{% if is_sequence(x) %}...{% endif %}') 1770 | -- @name tests.is_sequence 1771 | function M.tests.is_sequence(value) 1772 | return type(value) == 'table' and #value > 0 1773 | end 1774 | 1775 | --- 1776 | -- Returns whether or not value *value* is a sequence (a table with non-zero 1777 | -- length) or a generator. 1778 | -- At the moment, all functions are considered generators. 1779 | -- @param value The value to test. 1780 | -- @usage expand('{% if is_iterable(x) %}...{% endif %}') 1781 | -- @name tests.is_iterable 1782 | function M.tests.is_iterable(value) 1783 | return M.tests.is_sequence(value) or type(value) == 'function' 1784 | end 1785 | 1786 | --- 1787 | -- Returns whether or not value *value* is a function. 1788 | -- @param value The value to test. 1789 | -- @usage expand('{% if is_callable(x) %}...{% endif %}') 1790 | -- @name tests.is_callable 1791 | function M.tests.is_callable(value) return type(value) == 'function' end 1792 | 1793 | --- 1794 | -- Returns whether or not value *value* is the same as value *other*. 1795 | -- @param value The value to test. 1796 | -- @param other The value to compare with. 1797 | -- @usage expand('{% if is_sameas(x, y) %}...{% endif %}') 1798 | -- @name tests.is_sameas 1799 | function M.tests.is_sameas(value, other) return value == other end 1800 | 1801 | --- 1802 | -- Returns whether or not value *value* is HTML-safe. 1803 | -- @param value The value to test. 1804 | -- @usage expand('{% if is_escaped(x) %}...{% endif %}') 1805 | -- @name tests.is_escaped 1806 | function M.tests.is_escaped(value) 1807 | return getmetatable(value) and getmetatable(value).__tostring ~= nil 1808 | end 1809 | 1810 | return M 1811 | --------------------------------------------------------------------------------