├── .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 | [![Travis Build Status](https://travis-ci.org/FSMaxB/liluat.svg?branch=master)](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 | 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 | 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 | 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 | 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 |
  1. {{= i}}
  2. 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 |
  1. {{= i}}
  2. 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("
  1. ") 18 | __liluat_output_function( i) 19 | __liluat_output_function("
  2. \ 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 = "text

This 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 | --------------------------------------------------------------------------------