├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .luacheckrc ├── .luacov ├── .travis.yml ├── .travis └── before.sh ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── benchmark ├── bench-01.lua └── bench.lua ├── bin └── aspect ├── changelog.md ├── docs ├── _config.yml ├── api.md ├── assets │ ├── aspect.png │ └── luarocks.svg ├── cli.md ├── dev.md ├── dev │ ├── date.md │ └── tokenizer.md ├── filters.md ├── filters │ ├── abs.md │ ├── batch.md │ ├── columns.md │ ├── date.md │ ├── date_modify.md │ ├── default.md │ ├── escape.md │ ├── first.md │ ├── format.md │ ├── join.md │ ├── json_encode.md │ ├── keys.md │ ├── last.md │ ├── length.md │ ├── lower.md │ ├── merge.md │ ├── nl2br.md │ ├── raw.md │ ├── replace.md │ ├── split.md │ ├── striptags.md │ ├── trim.md │ ├── truncate.md │ ├── upper.md │ ├── utf.length.md │ ├── utf.lower.md │ ├── utf.truncate.md │ └── utf.upper.md ├── funcs.md ├── funcs │ ├── date.md │ ├── dump.md │ ├── env.md │ ├── include.md │ └── range.md ├── index.md ├── spec.md ├── syntax.md ├── tags.md ├── tags │ ├── apply.md │ ├── autoescape.md │ ├── do.md │ ├── extends.md │ ├── for.md │ ├── if.md │ ├── include.md │ ├── macro.md │ ├── set.md │ ├── verbatim.md │ └── with.md ├── tests.md └── tests │ ├── defined.md │ ├── divisibleby.md │ ├── empty.md │ ├── even.md │ ├── iterable.md │ ├── null.md │ ├── odd.md │ └── sameas.md ├── readme.md ├── rockspec ├── aspect-1.13-0.rockspec ├── aspect-1.14-0.rockspec ├── aspect-2.0-0.rockspec ├── aspect-2.2-0.rockspec ├── aspect-2.3-0.rockspec └── aspect-git-1.rockspec ├── spec ├── aspect_spec.lua └── fixture │ ├── complex.view │ ├── data.json │ ├── footer.view │ └── greeting.view └── src └── aspect ├── ast.lua ├── ast └── ops.lua ├── cli.lua ├── compiler.lua ├── config.lua ├── date.lua ├── error.lua ├── filters.lua ├── funcs.lua ├── init.lua ├── loader ├── array.lua ├── filesystem.lua └── resty.lua ├── output.lua ├── tags.lua ├── template.lua ├── tests.lua ├── tokenizer.lua ├── utils.lua └── utils ├── batch.lua └── range.lua /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Type of '....' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Desktop (please complete the following information):** 26 | - OS: [e.g. iOS] 27 | - Browser [e.g. chrome, safari] 28 | - Version [e.g. 22] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Browser [e.g. stock browser, safari] 34 | - Version [e.g. 22] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### Summary 8 | 9 | SUMMARY_GOES_HERE 10 | 11 | ### Full changelog 12 | 13 | * [Implement ...] 14 | * [Add related tests] 15 | * ... 16 | 17 | ### Issues resolved 18 | 19 | Fix #XXX 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | lua_modules 3 | *.out -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "min" 2 | files["spec/*_spec.lua"].std = "+busted" -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | modules = { 2 | ["aspect.*"] = "src", 3 | ["aspect.utils.*"] = "src/aspect" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | branches: 4 | only: 5 | - master 6 | - develop 7 | 8 | matrix: 9 | include: 10 | - os: linux 11 | env: 12 | - LUA="lua=5.1" 13 | - LUA_CJSON="" 14 | - os: linux 15 | env: 16 | - LUA="lua=5.2" 17 | - LUA_CJSON="2.1.0" 18 | - os: linux 19 | env: 20 | - LUA="lua=5.3" 21 | - LUA_CJSON="2.1.0" 22 | # - os: linux 23 | # env: 24 | # - LUA="lua=5.4" 25 | # - LUA_CJSON="2.1.0" 26 | - os: linux 27 | env: 28 | - LUA="luajit=2.0" 29 | - LUA_CJSON="2.1.0" 30 | - os: linux 31 | env: 32 | - LUA="luajit=2.1" 33 | - LUA_CJSON="" 34 | 35 | install: 36 | - pip install codecov 37 | - pip install hererocks 38 | - hererocks lua_install --$LUA -r latest 39 | - source lua_install/bin/activate 40 | - .travis/before.sh 41 | 42 | script: 43 | - busted -c 44 | 45 | after_script: 46 | - luacov 47 | - codecov -X gcov -------------------------------------------------------------------------------- /.travis/before.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | luarocks install busted 6 | luarocks install cluacov 7 | luarocks install lua-cjson $LUA_CJSON 8 | luarocks install luautf8 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at a.cobest@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Aspect 2 | 3 | Hello, and welcome! Whether you are looking for help, trying to report a bug, 4 | thinking about getting involved in the project or about to submit a patch, this 5 | document is for you! Its intent is to be both an entry point for newcomers to 6 | the community (with various technical backgrounds), and a guide/reference for 7 | contributors and maintainers. 8 | 9 | ## Where to report bugs? 10 | 11 | Feel free to [submit an issue](https://github.com/unifire-app/aspect/issues/new/choose) on 12 | the GitHub repository, we would be grateful to hear about it! Please make sure 13 | to respect the GitHub issue template, and include: 14 | 15 | 1. A summary of the issue 16 | 2. A list of steps to reproduce the issue 17 | 3. The version of Aspect you encountered the issue with 18 | 4. If posible get template info with `aspect --debug --lint path/to/template.tmpl` 19 | 20 | ## Where to submit feature requests? 21 | 22 | You can [submit an issue](https://github.com/unifire-app/aspect/issues/new/choose) for feature 23 | requests. Please add as much detail as you can when doing so. Please, if possible, writes tests. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ivan Shalganov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark/bench-01.lua: -------------------------------------------------------------------------------- 1 | local aspect = require("aspect.template") 2 | local var_dump = require("aspect.utils").var_dump 3 | local table_new = table.new or function () return {} end 4 | local os = os 5 | 6 | return function (iterations) 7 | iterations = iterations or 1000 8 | local gc = collectgarbage 9 | local total = 0 10 | local template = aspect.new({ 11 | cache = true, 12 | loader = function (name) 13 | var_dump("load ", name) 14 | os.exit() 15 | end 16 | }) 17 | --local parse = template.parse 18 | --local compile = template.compile 19 | local view = [[ 20 | ]] 25 | 26 | print(string.format("Running %d iterations in each test", iterations)) 27 | 28 | local cmp, err = template:parse("runtime", view) -- warm up and check syntax 29 | if err then 30 | print("Parse error: " .. tostring(err), "Template: " .. view) 31 | os.exit() 32 | end 33 | 34 | gc() 35 | gc() 36 | 37 | local x = os.clock() 38 | for _ = 1, iterations do 39 | template:parse("runtime", view) 40 | end 41 | local z = os.clock() - x 42 | print(string.format(" Parsing Time: %.6f", z)) 43 | total = total + z 44 | 45 | gc() 46 | gc() 47 | 48 | x = os.clock() 49 | for _ = 1, iterations do 50 | template:compile("runtime", view, false) 51 | end 52 | z = os.clock() - x 53 | print(string.format("Compilation Time: %.6f (template)", z)) 54 | total = total + z 55 | 56 | template.cache = {} 57 | template:compile("runtime", view, true) 58 | 59 | gc() 60 | gc() 61 | 62 | 63 | x = os.clock() 64 | for _ = 1, iterations do 65 | template:get_view("runtime") 66 | end 67 | z = os.clock() - x 68 | print(string.format("Compilation Time: %.6f (template, cached)", z)) 69 | total = total + z 70 | 71 | local context = { "Emma", "James", "Nicholas", "Mary" } 72 | 73 | template.cache = {} 74 | 75 | gc() 76 | gc() 77 | 78 | x = os.clock() 79 | for _ = 1, iterations do 80 | template:eval("runtime", view, context) 81 | end 82 | z = os.clock() - x 83 | print(string.format(" Execution Time: %.6f (same template)", z)) 84 | total = total + z 85 | 86 | template.cache = {} 87 | template:compile("runtime", view, true) 88 | 89 | gc() 90 | gc() 91 | 92 | x = os.clock() 93 | for _ = 1, iterations do 94 | template:render("runtime", context) 95 | end 96 | z = os.clock() - x 97 | print(string.format(" Execution Time: %.6f (same template, cached)", z)) 98 | total = total + z 99 | 100 | template.cache = {} 101 | 102 | local views = table_new(iterations, 0) 103 | for i = 1, iterations do 104 | views[i] = "

Iteration " .. i .. "

