├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── ci
├── install.sh
├── setup.sh
└── test.sh
├── example.lua
├── liluat-1.2.0-1.rockspec
├── liluat-scm-2.rockspec
├── liluat.lua
├── runliluat.lua
└── spec
├── basepath_tests
├── a.template
├── b.template
├── base_a.template
└── base_b.template
├── content.html.template
├── cycle_a.template
├── cycle_b.template
├── cycle_c.template
├── index.html.template
├── index.html.template.inlined
├── index.html.template.lua
├── jinja.template
├── jinja.template.lua
├── liluat_spec.lua
├── options
├── preload.lua
├── read_entire_file-test
├── runliluat_spec.lua
├── template
├── template-jinja
└── values
/.gitignore:
--------------------------------------------------------------------------------
1 | .*.swp
2 | *.rock
3 | luacov.stats.out
4 | luacov.report.out
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: c
2 | sudo: false
3 | os:
4 | - linux
5 | - osx
6 |
7 | env:
8 | - LUA='Lua5.1'
9 | - LUA='Lua5.2'
10 | - LUA='Lua5.3'
11 | - LUA='LuaJIT2.0'
12 | - LUA='LuaJIT2.1'
13 |
14 | before_install:
15 | - bash ci/setup.sh
16 |
17 | install:
18 | - bash ci/install.sh
19 |
20 | script: bash ci/test.sh
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
2 |
3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # liluat
2 |
3 | [](https://travis-ci.org/FSMaxB/liluat)
4 |
5 | Liluat is a lightweight Lua based template engine. While simple to use it's still powerfull because you can embed arbitrary Lua code in templates. It doesn't need external dependencies other than Lua itself.
6 |
7 | Liluat is a fork of version 1.0 of [slt2](https://github.com/henix/slt2) by henix. Although the core concept was taken from slt2, the code has been almost completely rewritten.
8 |
9 | ## Table of contents
10 | 1. [OS support](#os-support)
11 | 2. [Lua support](#lua-support)
12 | 3. [Installation](#installation)
13 | 4. [Basic Syntax](#basic-syntax)
14 | 5. [Example](#example)
15 | 6. [API](#api)
16 | 7. [Trimming](#trimming)
17 | 8. [Options](#options)
18 | 9. [Command line utility](#command-line-utility)
19 | 10. [Sandboxing](#sandboxing)
20 | 11. [Caveats](#caveats)
21 | 12. [License](#license)
22 | 13. [Contributing](#contributing)
23 |
24 | ## OS support
25 | Liluat is developed on GNU/Linux and automatically tested on GNU/Linux and Mac OS X. I have much confidence that it will also work on FreeBSD, other BSDs and on other POSIX compatible systems like e.g. Cygwin.
26 |
27 | Windows was not tested, but it might work with some limitations:
28 | * absolute paths in template includes won't be properly detected because they don't start with a `/`
29 | * `\` is not supported as path separator
30 | * template files with Windows style line endings (`"\r\n"`) aren't supported
31 | * the unit tests for the command line won't work because they rely on a POSIX shell being available
32 |
33 | ## Lua support
34 | Liluat is automatically tested on the following Lua implementations:
35 |
36 | * Lua 5.1
37 | * Lua 5.2
38 | * Lua 5.3
39 | * LuaJIT 2.0
40 | * LuaJIT 2.1 (beta)
41 |
42 | ## Installation
43 | Lua is available via [luarocks](https://luarocks.org), the following command installs it via luarocks:
44 | ```
45 | # luarocks install liluat
46 | ```
47 | You might need to add `--local` if you don't have admin (root) privileges.
48 |
49 | Alternatively just copy the file `liluat.lua` to your software (this won't install the command line interface though).
50 |
51 | ## Basic syntax
52 | There are three different types of template blocks:
53 |
54 | ### Code
55 | You can write arbitrary Lua code in the form:
56 | ```
57 | {{ some code }}
58 | ```
59 | This allows for writing simple loops and conditions or even more complex logic.
60 | ### Expressions
61 | You can write arbitrary Lua expression that can be converted into a string like this:
62 | ```
63 | {{= expression}}
64 | ```
65 | ### Includes
66 | You can include other template files like this:
67 | ```
68 | {{include: 'templates/file_name'}}
69 | ```
70 | By default the include path is either relative to the directory where the template that does the include is in or it is an absolute path starting with a `/`, e.g. `'/tmp/template-dfjCm'`. You can change this behavior using the `base_path` option, see [Options](#options).
71 |
72 | Liluat is able to detect cyclic inclusion in most cases (eg. not if you used symlinks to create a cycle in the filesystem).
73 |
74 | ### More
75 | There is more to the syntax of liluat, but that will be explained later on in the section [Trimming](#trimming).
76 |
77 | ## Example
78 | Some basic template in action.
79 |
80 | See `example.lua`:
81 | ```lua
82 | local liluat = require("liluat")
83 |
84 | local template = [[
85 |
86 |
87 |
88 |
89 | {{= title}}
90 |
91 |
92 | Vegetables
93 |
94 | {{ -- write regular lua code in the template}}
95 | {{for _,vegetable in ipairs(vegetables) do}}
96 | - {{= vegetable}}
97 | {{end}}
98 |
99 |
100 |
101 | ]]
102 |
103 | -- values to render the template with
104 | local values = {
105 | title = "A fine selection of vegetables.",
106 | vegetables = {
107 | "carrot",
108 | "cucumber",
109 | "broccoli",
110 | "tomato"
111 | }
112 | }
113 |
114 | -- compile the template into lua code
115 | local compiled_template = liluat.compile(template)
116 |
117 | local rendered_template = liluat.render(compiled_template, values)
118 |
119 | io.write(rendered_template)
120 | ```
121 |
122 | Output:
123 | ```html
124 |
125 |
126 |
127 |
128 | A fine selection of vegetables.
129 |
130 |
131 | Vegetables
132 |
133 | - carrot
134 | - cucumber
135 | - broccoli
136 | - tomato
137 |
138 |
139 |
140 | ```
141 |
142 | ## API
143 |
144 | ### liluat.compile(template, [options], [template\_name], [start\_path])
145 | Compile the template into Lua code that can later be rendered. Returns a compiled template.
146 | * `template`: The template to compile
147 | * `options`: A table containing different configuration options, see the [Options](#options) section.
148 | * `template_name`: A name to identify the template with. This is especially useful to be able to find out where a Lua syntax or runtime error is coming from.
149 | * `start_path`: Path to start in as a working directory. If the `base_path` option is not set, this is the path to which the first inclusion is relative to.
150 |
151 | ### liluat.compile\_file(filename, [options])
152 | Same as `liluat.compile` but loads the template from a file. `template_name` is set to the filename and `start_path` is set to the path where the file is in. Returns a compiled template.
153 | * `filename`: File to load the template from.
154 | * `options`: A table containing different configuration options, see the [Options](#options) section.
155 |
156 | ### liluat.render(compiled\_template, [values], [options])
157 | Render a compiled template into a string, using the given values. This runs the compiled template in a sandbox with `values` added to it's environment.
158 | * `compiled_template`: This is the output of `liluat.compile`. Essentially Lua code with some meta data.
159 | * `values`: A Lua table containing any kind of values. This can even be functions or custom data types from C. These values are accessible inside the template.
160 | * `options`: A table containing different configuration options, see the [Options](#options) section. NOTE: Most of those options only change the behavior of `liluat.compile`.
161 |
162 | ### liluat.render\_coroutine(compiled\_template, [values], [options])
163 | Same as `liluat.render` but returns a function that can be run in a coroutine and will return one chunk of data at a time (so you can kind of "stream" the template rendering).
164 |
165 | ### liluat.inline(template, [options], [start\_path])
166 | Load a template and return a template string where all the included templates have been inlined.
167 | * `template`: A template string to be inlined.
168 | * `options`: A table containing different configuration options, see the [Options](#options) section.
169 | * `start_path`: Path to start in as a working directory. If the `base_path` option is not set, this is the path to which the first inclusion is relative to.
170 |
171 | ### liluat.get\_dependencies(template, [options], [start\_path])
172 | Get a table containing all of the files that a template includes (also recursively).
173 | * `template`: The template to examine.
174 | * `options`: A table containing different configuration options, see the [Options] section.
175 | * `start_path`: Path to start in as a working directory. If the `base_path` option is not set, this is the path to which the first inclusion is relative to.
176 |
177 | ## Trimming
178 | An important feature not yet talked about is trimming. In order to be able to write templates that look nice and readable while still keeping the output nice, some kinds of whitespaces need to be trimmed in some cases.
179 |
180 | There are two kinds of trimming that liluat supports:
181 |
182 | ### Left trimming
183 | In case a line contains only whitespaces in front of a template block, those are removed when left trimming is enabled.
184 |
185 | ### Right trimming
186 | Right trimming, if enabled, removes newline characters directly following a template block.
187 |
188 | ### Settings
189 | The trimming can be globally enabled and disabled via the `trim_left` and `trim_right` options. Possible values are:
190 | * `"all"`: trim all template blocks
191 | * `"expression"`: trim only expression blocks
192 | * `"code"`: trim only code blocks, this is the default
193 | * `false`: disable trimming
194 |
195 | Include blocks are not trimmed.
196 |
197 | ### Local override
198 | You can locally override left and right trimming via `+` and `-`, where `+` means, no trimming, and `-` means trimming. For example, the block `{{+ code -}}` will be trimmed right, but not left, no matter what the global trimming settings are.
199 |
200 | ### Example
201 | In this example, `trim_left` and `trim_right` are set to `"code"`, which is the default.
202 |
203 | ```
204 | {{for i = 1, 4 do}}
205 | {{= i}}
206 | {{end}}
207 | {{for i = 5, 8 do}}
208 | {{-= i-}}
209 | {{end}}
210 | {{for i = 9, 12 do+}}
211 | {{-= i}}
212 | {{end}}
213 | ```
214 |
215 | Output:
216 | ```
217 | 1
218 | 2
219 | 3
220 | 4
221 | 5678
222 | 9
223 |
224 | 10
225 |
226 | 11
227 |
228 | 12
229 | ```
230 |
231 | ## Options
232 | The following options can be passed via the `options` table:
233 | * `start_tag`: Start tag to be used instead of `{{`
234 | * `end_tag`: End tag to be used instead of `}}`
235 | * `trim_right`: one of `"all"`, `"code"`, `"expression"` or `false` to disable. Default is `"code"`. See the section [Trimming](#trimming) for more information.
236 | * `trim_left`: one of `"all"`, `"code"`, `"expression"` or `false` to disable. Default is `"code"`. See the section [Trimming](#trimming) for more information.
237 | * `base_path`: Path that is used as base path for includes. If `nil` or `false`, all include paths are interpreted relative to the files path itself. Not that this doesn't influence absolute paths.
238 | * `reference`: If set to `true`, `liluat.render` will reference the environment in the sandbox instead of recursively copyiing it. This reduces part of the security of the sandbox, because values can now leak out of it. However, this option is useful if you pass in environments that use a lot of memory or contain reference cycles, see [Caveats/Environment is copied](#environment-is-copied).
239 |
240 | ## Command line utility
241 | Liluat comes with a command line interface:
242 |
243 | ```
244 | $ runliluat --help
245 | Usage: runliluat [options]
246 | Options:
247 | -h|--help
248 | Show this message.
249 | --values lua_table
250 | Table containing values to use in the template.
251 | --value-file file_name
252 | Use a file to define the table of values to use in the template.
253 | -t|--template-file file_name
254 | File that contains the template
255 | -n|--name template_name
256 | Name to use for the template
257 | -d|--dependencies
258 | List the dependencies of the template (list of included files)
259 | -i|--inline
260 | Inline all the included files into one template.
261 | -o|--output filename
262 | File to write the output to (defaults to stdout)
263 | --options lua_table
264 | A table of options for liluat
265 | --options-file file_name
266 | Read the options from a file.
267 | --stdin "template"
268 | Get the template from stdin.
269 | --stdin "values"
270 | Get the table of values from stdin.
271 | --stdin "options"
272 | Get the options from stdin.
273 | -v|--version
274 | Print the current version.
275 | --path path
276 | Root path of the templates.
277 | ```
278 |
279 | ## Sandboxing
280 | All the code in the templates is run in a sandbox. To achieve this, the code is run with its own global environment, Lua bytecode is forbidden and only a subset of Lua's standard library functions is allowed via a whitelist. If you require additional standard library functions, you have to pass them in manually via the `values` parameter.
281 |
282 | The whitelist currently contains the following:
283 | ```
284 | ipairs
285 | next
286 | pairs
287 | rawequal
288 | rawget
289 | rawset
290 | select
291 | tonumber
292 | tostring
293 | type
294 | unpack
295 | string
296 | table
297 | math
298 | os.date
299 | os.difftime
300 | os.time
301 | coroutine
302 | ```
303 |
304 | ## Caveats
305 | This section documents known issues that can arise in certain usage scenarios.
306 |
307 | ### Environment is copied
308 | Due to the sandboxing, the entire environment passed into `liluat.render` or `liluat.render_coroutine` is recursively copied. This can have the following consequences (and probably more):
309 |
310 | * High memory usage if the environment uses a large amount of memory. Because a copy is created, liluat needs the same amount once again for the copy. This can get even worse when you render multiple templates with a big environment, because Lua's incremental garbage collector might not be fast enough to clean it up right away.
311 | * Environments that contain reference cycles will trigger an infinite loop that results in a stack overflow.
312 | * All metatables are removed from the values in the sandbox. This also means that most object oriented modules will break if you add them to the environment.
313 |
314 | All those above issues can be fixed by setting the `reference` option to `true`, see [Options](#options). Note though that this will decrease the security of the sandbox, because changes to the environment that happen in the sandbox will leave the sandbox.
315 |
316 | ## License
317 | Liluat is free software licensed under the MIT license:
318 |
319 | > liluat - Lightweight Lua Template engine
320 | >
321 | > Project page: https://github.com/FSMaxB/liluat
322 | >
323 | > liluat is based on slt2 by henix, see https://github.com/henix/slt2
324 | >
325 | > Copyright © 2016 Max Bruckner
326 | > Copyright © 2011-2016 henix
327 | >
328 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
329 | >
330 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
331 | >
332 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
333 |
334 | ## Contributing
335 | If you find a bug or have a suggestion on what could be improved, write an issue on GitHub or write me an email.
336 |
337 | I will also gladly accept pull requests via GitHub or email if I think that it will benefit the library. Be sure to talk to me first to increase your success rate and prevent possible frustration/misunderstandings.
338 |
339 | ### Coding style
340 | * use tabs for indentation
341 | * don't leave trailing spaces
342 |
343 | Other than that: Take a look at what's already there and try to adapt.
344 |
345 | ### Unit tests
346 | Write unit tests for everything you do. I'm using the [busted](http://olivinelabs.com/busted/) unit testing framework. **Every commit** needs to pass the tests on every supported Lua implementation. Note that pull requests get automatically tested on Travis-CI.
347 |
--------------------------------------------------------------------------------
/ci/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export PATH="$HOME/prefix/bin/:$PATH"
3 |
4 | eval $(luarocks path --bin)
5 |
6 | luarocks install --local luasec
7 | luarocks install --local busted
8 |
--------------------------------------------------------------------------------
/ci/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # A script for setting up environment for travis-ci testing.
3 | # Sets up Lua and Luarocks.
4 | # LUA must be "Lua5.1", "Lua5.2", "Lua5.3", "LuaJIT2.0" or "LuaJIT2.1".
5 | #
6 | # This file is based on work by Olivine Labs, LLC.
7 | # See https://github.com/Olivine-Labs/busted/.travis_setup.sh
8 |
9 | set -e
10 |
11 | mkdir "$HOME/prefix"
12 | export PATH="$HOME/prefix/bin:$PATH"
13 |
14 | if [ "$LUA" == "LuaJIT2.0" ]; then
15 | wget -O - https://github.com/LuaJIT/LuaJIT/archive/v2.0.5.tar.gz | tar xz
16 | cd LuaJIT-2.0.5
17 | make && make install INSTALL_TSYMNAME=lua PREFIX="$HOME/prefix/"
18 | elif [ "$LUA" == "LuaJIT2.1" ]; then
19 | wget -O - https://github.com/LuaJIT/LuaJIT/archive/v2.1.0-beta3.tar.gz | tar xz
20 | cd LuaJIT-2.1.0-beta3
21 | make && make install INSTALL_TSYMNAME=lua PREFIX="$HOME/prefix/"
22 | ln -sf luajit-2.1.0-beta3 "$HOME/prefix/bin/lua"
23 | ln -sf luajit-2.1 "$HOME/prefix/include/lua5.1"
24 | else
25 | if [ "$LUA" == "Lua5.1" ]; then
26 | wget -O - http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz
27 | cd lua-5.1.5;
28 | elif [ "$LUA" == "Lua5.2" ]; then
29 | wget -O - http://www.lua.org/ftp/lua-5.2.4.tar.gz | tar xz
30 | cd lua-5.2.4;
31 | elif [ "$LUA" == "Lua5.3" ]; then
32 | wget -O - http://www.lua.org/ftp/lua-5.3.4.tar.gz | tar xz
33 | cd lua-5.3.4;
34 | fi
35 | if [[ "$OSTYPE" == "linux-gnu" ]]; then
36 | make linux
37 | elif [[ "$OSTYPE" == "darwin"* ]]; then
38 | make macosx
39 | elif [[ "$OSTYPE" == "freebsd"* ]]; then
40 | make freebsd
41 | else
42 | make generic
43 | fi
44 | make install INSTALL_TOP="$HOME/prefix"
45 | fi
46 |
47 | cd ..
48 | wget -O - http://luarocks.org/releases/luarocks-2.4.2.tar.gz | tar xz || wget -O - http://keplerproject.github.io/luarocks/releases/luarocks-2.4.2.tar.gz | tar xz
49 | cd luarocks-2.4.2
50 |
51 | if [ "$LUA" == "LuaJIT2.0" ]; then
52 | ./configure --with-lua-include="$HOME/prefix/include/luajit-2.0/" --prefix="$HOME/prefix/";
53 | else
54 | ./configure --with-lua-include="$HOME/prefix/include/" --prefix="$HOME/prefix/";
55 | fi
56 |
57 | make build && make install
58 | cd ..
59 |
--------------------------------------------------------------------------------
/ci/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export PATH="$HOME/prefix/bin/:$PATH"
3 |
4 | eval $(luarocks path --bin)
5 |
6 | busted
7 |
--------------------------------------------------------------------------------
/example.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 | local liluat = require("liluat")
3 |
4 | local template = [[
5 |
6 |
7 |
8 |
9 | {{= title}}
10 |
11 |
12 | Vegetables
13 |
14 | {{ -- write regular lua code in the template}}
15 | {{for _,vegetable in ipairs(vegetables) do}}
16 | - {{= vegetable}}
17 | {{end}}
18 |
19 |
20 |
21 | ]]
22 |
23 | -- values to render the template with
24 | local values = {
25 | title = "A fine selection of vegetables.",
26 | vegetables = {
27 | "carrot",
28 | "cucumber",
29 | "broccoli",
30 | "tomato"
31 | }
32 | }
33 |
34 | -- compile the template into lua code
35 | local compiled_template = liluat.compile(template)
36 |
37 | local rendered_template = liluat.render(compiled_template, values)
38 |
39 | io.write(rendered_template)
40 | --[[ output:
41 |
42 |
43 |
44 |
45 | A fine selection of vegetables.
46 |
47 |
48 | Vegetables
49 |
50 | - carrot
51 | - cucumber
52 | - broccoli
53 | - tomato
54 |
55 |
56 |
57 | --]]
58 |
--------------------------------------------------------------------------------
/liluat-1.2.0-1.rockspec:
--------------------------------------------------------------------------------
1 | package = "liluat"
2 | version = "1.2.0-1"
3 |
4 | source = {
5 | url = "git://github.com/FSMaxB/liluat",
6 | tag = "v1.2.0"
7 | }
8 |
9 | description = {
10 | summary = "Lightweight Lua based template engine.",
11 | detailed = "Liluat is a lightweight Lua based template engine. While simple to use it's still powerfull because you can embed arbitrary Lua code in templates. It doesn't need external dependencies other than Lua itself.",
12 | homepage = "https://github.com/FSMaxB/liluat",
13 | license = "MIT "
14 | }
15 |
16 | dependencies = {
17 | "lua >= 5.1"
18 | }
19 |
20 | build = {
21 | type = "builtin",
22 | modules = {
23 | liluat = "liluat.lua"
24 | },
25 | install = {
26 | bin = {
27 | runliluat = "runliluat.lua"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/liluat-scm-2.rockspec:
--------------------------------------------------------------------------------
1 | package = "liluat"
2 | version = "scm-2"
3 |
4 | source = {
5 | url = "git://github.com/FSMaxB/liluat",
6 | }
7 |
8 | description = {
9 | summary = "Lightweight Lua based template engine.",
10 | detailed = "Liluat is a lightweight Lua based template engine. While simple to use it's still powerfull because you can embed arbitrary Lua code in templates. It doesn't need external dependencies other than Lua itself.",
11 | homepage = "https://github.com/FSMaxB/liluat",
12 | license = "MIT "
13 | }
14 |
15 | dependencies = {
16 | "lua >= 5.1"
17 | }
18 |
19 | build = {
20 | type = "builtin",
21 | modules = {
22 | liluat = "liluat.lua"
23 | },
24 | install = {
25 | bin = {
26 | runliluat = "runliluat.lua"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/liluat.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | -- liluat - Lightweight Lua Template engine
3 | --
4 | -- Project page: https://github.com/FSMaxB/liluat
5 | --
6 | -- liluat is based on slt2 by henix, see https://github.com/henix/slt2
7 | --
8 | -- Copyright © 2016 Max Bruckner
9 | -- Copyright © 2011-2016 henix
10 | --
11 | -- Permission is hereby granted, free of charge, to any person obtaining a copy
12 | -- of this software and associated documentation files (the "Software"), to deal
13 | -- in the Software without restriction, including without limitation the rights
14 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 | -- copies of the Software, and to permit persons to whom the Software is furnished
16 | -- to do so, subject to the following conditions:
17 | --
18 | -- The above copyright notice and this permission notice shall be included in
19 | -- all copies or substantial portions of the Software.
20 | --
21 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 | -- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
26 | -- IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 | --]]
28 |
29 | local liluat = {
30 | private = {} --used to expose private functions for testing
31 | }
32 |
33 | -- print the current version
34 | liluat.version = function ()
35 | return "1.2.0"
36 | end
37 |
38 | -- returns a string containing the fist line until the last line
39 | local function string_lines(lines, first, last)
40 | -- allow negative line numbers
41 | first = (first >= 1) and first or 1
42 |
43 | local start_position
44 | local current_position = 1
45 | local line_counter = 1
46 | repeat
47 | if line_counter == first then
48 | start_position = current_position
49 | end
50 | current_position = lines:find('\n', current_position + 1, true)
51 | line_counter = line_counter + 1
52 | until (line_counter == (last + 1)) or (not current_position)
53 |
54 | return lines:sub(start_position, current_position)
55 | end
56 | liluat.private.string_lines = string_lines
57 |
58 | -- escape a string for use in lua patterns
59 | -- (this simply prepends all non alphanumeric characters with '%'
60 | local function escape_pattern(text)
61 | return text:gsub("([^%w])", "%%%1" --[[function (match) return "%"..match end--]])
62 | end
63 | liluat.private.escape_pattern = escape_pattern
64 |
65 | -- recursively copy a table
66 | local function clone_table(table)
67 | local clone = {}
68 |
69 | for key, value in pairs(table) do
70 | if type(value) == "table" then
71 | clone[key] = clone_table(value)
72 | else
73 | clone[key] = value
74 | end
75 | end
76 |
77 | return clone
78 | end
79 | liluat.private.clone_table = clone_table
80 |
81 | -- recursively merge two tables, the second one has precedence
82 | -- if 'shallow' is set, the second table isn't copied recursively,
83 | -- its content is only referenced instead
84 | local function merge_tables(a, b, shallow)
85 | a = a or {}
86 | b = b or {}
87 |
88 | local merged = clone_table(a)
89 |
90 | for key, value in pairs(b) do
91 | if (type(value) == "table") and (not shallow) then
92 | if a[key] then
93 | merged[key] = merge_tables(a[key], value)
94 | else
95 | merged[key] = clone_table(value)
96 | end
97 | else
98 | merged[key] = value
99 | end
100 | end
101 |
102 | return merged
103 | end
104 | liluat.private.merge_tables = merge_tables
105 |
106 | local default_options = {
107 | start_tag = "{{",
108 | end_tag = "}}",
109 | trim_right = "code",
110 | trim_left = "code"
111 | }
112 |
113 | -- initialise table of options (use the provided, default otherwise)
114 | local function initialise_options(options)
115 | return merge_tables(default_options, options)
116 | end
117 |
118 | -- creates an iterator that iterates over all chunks in the given template
119 | -- a chunk is either a template delimited by start_tag and end_tag or a normal text
120 | -- the iterator also returns the type of the chunk as second return value
121 | local function all_chunks(template, options)
122 | options = initialise_options(options)
123 |
124 | -- pattern to match a template chunk
125 | local template_pattern = escape_pattern(options.start_tag) .. "([+-]?)(.-)([+-]?)" .. escape_pattern(options.end_tag)
126 | local include_pattern = "^"..escape_pattern(options.start_tag) .. "[+-]?include:(.-)[+-]?" .. escape_pattern(options.end_tag)
127 | local expression_pattern = "^"..escape_pattern(options.start_tag) .. "[+-]?=(.-)[+-]?" .. escape_pattern(options.end_tag)
128 | local position = 1
129 |
130 | return function ()
131 | if not position then
132 | return nil
133 | end
134 |
135 | local template_start, template_end, trim_left, template_capture, trim_right = template:find(template_pattern, position)
136 |
137 | local chunk = {}
138 | if template_start == position then -- next chunk is a template chunk
139 | if trim_left == "+" then
140 | chunk.trim_left = false
141 | elseif trim_left == "-" then
142 | chunk.trim_left = true
143 | end
144 | if trim_right == "+" then
145 | chunk.trim_right = false
146 | elseif trim_right == "-" then
147 | chunk.trim_right = true
148 | end
149 |
150 | local include_start, include_end, include_capture = template:find(include_pattern, position)
151 | local expression_start, expression_end, expression_capture
152 | if not include_start then
153 | expression_start, expression_end, expression_capture = template:find(expression_pattern, position)
154 | end
155 |
156 | if include_start then
157 | chunk.type = "include"
158 | chunk.text = include_capture
159 | elseif expression_start then
160 | chunk.type = "expression"
161 | chunk.text = expression_capture
162 | else
163 | chunk.type = "code"
164 | chunk.text = template_capture
165 | end
166 |
167 | position = template_end + 1
168 | return chunk
169 | elseif template_start then -- next chunk is a text chunk
170 | chunk.type = "text"
171 | chunk.text = template:sub(position, template_start - 1)
172 | position = template_start
173 | return chunk
174 | else -- no template chunk found --> either text chunk until end of file or no chunk at all
175 | chunk.text = template:sub(position)
176 | chunk.type = "text"
177 | position = nil
178 | return (#chunk.text > 0) and chunk or nil
179 | end
180 | end
181 | end
182 | liluat.private.all_chunks = all_chunks
183 |
184 | local function read_entire_file(path)
185 | assert(path)
186 | local file = assert(io.open(path))
187 | local file_content = file:read('*a')
188 | file:close()
189 | return file_content
190 | end
191 | liluat.private.read_entire_file = read_entire_file
192 |
193 | -- a whitelist of allowed functions
194 | local sandbox_whitelist = {
195 | ipairs = ipairs,
196 | next = next,
197 | pairs = pairs,
198 | rawequal = rawequal,
199 | rawget = rawget,
200 | rawset = rawset,
201 | select = select,
202 | tonumber = tonumber,
203 | tostring = tostring,
204 | type = type,
205 | unpack = unpack,
206 | string = string,
207 | table = table,
208 | math = math,
209 | os = {
210 | date = os.date,
211 | difftime = os.difftime,
212 | time = os.time,
213 | },
214 | coroutine = coroutine
215 | }
216 |
217 | -- puts line numbers in front of a string and optionally highlights a single line
218 | local function prepend_line_numbers(lines, first, highlight)
219 | first = (first and (first >= 1)) and first or 1
220 | lines = lines:gsub("\n$", "") -- make sure the last line isn't empty
221 | lines = lines:gsub("^\n", "") -- make sure the first line isn't empty
222 |
223 | local current_line = first + 1
224 | return string.format("%3d: ", first) .. lines:gsub('\n', function ()
225 | local highlight_char = ' '
226 | if current_line == tonumber(highlight) then
227 | highlight_char = '> '
228 | end
229 |
230 | local replacement = string.format("\n%3d:%s", current_line, highlight_char)
231 | current_line = current_line + 1
232 |
233 | return replacement
234 | end)
235 | end
236 | liluat.private.prepend_line_numbers = prepend_line_numbers
237 |
238 | -- creates a function in a sandbox from a given code,
239 | -- name of the execution context and an environment
240 | -- that will be available inside the sandbox,
241 | -- optionally overwrite the whitelist
242 | local function sandbox(code, name, environment, whitelist, reference)
243 | whitelist = whitelist or sandbox_whitelist
244 | name = name or 'unknown'
245 |
246 | -- prepare the environment
247 | environment = merge_tables(whitelist, environment, reference)
248 |
249 | local func
250 | local error_message
251 | if setfenv then --Lua 5.1 and compatible
252 | if code:byte(1) == 27 then
253 | error("Lua bytecode not permitted.", 2)
254 | end
255 | func, error_message = loadstring(code)
256 | if func then
257 | setfenv(func, environment)
258 | end
259 | else -- Lua 5.2 and later
260 | func, error_message = load(code, name, 't', environment)
261 | end
262 |
263 | -- handle compile error and print pretty error message
264 | if not func then
265 | local line_number, message = error_message:match(":(%d+):(.*)")
266 | -- lines before and after the error
267 | local lines = string_lines(code, line_number - 3, line_number + 3)
268 | error(
269 | 'Syntax error in sandboxed code "' .. name .. '" in line ' .. line_number .. ':\n'
270 | .. message .. '\n\n'
271 | .. prepend_line_numbers(lines, line_number - 3, line_number),
272 | 3
273 | )
274 | end
275 |
276 | return func
277 | end
278 | liluat.private.sandbox = sandbox
279 |
280 | local function parse_string_literal(string_literal)
281 | return sandbox('return' .. string_literal, nil, nil, {})()
282 | end
283 | liluat.private.parse_string_literal = parse_string_literal
284 |
285 | -- add an include to the include_list and throw an error if
286 | -- an inclusion cycle is detected
287 | local function add_include_and_detect_cycles(include_list, path)
288 | local parent = include_list[0]
289 | while parent do -- while the root hasn't been reached
290 | if parent[path] then
291 | error("Cyclic inclusion detected")
292 | end
293 |
294 | parent = parent[0]
295 | end
296 |
297 | include_list[path] = {
298 | [0] = include_list
299 | }
300 | end
301 | liluat.private.add_include_and_detect_cycles = add_include_and_detect_cycles
302 |
303 | -- extract the name of a directory from a path
304 | local function dirname(path)
305 | return path:match("^(.*/).-$") or ""
306 | end
307 | liluat.private.dirname = dirname
308 |
309 | -- splits a template into chunks
310 | -- chunks are either a template delimited by start_tag and end_tag
311 | -- or a text chunk (everything else)
312 | -- @return table
313 | local function parse(template, options, output, include_list, current_path)
314 | options = initialise_options(options)
315 | current_path = current_path or "." -- current include path
316 |
317 | include_list = include_list or {} -- a list of files that were included
318 | local output = output or {}
319 |
320 | for chunk in all_chunks(template, options) do
321 | -- handle includes
322 | if chunk.type == "include" then -- include chunk
323 | local include_path_literal = chunk.text
324 | local path = parse_string_literal(include_path_literal)
325 |
326 | -- build complete path
327 | if path:find("^/") then
328 | --absolute path, don't modify
329 | elseif options.base_path then
330 | path = options.base_path .. "/" .. path
331 | else
332 | path = dirname(current_path) .. path
333 | end
334 |
335 | add_include_and_detect_cycles(include_list, path)
336 |
337 | local included_template = read_entire_file(path)
338 | parse(included_template, options, output, include_list[path], path)
339 | elseif (chunk.type == "text") and output[#output] and (output[#output].type == "text") then
340 | -- ensure that no two text chunks follow each other
341 | output[#output].text = output[#output].text .. chunk.text
342 | else -- other chunk
343 | table.insert(output, chunk)
344 | end
345 |
346 | end
347 |
348 | return output
349 | end
350 | liluat.private.parse = parse
351 |
352 | -- inline included template files
353 | -- @return string
354 | function liluat.inline(template, options, start_path)
355 | options = initialise_options(options)
356 |
357 | local output = {}
358 | for _,chunk in ipairs(parse(template, options, nil, nil, start_path)) do
359 | if chunk.type == "expression" then
360 | table.insert(output, options.start_tag .. "=" .. chunk.text .. options.end_tag)
361 | elseif chunk.type == "code" then
362 | table.insert(output, options.start_tag .. chunk.text .. options.end_tag)
363 | else
364 | table.insert(output, chunk.text)
365 | end
366 | end
367 |
368 | return table.concat(output)
369 | end
370 |
371 | -- @return { string }
372 | function liluat.get_dependencies(template, options, start_path)
373 | options = initialise_options(options)
374 |
375 | local include_list = {}
376 | parse(template, options, nil, include_list, start_path)
377 |
378 | local dependencies = {}
379 | local have_seen = {} -- list of includes that were already added
380 | local function recursive_traversal(list)
381 | for key, value in pairs(list) do
382 | if (type(key) == "string") and (not have_seen[key]) then
383 | have_seen[key] = true
384 | table.insert(dependencies, key)
385 | recursive_traversal(value)
386 | end
387 | end
388 | end
389 |
390 | recursive_traversal(include_list)
391 | return dependencies
392 | end
393 |
394 | -- compile a template into lua code
395 | -- @return { name = string, code = string / function}
396 | function liluat.compile(template, options, template_name, start_path)
397 | options = initialise_options(options)
398 | template_name = template_name or 'liluat.compile'
399 |
400 | local output_function = "__liluat_output_function"
401 |
402 | -- split the template string into chunks
403 | local lexed_template = parse(template, options, nil, nil, start_path)
404 |
405 | -- table of code fragments the template is compiled into
406 | local lua_code = {}
407 |
408 | for i, chunk in ipairs(lexed_template) do
409 | -- check if the chunk is a template (either code or expression)
410 | if chunk.type == "expression" then
411 | table.insert(lua_code, output_function..'('..chunk.text..')')
412 | elseif chunk.type == "code" then
413 | table.insert(lua_code, chunk.text)
414 | else --text chunk
415 | -- determine if this block needs to be trimmed right
416 | -- (strip newline)
417 | local trim_right = false
418 | if lexed_template[i - 1] and (lexed_template[i - 1].trim_right == true) then
419 | trim_right = true
420 | elseif lexed_template[i - 1] and (lexed_template[i - 1].trim_right == false) then
421 | trim_right = false
422 | elseif options.trim_right == "all" then
423 | trim_right = true
424 | elseif options.trim_right == "code" then
425 | trim_right = lexed_template[i - 1] and (lexed_template[i - 1].type == "code")
426 | elseif options.trim_right == "expression" then
427 | trim_right = lexed_template[i - 1] and (lexed_template[i - 1].type == "expression")
428 | end
429 |
430 | -- determine if this block needs to be trimmed left
431 | -- (strip whitespaces in front)
432 | local trim_left = false
433 | if lexed_template[i + 1] and (lexed_template[i + 1].trim_left == true) then
434 | trim_left = true
435 | elseif lexed_template[i + 1] and (lexed_template[i + 1].trim_left == false) then
436 | trim_left = false
437 | elseif options.trim_left == "all" then
438 | trim_left = true
439 | elseif options.trim_left == "code" then
440 | trim_left = lexed_template[i + 1] and (lexed_template[i + 1].type == "code")
441 | elseif options.trim_left == "expression" then
442 | trim_left = lexed_template[i + 1] and (lexed_template[i + 1].type == "expression")
443 | end
444 |
445 | if trim_right and trim_left then
446 | -- both at once
447 | if i == 1 then
448 | if chunk.text:find("^.*\n") then
449 | chunk.text = chunk.text:match("^(.*\n)%s-$")
450 | elseif chunk.text:find("^%s-$") then
451 | chunk.text = ""
452 | end
453 | elseif chunk.text:find("^\n") then --have to trim a newline
454 | if chunk.text:find("^\n.*\n") then --at least two newlines
455 | chunk.text = chunk.text:match("^\n(.*\n)%s-$") or chunk.text:match("^\n(.*)$")
456 | elseif chunk.text:find("^\n%s-$") then
457 | chunk.text = ""
458 | else
459 | chunk.text = chunk.text:gsub("^\n", "")
460 | end
461 | else
462 | chunk.text = chunk.text:match("^(.*\n)%s-$") or chunk.text
463 | end
464 | elseif trim_left then
465 | if i == 1 and chunk.text:find("^%s-$") then
466 | chunk.text = ""
467 | else
468 | chunk.text = chunk.text:match("^(.*\n)%s-$") or chunk.text
469 | end
470 | elseif trim_right then
471 | chunk.text = chunk.text:gsub("^\n", "")
472 | end
473 | if not (chunk.text == "") then
474 | table.insert(lua_code, output_function..'('..string.format("%q", chunk.text)..')')
475 | end
476 | end
477 | end
478 |
479 | return {
480 | name = template_name,
481 | code = table.concat(lua_code, '\n')
482 | }
483 | end
484 |
485 | -- compile a file
486 | -- @return { name = string, code = string / function }
487 | function liluat.compile_file(filename, options)
488 | return liluat.compile(read_entire_file(filename), options, filename, filename)
489 | end
490 |
491 | -- @return a coroutine function
492 | function liluat.render_coroutine(template, environment, options)
493 | options = initialise_options(options)
494 | environment = merge_tables({__liluat_output_function = coroutine.yield}, environment, options.reference)
495 |
496 | return sandbox(template.code, template.name, environment, nil, options.reference)
497 | end
498 |
499 | -- @return string
500 | function liluat.render(t, env, options)
501 | options = initialise_options(options)
502 |
503 | local result = {}
504 |
505 | -- add closure that renders the text into the result table
506 | env = merge_tables({
507 | __liluat_output_function = function (text)
508 | table.insert(result, text) end
509 | },
510 | env,
511 | options.reference
512 | )
513 |
514 | -- compile and run the lua code
515 | local render_function = sandbox(t.code, t.name, env, nil, options.reference)
516 | local status, error_message = pcall(render_function)
517 | if not status then
518 | local line_number, message = error_message:match(":(%d+):(.*)")
519 | -- lines before and after the error
520 | local lines = string_lines(t.code, line_number - 3, line_number + 3)
521 | error(
522 | 'Runtime error in sandboxed code "' .. t.name .. '" in line ' .. line_number .. ':\n'
523 | .. message .. '\n\n'
524 | .. prepend_line_numbers(lines, line_number - 3, line_number),
525 | 2
526 | )
527 | end
528 |
529 | return table.concat(result)
530 | end
531 |
532 | return liluat
533 |
--------------------------------------------------------------------------------
/runliluat.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 |
3 | --[[
4 | -- runliluat - Use liluat from the command line
5 | --
6 | -- Project page: https://github.com/FSMaxB/liluat
7 | --
8 | -- liluat is based on slt2 by henix, see https://github.com/henix/slt2
9 | --
10 | -- Copyright © 2016 Max Bruckner
11 | -- Copyright © 2011-2016 henix
12 | --
13 | -- Permission is hereby granted, free of charge, to any person obtaining a copy
14 | -- of this software and associated documentation files (the "Software"), to deal
15 | -- in the Software without restriction, including without limitation the rights
16 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17 | -- copies of the Software, and to permit persons to whom the Software is furnished
18 | -- to do so, subject to the following conditions:
19 | --
20 | -- The above copyright notice and this permission notice shall be included in
21 | -- all copies or substantial portions of the Software.
22 | --
23 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 | -- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
28 | -- IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 | --]]
30 |
31 | local liluat = require('liluat')
32 |
33 | local function print_usage()
34 | print([[
35 | Usage: runliluat [options]
36 | Options:
37 | -h|--help
38 | Show this message.
39 | --values lua_table
40 | Table containing values to use in the template.
41 | --value-file file_name
42 | Use a file to define the table of values to use in the template.
43 | -t|--template-file file_name
44 | File that contains the template
45 | -n|--name template_name
46 | Name to use for the template
47 | -d|--dependencies
48 | List the dependencies of the template (list of included files)
49 | -i|--inline
50 | Inline all the included files into one template.
51 | -o|--output filename
52 | File to write the output to (defaults to stdout)
53 | --options lua_table
54 | A table of options for liluat
55 | --options-file file_name
56 | Read the options from a file.
57 | --stdin "template"
58 | Get the template from stdin.
59 | --stdin "values"
60 | Get the table of values from stdin.
61 | --stdin "options"
62 | Get the options from stdin.
63 | -v|--version
64 | Print the current version.
65 | --path path
66 | Root path of the templates.
67 | ]])
68 | end
69 |
70 | local function print_error(error_message, fallback_message)
71 | if error_message then
72 | io.stderr:write("ERROR: "..error_message.."\n")
73 | elseif fallback_message then
74 | io.stderr:write("ERROR: "..fallback_message.."\n")
75 | else
76 | io.stderr:write("ERROR: An unknown error happened.\n")
77 | end
78 | end
79 |
80 | if #arg == 0 then
81 | print_error("No parameter given.")
82 | print_usage()
83 | os.exit(1)
84 | end
85 |
86 |
87 | local value_string
88 | local options_string
89 | local template
90 | local list_dependencies = false
91 | local inline = false
92 | local print_version = false
93 | local output_file
94 | local path
95 | local template_path
96 | local template_name
97 | -- go through all the command line parameters
98 | repeat
99 | if (arg[1] == "-h") or (arg[1] == "--help") then
100 | print_usage()
101 | os.exit(0)
102 | elseif arg[1] == "--values" then
103 | value_string = arg[2]
104 | table.remove(arg, 2)
105 | elseif arg[1] == "--value-file" then
106 | local success
107 | success, value_string = pcall(function () return liluat.private.read_entire_file(arg[2]) end)
108 | if not success then
109 | print_error(value_string, "Failed to read value file "..string.format("%q", arg[2])..".")
110 | os.exit(1)
111 | end
112 | table.remove(arg, 2)
113 | elseif (arg[1] == "-n") or (arg[1] == "--name") then
114 | template_name = arg[2]
115 | table.remove(arg, 2)
116 | elseif (arg[1] == "-t") or (arg[1] == "--template-file") then
117 | template_path = arg[2]
118 | table.remove(arg, 2)
119 | elseif (arg[1] == "-d") or (arg[1] == "--dependencies") then
120 | list_dependencies = true
121 | elseif (arg[1] == "-i") or (arg[1] == "--inline") then
122 | inline = true
123 | elseif (arg[1] == "-o") or (arg[1] == "--output") then
124 | output_file = arg[2]
125 | table.remove(arg, 2)
126 | elseif arg[1] == "--options" then
127 | options_string = arg[2]
128 | table.remove(arg, 2)
129 | elseif arg[1] == "--options-file" then
130 | local success
131 | success, options_string = pcall(function () return liluat.private.read_entire_file(arg[2]) end)
132 | if not success then
133 | print_error(options, "Failed to read options file "..string.format("%q", arg[2])..".")
134 | os.exit(1)
135 | end
136 | table.remove(arg, 2)
137 | elseif arg[1] == "--stdin" then
138 | local success
139 | if arg[2] == "template" then
140 | template = io.stdin:read("*all")
141 | elseif arg[2] == "values" then
142 | value_string = io.stdin:read("*all")
143 | elseif arg[2] == "options" then
144 | options_string = io.stdin:read("*all")
145 | else
146 | print_error('Invalid paramter for "--stdin".')
147 | os.exit(1)
148 | end
149 | table.remove(arg, 2)
150 | elseif (arg[1] == "-v") or (arg[1] == "--version") then
151 | print_version = true
152 | elseif arg[1] == "--path" then
153 | path = arg[2]
154 | table.remove(arg, 2)
155 | else
156 | print_error("Invalid parameter "..string.format("%q", arg[1])..".")
157 | print_usage()
158 | os.exit(1)
159 | end
160 |
161 | table.remove(arg, 1)
162 | until #arg == 0
163 |
164 | --open the output file, if specified
165 | local file, error_message
166 | if output_file then
167 | file = io.open(output_file, "w+")
168 | if not file then
169 | print_error("Failed to open output file "..string.format("%q", output_file)..".")
170 | end
171 | end
172 |
173 | --write to stdout or the output file
174 | local function write_out(text)
175 | if file then
176 | file:write(text)
177 | else
178 | io.write(text)
179 | end
180 | end
181 |
182 | --check if flags are compatible
183 | if list_dependencies and inline and print_version then
184 | print_error("Can't print_version, determine dependencies and inline a template at the same time.")
185 | os.exit(1)
186 | end
187 |
188 | if list_dependencies and inline then
189 | print_error("Can't both determine dependencies and inline a template.")
190 | os.exit(1)
191 | end
192 |
193 | if list_dependencies and print_version then
194 | print_error("Can't both determine dependencies and print the version.")
195 | os.exit(1)
196 | end
197 |
198 | if print_version and inline then
199 | print_error("Can't both print the version and inline a template.")
200 | os.exit(1)
201 | end
202 |
203 | if template_path and template then
204 | print_error("Can't both load a template from stdin and a file.")
205 | os.exit(1)
206 | end
207 | -----
208 |
209 | if print_version then
210 | write_out(liluat.version().."\n")
211 | os.exit(0)
212 | end
213 |
214 | if (not template) and (not template_path) then
215 | print_error("No template specified.")
216 | os.exit(1)
217 | end
218 |
219 | local options = {}
220 | if options_string then
221 | options = liluat.private.sandbox("return "..options_string, "options")()
222 | end
223 |
224 | -- template to be loaded from a file
225 | if template_path and (list_dependencies or inline) then
226 | local success
227 | success, template = pcall(function () return liluat.private.read_entire_file(template_path) end)
228 | if not success then
229 | print_error(template, "Failed to read template file "..string.format("%q", template_path)..".")
230 | os.exit(1)
231 | end
232 | end
233 |
234 | if list_dependencies then
235 | local dependencies = liluat.get_dependencies(template, options, path or template_path)
236 | write_out(table.concat(dependencies, "\n").."\n")
237 | os.exit(0)
238 | end
239 |
240 | if inline then
241 | write_out(liluat.inline(template, options, path or template_path))
242 | os.exit(0)
243 | end
244 |
245 | local values = {}
246 | if value_string then
247 | values = liluat.private.sandbox("return "..value_string, "values")()
248 | end
249 |
250 | -- now process the template
251 | if template_path then
252 | write_out(liluat.render(liluat.compile_file(template_path, options), values))
253 | else
254 | write_out(liluat.render(liluat.compile(template, options, template_name, path), values))
255 | end
256 | os.exit(0)
257 |
--------------------------------------------------------------------------------
/spec/basepath_tests/a.template:
--------------------------------------------------------------------------------
1 | {{include: "../content.html.template"}}
2 |
--------------------------------------------------------------------------------
/spec/basepath_tests/b.template:
--------------------------------------------------------------------------------
1 | {{include: "content.html.template"}}
2 |
--------------------------------------------------------------------------------
/spec/basepath_tests/base_a.template:
--------------------------------------------------------------------------------
1 | {{include: "a.template"}}
2 |
--------------------------------------------------------------------------------
/spec/basepath_tests/base_b.template:
--------------------------------------------------------------------------------
1 | {{include: "basepath_tests/b.template"}}
2 |
--------------------------------------------------------------------------------
/spec/content.html.template:
--------------------------------------------------------------------------------
1 | This is the index page.
2 |
--------------------------------------------------------------------------------
/spec/cycle_a.template:
--------------------------------------------------------------------------------
1 | {{include: "cycle_b.template"}}
2 |
--------------------------------------------------------------------------------
/spec/cycle_b.template:
--------------------------------------------------------------------------------
1 | {{include: "cycle_c.template"}}
2 |
--------------------------------------------------------------------------------
/spec/cycle_c.template:
--------------------------------------------------------------------------------
1 | {{include: "cycle_a.template"}}
2 |
--------------------------------------------------------------------------------
/spec/index.html.template:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{= title}}
6 |
7 |
8 | {{include: "content.html.template"}}
9 |
10 | {{for i = 1, 5 do}}
11 | - {{= i}}
12 | {{end}}
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/spec/index.html.template.inlined:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{= title}}
6 |
7 |
8 | This is the index page.
9 |
10 |
11 | {{for i = 1, 5 do}}
12 | - {{= i}}
13 | {{end}}
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/spec/index.html.template.lua:
--------------------------------------------------------------------------------
1 | return {
2 | code = [[
3 | __liluat_output_function("\
4 | \
5 | \
6 | \
7 | ")
8 | __liluat_output_function( title)
9 | __liluat_output_function("\
10 | \
11 | \
12 | This is the index page.
\
13 | \
14 | \
15 | ")
16 | for i = 1, 5 do
17 | __liluat_output_function(" - ")
18 | __liluat_output_function( i)
19 | __liluat_output_function("
\
20 | ")
21 | end
22 | __liluat_output_function("
\
23 | \
24 | \
25 | ")]],
26 | name = "spec/index.html.template"
27 | }
28 |
--------------------------------------------------------------------------------
/spec/jinja.template:
--------------------------------------------------------------------------------
1 | {% i = 1 %}
2 |
--------------------------------------------------------------------------------
/spec/jinja.template.lua:
--------------------------------------------------------------------------------
1 | return {
2 | name = "spec/jinja.template",
3 | code = [[
4 | i = 1 ]]
5 | }
6 |
--------------------------------------------------------------------------------
/spec/liluat_spec.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | -- Tests for liluat using the "busted" unit testing framework.
3 | --
4 | -- Copyright © 2016 Max Bruckner
5 | --
6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy
7 | -- of this software and associated documentation files (the "Software"), to deal
8 | -- in the Software without restriction, including without limitation the rights
9 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | -- copies of the Software, and to permit persons to whom the Software is furnished
11 | -- to do so, subject to the following conditions:
12 | --
13 | -- The above copyright notice and this permission notice shall be included in
14 | -- all copies or substantial portions of the Software.
15 | --
16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | -- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
21 | -- IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | --]]
23 |
24 | -- preload to make sure that 'require' loads the local liluat and not the globally installed one
25 | package.loaded["liluat"] = loadfile("liluat.lua")()
26 | local liluat = require("liluat")
27 |
28 | describe("liluat", function ()
29 | it("should return an empty string for empty templates", function ()
30 | assert.equal("", liluat.render(liluat.compile(""), {}))
31 | end)
32 |
33 | it("should render some example template", function ()
34 | local tmpl = liluat.compile([[
35 | {{ if user ~= nil then }}
36 | Hello, {{= escapeHTML(user.name) }}!
37 | {{ else }}
38 | login
39 | {{ end }}
40 |
41 | ]])
42 |
43 | local expected_output = [[
44 | Hello, <world>!
45 |
46 | ]]
47 |
48 | local function escapeHTML(str)
49 | local tt = {
50 | ['&'] = '&',
51 | ['<'] = '<',
52 | ['>'] = '>',
53 | ['"'] = '"',
54 | ["'"] = ''',
55 | }
56 | local r = str:gsub('[&<>"\']', tt)
57 | return r
58 | end
59 |
60 | assert.equal(expected_output, liluat.render(tmpl, {user = {name = ""}, escapeHTML = escapeHTML }))
61 | end)
62 |
63 | describe("string_lines", function ()
64 | it("should return ranges of lines", function ()
65 | local lines = "1\n2\n3\n4\n5\n6\n7"
66 |
67 | assert.equal("\n2\n3\n4\n", liluat.private.string_lines(lines, 2, 4))
68 | end)
69 |
70 | it("should return ranges of lines until end of string", function ()
71 | local lines = "1\n2\n3\n4\n5\n6\n7"
72 |
73 | assert.equal("\n5\n6\n7", liluat.private.string_lines(lines, 5, 7))
74 | end)
75 |
76 | it("should work with to large line numbers", function ()
77 | local lines = "1\n2\n3\n4\n5\n6\n7"
78 |
79 | assert.equal("\n6\n7", liluat.private.string_lines(lines, 6, 10))
80 | end)
81 |
82 | it("should work with negative line numbers", function ()
83 | local lines = "1\n2\n3\n4\n5\n6\n7"
84 |
85 | assert.equal("1\n2\n", liluat.private.string_lines(lines, -1, 2))
86 | end)
87 | end)
88 |
89 | describe("prepend_line_numbers", function()
90 | it("should prepend line numbers", function()
91 | local lines = [[
92 | --2
93 | --3
94 | --4
95 | --5
96 | ]]
97 | local expected = [[
98 | 2: --2
99 | 3: --3
100 | 4: --4
101 | 5: --5]]
102 |
103 | assert.equal(expected, liluat.private.prepend_line_numbers(lines, 2))
104 | end)
105 |
106 | it("should prepend line numbers with empty line in front", function()
107 | local lines = [[
108 |
109 | --2
110 | --3
111 | --4
112 | --5
113 | ]]
114 | local expected = [[
115 | 2: --2
116 | 3: --3
117 | 4: --4
118 | 5: --5]]
119 |
120 | assert.equal(expected, liluat.private.prepend_line_numbers(lines, 2))
121 | end)
122 |
123 | it("should prepend line numbers with empty line after it", function()
124 | local lines = [[
125 | --2
126 | --3
127 | --4
128 | --5
129 | ]]
130 | local expected = [[
131 | 2: --2
132 | 3: --3
133 | 4: --4
134 | 5: --5]]
135 |
136 | assert.equal(expected, liluat.private.prepend_line_numbers(lines, 2))
137 | end)
138 |
139 | it("should prepend line numbers and highlight a line", function()
140 | local lines = [[
141 | --2
142 | --3
143 | --4
144 | --5
145 | ]]
146 | local expected = [[
147 | 2: --2
148 | 3:> --3
149 | 4: --4
150 | 5: --5]]
151 |
152 | assert.equal(expected, liluat.private.prepend_line_numbers(lines, 2, 3))
153 | end)
154 |
155 | it("should prepend line numbers and start with 1", function()
156 | local lines = [[
157 | --1
158 | --2
159 | --3
160 | --4
161 | ]]
162 | local expected = [[
163 | 1: --1
164 | 2: --2
165 | 3: --3
166 | 4: --4]]
167 |
168 | assert.equal(expected, liluat.private.prepend_line_numbers(lines))
169 | end)
170 | end)
171 |
172 | describe("clone_table", function ()
173 | it("should clone a table", function ()
174 | local table = {
175 | a = {
176 | b = 1,
177 | c = {
178 | d = 2
179 | }
180 | },
181 | e = 3
182 | }
183 |
184 | local clone = liluat.private.clone_table(table)
185 |
186 | assert.same(table, clone)
187 | assert.not_equal(table, clone)
188 | assert.not_equal(table.a, clone.a)
189 | assert.not_equal(table.a.c, clone.a.c)
190 | end)
191 | end)
192 |
193 | describe("merge_tables", function ()
194 | it("should merge two tables", function ()
195 | local a = {
196 | a = 1,
197 | b = 2,
198 | c = {
199 | d = 3,
200 | e = {
201 | f = 4
202 | }
203 | },
204 | g = {
205 | h = 5
206 | }
207 | }
208 |
209 | local b = {
210 | b = 3,
211 | x = 5,
212 | y = {
213 | z = 4
214 | },
215 | c = {
216 | j = 5
217 | }
218 | }
219 |
220 | local expected_output = {
221 | a = 1,
222 | b = 3,
223 | c = {
224 | d = 3,
225 | e = {
226 | f = 4
227 | },
228 | j = 5
229 | },
230 | g = {
231 | h = 5
232 | },
233 | x = 5,
234 | y = {
235 | z = 4
236 | }
237 | }
238 |
239 | assert.same(expected_output, liluat.private.merge_tables(a, b))
240 | end)
241 |
242 | it("should merge the second table as reference, if 'reference' parameter is set", function ()
243 | local a = {
244 | a = 1,
245 | b = 2,
246 | c = {
247 | d = 3,
248 | e = {
249 | f = 4
250 | }
251 | },
252 | g = {
253 | h = 5
254 | }
255 | }
256 |
257 | local b = {
258 | b = 3,
259 | x = 5,
260 | y = {
261 | z = 4
262 | },
263 | c = {
264 | j = 5
265 | }
266 | }
267 |
268 | local expected_output = {
269 | a = 1,
270 | b = 3,
271 | c = {
272 | j = 5
273 | },
274 | g = {
275 | h = 5
276 | },
277 | x = 5,
278 | y = {
279 | z = 4
280 | }
281 | }
282 |
283 | local merged_table = liluat.private.merge_tables(a, b, true)
284 |
285 | assert.same(expected_output, merged_table)
286 |
287 | -- make sure it is actually referenced
288 | assert.equal(b.c, merged_table.c)
289 | end)
290 |
291 | it("should merge nil tables", function ()
292 | local a = {
293 | a = 1
294 | }
295 |
296 | assert.same({a = 1}, liluat.private.merge_tables(nil, a))
297 | assert.same({a = 1}, liluat.private.merge_tables(a, nil))
298 | assert.same({}, liluat.private.merge_tables(nil, nil))
299 | end)
300 | end)
301 |
302 | describe("escape_pattern", function ()
303 | it("should escape lua pattern special characters", function ()
304 | local input = ".%a%c%d%l%p%s%u%w%x%z().%%+-*?[]^$"
305 | local expected_output = "%.%%a%%c%%d%%l%%p%%s%%u%%w%%x%%z%(%)%.%%%%%+%-%*%?%[%]%^%$"
306 | local escaped_pattern = liluat.private.escape_pattern(input)
307 |
308 | assert.equals(expected_output, escaped_pattern)
309 | assert.truthy(input:find(escaped_pattern))
310 | end)
311 | end)
312 |
313 | describe("all_chunks", function ()
314 | it("should iterate over all chunks", function ()
315 | local template = [[
316 | {{= expression}} bla {{code}}
317 | {{other code}} some text
318 | {{more code}}{{}}
319 | {{include: "bla"}}
320 | some more text]]
321 | local result = {}
322 |
323 | for chunk in liluat.private.all_chunks(template) do
324 | table.insert(result, chunk)
325 | end
326 |
327 | local expected_output = {
328 | {
329 | text = " expression",
330 | type = "expression"
331 | },
332 | {
333 | text = " bla ",
334 | type = "text"
335 | },
336 | {
337 | text = "code",
338 | type = "code"
339 | },
340 | {
341 | text = "\n ",
342 | type = "text"
343 | },
344 | {
345 | text = "other code",
346 | type = "code"
347 | },
348 | {
349 | text = " some text\n",
350 | type = "text"
351 | },
352 | {
353 | text = "more code",
354 | type = "code"
355 | },
356 | {
357 | text = "",
358 | type = "code"
359 | },
360 | {
361 | text = "\n",
362 | type = "text"
363 | },
364 | {
365 | text = ' "bla"',
366 | type = "include"
367 | },
368 | {
369 | text = "\nsome more text",
370 | type = "text"
371 | }
372 | }
373 |
374 | assert.same(expected_output, result)
375 | end)
376 |
377 | it("should detect manual trim_left", function ()
378 | local template = "\t{{-code}}"
379 |
380 | local chunks = {}
381 | for chunk in liluat.private.all_chunks(template) do
382 | table.insert(chunks, chunk)
383 | end
384 |
385 | local expected_output = {
386 | {
387 | text = "\t",
388 | type = "text"
389 | },
390 | {
391 | text = "code",
392 | type = "code",
393 | trim_left = true
394 | }
395 | }
396 |
397 | assert.same(expected_output, chunks)
398 | end)
399 |
400 | it("should detect manually disabled trim left", function ()
401 | local template = "\t{{+code}}"
402 |
403 | local chunks = {}
404 | for chunk in liluat.private.all_chunks(template) do
405 | table.insert(chunks, chunk)
406 | end
407 |
408 | local expected_output = {
409 | {
410 | text = "\t",
411 | type = "text"
412 | },
413 | {
414 | text = "code",
415 | type = "code",
416 | trim_left = false
417 | }
418 | }
419 |
420 | assert.same(expected_output, chunks)
421 | end)
422 |
423 | it("should detect manual trim_right", function ()
424 | local template = "{{code-}}\n"
425 |
426 | local chunks = {}
427 | for chunk in liluat.private.all_chunks(template) do
428 | table.insert(chunks, chunk)
429 | end
430 |
431 | local expected_output = {
432 | {
433 | text = "code",
434 | type = "code",
435 | trim_right = true
436 | },
437 | {
438 | text = "\n",
439 | type = "text"
440 | }
441 | }
442 |
443 | assert.same(expected_output, chunks)
444 | end)
445 |
446 | it("should detect manually disabled trim_right", function ()
447 | local template = "{{code+}}\n"
448 |
449 | local chunks = {}
450 | for chunk in liluat.private.all_chunks(template) do
451 | table.insert(chunks, chunk)
452 | end
453 |
454 | local expected_output = {
455 | {
456 | text = "code",
457 | type = "code",
458 | trim_right = false
459 | },
460 | {
461 | text = "\n",
462 | type = "text"
463 | }
464 | }
465 |
466 | assert.same(expected_output, chunks)
467 | end)
468 |
469 | it("should detect manual trim_left and trim_right", function ()
470 | local template = "\t{{-code-}}\n"
471 |
472 | local chunks = {}
473 | for chunk in liluat.private.all_chunks(template) do
474 | table.insert(chunks, chunk)
475 | end
476 |
477 | local expected_output = {
478 | {
479 | text = "\t",
480 | type = "text"
481 | },
482 | {
483 | text = "code",
484 | type = "code",
485 | trim_right = true,
486 | trim_left = true
487 | },
488 | {
489 | text = "\n",
490 | type = "text"
491 | }
492 | }
493 |
494 | assert.same(expected_output, chunks)
495 | end)
496 | end)
497 |
498 | describe("read_entire_file", function ()
499 | local file_content = liluat.private.read_entire_file("spec/read_entire_file-test")
500 | local expected = "This should be read by the 'read_entire_file' helper functions.\n"
501 |
502 | assert.equal(expected, file_content)
503 | end)
504 |
505 | describe("parse_string_literal", function()
506 | it("should properly resolve escape sequences", function ()
507 | local expected = "bl\"\'\\ub" .. "\n\t\r" .. "bla"
508 | local input = "\"bl\\\"\\\'\\\\ub\" .. \"\\n\\t\\r\" .. \"bla\""
509 |
510 | assert.equal(expected, liluat.private.parse_string_literal(input))
511 | end)
512 | end)
513 |
514 | describe("parse", function ()
515 | it("should create a list of chunks", function ()
516 | local template = [[
517 | {{= expression}} bla {{code}}
518 | {{other code}} some text
519 | {{more code}}{{}}
520 | some more text]]
521 |
522 | local expected_output = {
523 | {
524 | text = " expression",
525 | type = "expression"
526 | },
527 | {
528 | text = " bla ",
529 | type = "text"
530 | },
531 | {
532 | text = "code",
533 | type = "code"
534 | },
535 | {
536 | text = "\n ",
537 | type = "text"
538 | },
539 | {
540 | text = "other code",
541 | type = "code"
542 | },
543 | {
544 | text = " some text\n",
545 | type = "text"
546 | },
547 | {
548 | text = "more code",
549 | type = "code"
550 | },
551 | {
552 | text = "",
553 | type = "code"
554 | },
555 | {
556 | text = "\nsome more text",
557 | type = "text"
558 | }
559 | }
560 |
561 | assert.same(expected_output, liluat.private.parse(template))
562 | end)
563 |
564 | it("should include files", function ()
565 | local template = [[
566 | first line
567 | {{include: "spec/read_entire_file-test"}}
568 | another line]]
569 |
570 | local expected_output = {
571 | {
572 | text = "first line\nThis should be read by the 'read_entire_file' helper functions.\n\nanother line",
573 | type = "text"
574 | }
575 | }
576 |
577 | assert.same(expected_output, liluat.private.parse(template))
578 | end)
579 |
580 | it("should work with other start and end tags", function ()
581 | local template = "text {% --template%} more text"
582 | local expected_output = {
583 | {
584 | text = "text ",
585 | type = "text"
586 | },
587 | {
588 | text = " --template",
589 | type = "code"
590 | },
591 | {
592 | text = " more text",
593 | type = "text"
594 | }
595 | }
596 |
597 | local options = {
598 | start_tag = "{%",
599 | end_tag = "%}"
600 | }
601 | assert.same(expected_output, liluat.private.parse(template, options))
602 | end)
603 |
604 | it("should use existing table if specified", function ()
605 | local template = "bla {{= 5}} more bla"
606 | local output = {}
607 | local expected_output = {
608 | {
609 | text = "bla ",
610 | type = "text"
611 | },
612 | {
613 | text = " 5",
614 | type = "expression"
615 | },
616 | {
617 | text = " more bla",
618 | type = "text"
619 | }
620 | }
621 |
622 | local options = {
623 | start_tag = "{{",
624 | end_tag = "}}"
625 | }
626 | local result = liluat.private.parse(template, options, output)
627 |
628 | assert.equal(output, result)
629 | assert.same(expected_output, result)
630 | end)
631 |
632 | it("should detect cyclic inclusions", function ()
633 | local template = "{{include: 'spec/cycle_a.template'}}"
634 |
635 | assert.has_error(
636 | function ()
637 | liluat.private.parse(template)
638 | end,
639 | "Cyclic inclusion detected")
640 | end)
641 |
642 | it("should not create two or more text chunks in a row", function ()
643 | local template = 'text{{include: "spec/content.html.template"}}more text'
644 |
645 | local expected_output = {
646 | {
647 | text = "textThis is the index page.
\nmore text",
648 | type = "text"
649 | }
650 | }
651 |
652 | assert.same(expected_output, liluat.private.parse(template))
653 | end)
654 |
655 | it("should include relative paths", function ()
656 | local template_path = "spec/basepath_tests/base_a.template"
657 | local template = liluat.private.read_entire_file(template_path)
658 | local expected_output = {
659 | {
660 | text = "This is the index page.
\n\n\n",
661 | type = "text"
662 | }
663 | }
664 |
665 | assert.same(expected_output, liluat.private.parse(template, nil, nil, nil, template_path))
666 | end)
667 |
668 | it("should include paths relative to a base path", function ()
669 | local options = {
670 | base_path = "spec/basepath_tests"
671 | }
672 | local template_path = options.base_path .. "/base_a.template"
673 | local template = liluat.private.read_entire_file(template_path)
674 |
675 | local expected_output = {
676 | {
677 | text = "This is the index page.
\n\n\n",
678 | type = "text"
679 | }
680 | }
681 |
682 | assert.same(expected_output, liluat.private.parse(template, options))
683 | end)
684 |
685 | it("should include more paths relative to a base path", function ()
686 | local options = {
687 | base_path = "spec"
688 | }
689 | local template_path = options.base_path .. "/basepath_tests/base_b.template"
690 | local template = liluat.private.read_entire_file(template_path)
691 |
692 | local expected_output = {
693 | {
694 | text = "This is the index page.
\n\n\n",
695 | type = "text"
696 | }
697 | }
698 |
699 | assert.same(expected_output, liluat.private.parse(template, options))
700 | end)
701 | end)
702 |
703 | describe("sandbox", function ()
704 | it("should run code in a sandbox", function ()
705 | local code = "return i, 1"
706 | local i = 1
707 | local a, b = liluat.private.sandbox(code)()
708 |
709 | assert.is_nil(a)
710 | assert.equal(1, b)
711 | end)
712 |
713 | it("should pass an environment", function ()
714 | local code = "return i"
715 | assert.equal(1, liluat.private.sandbox(code, nil, {i = 1})())
716 | end)
717 |
718 | it("should not have access to non-whitelisted functions", function ()
719 | local code = "return load"
720 | assert.is_nil(liluat.private.sandbox(code)())
721 | end)
722 |
723 | it("should have access to whitelisted functions", function ()
724 | local code = "return os.time"
725 | assert.is_function(liluat.private.sandbox(code)())
726 | end)
727 |
728 | it("should accept custom whitelists", function ()
729 | local code = "return string and string.find"
730 | assert.is_nil(liluat.private.sandbox(code, nil, nil, {})())
731 | end)
732 |
733 | it("should handle compile errors and print its surrounding lines", function ()
734 | local code = [[
735 | -- 1
736 | -- 2
737 | -- 3
738 | -- 4
739 | -- 5
740 | -- 6
741 | "a" .. nil
742 | -- 8
743 | -- 9
744 | -- 10
745 | -- 11
746 | -- 12
747 | -- 13]]
748 |
749 | local expected = [[
750 | Syntax error in sandboxed code "code" in line 7:
751 | .*
752 | 4: %-%- 4
753 | 5: %-%- 5
754 | 6: %-%- 6
755 | 7:> "a" .. nil
756 | 8: %-%- 8
757 | 9: %-%- 9
758 | 10: %-%- 10]]
759 |
760 | local status, error_message = pcall(liluat.private.sandbox, code, "code")
761 |
762 | assert.is_false(status)
763 | assert.truthy(error_message:find(expected))
764 | end)
765 | end)
766 |
767 | describe("liluat.compile", function ()
768 | it("should not crash with two newlines and at least one character between two code blocks", function()
769 | local template = liluat.compile([[
770 | {{}}
771 |
772 | x{{}}
773 | ]])
774 | end)
775 |
776 | it("should not crash with tabs at the front either", function()
777 | local template = liluat.compile([[
778 | {{}}
779 |
780 | x{{}}
781 | ]])
782 | end)
783 |
784 | it("should compile templates into code", function ()
785 | local template = "a{{i = 0}}{{= i}}b"
786 | local expected_output = {
787 | name = "liluat.compile",
788 | code = [[
789 | __liluat_output_function("a")
790 | i = 0
791 | __liluat_output_function( i)
792 | __liluat_output_function("b")]]
793 | }
794 |
795 | assert.same(expected_output, liluat.compile(template))
796 | end)
797 |
798 | it("should accept template names", function ()
799 | local template = "a"
800 | local template_name = "my template"
801 | local expected_output = {
802 | name = "my template",
803 | code = '__liluat_output_function("a")'
804 | }
805 |
806 | assert.same(expected_output, liluat.compile(template, nil, template_name))
807 | end)
808 |
809 | it("should accept other template tags passed as options", function ()
810 | local template = "a{{i = 0}}{{= i}}b"
811 | local options = {
812 | start_tag = "{{",
813 | end_tag = "}}"
814 | }
815 | local expected_output = {
816 | name = "liluat.compile",
817 | code = [[
818 | __liluat_output_function("a")
819 | i = 0
820 | __liluat_output_function( i)
821 | __liluat_output_function("b")]]
822 | }
823 |
824 | assert.same(expected_output, liluat.compile(template, options))
825 | end)
826 |
827 | it("should trim all trailing newlines if told so", function ()
828 | local options = {
829 | trim_right = "all"
830 | }
831 | local template = [[
832 | some text
833 | {{for i = 1, 5 do}}
834 | {{= i}}
835 | {{end}}
836 | {{ -- comment}}
837 | some text]]
838 |
839 | local expected_output = {
840 | name = "liluat.compile",
841 | code = [[
842 | __liluat_output_function("some text\
843 | ")
844 | for i = 1, 5 do
845 | __liluat_output_function( i)
846 | end
847 | -- comment
848 | __liluat_output_function("some text")]]
849 | }
850 |
851 | assert.same(expected_output, liluat.compile(template, options))
852 | end)
853 |
854 | it("should trim trailing newlines after expressions if told so", function ()
855 | local options = {
856 | trim_right = "expression"
857 | }
858 | local template = [[
859 | some text
860 | {{for i = 1, 5 do}}
861 | {{= i}}
862 | {{end}}
863 | {{ -- comment}}
864 | some text]]
865 |
866 | local expected_output = {
867 | name = "liluat.compile",
868 | code = [[
869 | __liluat_output_function("some text\
870 | ")
871 | for i = 1, 5 do
872 | __liluat_output_function("\
873 | ")
874 | __liluat_output_function( i)
875 | end
876 | __liluat_output_function("\
877 | ")
878 | -- comment
879 | __liluat_output_function("\
880 | some text")]]
881 | }
882 |
883 | assert.same(expected_output, liluat.compile(template, options))
884 | end)
885 |
886 | it("should trim trailing newlines after code if told so", function ()
887 | local options = {
888 | trim_right = "code"
889 | }
890 | local template = [[
891 | some text
892 | {{for i = 1, 5 do}}
893 | {{= i}}
894 | {{end}}
895 | {{ -- comment}}
896 | some text]]
897 |
898 | local expected_output = {
899 | name = "liluat.compile",
900 | code = [[
901 | __liluat_output_function("some text\
902 | ")
903 | for i = 1, 5 do
904 | __liluat_output_function( i)
905 | __liluat_output_function("\
906 | ")
907 | end
908 | -- comment
909 | __liluat_output_function("some text")]]
910 | }
911 |
912 | assert.same(expected_output, liluat.compile(template, options))
913 | end)
914 |
915 | it("shouldn't trim newlines if told so", function ()
916 | local options = {
917 | trim_right = false
918 | }
919 | local template = [[
920 | some text
921 | {{for i = 1, 5 do}}
922 | {{= i}}
923 | {{end}}
924 | {{ -- comment}}
925 | some text]]
926 |
927 | local expected_output = {
928 | name = "liluat.compile",
929 | code = [[
930 | __liluat_output_function("some text\
931 | ")
932 | for i = 1, 5 do
933 | __liluat_output_function("\
934 | ")
935 | __liluat_output_function( i)
936 | __liluat_output_function("\
937 | ")
938 | end
939 | __liluat_output_function("\
940 | ")
941 | -- comment
942 | __liluat_output_function("\
943 | some text")]]
944 | }
945 |
946 | assert.same(expected_output, liluat.compile(template, options))
947 |
948 | end)
949 |
950 | it("should trim all spaces in front of template blocks if told so", function ()
951 | local options = {
952 | trim_left = "all",
953 | trim_right = false
954 | }
955 | local template = [[
956 | some text
957 | {{for i = 1, 5 do}}
958 |
959 | {{= i}}
960 | {{end}}
961 | some more text]]
962 |
963 | local expected_output = {
964 | name = "liluat.compile",
965 | code = [[
966 | __liluat_output_function("some text\
967 | ")
968 | for i = 1, 5 do
969 | __liluat_output_function("\
970 | \
971 | ")
972 | __liluat_output_function( i)
973 | __liluat_output_function("\
974 | ")
975 | end
976 | __liluat_output_function("\
977 | some more text")]]
978 | }
979 |
980 | local output = liluat.compile(template, options)
981 | output.code = output.code:gsub("\\9", "\t") --make the test work across lua versions
982 |
983 | assert.same(expected_output, output)
984 | end)
985 |
986 | it("should trim all spaces in front of expressions if told so", function ()
987 | local options = {
988 | trim_left = "expression",
989 | trim_right = false
990 | }
991 | local template = [[
992 | some text
993 | {{for i = 1, 5 do}}
994 | {{= i}}
995 | {{end}}
996 | some more text]]
997 |
998 | local expected_output = {
999 | name = "liluat.compile",
1000 | code = [[
1001 | __liluat_output_function("some text\
1002 | ")
1003 | for i = 1, 5 do
1004 | __liluat_output_function("\
1005 | ")
1006 | __liluat_output_function( i)
1007 | __liluat_output_function("\
1008 | ")
1009 | end
1010 | __liluat_output_function("\
1011 | some more text")]]
1012 | }
1013 |
1014 | local output = liluat.compile(template, options)
1015 | output.code = output.code:gsub("\\9", "\t") --make the test work across lua versions
1016 |
1017 | assert.same(expected_output, output)
1018 | end)
1019 |
1020 | it("should trim all spaces in front of code if told so", function ()
1021 | local options = {
1022 | trim_left = "code",
1023 | trim_right = false
1024 | }
1025 | local template = [[
1026 | some text
1027 | {{for i = 1, 5 do}}
1028 | {{= i}}
1029 | {{end}}
1030 | some more text]]
1031 |
1032 | local expected_output = {
1033 | name = "liluat.compile",
1034 | code = [[
1035 | __liluat_output_function("some text\
1036 | ")
1037 | for i = 1, 5 do
1038 | __liluat_output_function("\
1039 | ")
1040 | __liluat_output_function( i)
1041 | __liluat_output_function("\
1042 | ")
1043 | end
1044 | __liluat_output_function("\
1045 | some more text")]]
1046 | }
1047 |
1048 | local output = liluat.compile(template, options)
1049 | output.code = output.code:gsub("\\9", "\t") --make the test work across lua versions
1050 |
1051 | assert.same(expected_output, output)
1052 | end)
1053 |
1054 | it("shouldn't trim spaces if told so", function ()
1055 | local options = {
1056 | trim_left = false,
1057 | trim_right = false
1058 | }
1059 | local template = [[
1060 | some text
1061 | {{for i = 1, 5 do}}
1062 | {{= i}}
1063 | {{end}}
1064 | some more text]]
1065 |
1066 | local expected_output = {
1067 | name = "liluat.compile",
1068 | code = [[
1069 | __liluat_output_function("some text\
1070 | ")
1071 | for i = 1, 5 do
1072 | __liluat_output_function("\
1073 | ")
1074 | __liluat_output_function( i)
1075 | __liluat_output_function("\
1076 | ")
1077 | end
1078 | __liluat_output_function("\
1079 | some more text")]]
1080 | }
1081 |
1082 | local output = liluat.compile(template, options)
1083 | output.code = output.code:gsub("\\9", "\t") --make the test work across lua versions
1084 |
1085 |
1086 | assert.same(expected_output, output)
1087 | end)
1088 |
1089 | it("should trim both spaces and trailing newlines if told so", function ()
1090 | local options = {
1091 | trim_left = "all",
1092 | trim_right = "all"
1093 | }
1094 |
1095 | local template = [[
1096 | some text
1097 | {{= 1}}
1098 | {{= 2}}
1099 | {{= 3}}
1100 |
1101 | {{= 4}}
1102 | {{= 5}}
1103 |
1104 | {{= 6}}
1105 | {{= 7}}
1106 | {{= 8}}
1107 | more text]]
1108 |
1109 | local expected_output = {
1110 | name = "liluat.compile",
1111 | code = [[
1112 | __liluat_output_function("some text\
1113 | ")
1114 | __liluat_output_function( 1)
1115 | __liluat_output_function( 2)
1116 | __liluat_output_function( 3)
1117 | __liluat_output_function("\
1118 | ")
1119 | __liluat_output_function( 4)
1120 | __liluat_output_function( 5)
1121 | __liluat_output_function(" \
1122 | \
1123 | ")
1124 | __liluat_output_function( 6)
1125 | __liluat_output_function(" \
1126 | ")
1127 | __liluat_output_function( 7)
1128 | __liluat_output_function(" \
1129 | ")
1130 | __liluat_output_function( 8)
1131 | __liluat_output_function("more text")]]
1132 | }
1133 |
1134 | assert.same(expected_output, liluat.compile(template, options))
1135 | end)
1136 |
1137 | it("should trim left in the first line", function ()
1138 | local template = "\t{{code}}"
1139 |
1140 | local options = {
1141 | trim_left = "all"
1142 | }
1143 |
1144 | local expected_output = {
1145 | name = "liluat.compile",
1146 | code = "code"
1147 | }
1148 |
1149 | assert.same(expected_output, liluat.compile(template, options))
1150 | end)
1151 |
1152 | it("should locally override trim_left (force trim)", function ()
1153 | local template = "\t{{-code}}"
1154 |
1155 | local expected_output = {
1156 | code = "code",
1157 | name = "liluat.compile"
1158 | }
1159 |
1160 | local options = {
1161 | trim_left = false
1162 | }
1163 |
1164 | assert.same(expected_output, liluat.compile(template, options))
1165 | end)
1166 |
1167 | it("should locally override trim_left (force no trim)", function()
1168 | local template = " {{+code}}"
1169 |
1170 | local expected_output = {
1171 | code = '__liluat_output_function(" ")\ncode',
1172 | name = "liluat.compile"
1173 | }
1174 |
1175 | local options = {
1176 | trim_left = "all"
1177 | }
1178 |
1179 | assert.same(expected_output, liluat.compile(template, options))
1180 | end)
1181 |
1182 | it("should locally override trim_right (force trim)", function ()
1183 | local template = "{{code-}}\n"
1184 |
1185 | local options = {
1186 | trim_left = false
1187 | }
1188 |
1189 | local expected_output = {
1190 | code = "code",
1191 | name = "liluat.compile"
1192 | }
1193 |
1194 | assert.same(expected_output, liluat.compile(template, options))
1195 | end)
1196 |
1197 | it("should locally override trim_right (force no trim)", function ()
1198 | local template = "{{code+}}\n"
1199 |
1200 | local options = {
1201 | trim_left = "all"
1202 | }
1203 |
1204 | local expected_output = {
1205 | code = 'code\n__liluat_output_function("\\\n")',
1206 | name = "liluat.compile"
1207 | }
1208 |
1209 | assert.same(expected_output, liluat.compile(template, options))
1210 | end)
1211 |
1212 | it("should locally override trim_left and trim_right (force trim)", function ()
1213 | local template = " {{-code-}}\n"
1214 |
1215 | local options = {
1216 | trim_left = false,
1217 | trim_right = false
1218 | }
1219 |
1220 | local expected_output = {
1221 | code = 'code',
1222 | name = "liluat.compile"
1223 | }
1224 |
1225 | assert.same(expected_output, liluat.compile(template, options))
1226 | end)
1227 |
1228 | it("should locally override trim_left and trim_right (force no trim)", function ()
1229 | local template = " {{+code+}}\n"
1230 |
1231 | local options = {
1232 | trim_left = "all",
1233 | trim_right = "all"
1234 | }
1235 |
1236 | local expected_output = {
1237 | code = '__liluat_output_function(" ")\ncode\n__liluat_output_function("\\\n")',
1238 | name = "liluat.compile"
1239 | }
1240 |
1241 | assert.same(expected_output, liluat.compile(template, options))
1242 | end)
1243 | end)
1244 |
1245 | describe("liluat.compile_file", function ()
1246 | it("should load a template file", function ()
1247 | local template_path = "spec/index.html.template"
1248 | local expected_output = loadfile("spec/index.html.template.lua")()
1249 |
1250 | assert.same(expected_output, liluat.compile_file(template_path))
1251 | end)
1252 |
1253 | it("should accept different tags via the options", function ()
1254 | local template_path = "spec/jinja.template"
1255 | local options = {
1256 | start_tag = "{%",
1257 | end_tag = "%}"
1258 | }
1259 | local expected_output = loadfile("spec/jinja.template.lua")()
1260 |
1261 | assert.same(expected_output, liluat.compile_file(template_path, options))
1262 | end)
1263 | end)
1264 |
1265 | describe("get_dependencies", function ()
1266 | it("should list all includes", function ()
1267 | local template = '{{include: "spec/index.html.template"}}'
1268 | local expected_output = {
1269 | "spec/index.html.template",
1270 | "spec/content.html.template"
1271 | }
1272 |
1273 | assert.same(expected_output, liluat.get_dependencies(template))
1274 | end)
1275 |
1276 | it("should list every file only once", function ()
1277 | local template = '{{include: "spec/index.html.template"}}{{include: "spec/index.html.template"}}'
1278 | local expected_output = {
1279 | "spec/index.html.template",
1280 | "spec/content.html.template"
1281 | }
1282 |
1283 | assert.same(expected_output, liluat.get_dependencies(template))
1284 | end)
1285 | end)
1286 |
1287 | describe("liluat.inline", function ()
1288 | it("should inline a template", function ()
1289 | local template = liluat.private.read_entire_file("spec/index.html.template")
1290 | local expected_output = liluat.private.read_entire_file("spec/index.html.template.inlined")
1291 |
1292 | assert.equal(expected_output, liluat.inline(template, nil, "spec/"))
1293 | end)
1294 | end)
1295 |
1296 | describe("sandbox", function ()
1297 | it("should run code in a sandbox", function ()
1298 | local code = "return i, 1"
1299 | local i = 1
1300 | local a, b = liluat.private.sandbox(code)()
1301 |
1302 | assert.is_nil(a)
1303 | assert.equal(1, b)
1304 | end)
1305 |
1306 | it("should pass an environment", function ()
1307 | local code = "return i"
1308 | assert.equal(1, liluat.private.sandbox(code, nil, {i = 1})())
1309 | end)
1310 |
1311 | it("should not have access to non-whitelisted functions", function ()
1312 | local code = "return load"
1313 | assert.is_nil(liluat.private.sandbox(code)())
1314 | end)
1315 |
1316 | it("should have access to whitelisted functions", function ()
1317 | local code = "return os.time"
1318 | assert.is_function(liluat.private.sandbox(code)())
1319 | end)
1320 | end)
1321 |
1322 | describe("add_include_and_detect_cycles", function ()
1323 | it("should add includes", function ()
1324 | local include_list = {}
1325 |
1326 | liluat.private.add_include_and_detect_cycles(include_list, "a")
1327 | liluat.private.add_include_and_detect_cycles(include_list.a, "b")
1328 | liluat.private.add_include_and_detect_cycles(include_list.a.b, "c")
1329 | liluat.private.add_include_and_detect_cycles(include_list, "d")
1330 |
1331 | assert.is_nil(include_list[0])
1332 | assert.equal(include_list, include_list.a[0])
1333 | assert.is_table(include_list.a)
1334 | assert.equal(include_list.a, include_list.a.b[0])
1335 | assert.is_table(include_list.a.b)
1336 | assert.equal(include_list.a.b, include_list.a.b.c[0])
1337 | assert.is_table(include_list.a.b.c)
1338 | assert.is_equal(include_list, include_list.d[0])
1339 | assert.is_table(include_list.d)
1340 | end)
1341 |
1342 | it("should detect inclusion cycles", function ()
1343 | local include_list = {}
1344 |
1345 | liluat.private.add_include_and_detect_cycles(include_list, "a")
1346 | liluat.private.add_include_and_detect_cycles(include_list.a, "b")
1347 | assert.has_error(
1348 | function ()
1349 | liluat.private.add_include_and_detect_cycles(include_list.a.b, "a")
1350 | end,
1351 | "Cyclic inclusion detected")
1352 | end)
1353 | end)
1354 |
1355 | describe("dirname", function ()
1356 | it("should return the directory containing a file", function ()
1357 | assert.equal("/home/user/", liluat.private.dirname("/home/user/.bashrc"))
1358 | assert.equal("/home/user/", liluat.private.dirname("/home/user/"))
1359 | assert.equal("/home/", liluat.private.dirname("/home/user"))
1360 | assert.equal("./", liluat.private.dirname("./template"))
1361 | assert.equal("", liluat.private.dirname("."))
1362 | end)
1363 | end)
1364 |
1365 | describe("version", function ()
1366 | it("should return the current version number", function ()
1367 | assert.equal("1.2.0", liluat.version())
1368 | end)
1369 | end)
1370 |
1371 | describe("liluat.render", function ()
1372 | it("should handle runtime errors and print its surrounding lines", function ()
1373 | local code = [[
1374 | -- 1
1375 | -- 2
1376 | -- 3
1377 | -- 4
1378 | -- 5
1379 | -- 6
1380 | local test = "a" .. nil
1381 | -- 8
1382 | -- 9
1383 | -- 10
1384 | -- 11
1385 | -- 12
1386 | -- 13]]
1387 |
1388 | local expected = [[
1389 | Runtime error in sandboxed code "code" in line 7:
1390 | .*
1391 | 4: %-%- 4
1392 | 5: %-%- 5
1393 | 6: %-%- 6
1394 | 7:> local test = "a" .. nil
1395 | 8: %-%- 8
1396 | 9: %-%- 9
1397 | 10: %-%- 10]]
1398 |
1399 | local status, error_message = pcall(liluat.render, {name = 'code', code = code})
1400 |
1401 | assert.is_false(status)
1402 | assert.truthy(error_message:find(expected))
1403 | end)
1404 |
1405 | it("should accept the 'reference' option", function ()
1406 | local template = "{{= tostring(table_reference)}}"
1407 | local parameters = {table_reference = {}}
1408 |
1409 | local code = liluat.compile(template)
1410 |
1411 | assert.equal(tostring(parameters.table_reference), liluat.render(code, parameters, {reference = true}))
1412 | end)
1413 | end)
1414 |
1415 | describe("liluat.render_coroutine", function ()
1416 | it("should accept the 'reference' option", function ()
1417 | local template = "{{= tostring(table_reference)}}"
1418 | local parameters = {table_reference = {}}
1419 |
1420 | local code = liluat.compile(template)
1421 |
1422 | local thread = coroutine.wrap(liluat.render_coroutine(code, parameters, {reference = true}))
1423 |
1424 | local rendered_string = thread()
1425 |
1426 | assert.equal(tostring(parameters.table_reference), rendered_string)
1427 | end)
1428 | end)
1429 | end)
1430 |
--------------------------------------------------------------------------------
/spec/options:
--------------------------------------------------------------------------------
1 | {
2 | start_tag = "{%",
3 | end_tag = "%}"
4 | }
5 |
--------------------------------------------------------------------------------
/spec/preload.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 |
3 | -- preload to make sure that 'require' loads the local liluat and not the globally installed one
4 | package.loaded["liluat"] = loadfile("liluat.lua")()
5 |
6 | -- now load 'runliluat'
7 | dofile("runliluat.lua")
8 |
--------------------------------------------------------------------------------
/spec/read_entire_file-test:
--------------------------------------------------------------------------------
1 | This should be read by the 'read_entire_file' helper functions.
2 |
--------------------------------------------------------------------------------
/spec/runliluat_spec.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | -- Tests for runliluat using the "busted" unit testing framework.
3 | --
4 | -- Copyright © 2016 Max Bruckner
5 | --
6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy
7 | -- of this software and associated documentation files (the "Software"), to deal
8 | -- in the Software without restriction, including without limitation the rights
9 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | -- copies of the Software, and to permit persons to whom the Software is furnished
11 | -- to do so, subject to the following conditions:
12 | --
13 | -- The above copyright notice and this permission notice shall be included in
14 | -- all copies or substantial portions of the Software.
15 | --
16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | -- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
21 | -- IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | --]]
23 |
24 | -- preload to make sure that 'require' loads the local liluat and not the globally installed one
25 | package.loaded["liluat"] = loadfile("liluat.lua")()
26 | local liluat = require("liluat")
27 |
28 | -- custom exec function that works across lua versions
29 | local function execute_command(command)
30 | local exit_status
31 | if _VERSION == "Lua 5.1" then
32 | exit_status = os.execute(command)
33 | else
34 | _,_, exit_status = os.execute(command)
35 | end
36 |
37 | return exit_status
38 | end
39 |
40 | -- create a temporary file, open it and return a file descriptor as well as the filename
41 | local function tempfile()
42 | local filename = os.tmpname()
43 | local file = io.open(filename, "w+")
44 |
45 | return file, filename
46 | end
47 |
48 | -- execute a command while specifying its input and output
49 | local function execute_with_in_and_output(command, input)
50 | -- create input file and write the input to it
51 | local input_file, input_file_name
52 | if input then
53 | input_file, input_file_name = tempfile()
54 | input_file:write(input)
55 | input_file:close()
56 | end
57 |
58 | local stdout_file, stdout_file_name = tempfile()
59 | local stderr_file, stderr_file_name = tempfile()
60 |
61 | if input_file then
62 | command = "cat " .. input_file_name .. " | " .. command
63 | end
64 | command = command
65 | .. " 1> " .. stdout_file_name
66 | .. " 2> " .. stderr_file_name
67 |
68 |
69 | local exit_status = execute_command(command)
70 |
71 | if input_file_name then
72 | os.remove(input_file_name)
73 | end
74 | stdout_file:close()
75 | stderr_file:close()
76 |
77 | local stdout = liluat.private.read_entire_file(stdout_file_name)
78 | os.remove(stdout_file_name)
79 | local stderr = liluat.private.read_entire_file(stderr_file_name)
80 | os.remove(stderr_file_name)
81 |
82 | return exit_status, stdout, stderr
83 | end
84 |
85 | local function get_error_code()
86 | return (_VERSION == "Lua 5.1") and 256 or 1
87 | end
88 |
89 | local usage_string = [[
90 | Usage: runliluat [options]
91 | Options:
92 | -h|--help
93 | Show this message.
94 | --values lua_table
95 | Table containing values to use in the template.
96 | --value-file file_name
97 | Use a file to define the table of values to use in the template.
98 | -t|--template-file file_name
99 | File that contains the template
100 | -n|--name template_name
101 | Name to use for the template
102 | -d|--dependencies
103 | List the dependencies of the template (list of included files)
104 | -i|--inline
105 | Inline all the included files into one template.
106 | -o|--output filename
107 | File to write the output to (defaults to stdout)
108 | --options lua_table
109 | A table of options for liluat
110 | --options-file file_name
111 | Read the options from a file.
112 | --stdin "template"
113 | Get the template from stdin.
114 | --stdin "values"
115 | Get the table of values from stdin.
116 | --stdin "options"
117 | Get the options from stdin.
118 | -v|--version
119 | Print the current version.
120 | --path path
121 | Root path of the templates.
122 |
123 | ]]
124 |
125 | describe("runliluat test helpers", function ()
126 | it("should execute commands", function ()
127 | assert.equal(0, execute_command("true"))
128 | assert.not_equal(0, execute_command("false"))
129 | end)
130 |
131 | it("should get the output of a command", function ()
132 | local expected_output = {
133 | 0, "hello\n", ""
134 | }
135 |
136 | assert.same(expected_output, {execute_with_in_and_output("echo hello")})
137 | end)
138 |
139 | it("should get the error output of a command", function ()
140 | local expected_output = {
141 | 0, "", "hello\n"
142 | }
143 |
144 | assert.same(expected_output, {execute_with_in_and_output("sh -c 'echo hello >&2'")})
145 | end)
146 | end)
147 |
148 | describe("runliluat", function ()
149 | it("should complain when no parameters were given", function ()
150 | local expected_output = {
151 | get_error_code(), usage_string, "ERROR: No parameter given.\n"
152 | }
153 |
154 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua")})
155 | end)
156 |
157 | it("should complain on incorrect parameters", function ()
158 | local expected_output = {
159 | get_error_code(), usage_string, 'ERROR: Invalid parameter "-a".\n'
160 | }
161 |
162 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -a")})
163 | end)
164 |
165 | it("should print the help", function ()
166 | local expected_output = {
167 | 0, usage_string, ""
168 | }
169 |
170 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -h")})
171 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --help")})
172 | end)
173 |
174 | it("should print it's version", function ()
175 | local expected_output = {
176 | 0,
177 | liluat.version().."\n",
178 | ""
179 | }
180 |
181 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -v")})
182 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --version")})
183 | end)
184 |
185 | it("should have a rockspec file for the current version", function ()
186 | local exit_status, stdout, stderr = execute_with_in_and_output("ls liluat-`spec/preload.lua -v`-*.rockspec")
187 | assert.is_truthy(stdout:find("^liluat%-[%w%.]+%-%d+%.rockspec"))
188 | assert.equal(0, exit_status)
189 | assert.equal("", stderr)
190 | end)
191 |
192 | it("should complain when trying to print version, get dependencies and inline", function ()
193 | local expected_output = {
194 | get_error_code(),
195 | "",
196 | "ERROR: Can't print_version, determine dependencies and inline a template at the same time.\n"
197 | }
198 |
199 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -v -d -i")})
200 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --version -d -i")})
201 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --version --dependencies -i")})
202 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --version --dependencies --inline")})
203 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --version -d --inline")})
204 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -v -d --inline")})
205 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -v --dependencies --inline")})
206 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -v --dependencies -i")})
207 | end)
208 |
209 | it("should complain when trying to get dependencies and inline", function ()
210 | local expected_output = {
211 | get_error_code(),
212 | "",
213 | "ERROR: Can't both determine dependencies and inline a template.\n"
214 | }
215 |
216 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -d -i")})
217 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --dependencies -i")})
218 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -d --inline")})
219 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --dependencies --inline")})
220 | end)
221 |
222 | it("should complain when trying to get dependencies and print the version", function ()
223 | local expected_output = {
224 | get_error_code(),
225 | "",
226 | "ERROR: Can't both determine dependencies and print the version.\n"
227 | }
228 |
229 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -d -v")})
230 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -d --version")})
231 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --dependencies -v")})
232 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --dependencies --version")})
233 | end)
234 |
235 | it("should complain when trying to print the version and inline", function ()
236 | local expected_output = {
237 | get_error_code(),
238 | "",
239 | "ERROR: Can't both print the version and inline a template.\n"
240 | }
241 |
242 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -v -i")})
243 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -v --inline")})
244 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --version -i")})
245 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --version --inline")})
246 | end)
247 |
248 | it("should complain when trying to load a template from a file and stdin", function ()
249 | local expected_output = {
250 | get_error_code(),
251 | "",
252 | "ERROR: Can't both load a template from stdin and a file.\n"
253 | }
254 |
255 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -t spec/index.html.template --stdin template", "{{}}")})
256 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --template-file spec/index.html.template --stdin template", "{{}}")})
257 | end)
258 |
259 | it("should print dependencies", function ()
260 | local template = '{{include: "spec/index.html.template"}}'
261 | local expected_output = {
262 | 0,
263 | "spec/index.html.template\n"
264 | .. "spec/content.html.template\n",
265 | ""
266 | }
267 |
268 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -d --stdin template", template)})
269 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --dependencies --stdin template", template)})
270 | end)
271 |
272 | it("should get dependencies when loading a template from a file", function ()
273 | local template_path = "spec/index.html.template"
274 | local expected_output = {
275 | 0,
276 | "spec/content.html.template\n",
277 | ""
278 | }
279 |
280 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -d -t "..template_path)})
281 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --dependencies -t "..template_path)})
282 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -d --template-file "..template_path)})
283 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --dependencies --template-file "..template_path)})
284 | end)
285 |
286 | it("should inline a template", function ()
287 | local template = liluat.private.read_entire_file("spec/index.html.template")
288 | local expected_output = {
289 | 0,
290 | liluat.private.read_entire_file("spec/index.html.template.inlined"),
291 | ""
292 | }
293 |
294 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --path 'spec/' --stdin template -i", template)})
295 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --path 'spec/' --stdin template --inline", template)})
296 | end)
297 |
298 | it("should inline a template when loading it from a file", function ()
299 | local template_path = "spec/index.html.template"
300 | local expected_output = {
301 | 0,
302 | liluat.private.read_entire_file("spec/index.html.template.inlined"),
303 | ""
304 | }
305 |
306 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -i -t "..template_path)})
307 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -i --template-file "..template_path)})
308 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --inline -t "..template_path)})
309 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --inline --template-file "..template_path)})
310 | end)
311 |
312 | it("should accept template paths via --path", function ()
313 | local template_path = 'spec/basepath_tests/base_a.template'
314 | local template = liluat.private.read_entire_file(template_path)
315 | local expected_output = {
316 | 0,
317 | "This is the index page.
\n\n\n",
318 | ""
319 | }
320 |
321 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --path '"..template_path.."' --stdin template ", template)})
322 | end)
323 |
324 | it("should complain when no template is specified", function ()
325 | local expected_output = {
326 | get_error_code(),
327 | "",
328 | "ERROR: No template specified.\n"
329 | }
330 |
331 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -d")})
332 | end)
333 |
334 | it("should not crash when a template name is specified", function ()
335 | local expected_output = {
336 | 0,
337 | "",
338 | ""
339 | }
340 |
341 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --stdin template -n 'test'", "")})
342 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --stdin template --name 'test'", "")})
343 | end)
344 |
345 | it("should load values from a file", function ()
346 | local template = "{{= name}}"
347 | local value_path = "spec/values"
348 | local expected_output = {
349 | 0,
350 | "max",
351 | ""
352 | }
353 |
354 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --stdin template --value-file "..value_path, template)})
355 | end)
356 |
357 | it("should load values from stdin", function ()
358 | local template_path = "spec/template"
359 | local values = '{name = "max"}'
360 | local expected_output = {
361 | 0,
362 | "max\n",
363 | ""
364 | }
365 |
366 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --stdin values -t "..template_path, values)})
367 | end)
368 |
369 | it("should load values from a parameter", function ()
370 | local template = "{{= name}}"
371 | local values = '{name = "max"}'
372 |
373 | local expected_output = {
374 | 0,
375 | "max",
376 | ""
377 | }
378 |
379 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --stdin template --values '"..values.."'", template)})
380 | end)
381 |
382 | it("should load options from a file", function ()
383 | local options_path = "spec/options"
384 | local template = '{%= name%}'
385 | local values = '{name = "max"}'
386 |
387 | local expected_output = {
388 | 0,
389 | "max",
390 | ""
391 | }
392 |
393 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --stdin template --options-file "..options_path.." --values '"..values.."'", template)})
394 | end)
395 |
396 | it("should load options from stdin", function ()
397 | local options = '{start_tag = "{%", end_tag = "%}"}'
398 | local template_path = "spec/template-jinja"
399 | local values = '{name = "max"}'
400 |
401 | local expected_output = {
402 | 0,
403 | "max\n",
404 | ""
405 | }
406 |
407 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --stdin options -t "..template_path.." --values '"..values.."'", options)})
408 | end)
409 |
410 | it("should load options from a parameter", function ()
411 | local options = '{start_tag = "{%", end_tag = "%}"}'
412 | local template_path = "spec/template-jinja"
413 | local values = '{name = "max"}'
414 |
415 | local expected_output = {
416 | 0,
417 | "max\n",
418 | ""
419 | }
420 |
421 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --options '"..options.."' -t "..template_path.." --stdin values", values)})
422 | end)
423 |
424 | it("should load templates from a file", function ()
425 | local template_path = 'spec/template'
426 | local values = '{name = "max"}'
427 |
428 | local expected_output = {
429 | 0,
430 | "max\n",
431 | ""
432 | }
433 |
434 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua -t "..template_path.." --stdin values", values)})
435 | end)
436 |
437 | it("should load templates from stdin", function ()
438 | local template = "{{= name}}"
439 | local value_path = "spec/values"
440 |
441 | local expected_output = {
442 | 0,
443 | "max",
444 | ""
445 | }
446 |
447 | assert.same(expected_output, {execute_with_in_and_output("spec/preload.lua --value-file "..value_path.." --stdin template", template)})
448 | end)
449 |
450 | it("should write it's output to a file", function ()
451 | local file, filename = tempfile()
452 | file:close()
453 |
454 | execute_with_in_and_output("spec/preload.lua -o "..filename.. " -v")
455 | assert.equal(liluat.version().."\n", liluat.private.read_entire_file(filename))
456 |
457 | execute_with_in_and_output("spec/preload.lua --output "..filename.. " -v")
458 | assert.equal(liluat.version().."\n", liluat.private.read_entire_file(filename))
459 |
460 | os.remove(filename)
461 | end)
462 | end)
463 |
--------------------------------------------------------------------------------
/spec/template:
--------------------------------------------------------------------------------
1 | {{= name}}
2 |
--------------------------------------------------------------------------------
/spec/template-jinja:
--------------------------------------------------------------------------------
1 | {%= name%}
2 |
--------------------------------------------------------------------------------
/spec/values:
--------------------------------------------------------------------------------
1 | {name = "max"}
2 |
--------------------------------------------------------------------------------