├── .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 |
21 | {% for k, v in context%}
22 |
{{v}}
23 | {% endfor %}
24 |
]]
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 |
--------------------------------------------------------------------------------
/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 |
{{ column }}
25 | {% endfor %}
26 |
27 | {% endfor %}
28 |
29 | ```
30 | The above example will be rendered as:
31 |
32 | ```html
33 |
34 |
35 |
a
36 |
b
37 |
c
38 |
39 |
40 |
d
41 |
e
42 |
f
43 |
44 |
45 |
g
46 |
47 |
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 |
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 |
16 | {% for user in users %}
17 |
{{ user.username|e }}
18 | {% endfor %}
19 |
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 |
34 | {% for user in users %}
35 |
{{ user.username|e }}
36 | {% else %}
37 |
no user found
38 | {% endfor %}
39 |
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 |
67 | {% for key in users|keys %}
68 |
{{ key }}
69 | {% endfor %}
70 |
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 |
81 | {% for key, user in users %}
82 |
{{ key }}: {{ user.username|e }}
83 | {% endfor %}
84 |
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 |
23 | {% for user in users %}
24 |
{{ user.username|e }}
25 | {% endfor %}
26 |
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 |
38 | ...
39 |
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 |
16 | {% for item in seq %}
17 |
{{ item }}
18 | {% endfor %}
19 |
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 | [](https://travis-ci.org/unifire-app/aspect)
7 | [](https://codecov.io/gh/unifire-app/aspect)
8 | [](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
--------------------------------------------------------------------------------