\n" .. view 105 | end 106 | 107 | gc() 108 | gc() 109 | 110 | x = os.clock() 111 | for i = 1, iterations do 112 | template:eval("runtime" .. i, views[i], context) 113 | end 114 | z = os.clock() - x 115 | print(string.format(" Execution Time: %.6f (different template)", z)) 116 | total = total + z 117 | 118 | for i = 1, iterations do 119 | template:compile("runtime" .. i, views[i], true) 120 | end 121 | gc() 122 | gc() 123 | 124 | x = os.clock() 125 | for i = 1, iterations do 126 | template:render("runtime" .. i, context) 127 | end 128 | z = os.clock() - x 129 | print(string.format(" Execution Time: %.6f (different template, cached)", z)) 130 | total = total + z 131 | 132 | local contexts = table_new(iterations, 0) 133 | 134 | for i = 1, iterations do 135 | contexts[i] = { "Emma", "James", "Nicholas", "Mary" } 136 | end 137 | 138 | template.cache = {} 139 | 140 | gc() 141 | gc() 142 | 143 | x = os.clock() 144 | for i = 1, iterations do 145 | template:eval("runtime" .. i, views[i], contexts[i]) 146 | end 147 | z = os.clock() - x 148 | print(string.format(" Execution Time: %.6f (different template, different context)", z)) 149 | total = total + z 150 | 151 | for i = 1, iterations do 152 | template:compile("runtime" .. i, views[i], true) 153 | end 154 | 155 | gc() 156 | gc() 157 | 158 | x = os.clock() 159 | for i = 1, iterations do 160 | template:render("runtime" .. i, contexts[i]) 161 | end 162 | z = os.clock() - x 163 | print(string.format(" Execution Time: %.6f (different template, different context, cached)", z)) 164 | total = total + z 165 | print(string.format(" Total Time: %.6f", total)) 166 | end -------------------------------------------------------------------------------- /benchmark/bench.lua: -------------------------------------------------------------------------------- 1 | package.path = "./src/?.lua;./benchmark/?.lua;" .. package.path 2 | 3 | local bench01 = require("bench-01") 4 | 5 | bench01(1000) -------------------------------------------------------------------------------- /bin/aspect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | -- Aspect command-line runner 3 | require('aspect.cli').shell() -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ChangeLog 2 | ========= 3 | 4 | 2.2 5 | --- 6 | 7 | - Fix bug [#7](https://github.com/unifire-app/aspect/issues/7) 8 | - Add more tests 9 | - Add/fix more docs 10 | 11 | 2.0 12 | --- 13 | 14 | - Tag `with` now supports expressions. 15 | - Extra build information 16 | - Function `aspect.utils.dump` now works with nil 17 | - Tokenizer with offset. 18 | - Add compiler contexts. More information about the template. 19 | - Improve template inheritance algorithm. The parent template is determined dynamically 20 | - Add filter `truncate` 21 | - UTF8 support: 22 | - add `require("aspect.config").utf` configuration 23 | - add filters `utf.lower`, `utf.upper`, `utf.truncate` 24 | - parsing localized dates 25 | - Internal improvements. 26 | 27 | Date: 28 | 29 | - Move `aspect.utils.date` to `aspect.date` 30 | - UTC time offset now in seconds (instead of minutes) 31 | - When formatting a date, if a time zone is not specified, then formatting will occur for the local time zone 32 | - Add date localization. 33 | 34 | 1.14 35 | ---- 36 | 37 | - Add `aspect:eval(...)` method 38 | - Add benchmark `benchmark/bench-01.lua` 39 | - Improve compiler performance. 40 | - Update docs 41 | 42 | 1.13 43 | ---- 44 | 45 | - Add `apply` tag 46 | - Run docs on github pages [aspect.unifire.app](https://aspect.unifire.app/) 47 | - Reformat docs 48 | - Add option `--dump` for CLI command (useful for debug) 49 | - Dynamic keys now works (`a[b]` or `a['b' ~ c]`) 50 | 51 | 1.12 52 | ---- 53 | 54 | - Add date utils `aspect.utils.date`. 55 | - Remove `date` dependency. 56 | - More tests and docs. 57 | - Bugfix. 58 | 59 | 1.11 60 | ---- 61 | 62 | - Improve tokenizer performance. Remove `pl.lexer` from `aspect.tokenizer`. 63 | - Improve filters performance. Remove `pl.tablex` and `pl.stringx` from `aspect.filters`. 64 | - Remove `penlight` dependency. 65 | - Remove `cjson` dependency. Add autodetect and configuration to `aspect.config`. 66 | - Bugfix. 67 | 68 | 1.10 69 | ---- 70 | 71 | - Add CLI Aspect starter `aspect.cli`. 72 | - Add console tool `bin/aspect`. 73 | - More tests and docs. 74 | - Bugfix. 75 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | name: "Aspect" 2 | description: "Aspect is a compiling template engine for Lua and LuaJIT." 3 | encoding: UTF-8 4 | markdown: kramdown 5 | url: "https://unifire-app.github.io/" 6 | exclude: 7 | - src 8 | - bin 9 | remote_theme: bzick/jekyll-docs-theme 10 | analytics: 11 | google: UA-164241665-1 12 | yandex: 64680703 13 | 14 | # theme configuration 15 | # #373737 #EE722E 16 | 17 | ui: 18 | mode: 'light' 19 | # header: 20 | # light: 21 | # color1: "#373737" 22 | # color2: "#EE722E" 23 | # trianglify: true 24 | 25 | project: 26 | version: 2.2 27 | download_url: https://github.com/unifire-app/aspect/releases 28 | download_text: Download 29 | 30 | license: 31 | software: BSD 3 Clause 32 | software_url: https://opensource.org/licenses/BSD-3-Clause 33 | 34 | docs: CC BY 3.0 35 | docs_url: https://creativecommons.org/licenses/by/3.0/ 36 | 37 | links: 38 | pages: 39 | - title: Syntax 40 | url: ./syntax.html 41 | - title: Tags 42 | url: ./tags.html 43 | - title: Filters 44 | url: ./filters.html 45 | - title: Funcs 46 | url: ./funcs.html 47 | - title: Tests 48 | url: ./tests.html 49 | - title: Spec 50 | url: ./spec.html 51 | - title: API 52 | url: ./api.html 53 | - title: CLI 54 | url: ./cli.html 55 | header: 56 | - title: '' 57 | url: https://travis-ci.org/unifire-app/aspect 58 | - title: '' 59 | url: https://codecov.io/gh/unifire-app/aspect 60 | - title: '' 61 | url: https://luarocks.org/modules/unifire/aspect 62 | footer: 63 | - title: GitHub 64 | icon: github 65 | brand: true 66 | url: https://github.com/unifire-app/aspect 67 | - title: Issues 68 | icon: bug 69 | url: https://github.com/unifire-app/aspect/issues?state=open 70 | homepage: 71 | - title: View on GitHub 72 | icon: github 73 | brand: true 74 | url: https://github.com/unifire-app/aspect 75 | social: 76 | github: 77 | user: unifire-app 78 | repo: aspect 79 | 80 | icons: 81 | favicon: /aspect.png 82 | -------------------------------------------------------------------------------- /docs/assets/aspect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifire-app/aspect/5633e2a071cd5dc1385dae91775335444d99cbd9/docs/assets/aspect.png -------------------------------------------------------------------------------- /docs/assets/luarocks.svg: -------------------------------------------------------------------------------- 1 | LuaRocksLuaRocksAspectAspect -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Command Line 4 | --- 5 | 6 | 7 | 8 | Usage: `aspect [options] data_file template_name`. data_file should be JSON. 9 | 10 | Result outputs to STDOUT. Logs and errors output to STDERR. 11 | 12 | See `aspect --help` for more information. 13 | 14 | Examples 15 | ------- 16 | 17 | Render JSON file to STDOUT or file: 18 | ```bash 19 | aspect path/to/fixture.json path/to/template.tmpl 20 | aspect path/to/fixture.json path/to/template.tmpl >path/to/result.txt 21 | ``` 22 | 23 | Render data from STDIN (using `-`): 24 | 25 | ```bash 26 | aspect - path/to/template.tmpl 27 | ``` 28 | 29 | Read template from STDIN (using `-`): 30 | ```bash 31 | aspect path/to/fixture.json - 32 | ``` 33 | 34 | Lint the template: 35 | ```bash 36 | aspect --lint path/to/template.tmpl 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /docs/dev.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Development 4 | --- 5 | 6 | Versioning 7 | ---------- 8 | 9 | Given a version number `X.Y`, increment the: 10 | - `X` version when you make incompatible API changes 11 | - `Y` version when you add functionality in a backwards compatible manner or bug fix. 12 | 13 | Convention 14 | ---------- 15 | 16 | - [EmmyLua](https://github.com/EmmyLua) with annotations is used for development. 17 | - For unit test is used [busted](https://olivinelabs.com/busted/) 18 | 19 | 20 | Debug 21 | ----- 22 | 23 | Dump (buggy) template: 24 | 25 | ```bash 26 | bin/aspect --dump path/to/template.view 27 | ``` -------------------------------------------------------------------------------- /docs/dev/date.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Components › tokenizer 4 | --- 5 | 6 | Documentation coming soon. See [date.lua](https://github.com/unifire-app/aspect/blob/master/src/aspect/date.lua) -------------------------------------------------------------------------------- /docs/dev/tokenizer.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Components › tokenizer 4 | --- 5 | 6 | Documentation coming soon. See [tokenizer.lua](https://github.com/unifire-app/aspect/blob/master/src/aspect/tokenizer.lua) -------------------------------------------------------------------------------- /docs/filters.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters 4 | --- 5 | 6 | 7 | 8 | * [abs](./filters/abs.md) 9 | * [batch(size)](./filters/batch.md) 10 | * [column(column)](./filters/columns.md) 11 | * [date(format)](./filters/date.md) 12 | * [date_modify(offset)](./filters/date_modify.md) 13 | * [default(value, boolean)](./filters/default.md) 14 | * [escape(type), e(type)](./filters/escape.md) 15 | * [first](./filters/first.md) 16 | * [format(...)](./filters/format.md) 17 | * [join(delim, last_delim)](./filters/join.md) 18 | * [json_encode](./filters/json_encode.md) 19 | * [keys](./filters/keys.md) 20 | * [last](./filters/last.md) 21 | * [length](./filters/length.md) 22 | * [lower](./filters/lower.md) 23 | * [merge(items)](./filters/merge.md) 24 | * [nl2br](./filters/nl2br.md) 25 | * [raw](./filters/raw.md) 26 | * [replace(what)](./filters/replace.md) 27 | * [split(delimiter, limit)](./filters/split.md) 28 | * [striptags](./filters/striptags.md) 29 | * [trim](./filters/trim.md) 30 | * [truncate](./filters/truncate.md) 31 | * [upper](./filters/lower.md) 32 | * [utf.lower](./filters/utf.lower.md) 33 | * [utf.length](./filters/utf.length.md) 34 | * [utf.truncate(length, ending)](./filters/utf.truncate.md) 35 | * [utf.upper](./filters/utf.upper.md) 36 | 37 | [Add filters](./api.md#add-filters) 38 | 39 | -------------------------------------------------------------------------------- /docs/filters/abs.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › abs 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `abs`. 11 | * no args 12 | --- 13 | 14 | The `abs` filter returns the absolute value. 15 | 16 | ```twig 17 | {# number = -5 #} 18 | 19 | {{ number|abs }} 20 | 21 | {# outputs 5 #} 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /docs/filters/batch.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › batch 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `batch(size)`: 11 | * `size`: The size of the batch; fractional numbers will be rounded up 12 | 13 | --- 14 | 15 | The `batch` filter "batches" items by returning a list of lists with the given number of items: 16 | 17 | ```twig 18 | {% set items = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] %} 19 | 20 | 21 | {% for row in items|batch(3) %} 22 | 23 | {% for column in row %} 24 | 25 | {% endfor %} 26 | 27 | {% endfor %} 28 |
{{ column }}
29 | ``` 30 | The above example will be rendered as: 31 | 32 | ```html 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
abc
def
g
48 | ``` 49 | 50 | -------------------------------------------------------------------------------- /docs/filters/columns.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › columns 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `column(name)`: 11 | * `name`: The column name to extract 12 | 13 | --- 14 | 15 | The `column` filter returns the values from a single column in the input sequence. 16 | 17 | ```twig 18 | {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %} 19 | 20 | {% set fruits = items|column('fruit') %} 21 | 22 | {# fruits now contains ['apple', 'orange'] #} 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /docs/filters/date.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › date 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `date(format)`: 11 | * `format`: The date format 12 | 13 | --- 14 | 15 | Parsing 16 | ------- 17 | 18 | Value must have number/words representing date and/or time. 19 | Use commas and spaces as delimiters. 20 | The stated day of the week is ignored whether its correct or not. A string containing an invalid date is an error. 21 | For example, a string containing two years or two months is an error. 22 | Time must be supplied if date is not given, vice versa. 23 | 24 | **Time Format.** Hours, minutes, and seconds are separated by colons, although all need not be specified. 25 | "10:11", and "10:11:12" are all valid. 26 | If the 24-hour clock is used, it is an error to specify "PM" for times later than 12 noon. 27 | For example, "23:15 PM" is an error. 28 | 29 | **Time Zone Format.** First character is a sign "+" (to the east) or "-" (to the west). 30 | Hours and minutes in the UTC offset can be separated by a colon. 31 | Another format is `[sign][number]`. If `[number]` is less than 24, it is the offset in hours e.g. "-10" = -10 hours. 32 | 33 | **Supported Format.** 34 | 35 | * `YYYY-MM-DD`, `MM/DD/YYYY`, `MMMM DD YYYY`, `DD MMMM YYYY` — where YYYY is the year, MM is the month of the year, 36 | MMMM is the month full name or abbr, and DD is the day of the month. 37 | * `DATE HH:MM:SS` — Where DATE is the date format discuss above, HH is the hour, 38 | MM is the miute, SS is the seconds. 39 | * `DATE TIME +HH:MM`, `DATE TIME -HHMM`, `DATE TIME UTC` 40 | 41 | [Add your date formats](./../api.md#date-parser). 42 | 43 | **Parsable month value.** 44 | 45 | If a function needs a month value it must be a string or a number. 46 | If the month is a string, it must be the name of the month full or abbreviated. 47 | If the month is a number, that number must be 1-12 (January-December). 48 | 49 | [Add localization of months](./../api.md#date-localization). 50 | 51 | Formatting 52 | ---------- 53 | 54 | The `format` string follows the same rules as the `strftime` standard C function. 55 | 56 | | Spec | Description | Examples | 57 | |------|-------------|----------| 58 | | **Day** | | | 59 | | `%a` | Abbreviated weekday name | `Sun`, `Mon` | 60 | | `%A` | Full weekday name | `Sunday` | 61 | | `%d` | The day of the month as a number (range 1 - 31) | `1`, `28` | 62 | | `%j` | The day of the year as a number (001 - 366) | `052`, `230` | 63 | | `%u` | ISO 8601 day of the week, to 7 for Sunday | `7`, `1` | 64 | | `%w` | The day of the week as a decimal, Sunday being 0 | `1` | 65 | | **Week** | | | 66 | | `%U` | Sunday week of the year, from 00 | `48` | 67 | | `%V` | ISO 8601 week of the year, from 01 | `48` | 68 | | `%W` | Monday week of the year, from 00 | `48` | 69 | | **Month** | | | 70 | | `%b` | Abbreviated month name | `Dec`, `Jan` | 71 | | `%B` | Full month name | `December` | 72 | | `%m` | Month of the year, from 01 to 12 | `05`, `11` | 73 | | **Year** | | | 74 | | `%C` | Two digit representation of the century | `19`, `20`, `30` | 75 | | `%g` | year for ISO 8601 week, from 00 | `79` | 76 | | `%G` | year for ISO 8601 week, from 0000 | `1979` | 77 | | `%y` | Two digit representation of the year | `00`, `25` | 78 | | `%Y` | Four digit representation for the year | `2000`, `0325` | 79 | | **Time** | | | 80 | | `%H` | hour of the 24-hour day, from 00 | `06` | 81 | | `%I` | The hour as a number using a 12-hour clock (01 - 12) | `02`, `10` | 82 | | `%M` | Minutes after the hour | `55` | 83 | | `%p` | AM/PM indicator | `AM`, `PM` | 84 | | `%s` | Unix Epoch Time timestamp | `305815200`, `1234567890` | 85 | | `%S` | The second as a number | `59`, `20`, `01` | 86 | | `%z` | Time zone offset as 'big number' | `+1000`, `-0230` | 87 | | `%Z` | Time zone offset as 'short number' | `+3`, `+1:30` | 88 | | **Misc** | | | 89 | | `%%` | percent character | `%` | 90 | | `%n` | A newline character (`\n`) | | 91 | | `%t` | A Tab character (`\t`) | | 92 | 93 | 94 | **Aliases**: 95 | 96 | | Spec | Description | Format | Example | 97 | |------|-------------|--------|---------| 98 | | `$c` | Preferred date | `%a %b %d %H:%m%s %Y` | `Tue Jun 23 15:45:01 2020` | 99 | | `$r` | 12-hour time, from 01:00:00 AM | `%I:%M:%S %p` | `06:55:15 AM` | 100 | | `$R` | 12-hour:minute, from 01:00 | `%I:%M` | `06:55` | 101 | | `$T` | 24-hour time, from 00:00:00 | `%H:%M:%S` | `06:55:15` | 102 | | `$D` | month/day/year from 01/01/00 | `%m/%d/%y` | `12/02/79` | 103 | | `$F` | year-month-day | `%Y-%m-%d` | `1979-12-02` | 104 | 105 | 106 | [Add more aliases](./../api.md#date-format-aliases) 107 | -------------------------------------------------------------------------------- /docs/filters/date_modify.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › date_modify 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `date_modify`: 11 | * no args 12 | 13 | --- 14 | 15 | The `date_modify` filter modifies a [date](./date.md) with a given modifier values: 16 | 17 | ```twig 18 | {{ post.published_at|date_modify({days: -2})|date("Y-m-d") }} 19 | ``` 20 | 21 | Positive number increase value, negative value decrease value. 22 | 23 | Possible modifiers: 24 | 25 | | keys | action | 26 | |------|--------| 27 | | days, day | incr or decr days | 28 | | hours, hour | incr or decr hours | 29 | | minutes, minute, mins, min | incr or decr minutes | 30 | | seconds, second, secs, sec | incr or decr seconds | 31 | | months, month | incr or decr months | 32 | | years, year | incr or decr years | 33 | 34 | -------------------------------------------------------------------------------- /docs/filters/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › default 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `default(default, boolean)`: 11 | * `default`: The default value 12 | * `boolean`: Cast the value to boolean (default: `false`) 13 | 14 | --- 15 | 16 | The `default` filter returns the passed default value if the value is undefined or empty, otherwise the value of the variable: 17 | 18 | ```twig 19 | {{ var|default('var is not defined') }} 20 | 21 | {{ var.foo|default('foo item on var is not defined') }} 22 | 23 | {{ var['foo']|default('foo item on var is not defined') }} 24 | ``` 25 | 26 | If you want to use default with variables that evaluate to false you have to set the second parameter to `true`: 27 | 28 | ```twig 29 | {{ ''|default('passed var is empty', true) }} 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /docs/filters/escape.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › escape 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `escape(strategy)`: 11 | * `strategy`: The escaping strategy. By default is `html`. 12 | 13 | --- 14 | 15 | The `escape` filter escapes a string using strategies that depend on the context. 16 | 17 | By default, it uses the HTML escaping strategy: 18 | 19 | ```twig 20 |

21 | {{ user.username|escape }} 22 |

23 | ``` 24 | 25 | For convenience, the `e` filter is defined as an alias: 26 | 27 | ```twig 28 |

29 | {{ user.username|e }} 30 |

31 | ``` 32 | 33 | The escape filter can also be used in other contexts than HTML thanks to an optional 34 | argument which defines the escaping strategy to use: 35 | 36 | ```twig 37 | {{ user.username|e }} 38 | {# is equivalent to #} 39 | {{ user.username|e('html') }} 40 | ``` 41 | 42 | And here is how to escape variables included in JavaScript code: 43 | 44 | ```twig 45 | {{ user.username|escape('js') }} 46 | {{ user.username|e('js') }} 47 | ``` 48 | 49 | The `escape` filter supports the following escaping strategies for HTML documents: 50 | 51 | * `html`: escapes a string for the **HTML body** context. 52 | * `js`: escapes a string for the **JavaScript** context. 53 | * `url`: escapes a string for the **URI or parameter** contexts. 54 | This should not be used to escape an entire URI; only a subcomponent being inserted. 55 | 56 | Also you cat [add your custom escaper](./api.md#custom-escaper). 57 | 58 | Built-in escapers cannot be overridden mainly because they should be considered 59 | as the final implementation and also for better performance. 60 | 61 | -------------------------------------------------------------------------------- /docs/filters/first.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › first 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `first`: 11 | * no args 12 | 13 | --- 14 | 15 | The `first` filter returns the first "element" of a sequence, a mapping, or a string: 16 | 17 | ```twig 18 | {{ [1, 2, 3, 4]|first }} 19 | {# outputs 1 #} 20 | 21 | {{ { a: 1, b: 2, c: 3, d: 4 }|first }} 22 | {# outputs 1 #} 23 | 24 | {{ '1234'|first }} 25 | {# outputs 1 #} 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /docs/filters/format.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › format 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `format(...)`: 11 | * `...` — any values 12 | 13 | --- 14 | 15 | The `format` filter formats a given string by replacing the placeholders. 16 | 17 | ```twig 18 | {{ "I like %s and %s."|format(foo, "bar") }} 19 | 20 | {# outputs I like foo and bar 21 | if the foo parameter equals to the foo string. #} 22 | ``` 23 | 24 | Placeholders follows the printf c notation: 25 | 26 | | specifier | Output | Example | 27 | |-----------|--------|---------| 28 | | %d or %i | Signed decimal integer | 392 | 29 | | %u | Unsigned decimal integer | 7235 | 30 | | %o | Unsigned octal | 610 | 31 | | %x | Unsigned hexadecimal integer | 7fa | 32 | | %X | Unsigned hexadecimal integer (uppercase) | 7FA | 33 | | %f | Decimal floating point, lowercase | 392.65 | 34 | | %F | Decimal floating point, uppercase | 392.65 | 35 | | %e | Scientific notation (mantissa/exponent), lowercase | 3.9265e+2 | 36 | | %E | Scientific notation (mantissa/exponent), uppercase | 3.9265E+2 | 37 | | %g | Use the shortest representation: %e or %f | 392.65 | 38 | | %G | Use the shortest representation: %E or %F | 392.65 | 39 | | %a | Hexadecimal floating point, lowercase | -0xc.90fep-2 | 40 | | %A | Hexadecimal floating point, uppercase | -0XC.90FEP-2 | 41 | | %c | Character | a | 42 | | %s | String of characters | sample | 43 | | %n | Nothing printed. The corresponding argument must be a pointer to a signed int. The number of characters written so far is stored in the pointed location. | 44 | | %% | A % followed by another % character will write a single % to the stream. | % | 45 | 46 | -------------------------------------------------------------------------------- /docs/filters/join.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › join 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `join(glue, last)`: 11 | * `glue`: The separator 12 | * `last`: The separator for the last pair of input items 13 | 14 | --- 15 | 16 | The `join` filter returns a string which is the concatenation of the items of a sequence: 17 | 18 | ```twig 19 | {{ [1, 2, 3]|join }} 20 | {# returns 123 #} 21 | ``` 22 | 23 | The separator between elements is an empty string per default, but you can define it with the optional first parameter: 24 | 25 | ```twig 26 | {{ [1, 2, 3]|join('|') }} 27 | {# outputs 1|2|3 #} 28 | ``` 29 | 30 | A second parameter can also be provided that will be the separator used between the last two items of the sequence: 31 | 32 | ```twig 33 | {{ [1, 2, 3]|join(', ', ' and ') }} 34 | {# outputs 1, 2 and 3 #} 35 | ``` 36 | 37 | -------------------------------------------------------------------------------- /docs/filters/json_encode.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › json_encode 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | The `json_encode` filter returns the JSON representation of a value: 11 | 12 | ```twig 13 | {{ data|json_encode() }} 14 | ``` 15 | 16 | For this filter you have to install any json package (`cjson`, `json`) or configure `aspect.config` manually. 17 | 18 | -------------------------------------------------------------------------------- /docs/filters/keys.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › keys 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter: `keys`: 11 | * no args 12 | 13 | --- 14 | 15 | The `keys` filter returns the keys of an array. 16 | It is useful when you want to iterate over the keys of an array: 17 | 18 | ```twig 19 | {% for key in array|keys %} 20 | ... 21 | {% endfor %} 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /docs/filters/last.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › last 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter: `last`: 11 | * no args 12 | 13 | --- 14 | 15 | The last filter returns the last "element" of a sequence, a mapping, or a string: 16 | 17 | ```twig 18 | {{ [1, 2, 3, 4]|last }} 19 | {# outputs 4 #} 20 | 21 | {{ { a: 1, b: 2, c: 3, d: 4 }|last }} 22 | {# outputs 4 #} 23 | 24 | {{ '1234'|last }} 25 | {# outputs 4 #} 26 | ``` 27 | 28 | **Note.** If the object has the `__pairs` function, then it will be used to search for the last element. 29 | 30 | -------------------------------------------------------------------------------- /docs/filters/length.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › length 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter: `length`: 11 | * no args 12 | 13 | --- 14 | 15 | The `length` filter returns the number of items of a sequence or mapping, or the length of a string. 16 | 17 | ```twig 18 | {% if users|length > 10 %} 19 | ... 20 | {% endif %} 21 | ``` 22 | 23 | `length` behavior: 24 | 25 | * string: count of bytes (this is not count of symbols) 26 | * table with `__len` meta function: result of `table:__len()` 27 | * table with `__pairs` meta function: count of elements `table:__pairs()` 28 | * table: count of elements 29 | * other: always `0` 30 | 31 | -------------------------------------------------------------------------------- /docs/filters/lower.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › lower 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | Filter `lower`: 9 | * no args 10 | 11 | --- 12 | 13 | 14 | 15 | The `lower` filter converts a value to lowercase: 16 | 17 | ```twig 18 | {{ 'WELCOME'|lower }} 19 | 20 | {# outputs 'welcome' #} 21 | ``` 22 | 23 | **Note.** For UTF8 strings you have to install [luautf8](https://luarocks.org/modules/dannote/utf8) package 24 | 25 | -------------------------------------------------------------------------------- /docs/filters/merge.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › merge 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `merge(array)`: 11 | * `array` — table or iterator to merge. 12 | 13 | --- 14 | 15 | The `merge` filter merges an array or hash with another array or hash: 16 | 17 | ```twig 18 | {% set values = [1, 2] %} 19 | 20 | {% set values = values|merge(['apple', 'orange']) %} 21 | 22 | {# values now contains [1, 2, 'apple', 'orange'] #} 23 | ``` 24 | 25 | New values are added at the end of the existing ones. 26 | 27 | The `merge` filter also works on hashes: 28 | 29 | ```twig 30 | {% set items = { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'unknown' } %} 31 | 32 | {% set items = items|merge({ 'peugeot': 'car', 'renault': 'car' }) %} 33 | 34 | {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'renault': 'car' } #} 35 | ``` 36 | 37 | For hashes, the merging process occurs on the keys: if the key does not already exist, it is added but if the key already exists, its value is overridden. 38 | 39 | If you want to ensure that some values are defined in an array (by given default values), reverse the two elements in the call: 40 | 41 | ```twig 42 | {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} 43 | 44 | {% set items = { 'apple': 'unknown' }|merge(items) %} 45 | 46 | {# items now contains { 'apple': 'fruit', 'orange': 'fruit' } #} 47 | ``` 48 | 49 | -------------------------------------------------------------------------------- /docs/filters/nl2br.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › nl2br 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `nl2br`: 11 | * no args 12 | 13 | --- 14 | 15 | The `nl2br` filter inserts HTML line breaks before all newlines in a string: 16 | 17 | ```twig 18 | {{ "I like Aspect.\nYou will like it too."|nl2br }} 19 | {# outputs 20 | 21 | I like Aspect.
22 | You will like it too. 23 | 24 | #} 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /docs/filters/raw.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › raw 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `raw`: 11 | * no args 12 | 13 | --- 14 | 15 | The `raw` filter marks the value as being "safe", which means that in an environment with automatic escaping enabled 16 | this variable will not be escaped if raw is the last filter applied to it: 17 | 18 | ```twig 19 | {% autoescape %} 20 | {{ var|raw }} {# var won't be escaped #} 21 | {% endautoescape %} 22 | ``` 23 | 24 | **Note.** Be careful when using the `raw` filter inside expressions: 25 | ```twig 26 | {% autoescape %} 27 | {% set hello = 'Hello' %} 28 | {% set hola = 'Hola' %} 29 | 30 | {{ false ? 'Hola' : hello|raw }} 31 | does not render the same as 32 | {{ false ? hola : hello|raw }} 33 | but renders the same as 34 | {{ (false ? hola : hello)|raw }} 35 | {% endautoescape %} 36 | ``` 37 | The first ternary statement is not escaped: `hello` is marked as being safe and Aspect does not escape static values (see [escape](./escape.md)). 38 | In the second ternary statement, even if `hello` is marked as safe, `hola` remains unsafe and so is the whole expression. 39 | The third ternary statement is marked as safe and the result is not escaped. 40 | 41 | -------------------------------------------------------------------------------- /docs/filters/replace.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › replace 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `replace(from)`: 11 | * from: The placeholder values 12 | 13 | --- 14 | 15 | The replace filter formats a given string by replacing the placeholders (placeholders are free-form): 16 | 17 | ```twig 18 | {{ "I like %this% and %that%."|replace({'%this%': foo, '%that%': "bar"}) }} 19 | 20 | {# outputs I like foo and bar 21 | if the foo parameter equals to the foo string. #} 22 | 23 | {# using % as a delimiter is purely conventional and optional #} 24 | 25 | {{ "I like this and --that--."|replace({'this': foo, '--that--': "bar"}) }} 26 | 27 | {# outputs I like foo and bar #} 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /docs/filters/split.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › split 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `split(delimiter, limit)`: 11 | * `delimiter`: The delimiter 12 | * `limit`: The limit argument 13 | 14 | --- 15 | 16 | The `split` filter splits a string by the given delimiter and returns a list of strings: 17 | 18 | ```twig 19 | {% set foo = "one,two,three"|split(',') %} 20 | {# foo contains ['one', 'two', 'three'] #} 21 | ``` 22 | 23 | If `limit` is set, the returned array will contain a maximum of limit elements with the last element containing the rest of string; 24 | 25 | ```twig 26 | {% set foo = "one,two,three,four,five"|split(',', 3) %} 27 | {# foo contains ['one', 'two', 'three,four,five'] #} 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /docs/filters/striptags.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › striptags 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `striptags`: 11 | * no args 12 | 13 | --- 14 | 15 | The `striptags` filter strips SGML/XML tags and replace adjacent whitespace by one space: 16 | 17 | ```twig 18 | {{ some_html|striptags }} 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /docs/filters/trim.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › trim 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `trim(character_mask, side)`: 11 | * `character_mask`: The characters to strip. 12 | * `side`: The default is to strip from the left and the right (both) sides, 13 | but left and right will strip from either the left side or right side only. 14 | 15 | --- 16 | 17 | The `trim` filter strips whitespace (or other characters) from the beginning and end of a string: 18 | 19 | ```twig 20 | {{ ' I like Aspect. '|trim }} 21 | 22 | {# outputs 'I like Aspect.' #} 23 | 24 | {{ ' I like Aspect.'|trim('.') }} 25 | 26 | {# outputs ' I like Aspect' #} 27 | 28 | {{ ' I like Aspect. '|trim(side='left') }} 29 | 30 | {# outputs 'I like Aspect. ' #} 31 | 32 | {{ ' I like Aspect. '|trim(' ', 'right') }} 33 | 34 | {# outputs ' I like Aspect.' #} 35 | ``` 36 | 37 | -------------------------------------------------------------------------------- /docs/filters/truncate.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › truncate 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `truncate(length, ending)`: 11 | * `length`: This determines how many characters to truncate to. 12 | * `ending`: This is a text string that replaces the truncated text. Its length is NOT included in the truncation length setting. 13 | 14 | --- 15 | 16 | This truncates a string to a character length. 17 | As an optional second parameter, you can specify a string of text to display at the end if the sting was truncated. 18 | 19 | **Note** `truncate` filter works only with ascii symbols. 20 | For UTF8 string use `utf.truncate` filter (requires [utf8 module](./../api.md#configure-utf8)) 21 | 22 | ```twig 23 | {{ long_title }} 24 | {{ long_title|truncate }} 25 | {{ long_title|truncate(30) }} 26 | {{ long_title|truncate(30, "") }} 27 | {{ long_title|truncate(30, "---") }} 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /docs/filters/upper.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › upper 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Filter `upper` 11 | * no args 12 | 13 | --- 14 | 15 | The `upper` filter converts a value to uppercase: 16 | 17 | ```twig 18 | {{ 'welcome'|upper }} 19 | 20 | {# outputs 'WELCOME' #} 21 | ``` 22 | 23 | **Note.** For UTF8 strings you have to install [luautf8](https://luarocks.org/modules/dannote/utf8) package 24 | 25 | -------------------------------------------------------------------------------- /docs/filters/utf.length.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › utf.length 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Same as [length](./length.md), but for unicode strings. 11 | Requires [utf8 module](./../api.md#configure-utf8) 12 | 13 | -------------------------------------------------------------------------------- /docs/filters/utf.lower.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › utf.lower 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Same as [lower](./lower.md), but for unicode strings. 11 | Requires [utf8 module](./../api.md#configure-utf8) 12 | 13 | -------------------------------------------------------------------------------- /docs/filters/utf.truncate.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › utf.truncate 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Same as [truncate](./truncate.md), but for unicode strings. 11 | Requires [utf8 module](./../api.md#configure-utf8) 12 | 13 | -------------------------------------------------------------------------------- /docs/filters/utf.upper.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Filters › utf.upper 4 | --- 5 | 6 | [← filters](./../filters.md) 7 | 8 | 9 | 10 | Same as [upper](./upper.md), but for unicode strings. 11 | Requires [utf8 module](./../api.md#configure-utf8) 12 | 13 | -------------------------------------------------------------------------------- /docs/funcs.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Functions 4 | --- 5 | 6 | 7 | 8 | * [block(name, template)](./tags/extends.md#function-block) 9 | * [date(date)](./funcs/date.md) 10 | * [dump()](./funcs/dump.md) 11 | * [include()](./funcs/include.md) 12 | * [parent()](./tags/extends.md#function-parent) 13 | * [range(low, high, step)](./funcs/range.md) 14 | 15 | [Add functions](./api.md#add-functions) 16 | 17 | -------------------------------------------------------------------------------- /docs/funcs/date.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Functions › date 4 | --- 5 | 6 | [← functions](./../funcs.md) 7 | 8 | 9 | 10 | Converts an argument to a date to allow date comparison: 11 | 12 | ```twig 13 | {% if date(user.created_at) < date({days: -2}) %} 14 | {# do something #} 15 | {% endif %} 16 | ``` 17 | 18 | The argument must be in one supported [parsable date and time formats](../filters/date.md#parsing). 19 | or [date and time interval](../filters/date_modify.md) 20 | 21 | If no argument is passed, the function returns the current date: 22 | 23 | ```twig 24 | {% if date(user.created_at) < date() %} 25 | {# always! #} 26 | {% endif %} 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /docs/funcs/dump.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Functions › dump 4 | --- 5 | 6 | [← functions](./../funcs.md) 7 | 8 | 9 | 10 | The dump function dumps information about a template variable. 11 | This is mostly useful to debug a template that does not behave as expected by introspecting its variables: 12 | 13 | 14 | ```twig 15 | {{ dump(user) }} 16 | ``` 17 | 18 | In an HTML context, wrap the output with a `pre` tag to make it easier to read: 19 | 20 | ```twig 21 |
22 |     {{ dump(user) }}
23 | 
24 | ``` 25 | 26 | Hide dump with HTML comments in the HTML document: 27 | 28 | ```twig 29 | 32 | ``` 33 | 34 | You can debug several variables by passing them as additional arguments: 35 | 36 | ```twig 37 | {{ dump(user, categories) }} 38 | ``` 39 | 40 | If you don't pass any value, all variables from the current context are dumped: 41 | 42 | ```twig 43 | {{ dump() }} 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /docs/funcs/env.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Functions › env 4 | --- 5 | 6 | [← functions](./../env.md) 7 | 8 | 9 | 10 | `env` returns the environment value for a given string: 11 | 12 | ```twig 13 | {{ some_date|date(env('config.date.format')) }} 14 | ``` 15 | 16 | All environment values set using the [env option](./../api.md#options) when creating 17 | a template engine or when starting a template render 18 | 19 | Use the `defined` test to check if a environment value is defined: 20 | 21 | ```twig 22 | {% if env('req.args.page') is defined %} 23 | ... 24 | {% endif %} 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /docs/funcs/include.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Functions › include 4 | --- 5 | 6 | [← functions](./../funcs.md) 7 | 8 | 9 | 10 | The `include` function returns the rendered content of a template: 11 | 12 | ```twig 13 | {{ include('template.html') }} 14 | {{ include(some_var) }} 15 | ``` 16 | 17 | Included templates have access to the variables of the active context. 18 | 19 | If you are using the filesystem loader, the templates are looked for in the paths defined by it. 20 | 21 | The context is passed by default to the template but you can also pass additional variables: 22 | 23 | ```twig 24 | {# template.html will have access to the variables from the current context and the additional ones provided #} 25 | {{ include('template.html', {foo: 'bar'}) }} 26 | ``` 27 | 28 | You can disable access to the context by setting with_context to false: 29 | 30 | ```twig 31 | {# only the foo variable will be accessible #} 32 | {{ include('template.html', {foo: 'bar'}, with_context = false) }} 33 | ``` 34 | 35 | ```twig 36 | {# no variables will be accessible #} 37 | {{ include('template.html', with_context = false) }} 38 | ``` 39 | 40 | When you set the ignore_missing flag, Twig will return an empty string if the template does not exist: 41 | 42 | ```twig 43 | {{ include('sidebar.html', ignore_missing = true) }} 44 | ``` 45 | 46 | You can also provide a list of templates that are checked for existence before inclusion. 47 | The first template that exists will be rendered: 48 | 49 | ```twig 50 | {{ include(['page_detailed.html', 'page.html']) }} 51 | ``` 52 | 53 | If `ignore_missing` is set, it will fall back to rendering nothing if none of the templates exist, otherwise it will throw an exception. 54 | 55 | -------------------------------------------------------------------------------- /docs/funcs/range.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Functions › range 4 | --- 5 | 6 | [← functions](./../funcs.md) 7 | 8 | 9 | 10 | Returns a list containing an arithmetic progression of integers: 11 | 12 | ```twig 13 | {% for i in range(0, 3) %} 14 | {{ i }}, 15 | {% endfor %} 16 | 17 | {# outputs 0, 1, 2, 3, #} 18 | ``` 19 | 20 | When step is given (as the third parameter), it specifies the increment (or decrement for negative values): 21 | 22 | ```twig 23 | {% for i in range(0, 6, 2) %} 24 | {{ i }}, 25 | {% endfor %} 26 | 27 | {# outputs 0, 2, 4, 6, #} 28 | ``` 29 | 30 | **Note** that if the start is greater than the end, range assumes a step of -1: 31 | ```twig 32 | {% for i in range(3, 0) %} 33 | {{ i }}, 34 | {% endfor %} 35 | 36 | {# outputs 3, 2, 1, 0, #} 37 | ``` 38 | 39 | The `range` function returns the table with iterator (`__pairs`). 40 | 41 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: full 3 | homepage: true 4 | disable_anchors: true 5 | description: Aspect is a compiling template engine for Lua and LuaJIT. 6 | --- 7 | 8 | 9 | 10 | **Aspect** a compiling (to Lua code and bytecode) a template engine for Lua and LuaJIT. Adapted to work with OpenResty and Tarantool. 11 | Lua itself is a simple language, but with many limitations and conventions. 12 | 13 | Aspect makes it easy to work with Lua, letting you focus on the design of templates. 14 | Template syntax is very popular. This syntax is used in Twig, Jinja, Django, Liquid. 15 | 16 | **Yet another template engine?** Yes. But more powerful than any other template engines. Just check out all the [features](#features). 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | ## Synopsis 25 | {:.mt-lg-0} 26 | 27 | [Template syntax](./syntax.md): 28 | 29 | ```twig 30 | 31 | 32 | 33 | {% block head %} 34 | {{ page.title }} 35 | {% endblock %} 36 | 37 | 38 | {% block content %} 39 | 46 | 47 |

My Webpage

48 | {{ page.body }} 49 | {% endblock %} 50 |
{% include "footer.view" %}
51 | 52 | 53 | ``` 54 | 55 | [Lua API](./api.md): 56 | 57 | ```lua 58 | local aspect = require("aspect.template").new(options) 59 | local result, error 60 | = aspect:eval("
Hello, {{ username }}
", vars) 61 | ``` 62 | 63 | [Command line](./cli.md): 64 | 65 | ```bash 66 | $ aspect /path/to/data.json /path/to/template.tpl 67 | ``` 68 | 69 |
70 |
71 | 72 | ## Installation 73 | {:.mt-lg-0} 74 | 75 | **Using LuaRocks** 76 | 77 | Installing Aspect using [LuaRocks](https://luarocks.org) is simple: 78 | 79 | ```bash 80 | luarocks install aspect 81 | ``` 82 | 83 | **Without LuaRocks** 84 | 85 | Or add `src/?.lua` to `package.path`: 86 | 87 | ```lua 88 | package.path = '/path/to/aspect/src/?.lua;' .. package.path 89 | ``` 90 | 91 | ## Documentation links 92 | 93 |
94 |
95 | 96 | **For template designers:** 97 | 98 | - [Specification](./spec.md) 99 | - [Template Syntax](./syntax.md) 100 | - [Operators](./syntax.md#operators) 101 | - [Tags](./tags.md) 102 | - [Filters](./filters.md) 103 | - [Functions](./funcs.md) 104 | - [Tests](./tests.md) 105 | 106 |
107 |
108 | 109 | **For developers:** 110 | 111 | - [Lua API](./api.md) 112 | - [CLI](./cli.md) 113 | - [Extending](./api.md#extending) 114 | - [Changelog](https://github.com/unifire-app/aspect/blob/master/changelog.md) 115 | 116 |
117 |
118 | 119 | ## Development 120 | 121 | - [Convention](./dev.md#convention) 122 | - [Code of conduct](https://github.com/unifire-app/aspect/blob/master/CODE_OF_CONDUCT.md) 123 | - [Debugging](./dev.md#debug) 124 | - Components 125 | - [Tokenizer](./dev/tokenizer.md) 126 | - [Date](./dev/date.md) 127 | 128 |
129 |
130 | 131 | ## Features 132 | 133 | * _Well known_: Aspect uses the most popular template syntax. 134 | [Twig](https://twig.symfony.com/) for PHP (maximum compatibility), [Jinja](https://jinja.palletsprojects.com/) for Python (minor differences), [Liquid](https://shopify.github.io/liquid/) for Ruby (minor syntax differences). 135 | * _Fast_: Aspect compiles templates optimized Lua code. 136 | Moreover, Lua code compiles into bytecode - the fastest representation of a template. 137 | Aspect will translate your template sources on first load into Lua bytecode for best runtime performance. 138 | * _Safe_: Aspect always runs templates in the sandbox (isolated environment). 139 | This allows Aspect to be used as a template language for applications where users may modify the template design. 140 | * _Flexible_: Aspect is powered by a flexible parser and compiler. 141 | It allows developers to define their own custom [tags](api.md#add-tags), 142 | [filters](api.md#add-filters), [functions](api.md#add-functions), [tests](api.md#add-tests) and [operators](api.md#add-operators). 143 | * _Supports_ lua 5.1/5.2/5.3/5.4, luajit 2.0/2.1 (including OpenResty). 144 | * _Convenient_. Aspect makes it easy for users to work with templates. 145 | Aspect has [automatic type casting](spec.md#working-with-strings), [automatic checking](spec.md#working-with-keys) of a variable and its keys, 146 | it changes some [annoying behavior](spec.md) of lua. 147 | * _CLI_: Aspect has a [console command](./cli.md) for the template engine. 148 | Generate configs and other files using popular syntax. 149 | * _Cache_. Aspect has a [multi-level cache](./api.md#cache). 150 | * _No dependencies_. No FFI. Pure Lua. 151 | * _Secure_. Aspect has powerful [automatic HTML escaping](./syntax.md#html-escaping) system for cross site scripting prevention. 152 | * [Template inheritance](./syntax.md#template-inheritance) makes it possible to use the same or a similar layout for all templates. 153 | * Aspect provides a convenient [debugging](api.md#debug-templates) process. 154 | * [Iterator and countable objects](./api.md#iterator-and-countable-objects) are supported. 155 | * [Date is supported](./filters/date.md). 156 | * [Chain rendering](./api.md#rendering-templates) (renders data chunk by chunk). 157 | 158 | 159 | -------------------------------------------------------------------------------- /docs/spec.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Specification 4 | --- 5 | 6 | 7 | 8 | - **What is it**: templater 9 | - **Syntax**: twig, jinja, django, liquid 10 | - **Language**: Lua 5.1+ 11 | - **Dependency**: none 12 | - **Compiler**: [simple tokenizer](https://github.com/unifire-app/aspect/blob/master/src/aspect/tokenizer.lua) + [AST](https://github.com/unifire-app/aspect/blob/master/src/aspect/ast.lua) 13 | - **Unittest**: [busted](https://olivinelabs.com/busted/) 14 | 15 | ## Limitations 16 | 17 | - New (local) variables cannot be called by the following names (by internal limitation): `and`, `break`, `do`, `else`, `elseif`, 18 | `end`, `false`, `for`, `function`, `if`, `in`, `local`, `nil`, `not`, `or`, `repeat`, `return`, `then`, 19 | `true`, `until`, `while`. These keywords generate error during compile. 20 | 21 | ## Working with keys 22 | 23 | Keys sequence `a.b.c` returns `nil` if variable `a` or any other keys (`b` or `c`) doesn't exits. 24 | The sequence of keys `{{ a.b.c }}` may be represented as lua code 25 | ```lua 26 | if a and is_table(a) and a.b and is_table(a.b) and a.b.c then 27 | return a.b.c 28 | else 29 | return nil 30 | end 31 | ``` 32 | 33 | ## Working with strings 34 | 35 | In case the value should be converted to a string. 36 | 37 | ```twig 38 | {{ data }} 39 | ``` 40 | 41 | | Value | String evaluation | Info | 42 | |---------------|--------------------|-------| 43 | | `nil` | empty string | | 44 | | cjson.null | empty string | see [cjson](https://github.com/openresty/lua-cjson) | 45 | | cbson.null() | empty string | see [cbson](https://github.com/isage/lua-cbson) | 46 | | lyaml.null | empty string | see [lyaml](https://github.com/gvvaughan/lyaml) | 47 | | yaml.null | empty string | see [yaml](https://www.tarantool.io/en/doc/2.4/reference/reference_lua/yaml/) | 48 | | ngx.null | empty string | see [ngx_lua](https://github.com/openresty/lua-nginx-module#core-constants) | 49 | | msgpack.null | empty string | see [msgpack](https://www.tarantool.io/en/doc/2.4/reference/reference_lua/msgpack/) | 50 | | box.NULL | empty string | see [tarantool box](https://www.tarantool.io/en/doc/2.4/reference/reference_lua/box_null/) | 51 | | other value | `tostring(...)` | 52 | 53 | You may [configure empty-string behavior](./api.md#empty-string-behaviour). 54 | 55 | ## Working with numbers 56 | 57 | - for [math operations](./syntax.md#math-operators) 58 | - in case the value should be converted to a number. 59 | 60 | ```twig 61 | {{ data + 2 }} 62 | ``` 63 | 64 | | Value | Number evaluation | Info | 65 | |--------------------------|---------------------------|----------------------------------------| 66 | | empty string | 0 | | 67 | | string "0" or '0' | 0 | | 68 | | false | 0 | | 69 | | true | 1 | | 70 | | any table | 0 | | 71 | | nil | 0 | | 72 | | userdata | `tonumber(tostring(...))` | if result is `nil` then `0` will be used | 73 | | string | `tonumber(...)` | if result is `nil` then `0` will be used | 74 | 75 | You may [configure number behavior](./api.md#number-behaviour). 76 | 77 | ## Working with booleans 78 | 79 | The rules to determine if an expression is `true` or `false` are (edge cases): 80 | 81 | ```twig 82 | {% if data %} 83 | ... 84 | {% endif %} 85 | ``` 86 | 87 | | Value | Boolean evaluation | Info | 88 | |-----------------------------|--------------------|--------| 89 | | empty string | false | | 90 | | other string | true | | 91 | | numeric zero | false | | 92 | | whitespace-only string | true | | 93 | | string "0" or '0' | true | | 94 | | nil | false | | 95 | | table with `__toboolean` | `__toboolean()` | | 96 | | table with `__len` | `__len() ~= 0` | | 97 | | empty table | false | | 98 | | non-empty table | true | | 99 | | cjson.null | false | see [cjson](https://github.com/openresty/lua-cjson) | 100 | | cjson.empty_array | false | see [cjson](https://github.com/openresty/lua-cjson) | 101 | | cbson.null() | false | see [cbson](https://github.com/isage/lua-cbson) | 102 | | cbson.null() | false | see [cbson](https://github.com/isage/lua-cbson) | 103 | | cbson.array() | false | see [cbson](https://github.com/isage/lua-cbson) | 104 | | lyaml.null | false | see [lyaml](https://github.com/gvvaughan/lyaml) | 105 | | yaml.null | false | see [yaml](https://www.tarantool.io/en/doc/2.4/reference/reference_lua/yaml/) | 106 | | ngx.null | false | see [ngx_lua](https://github.com/openresty/lua-nginx-module#core-constants) | 107 | | msgpack.null | false | see [msgpack](https://www.tarantool.io/en/doc/2.4/reference/reference_lua/msgpack/) | 108 | | box.NULL | false | see [tarantool box](https://www.tarantool.io/en/doc/2.4/reference/reference_lua/box_null/) | 109 | | userdata with `__toboolean` | `__toboolean()` | | 110 | | userdata with `__len` | `__len() ~= 0` | | 111 | 112 | Functions `__toboolean()` and `__len()` should be a part of the metatable. 113 | 114 | You may [configure `false` behavior](./api.md#condition-behaviour). 115 | 116 | ## Working with cycles 117 | 118 | Aspect supports iterators from Lua 5.2+ versions for Lua 5.1 and LuaJIT versions. 119 | 120 | ```twig 121 | {% for key, value in data %} 122 | ... 123 | {% endfor %} 124 | ``` 125 | 126 | | Value | Action | 127 | |--------------------------|----------------------| 128 | | string | not iterate | 129 | | empty table | not iterate | 130 | | number | not iterate | 131 | | nil | not iterate | 132 | | true/false | not iterate | 133 | | userdata with `__pairs` | iterate with `__pairs()` | 134 | | userdata | not iterate | 135 | | table with `__pairs` | iterate with `__pairs()` instead of `pairs()` | 136 | | other table | iterate with `pairs()` | 137 | 138 | The function `__pairs()` should be a part of the metatable 139 | and compatible with basic function `pairs()` (returns `iterator`, `key`, `value`) 140 | 141 | ## Working with counting 142 | 143 | When it is necessary to count the number of elements (filter `length` or variable `loop.length`). 144 | 145 | ```twig 146 | {{ data|length }} 147 | ``` 148 | 149 | | Value | Number evaluation | 150 | |--------------------------|----------------------| 151 | | string | `string.len(...)` | 152 | | empty table | 0 | 153 | | number | 0 | 154 | | nil | 0 | 155 | | true/false | 0 | 156 | | userdata | 0 | 157 | | table with `__len` | `__len(...)` | 158 | | table with `__pairs` | invoke `__pairs()` and count elements | 159 | | other tables | count of keys | 160 | | userdata with `__len` | `__len(...)` | 161 | | userdata with `__pairs` | invoke `__pairs()` and count elements | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /docs/tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags 4 | --- 5 | 6 | 7 | 8 | * [apply](./tags/apply.md) — apply filters on a block of template data. 9 | * [autoescape](./tags/autoescape.md) — automatically escaping 10 | * [do](./tags/do.md) — run expression 11 | * [extends](./tags/extends.md), [block](./tags/extends.md#block), [use](./tags/extends.md#use) - 12 | template inheritance ([read more](./syntax.md#template-inheritance)) 13 | * [for, else, break](./tags/for.md) — loop over each item in a sequence. 14 | * [if, elseif, elif, else](./tags/if.md) — conditional statement 15 | * [include](./tags/include.md) — includes a template and returns the rendered content 16 | * [macro](./tags/macro.md), [import](./tags/macro.md#importing-macros), [from](./tags/macro.md#importing-macros) — work with macros 17 | * [set](./tags/set.md) — assign values to variables 18 | * [verbatim](./tags/verbatim.md) — do not evaluate Aspect tags 19 | * [with](./tags/with.md) — create new scope for variables 20 | 21 | [Add tags](./api.md#add-tags) 22 | 23 | -------------------------------------------------------------------------------- /docs/tags/apply.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › apply 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | The `apply` tag allows you to apply Aspect filters on a block of template data: 11 | 12 | ```twig 13 | {% apply upper %} 14 | This text becomes uppercase 15 | {% endapply %} 16 | 17 | {# outputs "THIS TEXT BECOMES UPPERCASE" #} 18 | ``` 19 | 20 | You can also chain filters and pass arguments to them: 21 | 22 | ```twig 23 | {% apply lower|escape('html') %} 24 | SOME TEXT 25 | {% endapply %} 26 | 27 | {# outputs "<strong>some text</strong>" #} 28 | ``` 29 | 30 | **NOTE**: The filter buffers the data. With a large amount of data processed by the filter, a large amount of RAM will be used. 31 | 32 | -------------------------------------------------------------------------------- /docs/tags/autoescape.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › autoescape 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | Whether automatic escaping is enabled or not, you can mark a section of a template 11 | to be escaped or not by using the `autoescape` tag: 12 | 13 | ```twig 14 | {% autoescape %} 15 | Everything will be automatically escaped in this block 16 | using the HTML strategy 17 | {% endautoescape %} 18 | 19 | {% autoescape false %} 20 | Everything will be outputted as is in this block 21 | {% endautoescape %} 22 | ``` 23 | 24 | When automatic escaping is enabled everything is escaped by default except for values explicitly marked as safe. 25 | Those can be marked in the template by using the [raw](../filters/raw.md) filter: 26 | 27 | ```twig 28 | {% autoescape %} 29 | {{ safe_value|raw }} 30 | {% endautoescape %} 31 | ``` 32 | 33 | -------------------------------------------------------------------------------- /docs/tags/do.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › do 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | The `do` tag works exactly like the regular variable expression (`{{ ... }}`) just that it doesn't print anything: 11 | 12 | ```twig 13 | {% do 1 + 2 %} 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /docs/tags/extends.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › extends 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | The `extends` tag can be used to extend a template from another one. 11 | 12 | Tag `block` 13 | ------- 14 | 15 | ### Parent Template 16 | 17 | Let's define a base template, `base.view`, which defines a simple HTML skeleton document: 18 | 19 | ```twig 20 | 21 | 22 | 23 | {% block head %} 24 | 25 | {% block title %}{% endblock %} - My Webpage 26 | {% endblock %} 27 | 28 | 29 |
{% block content %}{% endblock %}
30 | 35 | 36 | 37 | ``` 38 | 39 | In this example, the [block](#tag-block) tags define four blocks that child templates can fill in. 40 | 41 | All the `block` tag does is to tell the template engine that a child template may override those portions of the template. 42 | 43 | ### Child Template 44 | 45 | A child template might look like this: 46 | 47 | ```twig 48 | {% extends "base.view" %} 49 | 50 | {% block title %}Index{% endblock %} 51 | {% block head %} 52 | {{ parent() }} 53 | 56 | {% endblock %} 57 | {% block content %} 58 |

Index

59 |

60 | Welcome on my awesome homepage. 61 |

62 | {% endblock %} 63 | ``` 64 | 65 | The `extends` tag is the key here. It tells the template engine that this template "extends" another template. 66 | When the template system evaluates this template, first it locates the parent. 67 | The extends tag should be the first tag in the template. 68 | 69 | Note that since the child template doesn't define the `footer` block, the value from the parent template is used instead. 70 | 71 | You can't define multiple `block` tags with the same name in the same template. 72 | This limitation exists because a block tag works in "both" directions. 73 | That is, a `block` tag doesn't just provide a hole to fill - it also defines the content that fills the hole in the `parent`. 74 | If there were two similarly-named `block` tags in a template, that template's parent wouldn't know which one of the blocks' content to use. 75 | 76 | ### Function `block` 77 | When a template uses inheritance and if you want to print a block multiple times, use the `block` function: 78 | 79 | ```twig 80 | {% block title %}{% endblock %} 81 | 82 |

{{ block('title') }}

83 | 84 | {% block body %}{% endblock %} 85 | ``` 86 | 87 | The `block` function can also be used to display one block from another template: 88 | 89 | ```twig 90 | 91 | {{ block("title", "common_blocks.view") }} 92 | ``` 93 | 94 | Use the `defined` test to check if a block exists in the context of the current template: 95 | 96 | ```twig 97 | {% if block("footer") is defined %} 98 | ... 99 | {% endif %} 100 | 101 | {% if block("footer", "common_blocks.view") is defined %} 102 | ... 103 | {% endif %} 104 | ``` 105 | 106 | ### Named Block End-Tags 107 | 108 | Aspect allows you to put the name of the block after the end tag for better readability 109 | (the name after the `endblock` word must match the block name): 110 | 111 | ```twig 112 | {% block sidebar %} 113 | {% block inner_sidebar %} 114 | ... 115 | {% endblock inner_sidebar %} 116 | {% endblock sidebar %} 117 | ``` 118 | 119 | ### Block Nesting and Scope 120 | 121 | Blocks can be nested for more complex layouts. Per default, blocks have access to variables from outer scopes: 122 | 123 | ```twig 124 | {% for item in seq %} 125 |
  • {% block loop_item %}{{ item }}{% endblock %}
  • 126 | {% endfor %} 127 | ``` 128 | 129 | Function `parent` 130 | ------------ 131 | 132 | When a template uses inheritance, it's possible to render the contents of the parent 133 | block when overriding a block by using the parent function: 134 | 135 | ```twig 136 | {% block sidebar %} 137 |

    Table Of Contents

    138 | ... 139 | {{ parent() }} 140 | {% endblock %} 141 | ``` 142 | The parent() call will return the content of the sidebar block as defined in the base.view template. 143 | 144 | How do blocks work? 145 | ------------------- 146 | 147 | A block provides a way to change how a certain part of a template is rendered but it does not interfere in any way with the logic around it. 148 | 149 | Let's take the following example to illustrate how a block works and more importantly, how it does not work: 150 | 151 | ```twig 152 | {# base.view #} 153 | 154 | {% for post in posts %} 155 | {% block post %} 156 |

    {{ post.title }}

    157 |

    {{ post.body }}

    158 | {% endblock %} 159 | {% endfor %} 160 | ``` 161 | 162 | If you render this template, the result would be exactly the same with or without the `block` tag. 163 | The `block` inside the `for` loop is just a way to make it overridable by a child template: 164 | 165 | ```twig 166 | {# child.view #} 167 | 168 | {% extends "base.view" %} 169 | 170 | {% block post %} 171 |
    172 |
    {{ post.title }}
    173 |
    {{ post.text }}
    174 |
    175 | {% endblock %} 176 | ``` 177 | 178 | Now, when rendering the child template, the loop is going to use the block defined in the child template 179 | instead of the one defined in the base one; the executed template is then equivalent to the following one: 180 | 181 | ```twig 182 | {% for post in posts %} 183 |
    184 |
    {{ post.title }}
    185 |
    {{ post.text }}
    186 |
    187 | {% endfor %} 188 | ``` 189 | 190 | Let's take another example: a block included within an `if` statement: 191 | 192 | ```twig 193 | {% if posts is empty %} 194 | {% block head %} 195 | {{ parent() }} 196 | 197 | 198 | {% endblock head %} 199 | {% endif %} 200 | ``` 201 | 202 | Contrary to what you might think, this template does not define a block conditionally; 203 | it just makes overridable by a child template the output of what will be rendered when the condition is `true`. 204 | 205 | If you want the output to be displayed conditionally, use the following instead: 206 | 207 | ```twig 208 | {% block head %} 209 | {{ parent() }} 210 | 211 | {% if posts is empty %} 212 | 213 | {% endif %} 214 | {% endblock head %} 215 | ``` 216 | 217 | Use 218 | --- 219 | 220 | Template inheritance is one of the most powerful features of Aspect but it is limited to single inheritance; 221 | a template can only extend one other template. 222 | This limitation makes template inheritance simple to understand and easy to debug: 223 | 224 | ```twig 225 | {% extends "base.view" %} 226 | 227 | {% block title %}{% endblock %} 228 | {% block content %}{% endblock %} 229 | ``` 230 | 231 | Horizontal reuse is a way to achieve the same goal as multiple inheritance, but without the associated complexity: 232 | 233 | ```twig 234 | {% extends "base.view" %} 235 | 236 | {% use "blocks.view" %} 237 | 238 | {% block title %}{% endblock %} 239 | {% block content %}{% endblock %} 240 | ``` 241 | 242 | The `use` statement tells Aspect to import the blocks defined in `blocks.view` into the current template 243 | (it's like `macros`, but for blocks): 244 | 245 | ```twig 246 | {# blocks.view #} 247 | 248 | {% block sidebar %}{% endblock %} 249 | ``` 250 | 251 | In this example, the `use` statement imports the `sidebar` block into the main template. 252 | The code is mostly equivalent to the following one (the imported blocks are not outputted automatically): 253 | 254 | ```twig 255 | {% extends "base.view" %} 256 | 257 | {% block sidebar %}{% endblock %} 258 | {% block title %}{% endblock %} 259 | {% block content %}{% endblock %} 260 | ``` 261 | 262 | -------------------------------------------------------------------------------- /docs/tags/for.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › for 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | Loop over each item in a sequence (also see [iterators](../api.md#iterator-and-countable-objects)). 11 | For example, to display a list of users provided in a variable called `users`: 12 | 13 | ```twig 14 |

    Members

    15 | 20 | ``` 21 | 22 | **Note** that Lua tables (no array) are **not ordered**. 23 | 24 | **Note** how the tag [works with different data](./../spec.md#working-with-cycles) 25 | 26 | 27 | The `else` clause 28 | ----------------- 29 | 30 | If no iteration took place because the sequence was empty, you can render a replacement block by using `else`: 31 | 32 | ```twig 33 | 40 | ``` 41 | 42 | The `break` clause 43 | ------------------ 44 | 45 | Break ends execution of the current `for` loop 46 | 47 | ```twig 48 | 57 | ``` 58 | 59 | Iterating over Keys 60 | ------------------- 61 | 62 | By default, a loop iterates over the values of the sequence. You can iterate on keys by using the [keys](./../filters/keys.md) filter: 63 | 64 | ```twig 65 |

    Members

    66 | 71 | ``` 72 | 73 | Iterating over Keys and Values 74 | ------------------------------ 75 | 76 | You can also access both keys and values: 77 | 78 | ```twig 79 |

    Members

    80 | 85 | ``` 86 | 87 | The loop variable 88 | ----------------- 89 | 90 | Inside of a `for` loop block you can access some special variables: 91 | 92 | | Variable | Description | 93 | |---------------------|---------------------------| 94 | | `loop.index` | The current iteration of the loop. (1 indexed) | 95 | | `loop.index0` | The current iteration of the loop. (0 indexed) | 96 | | `loop.revindex` | The number of iterations from the end of the loop (1 indexed) | 97 | | `loop.revindex0` | The number of iterations from the end of the loop (0 indexed) | 98 | | `loop.first` | True if first iteration | 99 | | `loop.last` | True if last iteration | 100 | | `loop.length` | The number of items in the sequence | 101 | | `loop.parent` | The parent context | 102 | | `loop.prev_item` | The item from the previous iteration of the loop. `Nil` during the first iteration. | 103 | | `loop.next_item` | The item from the following iteration of the loop. `Nil` during the last iteration. | 104 | | `loop.has_more` | True if has more items. | 105 | 106 | 107 | -------------------------------------------------------------------------------- /docs/tags/if.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › if 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | In the simplest form you can use it to test if an expression evaluates to `true`: 11 | 12 | ```twig 13 | {% if online == false %} 14 |

    Our website is in maintenance mode. Please, come back later.

    15 | {% endif %} 16 | ``` 17 | 18 | You can also test if an array is not empty: 19 | 20 | ```twig 21 | {% if users %} 22 | 27 | {% endif %} 28 | ``` 29 | 30 | **NOTE** If you want to test if the variable is defined, use `if users is defined` instead. 31 | 32 | You can also use `not` to check for values that evaluate to false: 33 | 34 | ```twig 35 | {% if not user.subscribed %} 36 |

    You are not subscribed to our mailing list.

    37 | {% endif %} 38 | ``` 39 | 40 | For multiple conditions, and and or can be used: 41 | 42 | ```twig 43 | {% if temperature > 18 and temperature < 27 %} 44 |

    It's a nice day for a walk in the park.

    45 | {% endif %} 46 | ``` 47 | 48 | For multiple branches `elseif` (or `elif`) and `else` can be used. You can use more complex `expressions` there too: 49 | 50 | ```twig 51 | {% if product.stock > 10 %} 52 | Available 53 | {% elseif product.stock > 0 %} 54 | Only {{ product.stock }} left! 55 | {% else %} 56 | Sold-out! 57 | {% endif %} 58 | ``` 59 | 60 | The rules to determine if an expression is true or false are (edge cases): 61 | 62 | | Value | Boolean evaluation | 63 | |--------------------------|--------------------| 64 | | empty string | false | 65 | | numeric zero | false | 66 | | whitespace-only string | true | 67 | | string "0" or '0' | true | 68 | | empty table | false | 69 | | nil | false | 70 | | non-empty table | true | 71 | | table with `__toboolean` | `__toboolean()` | 72 | | cjson.null | false | 73 | | cbson.null() | false | 74 | | lyaml.null | false | 75 | | yaml.null | false | 76 | | ngx.null | false | 77 | | msgpack.null | false | 78 | 79 | Function `__toboolean()` should be in the metatable. 80 | 81 | You can add your own [false-behaviour](./../api.md#condition-behaviour) 82 | 83 | -------------------------------------------------------------------------------- /docs/tags/include.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › include 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | The `include` statement includes a template and returns the rendered content of that file: 11 | 12 | ```twig 13 | {% include 'header.html' %} 14 | Body 15 | {% include 'footer.html' %} 16 | ``` 17 | 18 | Included templates have access to the variables of the active context. 19 | 20 | You can add additional variables by passing them after the `with` keyword: 21 | 22 | ```twig 23 | {# template.html will have access to the variables from the current context and the additional ones provided #} 24 | {% include 'template.html' with {foo: 'bar'} %} 25 | 26 | {% set vars = {'foo': 'bar'} %} 27 | {% include 'template.html' with vars %} 28 | ``` 29 | 30 | You can disable access to the context by appending the only keyword: 31 | 32 | ```twig 33 | {# only the foo variable will be accessible #} 34 | {% include 'template.html' with {'foo': 'bar'} only %} 35 | ``` 36 | 37 | ```twig 38 | {# no variables will be accessible #} 39 | {% include 'template.html' only %} 40 | ``` 41 | 42 | The template name can be any valid expression: 43 | 44 | ```twig 45 | {% include some_var %} 46 | {% include ajax ? 'ajax.html' : 'not_ajax.html' %} 47 | ``` 48 | 49 | You can mark an include with `ignore missing` in which case Aspect will ignore the statement if the template to be included does not exist. 50 | It has to be placed just after the template name. Here some valid examples: 51 | 52 | ```twig 53 | {% include 'sidebar.html' ignore missing %} 54 | {% include 'sidebar.html' ignore missing with {'foo': 'bar'} %} 55 | {% include 'sidebar.html' ignore missing only %} 56 | ``` 57 | 58 | You can also provide a list of templates that are checked for existence before inclusion. 59 | The first template that exists will be included: 60 | 61 | ```twig 62 | {% include ['page_detailed.html', 'page.html'] %} 63 | ``` 64 | 65 | If `ignore missing` is given, it will fall back to rendering nothing if none of the templates exist, otherwise it will throw an exception. 66 | 67 | -------------------------------------------------------------------------------- /docs/tags/macro.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › macro 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | Using Macros 11 | ------------ 12 | 13 | Macros are comparable with functions in regular programming languages. 14 | They are useful to reuse template fragments to not repeat yourself. 15 | 16 | Macros are defined in regular templates. 17 | 18 | Imagine having a generic helper template that define how to render HTML forms via macros (called `forms.html`): 19 | 20 | ```twig 21 | {% macro input(name, value, type = "text", size = 20) %} 22 | 23 | {% endmacro %} 24 | 25 | {% macro textarea(name, value, rows = 10, cols = 40) %} 26 | 27 | {% endmacro %} 28 | ``` 29 | 30 | Each macro argument can have a default value (here text is the default value for type if not provided in the call). 31 | 32 | Macros differ from native Lua functions in a few ways: 33 | 34 | * Arguments of a macro are always optional. 35 | * If extra positional arguments are passed to a macro, they end up in the special `_context` variable as a list of values. 36 | 37 | Macros don't have access to the current template variables. 38 | 39 | Importing Macros 40 | ---------------- 41 | 42 | There are two ways to import macros. 43 | You can import the complete template containing the macros into a local variable (via the `import` tag) 44 | or only import specific macros from the template (via the `from` tag). 45 | 46 | To `import` all macros from a template into a local variable, use the `import` tag: 47 | 48 | ```twig 49 | {% import "forms.html" as forms %} 50 | ``` 51 | 52 | The above import call imports the `forms.html` file (which can contain only macros, or a template and some macros), 53 | and import the macros as items of the `forms` local variable. 54 | 55 | The macros can then be called at will in the _current_ template: 56 | 57 | ```twig 58 |

    {{ forms.input('username') }}

    59 |

    {{ forms.input('password', null, 'password') }}

    60 | ``` 61 | 62 | Alternatively you can import names from the template into the current namespace via the `from` tag: 63 | 64 | ```twig 65 | {% from 'forms.html' import input as input_field, textarea %} 66 | 67 |

    {{ input_field('password', '', 'password') }}

    68 |

    {{ textarea('comment') }}

    69 | ``` 70 | 71 | When macro usages and definitions are in the same template, 72 | you don't need to import the macros as they are automatically available under the special `_self` variable: 73 | 74 | ```twig 75 |

    {{ _self.input('password', '', 'password') }}

    76 | 77 | {% macro input(name, value, type = "text", size = 20) %} 78 | 79 | {% endmacro %} 80 | ``` 81 | 82 | -------------------------------------------------------------------------------- /docs/tags/set.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › set 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | Inside code blocks you can also assign values to variables. 11 | Assignments use the `set` tag and can have multiple targets. 12 | 13 | Here is how you can assign the `bar` value to the `foo` variable: 14 | ```twig 15 | {% set foo = 'bar' %} 16 | ``` 17 | 18 | After the set call, the foo variable is available in the template like any other ones: 19 | 20 | ```twig 21 | {# displays bar #} 22 | {{ foo }} 23 | ``` 24 | 25 | The assigned value can be any valid [Aspect expression](../syntax.md#expressions): 26 | 27 | ```twig 28 | {% set foo = [1, 2] %} 29 | {% set foo = {'foo': 'bar'} %} 30 | {% set foo = 'foo' ~ 'bar' %} 31 | ``` 32 | 33 | The set tag can also be used to 'capture' chunks of text: 34 | 35 | ```twig 36 | {% set foo %} 37 | 40 | {% endset %} 41 | ``` 42 | 43 | **NOTE** Note that loops are scoped in Aspect; therefore a variable declared inside a for loop is not accessible outside the loop itself: 44 | 45 | ```twig 46 | {% for item in list %} 47 | {% set foo = item %} 48 | {% endfor %} 49 | 50 | {# foo is NOT available #} 51 | ``` 52 | If you want to access the variable, just declare it before the loop: 53 | ```twig 54 | {% set foo = "" %} 55 | {% for item in list %} 56 | {% set foo = item %} 57 | {% endfor %} 58 | 59 | {# foo is available #} 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /docs/tags/verbatim.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › verbatim 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | The `verbatim` tag marks sections as being raw text that should not be parsed. 11 | For example to put Aspect syntax as example into a template you can use this snippet: 12 | 13 | ```twig 14 | {% verbatim %} 15 | 20 | {% endverbatim %} 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /docs/tags/with.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tags › with 4 | --- 5 | 6 | [← tags](./../tags.md) 7 | 8 | 9 | 10 | Use the `with` tag to create a new inner scope. Variables set within this scope are not visible outside of the scope: 11 | 12 | ```twig 13 | {% with %} 14 | {% set foo = 42 %} 15 | {{ foo }} {# foo is 42 here #} 16 | {% endwith %} 17 | foo is not visible here any longer 18 | ``` 19 | 20 | Instead of defining variables at the beginning of the scope, 21 | you can pass a hash of variables you want to define in the `with` tag; 22 | the previous example is equivalent to the following one: 23 | 24 | ```twig 25 | {% with { foo: 42 } %} 26 | {{ foo }} {# foo is 42 here #} 27 | {% endwith %} 28 | foo is not visible here any longer 29 | 30 | {# it works with any expression that resolves to a hash #} 31 | {% set vars = { foo: 42 } %} 32 | {% with vars %} 33 | ... 34 | {% endwith %} 35 | ``` 36 | 37 | By default, the inner scope has access to the outer scope context; 38 | you can disable this behavior by appending the `only` keyword: 39 | 40 | ```twig 41 | {% set bar = 'bar' %} 42 | {% with { foo: 42 } only %} 43 | {# only foo is defined #} 44 | {# bar is not defined #} 45 | {% endwith %} 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /docs/tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tests 4 | --- 5 | 6 | 7 | 8 | * [is defined](./tests/defined.md) 9 | * [is divisible by()](./tests/divisibleby.md) 10 | * [is empty](./tests/empty.md) 11 | * [is iterable](./tests/iterable.md) 12 | * [is null](./tests/null.md) 13 | * [is odd](./tests/odd.md), [is even](./tests/even.md) 14 | * [is same as()](./tests/sameas.md) 15 | 16 | [Add tests](./api.md#add-tests) 17 | 18 | -------------------------------------------------------------------------------- /docs/tests/defined.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tests › defined 4 | --- 5 | 6 | [← tests](./../tests.md) 7 | 8 | 9 | 10 | `defined` checks if a variable is defined in the current context. 11 | 12 | ```twig 13 | {# defined works with variable names #} 14 | {% if foo is defined %} 15 | ... 16 | {% endif %} 17 | 18 | {# and attributes on variables names #} 19 | {% if foo.bar is defined %} 20 | ... 21 | {% endif %} 22 | 23 | {% if foo['bar'] is defined %} 24 | ... 25 | {% endif %} 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/tests/divisibleby.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tests › divisible by 4 | --- 5 | 6 | [← tests](./../tests.md) 7 | 8 | 9 | 10 | `divisible by` checks if a variable is divisible by a number: 11 | 12 | ```twig 13 | {% if loop.index is divisible by(3) %} 14 | ... 15 | {% endif %} 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /docs/tests/empty.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tests › empty 4 | --- 5 | 6 | [← tests](./../tests.md) 7 | 8 | 9 | 10 | `empty` checks if a variable is an empty string, an empty array, an empty hash, 11 | exactly false, exactly null or numeric zero. 12 | 13 | For objects that has the __len meta method, empty will check the return value of the __len() method. 14 | 15 | For objects that has the __toboolean meta method (and not __len), empty will check the return value of the __toboolean() meta method. 16 | 17 | ```twig 18 | {% if foo is empty %} 19 | ... 20 | {% endif %} 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /docs/tests/even.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tests › even 4 | --- 5 | 6 | [← tests](./../tests.md) 7 | 8 | 9 | 10 | `even` returns `true` if the given number is even: 11 | 12 | ```twig 13 | {{ var is even }} 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /docs/tests/iterable.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tests › iterable 4 | --- 5 | 6 | [← tests](./../tests.md) 7 | 8 | 9 | 10 | `iterable` checks if a variable is an array or a traversable object: 11 | 12 | ```twig 13 | {# evaluates to true if the foo variable is iterable #} 14 | {% if users is iterable %} 15 | {% for user in users %} 16 | Hello {{ user }}! 17 | {% endfor %} 18 | {% else %} 19 | {# users is probably a string #} 20 | Hello {{ users }}! 21 | {% endif %} 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /docs/tests/null.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tests › null 4 | --- 5 | 6 | [← tests](./../tests.md) 7 | 8 | 9 | 10 | `null` or `nil` returns `true` if the variable is `nil`: 11 | 12 | ```twig 13 | {{ var is null }} 14 | {{ var is nil }} 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /docs/tests/odd.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tests › odd 4 | --- 5 | 6 | [← tests](./../tests.md) 7 | 8 | 9 | 10 | `odd` returns `true` if the given number is odd: 11 | 12 | ```twig 13 | {{ var is odd }} 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /docs/tests/sameas.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tests › same as 4 | --- 5 | 6 | [← tests](./../tests.md) 7 | 8 | 9 | 10 | `same as` checks if a variable is the same as another variable. This is the equivalent to == in Lua: 11 | 12 | ```twig 13 | {% if foo.attribute is same as(false) %} 14 | the foo attribute really is the 'false' value 15 | {% endif %} 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Aspect Lua Template 2 | =================== 3 | 4 | 5 | 6 | [![Build Status](https://travis-ci.org/unifire-app/aspect.svg?branch=master)](https://travis-ci.org/unifire-app/aspect) 7 | [![codecov](https://codecov.io/gh/unifire-app/aspect/branch/master/graph/badge.svg)](https://codecov.io/gh/unifire-app/aspect) 8 | [![Luarocks](docs/assets/luarocks.svg)](https://luarocks.org/modules/unifire/aspect) 9 | 10 | Aspect is a template language for Lua and OpenResty. No dependencies. Pure Lua. 11 | 12 | Aspect uses a syntax similar to the Twig, Django and Jinja template languages which 13 | inspired the Aspect runtime environment. 14 | 15 | More Information 16 | ---------------- 17 | 18 | Read the [documentation](https://unifire-app.github.io/aspect/) for more information. 19 | 20 | 21 | **For template designers:** 22 | 23 | - [Specification](./docs/spec.md) 24 | - [Template Syntax](./docs/syntax.md) 25 | - [Operators](./docs/syntax.md#operators) 26 | - [Tags](./docs/tags.md) 27 | - [Filters](./docs/filters.md) 28 | - [Functions](./docs/funcs.md) 29 | - [Tests](./docs/tests.md) 30 | 31 | **For developers:** 32 | 33 | - [Lua API](./docs/api.md) 34 | - [CLI](./docs/cli.md) 35 | - [Extending](./docs/api.md#extending) 36 | - [Changelog](./changelog.md) 37 | -------------------------------------------------------------------------------- /rockspec/aspect-1.13-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "aspect" 2 | version = "1.13-0" 3 | source = { 4 | url = "https://github.com/unifire-app/aspect/archive/1.13.zip", 5 | dir = "aspect-1.13" 6 | } 7 | description = { 8 | summary = "Aspect is a powerful templating engine for Lua and OpenResty with syntax Twig/Django/Jinja/Liquid.", 9 | homepage = "https://aspect.unifire.app/", 10 | license = "BSD-3-Clause", 11 | } 12 | dependencies = { 13 | "lua >= 5.1" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["aspect"] = "src/aspect/init.lua", 19 | ["aspect.config"] = "src/aspect/config.lua", 20 | ["aspect.template"] = "src/aspect/template.lua", 21 | ["aspect.output"] = "src/aspect/output.lua", 22 | ["aspect.error"] = "src/aspect/error.lua", 23 | 24 | ["aspect.tags"] = "src/aspect/tags.lua", 25 | ["aspect.filters"] = "src/aspect/filters.lua", 26 | ["aspect.funcs"] = "src/aspect/funcs.lua", 27 | ["aspect.tests"] = "src/aspect/tests.lua", 28 | 29 | ["aspect.compiler"] = "src/aspect/compiler.lua", 30 | ["aspect.tokenizer"] = "src/aspect/tokenizer.lua", 31 | ["aspect.ast"] = "src/aspect/ast.lua", 32 | ["aspect.ast.ops"] = "src/aspect/ast/ops.lua", 33 | 34 | ["aspect.utils"] = "src/aspect/utils.lua", 35 | ["aspect.utils.batch"] = "src/aspect/utils/batch.lua", 36 | ["aspect.utils.range"] = "src/aspect/utils/range.lua", 37 | ["aspect.utils.date"] = "src/aspect/utils/date.lua", 38 | 39 | ["aspect.loader.array"] = "src/aspect/loader/array.lua", 40 | ["aspect.loader.filesystem"] = "src/aspect/loader/filesystem.lua", 41 | ["aspect.loader.resty"] = "src/aspect/loader/resty.lua", 42 | 43 | ["aspect.cli"] = "src/aspect/cli.lua", 44 | }, 45 | install = { 46 | bin = { 47 | ['aspect'] = 'bin/aspect' 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rockspec/aspect-1.14-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "aspect" 2 | version = "1.14-0" 3 | source = { 4 | url = "https://github.com/unifire-app/aspect/archive/1.14.zip", 5 | dir = "aspect-1.14" 6 | } 7 | description = { 8 | summary = "Aspect is a powerful templating engine for Lua and OpenResty with syntax Twig/Django/Jinja/Liquid.", 9 | homepage = "https://aspect.unifire.app/", 10 | license = "BSD-3-Clause", 11 | } 12 | dependencies = { 13 | "lua >= 5.1" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["aspect"] = "src/aspect/init.lua", 19 | ["aspect.config"] = "src/aspect/config.lua", 20 | ["aspect.template"] = "src/aspect/template.lua", 21 | ["aspect.output"] = "src/aspect/output.lua", 22 | ["aspect.error"] = "src/aspect/error.lua", 23 | 24 | ["aspect.tags"] = "src/aspect/tags.lua", 25 | ["aspect.filters"] = "src/aspect/filters.lua", 26 | ["aspect.funcs"] = "src/aspect/funcs.lua", 27 | ["aspect.tests"] = "src/aspect/tests.lua", 28 | 29 | ["aspect.compiler"] = "src/aspect/compiler.lua", 30 | ["aspect.tokenizer"] = "src/aspect/tokenizer.lua", 31 | ["aspect.ast"] = "src/aspect/ast.lua", 32 | ["aspect.ast.ops"] = "src/aspect/ast/ops.lua", 33 | 34 | ["aspect.utils"] = "src/aspect/utils.lua", 35 | ["aspect.utils.batch"] = "src/aspect/utils/batch.lua", 36 | ["aspect.utils.range"] = "src/aspect/utils/range.lua", 37 | ["aspect.utils.date"] = "src/aspect/utils/date.lua", 38 | 39 | ["aspect.loader.array"] = "src/aspect/loader/array.lua", 40 | ["aspect.loader.filesystem"] = "src/aspect/loader/filesystem.lua", 41 | ["aspect.loader.resty"] = "src/aspect/loader/resty.lua", 42 | 43 | ["aspect.cli"] = "src/aspect/cli.lua", 44 | }, 45 | install = { 46 | bin = { 47 | ['aspect'] = 'bin/aspect' 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rockspec/aspect-2.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "aspect" 2 | version = "2.0-0" 3 | source = { 4 | url = "https://github.com/unifire-app/aspect/archive/2.0.zip", 5 | dir = "aspect-2.0" 6 | } 7 | description = { 8 | summary = "Aspect is a powerful templating engine for Lua and OpenResty with syntax Twig/Django/Jinja/Liquid.", 9 | homepage = "https://aspect.unifire.app/", 10 | license = "BSD-3-Clause", 11 | } 12 | dependencies = { 13 | "lua >= 5.1" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["aspect"] = "src/aspect/init.lua", 19 | ["aspect.config"] = "src/aspect/config.lua", 20 | ["aspect.template"] = "src/aspect/template.lua", 21 | ["aspect.output"] = "src/aspect/output.lua", 22 | ["aspect.error"] = "src/aspect/error.lua", 23 | 24 | ["aspect.tags"] = "src/aspect/tags.lua", 25 | ["aspect.filters"] = "src/aspect/filters.lua", 26 | ["aspect.funcs"] = "src/aspect/funcs.lua", 27 | ["aspect.tests"] = "src/aspect/tests.lua", 28 | 29 | ["aspect.compiler"] = "src/aspect/compiler.lua", 30 | ["aspect.tokenizer"] = "src/aspect/tokenizer.lua", 31 | ["aspect.ast"] = "src/aspect/ast.lua", 32 | ["aspect.ast.ops"] = "src/aspect/ast/ops.lua", 33 | 34 | ["aspect.utils"] = "src/aspect/utils.lua", 35 | ["aspect.utils.batch"] = "src/aspect/utils/batch.lua", 36 | ["aspect.utils.range"] = "src/aspect/utils/range.lua", 37 | 38 | ["aspect.date"] = "src/aspect/date.lua", 39 | 40 | ["aspect.loader.array"] = "src/aspect/loader/array.lua", 41 | ["aspect.loader.filesystem"] = "src/aspect/loader/filesystem.lua", 42 | ["aspect.loader.resty"] = "src/aspect/loader/resty.lua", 43 | 44 | ["aspect.cli"] = "src/aspect/cli.lua", 45 | }, 46 | install = { 47 | bin = { 48 | ['aspect'] = 'bin/aspect' 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rockspec/aspect-2.2-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "aspect" 2 | version = "2.2-0" 3 | source = { 4 | url = "https://github.com/unifire-app/aspect/archive/2.2.zip", 5 | dir = "aspect-2.2" 6 | } 7 | description = { 8 | summary = "Aspect is a powerful templating engine for Lua and OpenResty with syntax Twig/Django/Jinja/Liquid.", 9 | homepage = "https://aspect.unifire.app/", 10 | license = "BSD-3-Clause", 11 | } 12 | dependencies = { 13 | "lua >= 5.1" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["aspect"] = "src/aspect/init.lua", 19 | ["aspect.config"] = "src/aspect/config.lua", 20 | ["aspect.template"] = "src/aspect/template.lua", 21 | ["aspect.output"] = "src/aspect/output.lua", 22 | ["aspect.error"] = "src/aspect/error.lua", 23 | 24 | ["aspect.tags"] = "src/aspect/tags.lua", 25 | ["aspect.filters"] = "src/aspect/filters.lua", 26 | ["aspect.funcs"] = "src/aspect/funcs.lua", 27 | ["aspect.tests"] = "src/aspect/tests.lua", 28 | 29 | ["aspect.compiler"] = "src/aspect/compiler.lua", 30 | ["aspect.tokenizer"] = "src/aspect/tokenizer.lua", 31 | ["aspect.ast"] = "src/aspect/ast.lua", 32 | ["aspect.ast.ops"] = "src/aspect/ast/ops.lua", 33 | 34 | ["aspect.utils"] = "src/aspect/utils.lua", 35 | ["aspect.utils.batch"] = "src/aspect/utils/batch.lua", 36 | ["aspect.utils.range"] = "src/aspect/utils/range.lua", 37 | 38 | ["aspect.date"] = "src/aspect/date.lua", 39 | 40 | ["aspect.loader.array"] = "src/aspect/loader/array.lua", 41 | ["aspect.loader.filesystem"] = "src/aspect/loader/filesystem.lua", 42 | ["aspect.loader.resty"] = "src/aspect/loader/resty.lua", 43 | 44 | ["aspect.cli"] = "src/aspect/cli.lua", 45 | }, 46 | install = { 47 | bin = { 48 | ['aspect'] = 'bin/aspect' 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rockspec/aspect-2.3-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "aspect" 2 | version = "2.3-0" 3 | source = { 4 | url = "https://github.com/unifire-app/aspect/archive/2.3.zip", 5 | dir = "aspect-2.3" 6 | } 7 | description = { 8 | summary = "Aspect is a powerful templating engine for Lua and OpenResty with syntax Twig/Django/Jinja/Liquid.", 9 | homepage = "https://aspect.unifire.app/", 10 | license = "BSD-3-Clause", 11 | } 12 | dependencies = { 13 | "lua >= 5.1" 14 | } 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["aspect"] = "src/aspect/init.lua", 19 | ["aspect.config"] = "src/aspect/config.lua", 20 | ["aspect.template"] = "src/aspect/template.lua", 21 | ["aspect.output"] = "src/aspect/output.lua", 22 | ["aspect.error"] = "src/aspect/error.lua", 23 | 24 | ["aspect.tags"] = "src/aspect/tags.lua", 25 | ["aspect.filters"] = "src/aspect/filters.lua", 26 | ["aspect.funcs"] = "src/aspect/funcs.lua", 27 | ["aspect.tests"] = "src/aspect/tests.lua", 28 | 29 | ["aspect.compiler"] = "src/aspect/compiler.lua", 30 | ["aspect.tokenizer"] = "src/aspect/tokenizer.lua", 31 | ["aspect.ast"] = "src/aspect/ast.lua", 32 | ["aspect.ast.ops"] = "src/aspect/ast/ops.lua", 33 | 34 | ["aspect.utils"] = "src/aspect/utils.lua", 35 | ["aspect.utils.batch"] = "src/aspect/utils/batch.lua", 36 | ["aspect.utils.range"] = "src/aspect/utils/range.lua", 37 | 38 | ["aspect.date"] = "src/aspect/date.lua", 39 | 40 | ["aspect.loader.array"] = "src/aspect/loader/array.lua", 41 | ["aspect.loader.filesystem"] = "src/aspect/loader/filesystem.lua", 42 | ["aspect.loader.resty"] = "src/aspect/loader/resty.lua", 43 | 44 | ["aspect.cli"] = "src/aspect/cli.lua", 45 | }, 46 | install = { 47 | bin = { 48 | ['aspect'] = 'bin/aspect' 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rockspec/aspect-git-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "aspect" 2 | version = "git-1" 3 | source = { 4 | url = "https://github.com/unifire-app/aspect/-/archive/master/aspect-master.zip" 5 | } 6 | description = { 7 | summary = "Aspect is a powerful templating engine for Lua and OpenResty with syntax Twig/Django/Jinja/Liquid.", 8 | homepage = "https://aspect.unifire.app/", 9 | license = "BSD-3-Clause", 10 | } 11 | dependencies = { 12 | "lua >= 5.1" 13 | } 14 | build = { 15 | type = "builtin", 16 | modules = { 17 | ["aspect"] = "src/aspect/init.lua", 18 | ["aspect.config"] = "src/aspect/config.lua", 19 | ["aspect.template"] = "src/aspect/template.lua", 20 | ["aspect.output"] = "src/aspect/output.lua", 21 | ["aspect.error"] = "src/aspect/error.lua", 22 | 23 | ["aspect.tags"] = "src/aspect/tags.lua", 24 | ["aspect.filters"] = "src/aspect/filters.lua", 25 | ["aspect.funcs"] = "src/aspect/funcs.lua", 26 | ["aspect.tests"] = "src/aspect/tests.lua", 27 | 28 | ["aspect.compiler"] = "src/aspect/compiler.lua", 29 | ["aspect.tokenizer"] = "src/aspect/tokenizer.lua", 30 | ["aspect.ast"] = "src/aspect/ast.lua", 31 | ["aspect.ast.ops"] = "src/aspect/ast/ops.lua", 32 | 33 | ["aspect.utils"] = "src/aspect/utils.lua", 34 | ["aspect.utils.batch"] = "src/aspect/utils/batch.lua", 35 | ["aspect.utils.range"] = "src/aspect/utils/range.lua", 36 | 37 | ["aspect.date"] = "src/aspect/date.lua", 38 | 39 | ["aspect.loader.array"] = "src/aspect/loader/array.lua", 40 | ["aspect.loader.filesystem"] = "src/aspect/loader/filesystem.lua", 41 | ["aspect.loader.resty"] = "src/aspect/loader/resty.lua", 42 | 43 | ["aspect.cli"] = "src/aspect/cli.lua", 44 | }, 45 | install = { 46 | bin = { 47 | ['aspect'] = 'bin/aspect' 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /spec/fixture/complex.view: -------------------------------------------------------------------------------- 1 | Use inline {{ var1 }}. 2 | Get key {{ var2.key }}. 3 | 4 | Try if: 5 | {% if var3.boolean %} 6 | if condition 7 | {% endif %} 8 | 9 | Try for: 10 | {% for k, v 11 | in 12 | var4.list %} 13 | Try {{ loop.length }}. 14 | Print {{ k }} and {{ v }} 15 | {% endfor %} 16 | 17 | {# checkout 18 | multiline 19 | comments #} 20 | 21 | Try include: 22 | 23 | {% include 'footer.view' %} 24 | 25 | Try uses 26 | 27 | {% use "blocks.view" %} 28 | 29 | Try macros: 30 | 31 | {% from 'macros.view' import square as mk_square %} 32 | 33 | {% macro mk1(x, y, z) %} 34 |
    {{ x }}^2 + {{ y }}^2 + {{ z }}^2 = {{ x**2
    35 |         + y**2
    36 |         + z**2 }}
    37 | {% endmacro %} 38 | 39 | {{ _self.mk1(z=2, x=3, y=var4.y) }} 40 | 41 | Try blocks: 42 | 43 | {% block one %} 44 | {# @desc Block named as one #} 45 | Use block with variable {{ var5.text }} 46 | {% set var6 = 'new variable' %} 47 | {% endblock %} 48 | 49 | {% block two %} 50 | Use block without variable but with {{ parent() }} 51 | {% endblock %} 52 | 53 | {% set var7 -%} 54 | Just use {{ var8.boop }} and {{ var7 }}. 55 | {%- endset %} 56 | 57 | {% apply|upper %} 58 | Hello there. {{ var9 }} 59 | {% endapply %} -------------------------------------------------------------------------------- /spec/fixture/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "say": "foo", 3 | "user": { 4 | "name": "nobody", 5 | "email": "email@dev.null" 6 | } 7 | } -------------------------------------------------------------------------------- /spec/fixture/footer.view: -------------------------------------------------------------------------------- 1 | Footer say {{ say }}. -------------------------------------------------------------------------------- /spec/fixture/greeting.view: -------------------------------------------------------------------------------- 1 | Hello, {{ user.name }}! 2 | We sent {{ say }} to {{ user.email }}. 3 | 4 | {% include 'footer.view' %} -------------------------------------------------------------------------------- /src/aspect/ast.lua: -------------------------------------------------------------------------------- 1 | local insert = table.insert 2 | local concat = table.concat 3 | local setmetatable = setmetatable 4 | local ipairs = ipairs 5 | local require = require 6 | local type = type 7 | local utils = require("aspect.utils") 8 | local cast = utils.cast_lua 9 | local var_dump = utils.var_dump 10 | 11 | --- Intermediate branch element 12 | --- @class aspect.ast.node 13 | --- @field op aspect.ast.op 14 | --- @field c aspect.ast.node|aspect.ast.leaf|nil condition for ternary operator 15 | --- @field l aspect.ast.node|aspect.ast.leaf left node or leaf 16 | --- @field r aspect.ast.node|aspect.ast.leaf right node or leaf 17 | --- @field p aspect.ast.node|nil parent node 18 | --- @field pos number token position 19 | local _node = {} 20 | 21 | --- Leaf - node with value. Branch finite element 22 | --- @class aspect.ast.leaf 23 | --- @field type string type of leaf (numeric, string, variable, expr, table) 24 | --- @field value string value of the leaf 25 | --- @field pos number token position 26 | local _leaf = {} 27 | 28 | --- AST builder 29 | --- @class aspect.ast 30 | --- @field current aspect.ast.node|aspect.ast.leaf 31 | --- @field nodes number 32 | local ast = {} 33 | 34 | --- @type table 35 | local ops_unary 36 | local ops 37 | local mt = {__index = ast} 38 | 39 | function ast.new() 40 | if not ops then 41 | ops_unary = {} 42 | ops = {} 43 | for _, op in ipairs(require("aspect.ast.ops")) do 44 | if type(op.token) == "table" then 45 | for _, token in ipairs(op.token) do 46 | if op.type == "unary" then 47 | ops_unary[token] = op 48 | else 49 | ops[token] = op 50 | end 51 | end 52 | else 53 | if op.type == "unary" then 54 | ops_unary[op.token] = op 55 | else 56 | ops[op.token] = op 57 | end 58 | end 59 | end 60 | end 61 | 62 | return setmetatable({ 63 | current = nil, 64 | nodes = 0 65 | }, mt) 66 | end 67 | 68 | --- @param compiler aspect.compiler 69 | --- @param tok aspect.tokenizer 70 | function ast:parse(compiler, tok) 71 | do 72 | local info, unary_op, leaf = {}, ops_unary[tok:get_token()] 73 | if unary_op then 74 | tok:next() 75 | end 76 | if tok:is("(") then 77 | leaf = { 78 | value = "(" .. compiler:parse_value(tok, info) .. ")", 79 | type = info.type, 80 | bracket = true, 81 | raw = info.raw 82 | } 83 | else 84 | leaf = { 85 | value = compiler:parse_value(tok, info), 86 | type = info.type, 87 | bracket = false, 88 | raw = info.raw 89 | } 90 | end 91 | if unary_op then 92 | self.current = { 93 | op = unary_op, 94 | l = nil, 95 | r = leaf 96 | } 97 | self.nodes = 2 98 | else 99 | self.current = leaf 100 | self.nodes = 1 101 | end 102 | end 103 | while tok:is_valid() do 104 | local token, cond, cond_type = tok:get_token(), nil, nil 105 | local op, op_unary, info = ops[token], nil, {} 106 | 107 | if op then -- binary or ternary operator 108 | if op.parse then 109 | cond, cond_type = op.parse(compiler, tok) 110 | if cond_type then 111 | cond = { 112 | value = cond, 113 | type = cond_type 114 | } 115 | end 116 | else 117 | tok:next() 118 | end 119 | if ops_unary[tok:get_token()] then -- has unary operator ('-' or 'not') 120 | op_unary = ops_unary[tok:get_token()] 121 | tok:next() 122 | end 123 | 124 | local leaf 125 | if tok:is("(") then 126 | leaf = { 127 | value = "(" .. compiler:parse_value(tok, info) .. ")", 128 | type = info.type, 129 | bracket = true, 130 | raw = info.raw 131 | } 132 | else 133 | leaf = { 134 | value = compiler:parse_value(tok, info), 135 | type = info.type, 136 | bracket = false, 137 | raw = info.raw 138 | } 139 | end 140 | self.nodes = self.nodes + 1 141 | if self.current.value then -- first element of the tree 142 | self:insert(op, leaf, cond) 143 | elseif self.current.op.order <= op.order then 144 | while self.current.p and self.current.op.order < op.order do 145 | self:up() 146 | end 147 | self:insert(op, leaf, cond) 148 | else -- self.current.op.order > op.order 149 | while self.current.r.op and self.current.op.order > op.order do -- just in case 150 | self:down() 151 | end 152 | self:fork(op, leaf, cond) 153 | end 154 | if op_unary then 155 | self:fork(op_unary, nil) 156 | end 157 | elseif ops_unary[token] then -- some unary operators are specific (e.g. 'is') 158 | op_unary = ops_unary[token] 159 | if op_unary.parse then 160 | cond, cond_type = op_unary.parse(compiler, tok) 161 | if cond then 162 | cond = { 163 | value = cond, 164 | type = cond_type or "expr" 165 | } 166 | end 167 | end 168 | if self.current.value then -- first element of the tree 169 | self:fork(op_unary, nil, cond) 170 | elseif self.current.op.order <= op_unary.order then 171 | while self.current.p and self.current.op.order < op_unary.order do 172 | self:up() 173 | end 174 | self:fork(op_unary, nil, cond) 175 | else -- self.current.op.order > op_unary.order 176 | while self.current.r.op and self.current.op.order > op_unary.order do -- just in case 177 | self:down() 178 | end 179 | self:fork(op_unary, nil, cond) 180 | end 181 | self.nodes = self.nodes + 1 182 | else 183 | break 184 | end 185 | end 186 | return self 187 | end 188 | 189 | --- Insert new node in the current branch 190 | --- @param op aspect.ast.op 191 | --- @param r aspect.ast.leaf 192 | function ast:insert(op, r, cond) 193 | --print("INSERT " .. op.token) 194 | local parent = self.current.p 195 | self.current = { 196 | p = parent, 197 | op = op, 198 | l = self.current, 199 | r = r, 200 | c = cond 201 | } 202 | if cond and cond.op then 203 | cond.p = self.current 204 | end 205 | if parent then 206 | parent.r = self.current 207 | end 208 | if self.current.l.op then 209 | self.current.l.p = self.current 210 | end 211 | end 212 | 213 | --- Insert new node and create new branch 214 | --- @param op aspect.ast.op 215 | --- @param r aspect.ast.leaf|nil if nil - unary operator, just create new branch with current leaf 216 | function ast:fork(op, r, cond) 217 | local current_r = self.current.r 218 | if op.type == "binary" and r then 219 | self.current.r = { 220 | p = self.current, 221 | op = op, 222 | l = current_r, 223 | r = r, 224 | c = cond 225 | } 226 | if self.current.r.l.op then 227 | self.current.r.l.p = self.current.r 228 | end 229 | self.current = self.current.r 230 | elseif op.type == "unary" and not r then 231 | if cond then -- unary with cond - test operator 232 | self.current = { 233 | p = self.current.p, 234 | op = op, 235 | l = nil, 236 | r = self.current, 237 | c = cond 238 | } 239 | if self.current.r.op then 240 | self.current.r.p = self.current 241 | end 242 | else 243 | self.current.r = { 244 | p = self.current, 245 | op = op, 246 | l = nil, 247 | r = current_r, 248 | c = cond 249 | } 250 | if current_r.op then 251 | current_r.p = self.current.r 252 | end 253 | end 254 | else 255 | assert(false, "right is nil but operator is not unary") 256 | end 257 | end 258 | 259 | --- Move pointer to up of tree (by nodes) 260 | function ast:up() 261 | if self.current.p then 262 | self.current = self.current.p 263 | end 264 | end 265 | 266 | --- Move pointer to down of tree (by nodes) 267 | function ast:down() 268 | if self.current.r and not self.current.value then 269 | self.current = self.current.r 270 | end 271 | end 272 | 273 | --- @return aspect.ast.node|aspect.ast.leaf 274 | function ast:get_root() 275 | local node = self.current 276 | while node.p do 277 | node = node.p 278 | end 279 | return node 280 | end 281 | 282 | --- @param node aspect.ast.node|aspect.ast.leaf 283 | --- @param indent string 284 | local function dump_visit(node, indent) 285 | local out = {} 286 | if node.op then -- if aspect.ast.node 287 | insert(out, "\n" .. indent .. "OP " .. node.op.token .. " " .. (node.op.delimiter or "") .. " (order " .. node.op.order .. " )") 288 | if node.c then 289 | insert(out, indent .. "c: " .. dump_visit(node.c, indent .. " ")) 290 | end 291 | if node.l then 292 | insert(out, indent .. "l: " .. dump_visit(node.l, indent .. " ")) 293 | end 294 | if node.r then 295 | insert(out, indent .. "r: " .. dump_visit(node.r, indent .. " ")) 296 | end 297 | elseif type(node.value) == "table" then 298 | insert(out, node.type .. "(" .. utils.dump(node.value) .. ")") 299 | else 300 | insert(out, node.type .. "(" .. node.value .. ")") 301 | end 302 | 303 | return concat(out, "\n") 304 | end 305 | 306 | --- @return string 307 | function ast:dump() 308 | return dump_visit(self:get_root(), "") 309 | end 310 | 311 | 312 | --- @param node aspect.ast.node 313 | --- @return aspect.ast.leaf 314 | local function pack_node(node) 315 | if node.op then -- if aspect.ast.node 316 | local left, right, cond = node.l, node.r, node.c 317 | if left then 318 | if left.op then 319 | left = pack_node(left) 320 | end 321 | left = cast(left.value, left.type, node.op.l) 322 | end 323 | if right then 324 | if right.op then 325 | right = pack_node(right) 326 | end 327 | right = cast(right.value, right.type, node.op.r) 328 | end 329 | if cond then 330 | if cond.op then 331 | cond = pack_node(cond) 332 | end 333 | cond = cast(cond.value, cond.type, node.op.c) 334 | end 335 | local v = node.op.pack(left, right, cond) 336 | if node.op.brackets then 337 | v = "(" .. v .. ")" 338 | end 339 | return { 340 | value = v, 341 | type = node.op.out, 342 | brackets = node.op.brackets, 343 | } 344 | else -- is leaf 345 | return node 346 | end 347 | end 348 | 349 | --- @param node aspect.ast.node 350 | --- @param callback function 351 | local function visit(node, callback) 352 | if node.op then -- if aspect.ast.node 353 | local left, right, cond = node.l, node.r, node.c 354 | if left and left.op then 355 | left = visit(left, callback) 356 | end 357 | if right and right.op then 358 | right = visit(right, callback) 359 | end 360 | if cond and cond.op then 361 | cond = visit(cond, callback) 362 | end 363 | return { 364 | value = callback(node.op, left, right, cond), 365 | type = node.op.out, 366 | } 367 | else -- is leaf 368 | return node 369 | end 370 | 371 | end 372 | 373 | --- @param callback nil|fun(left:any, right:any, cond:any) 374 | function ast:pack(callback) 375 | if callback then 376 | return visit(self:get_root(), callback) 377 | elseif self.nodes == 1 then -- 378 | return self.current 379 | else 380 | return pack_node(self:get_root()) 381 | end 382 | end 383 | 384 | return ast -------------------------------------------------------------------------------- /src/aspect/ast/ops.lua: -------------------------------------------------------------------------------- 1 | local require = require 2 | 3 | --- The operator settings 4 | --- @class aspect.ast.op 5 | --- @field order number operator precedence (from lower to higher) 6 | --- @field token string operator template token 7 | --- @field type string type of operator& one of: unary, binary, ternary 8 | --- @field parser fun(c:aspect.compiler, tok:aspect.tokenizer) how to parse operator in the template, returns cond 9 | --- @field pack fun(l:string, r:string, c:string) how to pack lua code 10 | --- @field delimiter string|nil ternary delimiter 11 | --- @field brackets boolean operator in brackets 12 | --- @field c string|nil fixture type of condition branch 13 | --- @field l string|nil fixture type of left member (branch) 14 | --- @field r string fixture type of right member (branch) 15 | --- @field out string fixture type of operator's result 16 | local _ = {} 17 | 18 | local ops = { 19 | -- ** 20 | { 21 | order = 4, 22 | token = "**", 23 | type = "binary", 24 | l = "number", 25 | r = "number", 26 | out = "number", 27 | pack = function (left, right) 28 | return left .. " ^ " .. right 29 | end 30 | }, 31 | -- - (unary) 32 | { 33 | order = 5, 34 | token = "-", 35 | type = "unary", 36 | r = "number", 37 | out = "number", 38 | pack = function (_, right) 39 | return "-" .. right 40 | end 41 | }, 42 | -- not (unary) 43 | { 44 | order = 5, 45 | token = "not", 46 | type = "unary", 47 | r = "boolean", 48 | out = "boolean", 49 | pack = function (_, right) 50 | return "not " .. right 51 | end 52 | }, 53 | 54 | -- is, is not 55 | { 56 | order = 6, 57 | token = "is", 58 | type = "unary", 59 | r = "any", 60 | out = "boolean", 61 | parse = function (compiler, tok) 62 | return compiler:parse_is(tok), "test" 63 | end, 64 | pack = function (left, right, test) 65 | local expr = "__.opts.t.is_" .. test.name .. "(__, " .. right .. ", " .. (test.expr or "nil") .. ")" 66 | if test["not"] then 67 | return "not " .. expr 68 | else 69 | return expr 70 | end 71 | end 72 | }, 73 | 74 | -- in, not in 75 | { 76 | order = 6, 77 | token = "in", 78 | type = "binary", 79 | l = "any", 80 | r = "any", 81 | out = "boolean", 82 | pack = function (left, right) 83 | return "__.f.inthe(" .. left .. ", " .. right .. ")" 84 | end 85 | }, 86 | { 87 | order = 6, 88 | token = "not", 89 | type = "binary", 90 | l = "any", 91 | r = "any", 92 | out = "boolean", 93 | parse = function (compiler, tok) 94 | tok:require("not"):next():require("in"):next() 95 | end, 96 | pack = function (left, right) 97 | return "not __.f.inthe(" .. left .. ", " .. right .. ")" 98 | end 99 | }, 100 | 101 | -- *, /, //, % 102 | { 103 | order = 7, 104 | token = "*", 105 | type = "binary", 106 | l = "number", 107 | r = "number", 108 | out = "number", 109 | pack = function (left, right) 110 | return left .. " * " .. right 111 | end 112 | }, 113 | { 114 | order = 7, 115 | token = "/", 116 | type = "binary", 117 | l = "number", 118 | r = "number", 119 | out = "number", 120 | pack = function (left, right) 121 | return left .. " / " .. right 122 | end 123 | }, 124 | { 125 | order = 7, 126 | token = "//", 127 | type = "binary", 128 | l = "number", 129 | r = "number", 130 | out = "number", 131 | pack = function (left, right) 132 | return "__.floor(" .. left .. " / " .. right .. ")" 133 | end 134 | }, 135 | { 136 | order = 7, 137 | token = "%", 138 | type = "binary", 139 | l = "number", 140 | r = "number", 141 | out = "number", 142 | pack = function (left, right) 143 | return left .. " % " .. right 144 | end 145 | }, 146 | 147 | -- +, - 148 | { 149 | order = 8, 150 | token = "+", 151 | type = "binary", 152 | l = "number", 153 | r = "number", 154 | out = "number", 155 | pack = function (left, right) 156 | return left .. " + " .. right 157 | end 158 | }, 159 | { 160 | order = 8, 161 | token = "-", 162 | type = "binary", 163 | l = "number", 164 | r = "number", 165 | out = "number", 166 | pack = function (left, right) 167 | return left .. " - " .. right 168 | end 169 | }, 170 | 171 | -- ~ 172 | { 173 | order = 9, 174 | token = "~", 175 | type = "binary", 176 | l = "string", 177 | r = "string", 178 | out = "string", 179 | pack = function (left, right) 180 | return left .. " .. " .. right 181 | end 182 | }, 183 | 184 | -- <, >, <=, >=, !=, == 185 | { 186 | order = 10, 187 | token = "<", 188 | type = "binary", 189 | l = "number", 190 | r = "number", 191 | out = "boolean", 192 | pack = function (left, right) 193 | return left .. " < " .. right 194 | end 195 | }, 196 | { 197 | order = 10, 198 | token = ">", 199 | type = "binary", 200 | l = "number", 201 | r = "number", 202 | out = "boolean", 203 | pack = function (left, right) 204 | return left .. " > " .. right 205 | end 206 | }, 207 | { 208 | order = 10, 209 | token = "<=", 210 | type = "binary", 211 | l = "number", 212 | r = "number", 213 | out = "boolean", 214 | pack = function (left, right) 215 | return left .. " <= " .. right 216 | end 217 | }, 218 | { 219 | order = 10, 220 | token = ">=", 221 | type = "binary", 222 | l = "number", 223 | r = "number", 224 | out = "boolean", 225 | pack = function (left, right) 226 | return left .. " >= " .. right 227 | end 228 | }, 229 | { 230 | order = 10, 231 | token = "!=", 232 | type = "binary", 233 | l = "any", 234 | r = "any", 235 | out = "boolean", 236 | pack = function (left, right) 237 | return left .. " ~= " .. right 238 | end 239 | }, 240 | { 241 | order = 10, 242 | token = "==", 243 | type = "binary", 244 | l = "any", 245 | r = "any", 246 | out = "boolean", 247 | pack = function (left, right) 248 | return left .. " == " .. right 249 | end 250 | }, 251 | 252 | -- ?:, ?? 253 | { 254 | order = 11, 255 | token = "??", 256 | type = "binary", 257 | l = "any", 258 | r = "any", 259 | out = "any", 260 | brackets = true, 261 | pack = function (left, right) 262 | return left .. " and " .. right 263 | end 264 | }, 265 | { 266 | order = 11, 267 | token = "?:", 268 | type = "binary", 269 | l = "boolean|any", 270 | r = "any", 271 | out = "any", 272 | brackets = true, 273 | pack = function (left, right) 274 | return left .. " or " .. right 275 | end 276 | }, 277 | 278 | -- ? ... : ... 279 | { 280 | order = 12, 281 | token = "?", 282 | type = "binary", -- yeah, be binary 283 | delimiter = ":", 284 | c = "any", -- center 285 | l = "boolean", -- left 286 | r = "any", -- right 287 | out = "any", 288 | brackets = true, 289 | --- @param compiler aspect.compiler 290 | --- @param tok aspect.tokenizer 291 | parse = function (compiler, tok) 292 | local ast = require("aspect.ast").new() 293 | local root = ast:parse(compiler, tok:require("?"):next()):get_root() 294 | tok:require(":"):next() 295 | return root 296 | end, 297 | pack = function (left, right, center) 298 | return "(" .. left .. ") and (" .. center .. ") or (" .. right .. ")" 299 | end 300 | }, 301 | 302 | -- and 303 | { 304 | order = 13, 305 | token = "and", 306 | type = "binary", 307 | l = "boolean", 308 | r = "boolean", 309 | out = "boolean", 310 | pack = function (left, right) 311 | return left .. " and " .. right 312 | end 313 | }, 314 | 315 | -- or 316 | { 317 | order = 14, 318 | token = "or", 319 | type = "binary", 320 | l = "boolean", 321 | r = "boolean", 322 | out = "boolean", 323 | pack = function (left, right) 324 | return left .. " or " .. right 325 | end 326 | }, 327 | } 328 | 329 | return ops -------------------------------------------------------------------------------- /src/aspect/cli.lua: -------------------------------------------------------------------------------- 1 | local var_dump = require("aspect.utils").var_dump 2 | local dump = require("aspect.utils").dump 3 | local numerate = require("aspect.utils").numerate_lines 4 | local get_keys = require("aspect.utils").keys 5 | local aspect = require("aspect.template") 6 | local fs_loader = require("aspect.loader.filesystem") 7 | local json = require("aspect.config").json 8 | local table = table 9 | 10 | local cli = { 11 | aliases = { 12 | lint = true, 13 | l = "lint", 14 | 15 | debug = true, 16 | d = "debug", 17 | 18 | include = true, 19 | I = "include", 20 | 21 | help = true, 22 | h = "help", 23 | 24 | escape = true, 25 | e = "escape", 26 | 27 | dump = true, 28 | p = "dump", 29 | 30 | arg = true, 31 | a = "arg" 32 | } 33 | } 34 | 35 | --- @param arguments table list of arguments 36 | --- @param aliases table list of possible arguments and short aliases 37 | --- @param with_values table arguments with values 38 | --- @return table with arguments and values (true - if no value) 39 | --- @return table list of paths at the end 40 | --- @return string error 41 | function cli.parse_args(arguments, aliases, with_values) 42 | with_values = with_values or {} 43 | aliases = aliases or {} 44 | local vals, paths, i = {}, {}, 1 45 | while i <= #arguments do 46 | local arg = arguments[i] 47 | local prefix, _name, value = arg:match("^(%-%-?)([%a_-]+)=?(.*)") 48 | if value == "" then 49 | value = nil 50 | end 51 | if prefix and #paths == 0 then 52 | if aliases[_name] then 53 | local name 54 | if prefix == "-" and aliases[_name] ~= true then -- this is alias 55 | name = aliases[_name] 56 | else 57 | name = _name 58 | end 59 | if with_values[name] and value then 60 | vals[name] = value 61 | elseif with_values[name] and not value then 62 | if not arguments[i + 1] or (arguments[i + 1] and arguments[i + 1]:match("^%-")) then 63 | return nil, nil, "no value for '".. _name .. "'" 64 | end 65 | if vals[name] then 66 | if type(vals[name]) ~= "table" then 67 | vals[name] = {vals[name]} 68 | end 69 | table.insert(vals[name], arguments[i + 1]) 70 | else 71 | vals[name] = arguments[i + 1] 72 | end 73 | i = i + 1 74 | elseif not with_values[name] and value then 75 | return nil, nil, "flag '" .. _name .. "' should be without value" 76 | else 77 | vals[name] = true 78 | end 79 | else 80 | return nil, nil, "unknown flag '" .. _name .. "'" 81 | end 82 | else 83 | table.insert(paths, 1, arg) 84 | end 85 | i = i + 1 86 | end 87 | return vals, paths 88 | end 89 | 90 | --- Read whole file 91 | --- @param path string file path 92 | --- @return string data 93 | --- @return string error 94 | function cli.read_file(path) 95 | local f, err, data 96 | f, err = io.open(path, "rb") 97 | if err then 98 | return nil, err 99 | end 100 | data, err = f:read("*all") 101 | if err then 102 | return nil, err 103 | end 104 | f:close() 105 | if not data or data == "" then 106 | return nil, "empty file" 107 | end 108 | return data 109 | end 110 | 111 | function cli.run(arguments) 112 | arguments = arguments or {} 113 | local autoescape = false 114 | local options, paths, err = cli.parse_args(arguments, cli.aliases, {include = true, arg = true} ) 115 | if err then 116 | return 1, "[ERROR] " .. err 117 | end 118 | --local options = parse_args(arguments, {include = true, I = true}) 119 | --for k, v in pairs(options) do 120 | -- if cli.aliases[k] then 121 | -- options[cli.aliases[k]] = v 122 | -- end 123 | --end 124 | if options.help or #arguments < 2 then 125 | return 0, "Aspect " .. aspect._VERSION .. ", the template engine.\n\n" ..[[ 126 | Usage: aspect [options] data_file template_name 127 | Syntax: https://github.com/unifire-app/aspect/blob/master/docs/syntax.md 128 | 129 | Params: 130 | data_file path to file with fixture (json) 131 | template_name path or name of the template for rendering 132 | 133 | Options: 134 | --help -h print this help 135 | --include -I use for loading other templates 136 | --escape -e enables auto-escaping with 'html' strategy. 137 | --lint just lint the template 138 | --dump dump information about template 139 | --debug -d print debug information 140 | --arg -a argument 141 | 142 | Examples: 143 | Render JSON file to STDOUT or file: 144 | $ aspect path/to/fixture.json path/to/template.tmpl 145 | $ aspect path/to/fixture.json path/to/template.tmpl >path/to/result.txt 146 | 147 | Render data from STDIN (using -): 148 | $ aspect - path/to/template.tmpl 149 | 150 | Read template from STDIN (using -): 151 | $ aspect path/to/fixture.json - 152 | 153 | Lint the template: 154 | $ aspect --lint path/to/template.tmpl 155 | ]] 156 | end 157 | local function verbose(...) 158 | io.stderr:write("[DEBUG] " .. dump(...) .. "\n") 159 | end 160 | 161 | if options.debug then 162 | verbose("command options", options) 163 | end 164 | 165 | if options.escape then 166 | autoescape = true 167 | end 168 | 169 | local template_file, template, tpl, loader = paths[1], nil, nil, nil 170 | --- @type aspect.compiler 171 | local build 172 | local data_file, data = paths[2], nil 173 | 174 | --- Select template 175 | local output 176 | local templater = aspect.new() 177 | if template_file == '-' then 178 | if options.debug then 179 | verbose("loading template from STDIN") 180 | end 181 | template, err = io.stdin:read("*a") 182 | template_file = "STDIN" 183 | else 184 | if options.debug then 185 | verbose("loading template from " .. tostring(template_file)) 186 | end 187 | template, err = cli.read_file(template_file) 188 | end 189 | if err then 190 | return 1, "[ERROR] Failed to load template: " .. err 191 | end 192 | if options.debug then 193 | verbose("template (" .. string.len(template) .. " bytes): ", numerate(template)) 194 | end 195 | if options.include then 196 | if options.debug then 197 | verbose("enable loader from " .. tostring(options.include)) 198 | end 199 | loader = fs_loader.new(options.include) 200 | end 201 | templater.loader = function(name, t) 202 | if name == template_file then 203 | return template 204 | elseif loader then 205 | return loader(name, t) 206 | end 207 | end 208 | tpl, err, build = templater:load(template_file) 209 | if err then 210 | return 1, "[ERROR] Failed to load and compile template: " .. tostring(err) 211 | end 212 | if options.debug then 213 | verbose("template code: ", numerate(build:get_code())) 214 | end 215 | if options.lint then 216 | return 0 217 | end 218 | if options.dump then 219 | local out = {} 220 | table.insert(out, "REFS:" .. dump(build.ctx.tpl_refs)) 221 | table.insert(out, "VARS:" .. dump(build.ctx.var_refs)) 222 | if next(build.blocks) then 223 | for bn, b in pairs(build.blocks) do 224 | table.insert(out, "BLOCK: " .. bn) 225 | table.insert(out, " DESCR: " .. (b.desc or "")) 226 | table.insert(out, " LOCATION: " 227 | .. b.start_line .. "-" .. b.end_line .. " lines or " .. b.start_pos .. "-" .. b.end_pos .. " bytes") 228 | table.insert(out, " REFS: " .. dump(b.ctx.tpl_refs)) 229 | table.insert(out, " VARS " .. dump(b.ctx.var_refs)) 230 | end 231 | end 232 | if next(build.macros) then 233 | for mn, m in pairs(build.macros) do 234 | table.insert(out, "MACRO: " .. mn) 235 | table.insert(out, " DESCR: " .. (m.desc or "")) 236 | table.insert(out, " LOCATION: " 237 | .. m.start_line .. "-" .. m.end_line .. " lines") 238 | table.insert(out, " ARGS: " .. dump(m.args)) 239 | table.insert(out, " REFS: " .. dump(m.ctx.tpl_refs)) 240 | table.insert(out, " VARS " .. dump(m.ctx.var_refs)) 241 | end 242 | end 243 | --if next(build.refs) then 244 | -- table.insert(out, "REFS:") 245 | -- for tn, t in pairs(build.refs) do 246 | -- table.insert(out, " " .. tn .. ": " .. table.concat(get_keys(t), ", ")) 247 | -- end 248 | --end 249 | --for bn, b in pairs(build.blocks) do 250 | -- table.insert(out, "BLOCK: " .. bn) 251 | -- table.insert(out, " lines: " .. b.start_line .. "-" .. b.end_line) 252 | -- table.insert(out, " use parent(): " .. (b.parent and 'yes' or 'no')) 253 | -- if next(b.used_vars) then 254 | -- table.insert(out, " used variables: ") 255 | -- for vn, v in pairs(b.used_vars) do 256 | -- table.insert(out, " " .. vn .. ":") 257 | -- local keys = get_keys(v.keys) 258 | -- if #keys > 0 then 259 | -- table.insert(out, " keys: " .. table.concat(keys, ", ")) 260 | -- end 261 | -- table.insert(out, " where: ") 262 | -- for _, w in pairs(v.where) do 263 | -- table.insert(out, " - " .. template_file .. ":" .. w.line .. " in tag " .. (w.tag or "--")) 264 | -- end 265 | -- end 266 | -- end 267 | -- --table.insert(out, "Block " .. bn .. ", lines " .. b.start_line .. "-" .. b.end_line .. ": " .. dump(b)) 268 | --end 269 | --for mn, m in pairs(build.blocks) do 270 | -- 271 | --end 272 | table.insert(out, "TEMPLATE:") 273 | table.insert(out, numerate(template, " ")) 274 | table.insert(out, "CODE:") 275 | table.insert(out, numerate(build:get_code(), " ")) 276 | return 0, table.concat(out, "\n") 277 | end 278 | 279 | --- Read data 280 | if data_file == '-' and not template then 281 | if options.debug then 282 | verbose("reading data from STDIN") 283 | end 284 | data, err = io.stdin:read("*a") 285 | data_file = "STDIN" 286 | else 287 | if options.debug then 288 | verbose("reading data from " .. tostring(data_file)) 289 | end 290 | data, err = cli.read_file(data_file) 291 | end 292 | if err then 293 | return 1, "[ERROR] Failed to read data: " .. err 294 | end 295 | if options.debug then 296 | verbose("data (" .. string.len(data) .. " bytes): ", data) 297 | end 298 | if not json.decode then 299 | return 1, "[ERROR] " .. json.error 300 | end 301 | data, err = json.decode(data) 302 | if not data then 303 | return 1, "[ERROR] Failed to decode data: " .. tostring(err) 304 | end 305 | 306 | output, err = templater:render(template_file, data, { 307 | autoescape = autoescape 308 | }) 309 | if err then 310 | return 1, "[ERROR] Failed to render template: " .. tostring(err) 311 | end 312 | return 0, tostring(output) 313 | end 314 | 315 | function cli.shell() 316 | local code, result = cli.run(_G.arg or {}) 317 | if code == 0 then 318 | if result then 319 | io.stdout:write(result) 320 | end 321 | os.exit(0) 322 | else 323 | io.stderr:write(result .. "\n") 324 | os.exit(code) 325 | end 326 | end 327 | 328 | return cli -------------------------------------------------------------------------------- /src/aspect/config.lua: -------------------------------------------------------------------------------- 1 | local pcall = pcall 2 | local require = require 3 | local getmetatable = getmetatable 4 | 5 | --- Internal configuration of 6 | --- Aspect Template Engine. Be careful. 7 | --- @class aspect.config 8 | local config = {} 9 | 10 | --- JSON configuration 11 | config.json = { 12 | encode = nil, 13 | decode = nil, 14 | error = "JSON encode/decode no available. Please install `cjson` or `json` or configure `require('aspect.config').json` before using Aspect" 15 | } 16 | 17 | --- UTF8 configuration 18 | config.utf8 = { 19 | len = nil, 20 | lower = nil, 21 | upper = nil, 22 | sub = nil, 23 | match = nil, 24 | } 25 | 26 | config.env = { 27 | 28 | } 29 | 30 | --- escape filter settings (HTML strategy) 31 | config.escape = { 32 | pattern = "[}{\"><'&]", 33 | replaces = { 34 | ["&"] = "&", 35 | ["<"] = "<", 36 | [">"] = ">", 37 | ['"'] = """, 38 | ["'"] = "'" 39 | } 40 | } 41 | 42 | --- condition aliases (also see bellow) 43 | config.is_false = { 44 | [""] = true, 45 | [0] = true, 46 | } 47 | 48 | --- empty string variants (also see bellow) 49 | config.is_empty_string = { 50 | [""] = true, 51 | } 52 | 53 | config.is_n = { 54 | } 55 | 56 | --- dynamically configure config.is_false and config.is_empty_string 57 | do 58 | --- https://github.com/openresty/lua-nginx-module#core-constants 59 | local ngx = ngx or {} 60 | if ngx.null then 61 | config.is_false[ngx.null] = true 62 | config.is_empty_string[ngx.null] = true 63 | end 64 | 65 | --- https://luarocks.org/modules/openresty/lua-cjson 66 | local has_cjson, cjson = pcall(require, "cjson.safe") 67 | if has_cjson then 68 | config.is_false[cjson.null] = true 69 | config.is_empty_string[cjson.null] = true 70 | config.json.encode = cjson.encode 71 | config.json.decode = cjson.decode 72 | else 73 | local has_json, json = pcall(require, "json") 74 | if has_json then 75 | config.json.encode = json.encode 76 | config.json.decode = json.decode 77 | if json.null then 78 | config.is_false[json.null] = true 79 | config.is_empty_string[json.null] = true 80 | end 81 | if json.empty_array then 82 | config.is_false[json.empty_array] = true 83 | end 84 | end 85 | end 86 | 87 | --- https://github.com/isage/lua-cbson 88 | local has_cbson, cbson = pcall(require, "cbson") 89 | if has_cbson then 90 | config.is_false[getmetatable(cbson.null())] = true 91 | config.is_false[getmetatable(cbson.array())] = true 92 | config.is_empty_string[getmetatable(cbson.null())] = true 93 | end 94 | 95 | --- https://www.tarantool.io/ru/doc/1.10/reference/reference_lua/yaml/ 96 | local has_yaml, yaml = pcall(require, "yaml") 97 | if has_yaml and yaml.NULL then 98 | config.is_false[yaml.NULL] = true 99 | config.is_empty_string[yaml.NULL] = true 100 | end 101 | 102 | --- https://github.com/gvvaughan/lyaml 103 | local has_lyaml, lyaml = pcall(require, "lyaml") 104 | if has_lyaml then 105 | config.is_false[lyaml.null] = true 106 | config.is_empty_string[lyaml.null] = true 107 | end 108 | 109 | --- https://www.tarantool.io/ru/doc/2.4/reference/reference_lua/msgpack/ 110 | local has_msgpack, msgpack = pcall(require, "msgpack") 111 | if has_msgpack then 112 | config.is_false[msgpack.NULL] = true 113 | config.is_empty_string[msgpack.NULL] = true 114 | end 115 | 116 | --- https://www.tarantool.io/en/doc/2.4/reference/reference_lua/box_null/ 117 | if box and box.NULL then 118 | config.is_false[box.NULL] = true 119 | config.is_empty_string[box.NULL] = true 120 | end 121 | end 122 | 123 | --- Detect UTF8 module 124 | do 125 | local utf8 126 | for _, name in ipairs({"lua-utf8", "utf8", "lutf8"}) do 127 | local ok, module = pcall(require, name) 128 | if ok then 129 | utf8 = module 130 | break 131 | end 132 | end 133 | if utf8 then 134 | config.utf8.len = utf8.len or utf8.length 135 | config.utf8.upper = utf8.upper 136 | config.utf8.lower = utf8.lower 137 | config.utf8.sub = utf8.sub 138 | config.utf8.match = utf8.match 139 | config.utf8.find = utf8.find 140 | end 141 | end 142 | 143 | --- Compiler configuration 144 | config.compiler = { 145 | is_boolean = { 146 | ["true"] = true, 147 | ["false"] = true 148 | }, 149 | special = { 150 | ["true"] = "true", 151 | ["false"] = "false", 152 | ["nil"] = "nil", 153 | ["null"] = "nil" 154 | }, 155 | --- danger variables names 156 | reserved_words = { 157 | ["and"] = true, 158 | ["break"] = true, 159 | ["do"] = true, 160 | ["else"] = true, 161 | ["elseif"] = true, 162 | ["end"] = true, 163 | ["false"] = true, 164 | ["for"] = true, 165 | ["function"] = true, 166 | ["if"] = true, 167 | ["in"] = true, 168 | ["local"] = true, 169 | ["nil"] = true, 170 | ["not"] = true, 171 | ["or"] = true, 172 | ["repeat"] = true, 173 | ["return"] = true, 174 | ["then"] = true, 175 | ["true"] = true, 176 | ["until"] = true, 177 | ["while"] = true, 178 | }, 179 | --- correspondence table for math operators 180 | math_ops = { 181 | ["+"] = "+", 182 | ["-"] = "-", 183 | ["/"] = "/", 184 | ["*"] = "*", 185 | ["%"] = "%", 186 | ["**"] = "^", 187 | }, 188 | --- correspondence table for comparison operators 189 | comparison_ops = { 190 | ["=="] = "==", 191 | ["!="] = "~=", 192 | [">="] = ">=", 193 | ["<="] = "<=", 194 | ["<"] = "<", 195 | [">"] = ">", 196 | }, 197 | --- correspondence table for logic operators 198 | logic_ops = { 199 | ["and"] = "and", 200 | ["or"] = "or" 201 | }, 202 | other_ops = { 203 | ["~"] = true, 204 | ["|"] = true, 205 | ["."] = true, 206 | ["["] = true, 207 | ["]"] = true, 208 | [","] = true 209 | }, 210 | --- reserved variables names 211 | reserved_vars = { 212 | _self = true, 213 | _context = true, 214 | _charset = true, 215 | __ = true 216 | }, 217 | 218 | tag_type = { 219 | EXPRESSION = 1, 220 | CONTROL = 2, 221 | COMMENT = 3, 222 | }, 223 | 224 | strip = { 225 | ["-"] = "%s", 226 | ["~"] = "[ \t]" 227 | } 228 | } 229 | 230 | config.tokenizer = { 231 | patterns = { 232 | NUMBER1 = '^[%+%-]?%d+%.?%d*[eE][%+%-]?%d+', -- 123.45e-32 233 | NUMBER2 = '^[%+%-]?%d+%.?%d*', -- 123 or 123.456 234 | NUMBER3 = '^0x[%da-fA-F]+', -- 0xDeadBEEF 235 | NUMBER4 = '^%d+%.?%d*[eE][%+%-]?%d+', 236 | NUMBER5 = '^%d+%.?%d*', 237 | WORD = '^[%a_][%w_]*', 238 | WSPACE = '^%s+', 239 | STRING1 = "^(['\"])%1", -- empty string 240 | STRING2 = [[^(['"])(\*)%2%1]], 241 | STRING3 = [[^(['"]).-[^\](\*)%2%1]], 242 | CHAR1 = "^''", 243 | CHAR2 = [[^'(\*)%1']], 244 | CHAR3 = [[^'.-[^\](\*)%1']], 245 | PREPRO = '^#.-[^\\]\n', 246 | } 247 | } 248 | 249 | config.loop = { 250 | keys = { 251 | parent = true, 252 | iteration = true, 253 | index = true, 254 | index0 = true, 255 | revindex = true, 256 | revindex0 = true, 257 | first = true, 258 | last = true, 259 | length = true, 260 | prev_item = true, 261 | next_item = true, 262 | has_more = true, 263 | 264 | --- trees 265 | level = true, 266 | level0 = true, 267 | first_node = true, 268 | last_node = true, 269 | path = true 270 | } 271 | } 272 | 273 | config.macro = { 274 | import_type = { 275 | GROUP = 1, 276 | SINGLE = 2 277 | } 278 | } 279 | 280 | config.escapers = {} 281 | 282 | config.date = { 283 | months = { 284 | ["jan"] = 1, ["january"] = 1, 285 | ["feb"] = 2, ["february"] = 2, 286 | ["mar"] = 3, ["march"] = 3, 287 | ["apr"] = 4, ["april"] = 4, 288 | ["may"] = 5, ["may"] = 5, 289 | ["jun"] = 6, ["june"] = 6, 290 | ["jul"] = 7, ["july"] = 7, 291 | ["aug"] = 8, ["august"] = 8, 292 | ["sep"] = 9, ["september"] = 9, 293 | ["oct"] = 10, ["october"] = 10, 294 | ["nov"] = 11, ["november"] = 11, 295 | ["dec"] = 12, ["december"] = 12, 296 | }, 297 | months_locale = { 298 | en = { 299 | [1] = {"Jan", "January"}, 300 | [2] = {"Feb", "February"}, 301 | [3] = {"Mar", "March"}, 302 | [4] = {"Apr", "April"}, 303 | [5] = {"May", "May"}, 304 | [6] = {"Jun", "June"}, 305 | [7] = {"Jul", "July"}, 306 | [8] = {"Aug", "August"}, 307 | [9] = {"Sep", "September"}, 308 | [10] = {"Oct", "October"}, 309 | [11] = {"Nov", "November"}, 310 | [12] = {"Dec", "December"}, 311 | } 312 | }, 313 | week = { 314 | ["mon"] = 1, ["monday"] = 1, 315 | ["tue"] = 2, ["tuesday"] = 2, 316 | ["wed"] = 3, ["wednesday"] = 3, 317 | ["thu"] = 4, ["thursday"] = 4, 318 | ["fri"] = 5, ["friday"] = 5, 319 | ["sat"] = 6, ["saturday"] = 6, 320 | ["sun"] = 7, ["sunday"] = 7, 321 | }, 322 | week_locale = { 323 | en = { 324 | [1] = {"Mon", "Monday"}, 325 | [2] = {"Tue", "Tuesday"}, 326 | [3] = {"Wed", "Wednesday"}, 327 | [4] = {"Thu", "Thursday"}, 328 | [5] = {"Fri", "Friday"}, 329 | [6] = {"Sat", "Saturday"}, 330 | [7] = {"Sun", "Sunday"}, 331 | }, 332 | }, 333 | aliases = { 334 | c = "%a %b %d %H:%m%s %Y", 335 | r = "%I:%M:%S %p", 336 | R = "%I:%M", 337 | T = "%H:%M:%S", 338 | D = "%m/%d/%y", 339 | F = "%Y-%m-%d" 340 | } 341 | } 342 | 343 | return config -------------------------------------------------------------------------------- /src/aspect/date.lua: -------------------------------------------------------------------------------- 1 | local tonumber = tonumber 2 | local tostring = tostring 3 | local pairs = pairs 4 | local ipairs = ipairs 5 | local type = type 6 | local setmetatable = setmetatable 7 | local os = os 8 | local string = string 9 | local math = math 10 | local var_dump = require("aspect.utils").var_dump 11 | local config = require("aspect.config").date 12 | local utf8 = require("aspect.config").utf8 13 | local month = config.months 14 | local current_offset = os.time() - os.time(os.date("!*t", os.time())) 15 | 16 | --- Merge table `b` into table `a` 17 | local function union(a,b) 18 | for k,x in pairs(b) do 19 | a[k] = x 20 | end 21 | return a 22 | end 23 | 24 | local function ctime(d, m, y) 25 | if utf8.lower then 26 | m = utf8.lower(m) 27 | else 28 | m = string.lower(m) 29 | end 30 | if not month[m] then 31 | return nil 32 | end 33 | return { 34 | year = tonumber(y) or tonumber(os.date("%Y")) or 1970, 35 | month = month[m], 36 | day = tonumber(d) 37 | } 38 | end 39 | 40 | --- How to works parsers 41 | --- 1. take `date` parser. 42 | --- 1.1 Iterate by patterns. 43 | --- 1.2 When pattern matched then `match` function will be called. 44 | --- 1.3 `Match` function returns table like os.date("*t") if success, nil if failed (if nil resume 1.1) 45 | --- 2. take `time` parser. search continues with the next character after matched `date` 46 | --- 2.1 Iterate by patterns. 47 | --- 2.2 When pattern matched then `match` function will be called. 48 | --- 2.3 `Match` function returns table like os.date("*t") if success, nil if failed (if nil resume 2.1) 49 | --- 3. take `zone` parser. search continues with the next character after matched `time` 50 | --- 2.1 Iterate by patterns. 51 | --- 2.2 When pattern matched then `match` function will be called. 52 | --- 2.3 `Match` function returns table like os.date("*t") if success, nil if failed (if nil resume 3.1) 53 | --- 4. calculate timestamp 54 | local parsers = { 55 | date = { 56 | -- 2020-12-02, 2020.12.02 57 | { 58 | pattern = "(%d%d%d%d)[.%-](%d%d)[.%-](%d%d)", 59 | match = function(y, m , d) 60 | return {year = tonumber(y), month = tonumber(m), day = tonumber(d)} 61 | end 62 | } , 63 | -- 02-12-2020, 02.12.2020 64 | { 65 | pattern = "(%d%d)[.%-](%d%d)[.%-](%d%d%d%d)", 66 | match = function(y, m , d) 67 | return { 68 | year = tonumber(y), 69 | month = tonumber(m), 70 | day = tonumber(d) 71 | } 72 | end 73 | }, 74 | -- rfc 1123: 14 Jan 2020, 14 January 2020 75 | { 76 | pattern = "(%d%d)[%s-]+(%a%a+)[%s-]+(%d%d%d%d)", 77 | match = function(d, m, y) 78 | return ctime(d, m, y) 79 | end 80 | }, 81 | -- rfc 1123: 14 Jan, 14 January 82 | { 83 | pattern = "(%d%d)[%s-]+(%a%a+)", 84 | match = function(d, m) 85 | return ctime(d, m) 86 | end 87 | }, 88 | -- ctime: Jan 14 2020, January 14 2020 89 | { 90 | pattern = "(%a%a+)%s+(%d%d)%s+(%d%d%d%d)", 91 | match = function(m, d, y) 92 | return ctime(d, m, y) 93 | end 94 | }, 95 | -- ctime: Jan 14, January 14 96 | { 97 | pattern = "(%a%a+)%s+(%d%d)", 98 | match = function(m, d) 99 | return ctime(d, m) 100 | end 101 | }, 102 | -- US format MM/DD/YYYY: 12/23/2020 103 | { 104 | pattern = "(%d%d)/(%d%d)/(%d%d%d%d)", 105 | match = function(m, d, y) 106 | return { 107 | year = tonumber(y), 108 | month = tonumber(m), 109 | day = tonumber(d) 110 | } 111 | end 112 | }, 113 | { 114 | pattern = "(%d%d)/(%d%d)/(%d%d%d%d)", 115 | match = function(m, d, y) 116 | return { 117 | year = tonumber(y), 118 | month = tonumber(m), 119 | day = tonumber(d) 120 | } 121 | end 122 | } 123 | }, 124 | time = { 125 | { 126 | pattern = "(%d%d):(%d%d):?(%d?%d?)", 127 | match = function(h, m, s) 128 | return { 129 | hour = tonumber(h), 130 | min = tonumber(m), 131 | sec = tonumber(s) or 0 132 | } 133 | end 134 | } 135 | }, 136 | zone = { 137 | -- +03:00, -11, +3:30 138 | { 139 | pattern = "([+-])(%d?%d):?(%d?%d?)", 140 | match = function (mod, h, m) 141 | local sign = (mod == "-") and -1 or 1 142 | return { 143 | offset = sign * (tonumber(h) * 60 + (tonumber(m) or 0)) * 60 144 | } 145 | end 146 | }, 147 | -- UTC marker 148 | { 149 | pattern = "UTC", 150 | match = function () 151 | return {offset = 0} 152 | end 153 | }, 154 | -- GMT marker 155 | { 156 | pattern = "GMT", 157 | match = function () 158 | return {offset = 0} 159 | end 160 | } 161 | } 162 | } 163 | 164 | --- @class aspect.date 165 | --- @param time number it is timestamp (UTC) 166 | --- @param offset number UTC time offset (timezone) is seconds 167 | local date = { 168 | _NAME = "aspect.date", 169 | 170 | parsers = parsers, 171 | local_offset = current_offset, 172 | } 173 | 174 | 175 | --- Parse about any textual datetime description into a Unix timestamp 176 | --- @param t string 177 | --- @return number UTC timestamp 178 | --- @return table datetime description: year, month, day, hour, min, sec, ... 179 | function date.strtotime(t) 180 | local from = 1 181 | local time = {day = 1, month = 1, year = 1970} 182 | local find, sub, match = utf8.find or string.find, utf8.sub or string.sub, utf8.match or string.match 183 | for _, parser in ipairs({parsers.date, parsers.time, parsers.zone}) do 184 | for _, matcher in ipairs(parser) do 185 | local i, j = find(t, matcher.pattern, from) 186 | if i then 187 | local res = matcher.match(match(sub(t, i, j), "^" .. matcher.pattern .. "$")) 188 | if res then 189 | union(time, res) 190 | from = j + 1 191 | break 192 | end 193 | end 194 | end 195 | end 196 | local ts = os.time(time) -- time zone ignores 197 | if not time.offset then -- no offset parsed - use local offset 198 | time.offset = current_offset 199 | else 200 | ts = ts - (time.offset - current_offset) 201 | end 202 | return ts, time 203 | end 204 | 205 | --- @param format string date format 206 | --- @param time_zone number|nil UTC time zone offset in seconds 207 | --- @param locale string|nil month and week language 208 | --- @return string 209 | function date:format(format, time_zone, locale) 210 | locale = locale or 'en' 211 | local utc = false 212 | local time = self.time 213 | local offset = time_zone or current_offset 214 | if format:sub(1, 1) == "!" then 215 | utc = true 216 | offset = 0 217 | else 218 | format = "!" .. format 219 | utc = false 220 | time = time + offset 221 | end 222 | --- replace aliases 223 | format = string.gsub(format, "%$(%w)", config.aliases) 224 | --- replace localizable specs 225 | local d = os.date("!*t", time) 226 | format = string.gsub(format, "%%([zZaAbB])", function (spec) 227 | if spec == "a" or spec == "A" then 228 | local i, from = d.wday - 1, nil -- there 1 is sunday, shut up ISO8601[2.2.8] 229 | if i == 0 then 230 | i = 7 231 | end 232 | from = config.week_locale[locale] or config.week_locale['en'] 233 | if spec == "A" then 234 | return from[i][2] 235 | else 236 | return from[i][1] 237 | end 238 | elseif spec == "b" or spec == "B" then 239 | local from = config.months_locale[locale] or config.months_locale['en'] 240 | if spec == "B" then 241 | return from[d.month][2] 242 | else 243 | return from[d.month][1] 244 | end 245 | elseif spec == "z" then 246 | return self.get_timezone(offset, "") 247 | elseif spec == "Z" then 248 | return self.get_timezone(offset, ":", true) 249 | end 250 | return '%' .. spec 251 | end) 252 | --var_dump({format = format, time = time, offset_h = offset / 60, utc = utc, zone = self.get_timezone(offset, ":")}) 253 | return os.date(format, time) 254 | end 255 | 256 | --- Returns offset as time zone 257 | --- @param offset number in seconds 258 | --- @param delim string hours and minutes delimiter 259 | --- @param short boolean use short format 260 | --- @return string like +03:00 or +03 261 | function date.get_timezone(offset, delim, short) 262 | delim = delim or ":" 263 | local sign = (offset < 0) and '-' or '+' 264 | if offset == 0 then 265 | if short then 266 | return "" 267 | else 268 | return sign .. "0000" 269 | end 270 | end 271 | offset = math.abs(offset / 60) -- throw away seconds and sign 272 | local h = offset / 60 273 | local m = offset % 60 274 | if short then 275 | if m == 0 then 276 | return string.format(sign .. "%d", h, m) 277 | else 278 | return string.format(sign .. "%d" .. delim .. "%02d", h, m) 279 | end 280 | else 281 | return string.format(sign .. "%02d" .. delim .. "%02d", h, m) 282 | end 283 | end 284 | 285 | --- @return string 286 | function date:__tostring() 287 | return self:format("%F %T UTC%Z") 288 | end 289 | 290 | --- @param b any 291 | --- @return string 292 | function date:__concat(b) 293 | return tostring(self) .. tostring(b) 294 | end 295 | 296 | --- @param b any 297 | --- @return aspect.date 298 | function date:__add(b) 299 | return date.new(self.time + date.new(b).time, self.offset) 300 | end 301 | 302 | --- @param b any 303 | --- @return aspect.date 304 | function date:__sub(b) 305 | return date.new(self.time - date.new(b).time, self.offset) 306 | end 307 | 308 | --- @param b number 309 | --- @return aspect.date 310 | function date:__mul(b) 311 | if type(b) == "number" and b > 0 then 312 | return date.new(self.time * b, self.offset) 313 | else 314 | return self 315 | end 316 | end 317 | 318 | --- @param b number 319 | --- @return aspect.date 320 | function date:__div(b) 321 | if type(b) == "number" and b > 0 then 322 | return date.new(self.time / b, self.offset) 323 | else 324 | return self 325 | end 326 | end 327 | 328 | function date:__eq(b) 329 | return self.time == date.new(b).time 330 | end 331 | 332 | --- @param b any 333 | --- @return boolean 334 | function date:__lt(b) 335 | return self.time < date.new(b).time 336 | end 337 | 338 | --- @param b any 339 | --- @return boolean 340 | function date:__le(b) 341 | return self.time <= date.new(b).time 342 | end 343 | 344 | local date_mods = { 345 | seconds = "sec", 346 | second = "sec", 347 | secs = "sec", 348 | sec = "sec", 349 | minutes = "min", 350 | minute = "min", 351 | mins = "min", 352 | min = "min", 353 | hours = "hour", 354 | hour = "hour", 355 | days = "day", 356 | day = "day", 357 | months = "month", 358 | month = "month", 359 | years = "year", 360 | year = "year", 361 | } 362 | 363 | function date:modify(t) 364 | local d = os.date("*t", self.time) 365 | for k, v in pairs(t) do 366 | if date_mods[k] then 367 | local name = date_mods[k] 368 | d[name] = d[name] + v 369 | end 370 | end 371 | self.time = os.time(d) 372 | return self 373 | end 374 | 375 | 376 | local mt = { 377 | __index = date, 378 | __tostring = date.__tostring, 379 | __add = date.__add, 380 | __sub = date.__sub, 381 | __mul = date.__mul, 382 | __div = date.__div, 383 | __eq = date.__eq, 384 | __lt = date.__lt, 385 | __le = date.__le, 386 | } 387 | 388 | 389 | function date.new(t, offset) 390 | local typ, time, info = type(t), 0, {} 391 | offset = offset or 0 392 | if typ == "number" then 393 | time = t 394 | elseif typ == "table" then 395 | if t._NAME == date._NAME then 396 | return t 397 | else 398 | local _t = {year = 1970, month = 1, day = 1} 399 | union(_t, t) 400 | time = os.time(_t) 401 | end 402 | elseif typ == "string" or typ == "userdata" then 403 | time, info = date.strtotime(tostring(t)) 404 | offset = info.offset 405 | else 406 | time = os.time() 407 | end 408 | 409 | return setmetatable({ 410 | time = time, 411 | offset = offset, 412 | info = info 413 | }, mt) 414 | end 415 | 416 | return date -------------------------------------------------------------------------------- /src/aspect/error.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | local error = error 3 | local type = type 4 | local traceback = debug.traceback 5 | 6 | --- @class aspect.error 7 | --- @field code string 8 | --- @field line number 9 | --- @field name string 10 | --- @field message string 11 | --- @field callstack string 12 | --- @field traceback string 13 | --- @field context string 14 | local err = { 15 | _NAME = "error" 16 | } 17 | 18 | --- @param e aspect.error 19 | local function to_string(e) 20 | local msg 21 | if e.name then 22 | msg = e.code .. " error: " .. e.message .. " in " .. e.name .. ":" .. e.line 23 | else 24 | msg = e.code .. " error: " .. e.message 25 | end 26 | if e.context then 27 | msg = msg .. "\nContext: " .. e.context .. " <-- there" 28 | end 29 | if e.callstack then 30 | msg = msg .. "\nCallstack:\n" .. e.callstack 31 | end 32 | if e.traceback then 33 | msg = msg .. "\nLua " .. e.traceback 34 | end 35 | return msg 36 | end 37 | 38 | local mt = { 39 | __index = err, 40 | __tostring = to_string 41 | } 42 | 43 | --- @param tok aspect.tokenizer 44 | --- @param code string 45 | --- @param message string 46 | function err.compiler_error(tok, code, message) 47 | local fields = { 48 | code = code, 49 | traceback = traceback(), 50 | } 51 | if tok then 52 | if code == "syntax" then 53 | if tok:is_valid() then 54 | fields.message = "unexpected token '" .. tok:get_token() .. "', " .. message 55 | else 56 | fields.message = "unexpected end of tag, " .. message 57 | end 58 | else 59 | fields.message = message 60 | end 61 | fields.context = tok:get_path_as_string() 62 | else 63 | fields.message = message 64 | end 65 | error(fields) 66 | end 67 | 68 | --- @param __ aspect.output 69 | --- @param message string|aspect.error 70 | function err.runtime_error(__, message) 71 | error(err.new_runtime(__, message)) 72 | end 73 | 74 | function err.is(wtf) 75 | return type(wtf) == "table" and wtf._NAME == "error" 76 | end 77 | 78 | --- @param __ aspect.output 79 | --- @param e aspect.error|string|table 80 | function err.new_runtime(__, e) 81 | if not err.is(e) then 82 | e = err.new(e, "runtime") 83 | end 84 | e:set_name(__.view.name, __.line, __:get_callstack()) 85 | return e 86 | end 87 | 88 | function err.new(e, code) 89 | if type(e) ~= 'table' then 90 | e = { 91 | message = tostring(e), 92 | } 93 | elseif e._NAME == "error" then 94 | return e 95 | end 96 | 97 | return setmetatable({ 98 | code = e.code or code or "internal", 99 | line = e.line or 0, 100 | name = e.name or "runtime", 101 | message = e.message or 'problems', 102 | callstack = nil, 103 | traceback = e.traceback or traceback(), 104 | context = e.context, 105 | }, mt) 106 | end 107 | 108 | function err:set_code(code) 109 | self.code = code 110 | return self 111 | end 112 | 113 | function err:set_name(name, line, callstack) 114 | self.name = name or 'unknown' 115 | self.line = line or 0 116 | self.callstack = callstack 117 | return self 118 | end 119 | 120 | function err:get_message() 121 | return self.message 122 | end 123 | 124 | 125 | return err -------------------------------------------------------------------------------- /src/aspect/funcs.lua: -------------------------------------------------------------------------------- 1 | local unpack = unpack or table.unpack 2 | local concat = table.concat 3 | local err = require("aspect.error") 4 | local compiler_error = err.compiler_error 5 | local runtime_error = err.runtime_error 6 | local config = require("aspect.config") 7 | local tags = require("aspect.tags") 8 | local tag_type = config.compiler.tag_type 9 | local dump = require("aspect.utils").dump 10 | local quote_string = require("aspect.utils").quote_string 11 | local strip = require("aspect.utils").strip 12 | local date = require("aspect.date") 13 | local utils = require("aspect.utils") 14 | local range = require("aspect.utils.range") 15 | local pairs = pairs 16 | local ipairs = ipairs 17 | 18 | local func = { 19 | args = {}, 20 | fn = {}, 21 | parsers = {} 22 | } 23 | 24 | --- Add function 25 | --- @param name string 26 | --- @param info table 27 | --- @param fn fun(...):any 28 | function func.add(name, info, fn) 29 | func.args[name] = info.args 30 | func.fn[name] = fn 31 | func.parsers[name] = info.parser 32 | end 33 | 34 | --- @param __ aspect.output 35 | --- @param name string 36 | --- @param context table 37 | local function parent(__, name, context) 38 | local block 39 | 40 | if __.blocks[name] then 41 | for i = __.blocks[name].i + 1, #__.views do 42 | block = __.views[i].blocks[name] 43 | if block then 44 | break 45 | end 46 | end 47 | else 48 | return nil 49 | end 50 | 51 | if context then 52 | if block then 53 | block(__, context) 54 | end 55 | elseif block then 56 | return true 57 | end 58 | end 59 | 60 | --- Function {{ parent() }} 61 | func.add('parent', { 62 | args = {}, 63 | --- @param compiler aspect.compiler 64 | parser = function(compiler) 65 | local tag = compiler:get_last_tag("block") 66 | if not tag then 67 | compiler_error(nil, "syntax", "{{ parent() }} should be called in the {% block %}") 68 | else 69 | tag.parent = true 70 | end 71 | if compiler.tag_type == tag_type.EXPRESSION then -- {{ parent(...) }} 72 | local vars = utils.implode_hashes(compiler:get_local_vars()) 73 | if vars then 74 | return '__.fn.parent(__, ' .. quote_string(tag.block_name) .. ', __.setmetatable({ ' .. vars .. '}, { __index = _context }))' ; 75 | else 76 | return '__.fn.parent(__, ' .. quote_string(tag.block_name) .. ', _context)' ; 77 | end 78 | else -- {% if parent(...) %} 79 | return '__.fn.parent(__, ' .. quote_string(tag.block_name) .. ', nil)' ; 80 | end 81 | end 82 | }, parent) 83 | 84 | --- @param __ aspect.output 85 | --- @param name string 86 | --- @param template string|nil 87 | local function block(__, context, name, template) 88 | local f 89 | if template then 90 | local view = __:get_view(template) 91 | if view.blocks[name] then 92 | f = view.blocks[name] 93 | end 94 | elseif __.blocks[name] then 95 | f = __.blocks[name].f 96 | end 97 | if context then 98 | if f then 99 | f(__, context) 100 | else 101 | runtime_error(__, "block " .. name .. " not found") 102 | end 103 | else 104 | return f ~= nil 105 | end 106 | end 107 | 108 | --- Function {{ block(name, [template]) }} 109 | func.add('block', { 110 | args = { 111 | [1] = {name = "name", type = "string"}, 112 | [2] = {name = "template", type = "string"}, 113 | }, 114 | --- @param compiler aspect.compiler 115 | --- @param args table 116 | parser = function(compiler, args) 117 | if not args.name then 118 | compiler_error(nil, "syntax", "function block() requires argument 'name'") 119 | end 120 | if compiler.tag_type == tag_type.EXPRESSION then -- {{ block(...) }} 121 | local vars, context = utils.implode_hashes(compiler:get_local_vars()), "_context" 122 | if vars then 123 | context = '__.setmetatable({ ' .. vars .. ' }, { __index = _context })' ; 124 | end 125 | return '__.fn.block(__, ' .. context .. ', ' .. args.name .. ', ' .. (args.template or 'nil') .. ')', false 126 | else -- {% if block(...) %} 127 | return '__.fn.block(__, nil, ' .. args.name .. ', ' .. (args.template or 'nil') .. ')' 128 | end 129 | end 130 | }, block) 131 | 132 | --- Function {{ include(name, [vars], [ignore_missing], [with_context]) }} 133 | func.add('include', { 134 | args = { 135 | [1] = {name = "name", type = "any"}, 136 | [2] = {name = "vars", type = "table"}, 137 | [3] = {name = "ignore_missing", type = "boolean"}, 138 | [4] = {name = "with_context", type = "boolean"}, 139 | }, 140 | --- @param compiler aspect.compiler 141 | --- @param args table 142 | parser = function (compiler, args) 143 | if compiler.tag_type == tag_type.EXPRESSION then 144 | args.with_vars = true 145 | return tags.include(compiler, args) 146 | else 147 | return '__.fn.include(__, ' .. args.name .. ', true, nil)' 148 | end 149 | end 150 | }, function (__, names, ignore, context) 151 | local view, error = __.opts.get(names) 152 | if not context then 153 | return view ~= nil 154 | end 155 | if error then 156 | runtime_error(__, error) 157 | elseif not view then 158 | if not ignore then 159 | runtime_error(__, "Template(s) not found. Trying " .. utils.join(names, ", ")) 160 | else 161 | return 162 | end 163 | end 164 | view.body(__, context) 165 | end) 166 | 167 | --- Function {{ range(from, to, step) }} 168 | func.add('range', { 169 | args = { 170 | [1] = {name = "from", type = "number"}, 171 | [2] = {name = "to", type = "number"}, 172 | [3] = {name = "step", type = "number"}, 173 | } 174 | }, function (__, args) 175 | if not args.from then 176 | __:notice("range(): requires 'from' argument") 177 | return {} 178 | end 179 | if not args.to then 180 | __:notice("range(): requires 'to' argument") 181 | return {} 182 | end 183 | if not args.step or args.step == 0 then 184 | args.step = 1 185 | end 186 | if args.step > 0 and args.to < args.from then 187 | __:notice("range(): 'to' less than 'from' with positive step") 188 | return {} 189 | elseif args.step < 0 and args.to > args.from then 190 | __:notice("range(): 'to' great than 'from' with negative step") 191 | return {} 192 | end 193 | return range.new(args.from, args.to, args.step) 194 | end) 195 | 196 | func.add("import", { 197 | args = {} 198 | }, function (__, from, names) 199 | if not from then 200 | runtime_error(__, "import(): requires 'from' argument") 201 | end 202 | local view, error = __.opts.get(from) 203 | if not view then 204 | runtime_error(__, error) 205 | end 206 | if names then 207 | local macros = {} 208 | for i = 1, #names do 209 | local macro = view.macros[ names[i] ] 210 | if not macro then 211 | runtime_error(__, "import(): macro '".. names[i] .. "' doesn't exists in the template " .. view.name) 212 | end 213 | macros[#macros + 1] = macro 214 | end 215 | return unpack(macros) 216 | else 217 | return view.macros or {} 218 | end 219 | end) 220 | 221 | --- {{ date(date) }} 222 | func.add("date", { 223 | args = { 224 | [1] = {name = "date", type = "any"} 225 | } 226 | }, function (__, args) 227 | return date.new(args.date) 228 | end) 229 | 230 | --- {{ dump(...) }} 231 | func.add("dump", { 232 | args = nil, 233 | --- @param compiler aspect.compiler 234 | --- @param _ nil 235 | --- @param tok aspect.tokenizer 236 | parser = function (compiler, _, tok) 237 | if tok:is(")") then 238 | return "__.fn.dump(__, nil, _context)" 239 | end 240 | local vars = {} 241 | while true do 242 | local from = tok:get_pos() 243 | local expr = compiler:parse_expression(tok) 244 | local name = tok:get_path_as_string(from, tok.i - 1) 245 | vars[#vars + 1] = "{name = " .. quote_string(strip(name)) .. ", value = " .. expr .. "}" 246 | if tok:is(")") then 247 | break 248 | end 249 | tok:require(","):next() 250 | end 251 | return "__.fn.dump(__, {" .. concat(vars, ",") .. "})" 252 | end 253 | }, function (__, args, ctx) 254 | local out = {} 255 | if args then 256 | out[1] = "Dump values:" 257 | for _, v in ipairs(args) do 258 | out[#out + 1] = v.name .. ": " .. dump(v.value) .. "\n" 259 | end 260 | else 261 | out[1] = "Dump context:" 262 | for name, value in pairs(ctx) do 263 | out[#out + 1] = name .. ": " .. dump(value) .. "\n" 264 | end 265 | end 266 | out[#out + 1] = "\nStack:\n" .. __:get_callstack() 267 | return concat(out, "\n") 268 | end) 269 | 270 | return func -------------------------------------------------------------------------------- /src/aspect/init.lua: -------------------------------------------------------------------------------- 1 | return require("aspect.template") 2 | -------------------------------------------------------------------------------- /src/aspect/loader/array.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | 3 | --- Array template loader 4 | --- @class aspect.loader.array 5 | local array_loader = {} 6 | local mt = { 7 | __call = function(self, name) 8 | return self[name] 9 | end 10 | } 11 | 12 | function array_loader.new(list) 13 | return setmetatable(list or {}, mt) 14 | end 15 | 16 | return array_loader -------------------------------------------------------------------------------- /src/aspect/loader/filesystem.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | local open = io.open 3 | 4 | --- Filesystem template loader 5 | --- @class aspect.loader.filesystem 6 | local fs_loader = {} 7 | local mt = { 8 | __call = function(self, name) 9 | local file = open(self.path .. "/" .. name, "rb") 10 | if not file then 11 | return nil 12 | end 13 | local content = file:read("*a") 14 | file:close() 15 | return content 16 | end 17 | } 18 | 19 | function fs_loader.new(path) 20 | return setmetatable({ 21 | path = path 22 | }, mt) 23 | end 24 | 25 | return fs_loader -------------------------------------------------------------------------------- /src/aspect/loader/resty.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | local capture = ngx.location.capture 3 | 4 | --- Ngx+lua (resty) template loader 5 | --- @class aspect.loader.resty 6 | local resty_loader = {} 7 | local mt = { 8 | __call = function(self, name) 9 | local res = capture(self.url .. name, { 10 | method = "GET" 11 | }) 12 | if res and res.status == 200 then 13 | return res.body 14 | end 15 | return nil 16 | end 17 | } 18 | 19 | --- @param url string the URL prefix (for example /.templates/) 20 | function resty_loader.new(url) 21 | return setmetatable({ 22 | url = url 23 | }, mt) 24 | end 25 | 26 | return resty_loader -------------------------------------------------------------------------------- /src/aspect/output.lua: -------------------------------------------------------------------------------- 1 | local concat = table.concat 2 | local remove = table.remove 3 | local setmetatable = setmetatable 4 | local getmetatable = getmetatable 5 | local type = type 6 | local pairs = pairs 7 | local ipairs = ipairs 8 | local next = next 9 | local tostring = tostring 10 | local gsub = string.gsub 11 | local len = string.len 12 | local ngx = ngx or {} 13 | local err = require("aspect.error") 14 | local var_dump = require("aspect.utils").var_dump 15 | local config = require("aspect.config") 16 | local e_pattern = config.escape.pattern 17 | local e_replaces = config.escape.replaces 18 | local runtime_error = err.runtime_error 19 | local is_false = config.is_false 20 | local is_n = config.is_n 21 | local is_empty_string = config.is_empty_string 22 | local is_empty = table.isempty or function(v) return next(v) == nil end 23 | local insert = table.insert 24 | local tonumber = tonumber 25 | local print = ngx.print or print 26 | local flush = ngx.flush 27 | 28 | 29 | --- @class aspect.output.parent 30 | --- @field list table 31 | --- @field pos number 32 | 33 | --- @class apsect.output.state {view:aspect.view, line:number, name:string} 34 | 35 | --- Output handler 36 | --- @class aspect.output 37 | --- @field fixture table runtime output fragments 38 | --- @field line number 39 | --- @field view aspect.view 40 | --- @field views aspect.view[] 41 | --- @field stack apsect.output.state[] 42 | --- @field parents table 43 | --- @field blocks table 44 | --- @field opts table template options and helpers 45 | --- @field result string template result 46 | local output = { 47 | ipairs = ipairs, 48 | pairs = pairs, 49 | concat = table.concat, 50 | insert = table.insert, 51 | tonumber = tonumber, 52 | setmetatable = setmetatable 53 | } 54 | 55 | local function __tostring(self) 56 | if self.result then 57 | return self.result 58 | elseif self.data then 59 | return concat(self.data) 60 | else 61 | return "" 62 | end 63 | end 64 | 65 | --- collect output 66 | local mt_collect = { 67 | __call = function (self, arg) 68 | self.data[#self.data + 1] = arg 69 | end, 70 | __tostring = __tostring, 71 | __index = output, 72 | } 73 | 74 | --- print output 75 | local mt_print = { 76 | __call = function (_, arg) 77 | print(arg) 78 | end, 79 | __tostring = __tostring, 80 | __index = output, 81 | } 82 | 83 | --- print output with buffering 84 | local mt_chunked_print = { 85 | __call = function (self, arg) 86 | self.size = self.size + len(arg) 87 | if self.size >= self.chink_size then 88 | print(concat(self.data) .. arg) 89 | if flush then 90 | flush(true) 91 | end 92 | self.data = {} 93 | self.size = 0 94 | else 95 | self.data[#self.data + 1] = arg 96 | end 97 | end, 98 | __tostring = __tostring, 99 | __index = output, 100 | } 101 | 102 | --- call callback for output strings 103 | local mt_call = { 104 | __call = function (self, arg) 105 | self.p(arg) 106 | end, 107 | __tostring = __tostring, 108 | __index = output, 109 | } 110 | 111 | --- call callback for output strings with buffer 112 | local mt_chunked_call = { 113 | __call = function (self, arg) 114 | self.size = self.size + len(arg) 115 | if self.size >= self.chink_size then 116 | self.p(concat(self.data) .. arg) 117 | self.data = {} 118 | self.size = 0 119 | else 120 | self.data[#self.data + 1] = arg 121 | end 122 | end, 123 | __tostring = __tostring, 124 | __index = output, 125 | } 126 | 127 | 128 | function output.new(opt, ctx, options) 129 | local mt = mt_collect 130 | if options.print then 131 | if options.print == true then 132 | if options.size and options.size > 0 then 133 | mt = mt_chunked_print 134 | else 135 | mt = mt_print 136 | end 137 | else 138 | if options.size and options.size > 0 then 139 | mt = mt_chunked_call 140 | else 141 | mt = mt_call 142 | end 143 | end 144 | end 145 | return setmetatable({ 146 | root = nil, 147 | view = nil, 148 | line = 0, 149 | data = {}, 150 | stack = {}, 151 | opts = opt, 152 | esc = opt.escape or options.autoescape or false, 153 | chunk_size = options.chunk_size or false, 154 | p = options.print, 155 | tz = opt.time_zone or options.time_zone, 156 | debug = opt.debug or options.debug or false, 157 | loc = opt.locale or options.locale or 'en', 158 | f = opt.f, 159 | fn = opt.fn, 160 | blocks = {}, 161 | ctx = ctx 162 | }, mt) 163 | end 164 | 165 | function output:set_print(p, size) 166 | if size and size == 0 then 167 | size = nil 168 | end 169 | self.p = p 170 | self.size = size 171 | end 172 | 173 | --- Push new state to call stack 174 | --- @param view aspect.view 175 | --- @param line number 176 | --- @param scope_name string 177 | function output:push_state(view, line, scope_name) 178 | if #self.stack > self.opts.stack_size then 179 | runtime_error(self, "Call stack overflow (maximum " .. self.opts.stack_size .. ")") 180 | end 181 | if self.view then 182 | self.stack[#self.stack + 1] = {self.view, self.line, self.scope_name} 183 | end 184 | self.view = view 185 | self.line = line 186 | self.scope_name = scope_name 187 | return self 188 | end 189 | 190 | function output:pop_state() 191 | if #self.stack > 0 then 192 | local stack = remove(self.stack) 193 | self.view = stack[1] 194 | self.line = stack[2] 195 | self.scope_name = stack[3] 196 | else 197 | self.view = nil 198 | self.line = 0 199 | self.scope_name = nil 200 | end 201 | return self 202 | end 203 | 204 | --- Extending the view with another view 205 | --- @param view aspect.view 206 | function output:push_view(view) 207 | if not self.views then 208 | self.views = {view} 209 | else 210 | self.views[#self.views + 1] = view 211 | end 212 | if next(view.blocks) then 213 | for n, b in pairs(view.blocks) do 214 | if not self.blocks[n] then 215 | self.blocks[n] = { 216 | f = b, 217 | i = #self.views, -- for parent() function 218 | } 219 | end 220 | end 221 | end 222 | end 223 | 224 | --- Add block to runtime scope 225 | --- @param view aspect.view 226 | function output:add_blocks(view) 227 | if next(view.blocks) then 228 | for n, b in pairs(view.blocks) do 229 | if not self.blocks[n] then 230 | self.blocks[n] = b 231 | --elseif self.blocks[n].parent then -- the block has parent ref 232 | -- local p 233 | -- if not self.parents then 234 | -- self.parents = {} 235 | -- end 236 | -- if not self.parents[n] then 237 | -- p = { 238 | -- pos = 1, 239 | -- list = {}, 240 | -- closed = false 241 | -- } 242 | -- self.parents[n] = p 243 | -- else 244 | -- p = self.parents[n] 245 | -- end 246 | -- if not p.closed then 247 | -- insert(p.list, b) 248 | -- end 249 | -- if not b.parent then -- if there is no parent () function in the parent block, stop collecting parent blocks 250 | -- p.closed = true 251 | -- end 252 | end 253 | end 254 | end 255 | end 256 | 257 | function output:get_callstack() 258 | local callstack = {"begin"} 259 | for _, c in ipairs(self.stack) do 260 | if c[3] then 261 | callstack[#callstack + 1] = c[1].name .. ":" .. c[2] .. " " .. c[3] 262 | else 263 | callstack[#callstack + 1] = c[1].name .. ":" .. c[2] 264 | end 265 | end 266 | if self.scope_name then 267 | callstack[#callstack + 1] = (self.view and self.view.name or "") .. ":" .. self.line .. " " .. self.scope_name 268 | else 269 | callstack[#callstack + 1] = (self.view and self.view.name or "") .. ":" .. self.line 270 | end 271 | return "\t" .. concat(callstack, "\n\t") 272 | end 273 | 274 | --- Cast value to boolean 275 | --- @param v any 276 | --- @return boolean 277 | function output.b(v) 278 | if not v or is_false[v] or is_false[getmetatable(v)] then 279 | return false 280 | elseif type(v) == "table" then 281 | local mt = getmetatable(v) 282 | if mt then 283 | if mt.__toboolean then 284 | return mt.__toboolean(v) 285 | elseif mt.__len then 286 | return mt.__len(v) ~= 0 287 | end 288 | end 289 | if is_empty(v) then 290 | return nil 291 | end 292 | end 293 | return true 294 | end 295 | 296 | --- Returns value if cast of this value to boolean is true 297 | --- @param v any 298 | --- @return any|nil 299 | function output.b2(v) 300 | if not v or is_false[v] or is_false[getmetatable(v)] then 301 | return nil 302 | elseif type(v) == "table" then 303 | local mt = getmetatable(v) 304 | if mt then 305 | if mt.__toboolean then 306 | return mt.__toboolean(v) and v 307 | elseif mt.__len then 308 | return mt.__len(v) ~= 0 309 | end 310 | end 311 | if is_empty(v) then 312 | return nil 313 | end 314 | end 315 | return v 316 | end 317 | 318 | --- Cast value to string 319 | --- @param v any 320 | --- @return string 321 | function output.s(v) 322 | if not v or is_empty_string[v] or is_empty_string[getmetatable(v)] then 323 | return "" 324 | else 325 | return tostring(v) 326 | end 327 | end 328 | 329 | --- Cast value to number 330 | --- @param v any 331 | --- @return number 332 | function output.n(v) 333 | local typ = type(v) 334 | if typ == "number" then 335 | return v 336 | elseif typ == "string" then 337 | return tonumber(v) or 0 338 | elseif typ == "table" and v._NAME == "aspect.date" then 339 | return v.time 340 | elseif typ == "boolean" then 341 | return v and 1 or 0 342 | elseif v then 343 | return tonumber(tostring(v)) or 0 344 | end 345 | return 0 346 | end 347 | 348 | function output.t(v) 349 | if type(v) == "table" then 350 | return v 351 | else 352 | return {} 353 | end 354 | end 355 | 356 | --- Cast value to the iterator (if possible) 357 | function output.i(v) 358 | return v 359 | end 360 | 361 | --- Get iterator of the v 362 | --- @param v any 363 | --- @return function iterator 364 | --- @return any object 365 | --- @return any key 366 | function output.iter(v) 367 | local typ, mt = type(v), getmetatable(v) 368 | if typ == "table" then 369 | if mt and mt.__pairs then 370 | return mt.__pairs(v) 371 | else 372 | return pairs(v) 373 | end 374 | elseif mt and typ == "userdata" then 375 | if mt.__pairs then 376 | return mt.__pairs(v) 377 | end 378 | end 379 | 380 | return next, {}, nil 381 | end 382 | 383 | --- Get 'recursive' value from tables 384 | --- @param v table|any 385 | function output.v(v, ...) 386 | for _, k in ipairs({...}) do 387 | if type(v) ~= "table" then 388 | return nil 389 | end 390 | if v[k] == nil then 391 | return nil 392 | else 393 | v = v[k] 394 | end 395 | end 396 | return v 397 | end 398 | 399 | function output:e(v) 400 | if v == nil then 401 | return 402 | end 403 | if type(v) ~= "string" then 404 | v = output.s(v) 405 | end 406 | if self.esc then 407 | return self(gsub(v, e_pattern, e_replaces)) 408 | else 409 | return self(v) 410 | end 411 | end 412 | 413 | function output:notice(msg) 414 | 415 | end 416 | 417 | --- Set autoescape 418 | function output:autoescape(state) 419 | if state == nil then 420 | return 421 | end 422 | if self.esc ~= state then 423 | self.esc = state 424 | return not state 425 | end 426 | end 427 | 428 | function output:get_view(name) 429 | return self.opts.get(name) 430 | end 431 | 432 | function output:finish() 433 | if not self.result then 434 | self.result = concat(self.data) 435 | self.data = nil 436 | end 437 | return self 438 | end 439 | 440 | return output -------------------------------------------------------------------------------- /src/aspect/tests.lua: -------------------------------------------------------------------------------- 1 | local tonumber = tonumber 2 | local getmetatable = getmetatable 3 | local type = type 4 | 5 | local tests = { 6 | args = { 7 | divisible = "by", 8 | constant = true, 9 | same = "as" 10 | }, 11 | fn = {} 12 | } 13 | 14 | --- Add test 15 | --- @param name string the test name 16 | --- @param fn fun(__:aspect.output, v:any, arg:any):boolean the test function 17 | --- @param has_arg string|boolean test has an argument (boolean), or test has 'function' line 'same as(...)' (string) 18 | function tests.add(name, fn, has_arg) 19 | tests.args[name] = has_arg 20 | tests.fn[name] = fn 21 | end 22 | 23 | function tests.fn.is_defined(__, v) 24 | return v ~= nil 25 | end 26 | 27 | function tests.fn.is_null(__, v) 28 | return v == nil 29 | end 30 | 31 | function tests.fn.is_nil(__, v) 32 | return v == nil 33 | end 34 | 35 | function tests.fn.is_divisible_by(__, v, number) 36 | return tonumber(v) % tonumber(number) == 0 37 | end 38 | 39 | function tests.fn.is_constant(__, v, const) 40 | return __.opts.consts[const] == v 41 | end 42 | 43 | function tests.fn.is_empty(__, v) 44 | return not __.b(v) 45 | end 46 | 47 | function tests.fn.is_iterable(__, v) 48 | local typ = type(v) 49 | if typ == "table" then 50 | return true 51 | elseif typ == "userdata" then 52 | return getmetatable(v) and getmetatable(v).__pairs ~= nil 53 | else 54 | return false 55 | end 56 | end 57 | 58 | function tests.fn.is_even(__, v) 59 | return (tonumber(v) % 2) == 0 60 | end 61 | 62 | function tests.fn.is_odd(__, v) 63 | return (tonumber(v) % 2) == 1 64 | end 65 | 66 | function tests.fn.is_same_as(__, v1, v2) 67 | return v1 == v2 68 | end 69 | 70 | return tests -------------------------------------------------------------------------------- /src/aspect/tokenizer.lua: -------------------------------------------------------------------------------- 1 | local compiler_error = require("aspect.error").compiler_error 2 | local setmetatable = setmetatable 3 | local concat = table.concat 4 | local strlen = string.len 5 | local tonumber = tonumber 6 | local patterns = require("aspect.config").tokenizer.patterns 7 | local compiler = require("aspect.config").compiler 8 | local utils = require("aspect.utils") 9 | local starts_with = utils.starts_with 10 | local strfind = string.find 11 | local strsub = string.sub 12 | local ipairs = ipairs 13 | 14 | --- Information about the token 15 | --- 1 - token type 16 | --- 2 - token string 17 | --- 3 - whitespace after token 18 | --- @class aspect.tokenizer.token 19 | 20 | --- @class aspect.tokenizer 21 | --- @field tokens table 22 | --- @field token function 23 | --- @field token any 24 | --- @field parsed_len number 25 | local tokenizer = {} 26 | local mt = {__index = tokenizer} 27 | 28 | local function unquote(tok) 29 | return tok:sub(2,-2) 30 | end 31 | 32 | local straight = { 33 | "word", 34 | "string", 35 | "number" 36 | } 37 | 38 | local matches = { 39 | -- pattern type sanitizer strict 40 | {patterns.WSPACE, "space", nil, false}, 41 | {patterns.NUMBER3, "number", tonumber, false}, 42 | {patterns.WORD, "word", nil, false}, 43 | {patterns.NUMBER4, "number", tonumber, false}, 44 | {patterns.NUMBER5, "number", tonumber, false}, 45 | {patterns.STRING1, "string", unquote, false}, 46 | {patterns.STRING2, "string", unquote, false}, 47 | {patterns.STRING3, "string", unquote, false}, 48 | {'}}', "stop", nil, true }, 49 | {'%}', "stop", nil, true }, 50 | {'-}}', "stop", nil, true }, 51 | {'-%}', "stop", nil, true }, 52 | {'==', nil, nil, true }, 53 | {'!=', nil, nil, true }, 54 | {'?:', nil, nil, true }, 55 | {'??', nil, nil, true }, 56 | {'<=', nil, nil, true }, 57 | {'>=', nil, nil, true }, 58 | {'**', nil, nil, true }, 59 | {'/', nil, nil, true }, 60 | {'^.', nil, nil, false } 61 | } 62 | 63 | --- Start the tokenizer 64 | --- @param s string input string 65 | --- @return aspect.tokenizer 66 | function tokenizer.new(s, start) 67 | local tokens = {} 68 | local indent, final_token 69 | local parsed_len = 0 70 | local idx, resume = start or 1, true 71 | while idx <= #s and resume do 72 | for _,m in ipairs(matches) do 73 | local typ, tok = m[2] 74 | local i1, i2 75 | if m[4] then 76 | if starts_with(s,m[1],idx) then 77 | tok = m[1] 78 | i2 = idx + #tok - 1 79 | end 80 | else 81 | i1, i2 = strfind(s,m[1],idx) 82 | if i1 then 83 | tok = strsub(s,i1,i2) 84 | end 85 | end 86 | if tok then 87 | if not typ then 88 | typ = tok 89 | end 90 | idx = i2 + 1 91 | parsed_len = parsed_len + strlen(tok) 92 | if typ == "stop" then 93 | final_token = tok 94 | resume = false 95 | break 96 | elseif typ == "space" then 97 | if tokens[#tokens] then 98 | tokens[#tokens][3] = tok 99 | else 100 | indent = tok 101 | end 102 | else 103 | tokens[#tokens + 1] = {typ, tok} 104 | end 105 | break 106 | end 107 | end 108 | end 109 | return setmetatable({ 110 | tokens = tokens, 111 | i = 1, 112 | token = tokens[1][2], 113 | typ = tokens[1][1], 114 | finish_token = final_token, 115 | parsed_len = parsed_len, 116 | start = start or 1, 117 | indent = indent 118 | }, mt) 119 | end 120 | 121 | function tokenizer:get_pos() 122 | return self.i 123 | end 124 | 125 | --- Returns the token value 126 | --- @return string 127 | function tokenizer:get_token() 128 | return self.token 129 | end 130 | 131 | --- Returns the next token value 132 | --- @return string 133 | function tokenizer:get_next_token() 134 | if self.tokens[self.i + 1] then 135 | return self.tokens[self.i + 1][2] 136 | end 137 | end 138 | 139 | --- Returns the token type 140 | --- @return string 141 | function tokenizer:get_token_type() 142 | return self.typ 143 | end 144 | 145 | --- Returns the next token type 146 | --- @return string 147 | function tokenizer:get_next_token_type() 148 | if self.tokens[self.i + 1] then 149 | return self.tokens[self.i + 1][1] 150 | end 151 | end 152 | 153 | --- Returns done tokens as string 154 | --- @param from number by default — 1 155 | --- @param to number by default — self.i 156 | --- @return string 157 | function tokenizer:get_path_as_string(from, to) 158 | local path = {self.indent} 159 | from = from or 1 160 | to = to or self.i 161 | for i = from, to do 162 | if self.tokens[i] then 163 | path[#path + 1] = self.tokens[i][2] .. (self.tokens[i][3] or "") 164 | end 165 | end 166 | return concat(path) 167 | end 168 | 169 | --- @return aspect.tokenizer 170 | function tokenizer:next() 171 | if not self.tokens[self.i] then 172 | return self 173 | end 174 | self.i = self.i + 1 175 | if self.tokens[self.i] then 176 | self.token = self.tokens[self.i][2] 177 | self.typ = self.tokens[self.i][1] 178 | else 179 | self.token = nil 180 | self.typ = nil 181 | end 182 | return self 183 | end 184 | 185 | --- Checks the token value 186 | --- @return boolean 187 | function tokenizer:is(token) 188 | return self.token == token 189 | end 190 | 191 | --- Checks the next token value 192 | --- @return boolean 193 | function tokenizer:is_next(token) 194 | return self:get_next_token() == token 195 | end 196 | 197 | --- Checks sequence of tokens (current token - start of the sequence) 198 | --- @param seq table 199 | --- @return boolean 200 | --- @return string name of sequence there failed 201 | function tokenizer:is_seq(seq) 202 | for i=0,#seq-1 do 203 | if not self.tokens[self.i + i] then 204 | return false, seq[i + 1] 205 | end 206 | if self.tokens[self.i + i][1] ~= seq[i + 1] then 207 | return false, seq[i + 1] 208 | end 209 | end 210 | return true 211 | end 212 | 213 | --- Checks the token value and if token value incorrect throw an error 214 | --- @param token string 215 | --- @return aspect.tokenizer 216 | function tokenizer:require(token) 217 | if self.token ~= token then 218 | compiler_error(self, "syntax", "expecting '" .. token .. "'") 219 | end 220 | return self 221 | end 222 | 223 | 224 | --- Checks the token type and if token type incorrect throw an error 225 | --- @param typ string 226 | --- @return aspect.tokenizer 227 | function tokenizer:require_type(typ) 228 | if self.typ ~= typ then 229 | compiler_error(self, "syntax", "expecting of " .. typ .. " type token") 230 | end 231 | return self 232 | end 233 | 234 | --- Checks if the token is simple word 235 | --- @return boolean 236 | function tokenizer:is_word() 237 | return self.typ == "word" 238 | end 239 | 240 | --- @return boolean 241 | function tokenizer:is_boolean() 242 | if self.token and compiler.is_boolean[self.token:lower()] then 243 | return true 244 | end 245 | return false 246 | end 247 | 248 | --- Checks if the next token is simple word 249 | --- @return boolean 250 | function tokenizer:is_next_word() 251 | return self:get_next_token_type() == "word" 252 | end 253 | 254 | --- Checks if the token is scalar value 255 | --- @return boolean 256 | function tokenizer:is_value() 257 | return self.typ == "string" or self.typ == "number" 258 | end 259 | 260 | --- Checks if the next token is scalar value 261 | --- @return boolean 262 | function tokenizer:is_next_value() 263 | local typ = self:get_next_token_type() 264 | return typ == "string" or typ == "number" 265 | end 266 | 267 | --- Checks if the token is string 268 | --- @return boolean 269 | function tokenizer:is_string() 270 | return self.typ == "string" 271 | end 272 | 273 | --- Checks if the next token is string 274 | --- @return boolean 275 | function tokenizer:is_next_string() 276 | return self:get_next_token_type() == "string" 277 | end 278 | 279 | --- Checks if the token is number 280 | --- @return boolean 281 | function tokenizer:is_number() 282 | return self.typ == "number" 283 | end 284 | 285 | --- Checks if the next token is number 286 | --- @return boolean 287 | function tokenizer:is_next_number() 288 | return self:get_next_token_type() == "number" 289 | end 290 | 291 | function tokenizer:is_op() 292 | return self.typ and not straight[self.typ] 293 | end 294 | 295 | function tokenizer:is_next_op() 296 | return self.tokens[self.i + 1] and not straight[self.tokens[self.i + 1][1]] 297 | end 298 | 299 | --- Checks if the token is valid (stream not finished) 300 | --- @return boolean 301 | function tokenizer:is_valid() 302 | return self.typ ~= nil 303 | end 304 | 305 | return tokenizer -------------------------------------------------------------------------------- /src/aspect/utils/batch.lua: -------------------------------------------------------------------------------- 1 | local output = require("aspect.output") 2 | local type = type 3 | local ceil = math.ceil 4 | local getmetatable = getmetatable 5 | local setmetatable = setmetatable 6 | local nkeys = require("aspect.utils") 7 | --local count = table.nkeys or tablex.size 8 | --if table.nkeys then -- new luajit function 9 | -- count = table.nkeys 10 | --end 11 | 12 | local function __pairs(self) 13 | return function () 14 | if not self.key and self.n > 0 then 15 | return nil 16 | end 17 | local l, c, v = {}, self.count 18 | self.n = self.n + 1 19 | self.key, v = self.iter(self.ctx, self.key) 20 | while self.key do 21 | l[self.key] = v 22 | c = c - 1 23 | if c == 0 then 24 | break 25 | end 26 | self.key, v = self.iter(self.ctx, self.key) 27 | end 28 | if c == self.count then 29 | return nil 30 | end 31 | return self.n, l 32 | end, self 33 | end 34 | 35 | local function __len(self) 36 | local typ = type(self.t) 37 | local mt = getmetatable(self.t) 38 | if typ == "table" then 39 | if mt and mt.__len then 40 | return ceil(mt.__len(self.t) / self.count) 41 | elseif mt and mt.__pairs then -- has custom iterator. we don't know how much elements will be 42 | return 0 43 | else 44 | return ceil(nkeys(self.t) / self.count) 45 | end 46 | elseif typ == "userdata" then 47 | if mt and mt.__len then 48 | return ceil(mt.__len(self.t) / self.count) 49 | end 50 | else 51 | return 0 52 | end 53 | end 54 | 55 | --- Iterator for batch filter 56 | --- @class aspect.utils.batch 57 | local batch = {} 58 | local mt = { 59 | __index = batch, 60 | __pairs = __pairs, 61 | __len = __len 62 | } 63 | 64 | --- @param t table 65 | --- @param cnt number 66 | function batch.new(t, cnt) 67 | if cnt == 0 then 68 | return nil 69 | end 70 | local iter, ctx, key = output.iter(t) 71 | if not iter then 72 | return nil 73 | end 74 | return setmetatable({ 75 | tbl = t, 76 | iter = iter, 77 | ctx = ctx, 78 | key = key, 79 | count = cnt, 80 | n = 0 81 | }, mt) 82 | end 83 | 84 | return batch -------------------------------------------------------------------------------- /src/aspect/utils/range.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | local floor = math.floor 3 | local abs = math.abs 4 | 5 | --- Range iterator 6 | --- @class aspect.utils.range 7 | --- @field from number 8 | --- @field to number 9 | --- @field step number 10 | --- @field incr boolean 11 | local range = {} 12 | 13 | --- Magic function for {{ for }} tag 14 | --- @return function iterator (see range.__iterate) 15 | --- @return table context object 16 | --- @return number initial key value 17 | function range:__pairs() 18 | return self.__iterate, self, 0 19 | end 20 | 21 | --- Iterator 22 | --- @return number the key 23 | --- @return number the value 24 | function range:__iterate(k) 25 | local i = self.from + k * self.step 26 | if self.incr then 27 | if i > self.to then 28 | return nil, nil 29 | else 30 | return k + 1, i 31 | end 32 | else 33 | if i < self.to then 34 | return nil, nil 35 | else 36 | return k + 1, i 37 | end 38 | end 39 | end 40 | 41 | --- Magic function for calculating the number of iterations (elements) 42 | --- @return number count of iterations/elements 43 | function range:__len() 44 | if self.incr then 45 | return floor(abs((self.to - self.from) / self.step)) + 1 46 | else 47 | return floor(abs((self.from - self.to) / self.step)) + 1 48 | end 49 | end 50 | 51 | local mt = { 52 | __index = range, 53 | __pairs = range.__pairs, 54 | __len = range.__len 55 | } 56 | 57 | --- Range constructor 58 | --- @param from number 59 | --- @param to number 60 | --- @param step number 61 | function range.new(from, to, step) 62 | return setmetatable({ 63 | incr = from < to, 64 | from = from, 65 | to = to, 66 | step = step, 67 | }, mt) 68 | end 69 | 70 | return range --------------------------------------------------------------------------------