Compare commits
319 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b476e50f89 | ||
|
|
2ed5763fe4 | ||
|
|
fff3d21e37 | ||
|
|
99be5f0459 | ||
|
|
2eeb7babd3 | ||
|
|
fc404b25d8 | ||
|
|
42972a1c10 | ||
|
|
6a4959cea0 | ||
|
|
ffe8f9dab0 | ||
|
|
96a004eb34 | ||
|
|
92ebfe59b1 | ||
|
|
71ad162268 | ||
|
|
b9702afbd4 | ||
|
|
4f1a5b3e3d | ||
|
|
3a4cd8aa27 | ||
|
|
4f14b4b044 | ||
|
|
b66abc3112 | ||
|
|
5bbd994581 | ||
|
|
3995ff9acf | ||
|
|
2e18892f4c | ||
|
|
39ed9aa753 | ||
|
|
d935f65d56 | ||
|
|
2627d3e0d1 | ||
|
|
1e77f1e85f | ||
|
|
47f2b33d80 | ||
|
|
1427e10698 | ||
|
|
e070ae7851 | ||
|
|
fc6c0208b2 | ||
|
|
34dbafa789 | ||
|
|
eb8c875853 | ||
|
|
098af2a7b6 | ||
|
|
7679b48164 | ||
|
|
7c499cc077 | ||
|
|
88e54ab4ba | ||
|
|
c2e25f25ac | ||
|
|
fe01beb4bb | ||
|
|
2e6a7215c5 | ||
|
|
f457cddd3f | ||
|
|
6b02fccf84 | ||
|
|
29e859f1aa | ||
|
|
8fa0bd275c | ||
|
|
91847208a3 | ||
|
|
86ed8770e1 | ||
|
|
0bc6bd974e | ||
|
|
fa68ba9df8 | ||
|
|
4827fb8e20 | ||
|
|
359d086c02 | ||
|
|
24c9746689 | ||
|
|
c4a84a6375 | ||
|
|
c30597457f | ||
|
|
b54292788f | ||
|
|
d6766b43da | ||
|
|
662849e968 | ||
|
|
4bfdb73175 | ||
|
|
a165a6715f | ||
|
|
ac2fd56e8e | ||
|
|
cb124319ec | ||
|
|
abeb30bb1c | ||
|
|
ed885f462a | ||
|
|
7756522317 | ||
|
|
8d68edd725 | ||
|
|
218822fcb0 | ||
|
|
ea7e1efac7 | ||
|
|
bb3f33724b | ||
|
|
c486617854 | ||
|
|
9a28142fa6 | ||
|
|
53c1550c5b | ||
|
|
27135f3ea3 | ||
|
|
5878c323a2 | ||
|
|
97ab3cf31d | ||
|
|
7688326204 | ||
|
|
079fdf39b8 | ||
|
|
e59609f140 | ||
|
|
d5f0be959f | ||
|
|
0edb38588d | ||
|
|
69cd8e4d3b | ||
|
|
6300dbc7bf | ||
|
|
b4dc8dbb76 | ||
|
|
2e80f70f67 | ||
|
|
a6dba67828 | ||
|
|
691fe523b3 | ||
|
|
c0e66eb96f | ||
|
|
0156f6f37b | ||
|
|
79a16854e7 | ||
|
|
a4b75f3c89 | ||
|
|
0f3a302108 | ||
|
|
1223efbc7e | ||
|
|
9357df35d1 | ||
|
|
a96fcff680 | ||
|
|
0017aee5a8 | ||
|
|
1e6846867e | ||
|
|
93c07e22b1 | ||
|
|
98461c75b0 | ||
|
|
9994972a24 | ||
|
|
cf7acea440 | ||
|
|
9e24ab658b | ||
|
|
a52ee21b72 | ||
|
|
4a93815d4c | ||
|
|
7e88cbde11 | ||
|
|
e7a0738bda | ||
|
|
46f179e3ed | ||
|
|
bf4be38377 | ||
|
|
14bac03990 | ||
|
|
3180b26673 | ||
|
|
000e9a7f1a | ||
|
|
7b9817ed50 | ||
|
|
482d595d01 | ||
|
|
f1fc747897 | ||
|
|
0444f45d2b | ||
|
|
86bfbf215f | ||
|
|
039bf4b7cb | ||
|
|
4308baf5f0 | ||
|
|
2455fb9ed0 | ||
|
|
64571464d9 | ||
|
|
5821e4849e | ||
|
|
793773f191 | ||
|
|
e217a9c873 | ||
|
|
584ed916ab | ||
|
|
65a461d0a1 | ||
|
|
89256b96f4 | ||
|
|
a8d680b30e | ||
|
|
6f48fe2d91 | ||
|
|
4f9063c147 | ||
|
|
5541eae818 | ||
|
|
d1717df6ff | ||
|
|
dc8759ba34 | ||
|
|
233dcfc59a | ||
|
|
19e4f6e506 | ||
|
|
e2f33d4337 | ||
|
|
20b9476e4b | ||
|
|
cf9714ffd0 | ||
|
|
5e78d6cc46 | ||
|
|
99efba56e9 | ||
|
|
39517b7514 | ||
|
|
fdde1dec02 | ||
|
|
8f6b403aa9 | ||
|
|
2331b11a52 | ||
|
|
26f30cbd9d | ||
|
|
d7b152089e | ||
|
|
d3706f074d | ||
|
|
aa7c36296b | ||
|
|
2d507e7c11 | ||
|
|
15facd97fb | ||
|
|
d75db241ac | ||
|
|
49936c36d4 | ||
|
|
2e04a71d59 | ||
|
|
6d05832997 | ||
|
|
6871387671 | ||
|
|
46bc1242f3 | ||
|
|
24359489ce | ||
|
|
f90597fba1 | ||
|
|
9e2a061795 | ||
|
|
2be672c6a5 | ||
|
|
2ebb79df8b | ||
|
|
63c2b935f7 | ||
|
|
abae80d39d | ||
|
|
d024da5567 | ||
|
|
98edad3566 | ||
|
|
872784f9b3 | ||
|
|
1a01ec592e | ||
|
|
f0591408be | ||
|
|
b7e200a8a0 | ||
|
|
b1da85b140 | ||
|
|
679344f53b | ||
|
|
ada4e81082 | ||
|
|
c99a40c5d9 | ||
|
|
c59b263446 | ||
|
|
ab6f1a032d | ||
|
|
e989317929 | ||
|
|
111306fb60 | ||
|
|
3eb2657a62 | ||
|
|
6ad609e562 | ||
|
|
38d7ec87f6 | ||
|
|
9af9cf4005 | ||
|
|
1975cfd627 | ||
|
|
429290e0b7 | ||
|
|
5ca1b78854 | ||
|
|
a2673bd66b | ||
|
|
9b6ee14aa3 | ||
|
|
3b5e8f2468 | ||
|
|
e84f8a41d4 | ||
|
|
2324808dca | ||
|
|
9fdbbc99e9 | ||
|
|
dfd57e9571 | ||
|
|
3293d8a526 | ||
|
|
393dc88a10 | ||
|
|
a014fecd23 | ||
|
|
a13401b046 | ||
|
|
60b378d482 | ||
|
|
1e3afc0dd5 | ||
|
|
72f3cb579a | ||
|
|
68e6ce3022 | ||
|
|
65c3052aee | ||
|
|
7bbd4f2817 | ||
|
|
7416e6150d | ||
|
|
feff3b18b1 | ||
|
|
f393efbd0b | ||
|
|
df650c6b20 | ||
|
|
3285bac373 | ||
|
|
80427a51e6 | ||
|
|
7bfb69cc82 | ||
|
|
5007ba2c9a | ||
|
|
2d73c58df6 | ||
|
|
4ffc888ba4 | ||
|
|
3c21975b97 | ||
|
|
df9065f5a8 | ||
|
|
05b71736aa | ||
|
|
aa1399be55 | ||
|
|
bdc14ab1e1 | ||
|
|
67d4c52535 | ||
|
|
48026cde2c | ||
|
|
dc4b965aaa | ||
|
|
2190afee0d | ||
|
|
9b7e6ba7ed | ||
|
|
bf0989d329 | ||
|
|
affd56ec99 | ||
|
|
070a82cb2d | ||
|
|
3ec009381d | ||
|
|
6deb93ac19 | ||
|
|
b4ba12bbde | ||
|
|
19d712b4a4 | ||
|
|
201b8e263c | ||
|
|
03928721c4 | ||
|
|
07835063ed | ||
|
|
3c13d81b21 | ||
|
|
1668830d9b | ||
|
|
14195b3199 | ||
|
|
ae75ea5911 | ||
|
|
9c9ebbe559 | ||
|
|
5cdf1d326b | ||
|
|
f78562a1fd | ||
|
|
0ccd8809e0 | ||
|
|
356393088b | ||
|
|
b792cd09b9 | ||
|
|
372b2e7576 | ||
|
|
0bfd4134f9 | ||
|
|
aca0a3181d | ||
|
|
a1a268d5ac | ||
|
|
465834d89c | ||
|
|
0af879ba8a | ||
|
|
a516de51ff | ||
|
|
1f4aae1859 | ||
|
|
cba1cbe388 | ||
|
|
3722998c35 | ||
|
|
22919dc5ce | ||
|
|
89b7da2e10 | ||
|
|
3bd3aec296 | ||
|
|
48a9a65bd5 | ||
|
|
c86ab9c5b9 | ||
|
|
dc774fe43b | ||
|
|
226becb258 | ||
|
|
507cc5c661 | ||
|
|
9b26b7d71a | ||
|
|
19366ec71b | ||
|
|
ba65ab5fbe | ||
|
|
8ac6e26876 | ||
|
|
f35be4b701 | ||
|
|
033ae61e42 | ||
|
|
1ea58b70f3 | ||
|
|
5883775f37 | ||
|
|
d1891038f8 | ||
|
|
d5acc7298c | ||
|
|
6464b3170a | ||
|
|
d03df12cba | ||
|
|
62f6016e94 | ||
|
|
16da9ac034 | ||
|
|
7d5d226017 | ||
|
|
05dc420808 | ||
|
|
f4ed872a45 | ||
|
|
f0abd34c32 | ||
|
|
4d76fb4e60 | ||
|
|
9bdef5fee0 | ||
|
|
20cc95fb87 | ||
|
|
1136ca8fca | ||
|
|
8f334563bf | ||
|
|
b03ec50a42 | ||
|
|
2ab9b85305 | ||
|
|
a297b4ec42 | ||
|
|
9de84d5ca4 | ||
|
|
e5378b7603 | ||
|
|
29dc14855c | ||
|
|
3935dac021 | ||
|
|
9c335caeb6 | ||
|
|
25f5583542 | ||
|
|
878c5cfde8 | ||
|
|
a0bde992c2 | ||
|
|
554d2ee07f | ||
|
|
4dc8bf3d1f | ||
|
|
dcf2611ac2 | ||
|
|
c1a485c429 | ||
|
|
f9006d515a | ||
|
|
6d01792cd6 | ||
|
|
620154e721 | ||
|
|
53d5a4f8c3 | ||
|
|
59bab00c97 | ||
|
|
3839bc4147 | ||
|
|
84f117b40c | ||
|
|
d1d8e6e17f | ||
|
|
44810a82e7 | ||
|
|
028b340b54 | ||
|
|
19a7abce4c | ||
|
|
0212e8781c | ||
|
|
5aac08cabf | ||
|
|
45fcebec57 | ||
|
|
fa34c2a98e | ||
|
|
1989c20932 | ||
|
|
2374786963 | ||
|
|
5a080f92cc | ||
|
|
5d61043f8c | ||
|
|
017692a0dc | ||
|
|
0710c404b5 | ||
|
|
529e357334 | ||
|
|
c70510b3a5 | ||
|
|
63f6b3a950 | ||
|
|
b3d551962a | ||
|
|
2495d1e781 | ||
|
|
068b4ffe13 | ||
|
|
914d45827f | ||
|
|
3a0e4ce17e |
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,26 +1,5 @@
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
|
||||
#
|
||||
# Pods/
|
||||
.conche/
|
||||
.build/
|
||||
Packages/
|
||||
Package.resolved
|
||||
Package.pins
|
||||
|
||||
22
.travis.yml
Normal file
22
.travis.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode8.3
|
||||
env: SWIFT_VERSION=3.1.1
|
||||
- os: osx
|
||||
osx_image: xcode9
|
||||
env: SWIFT_VERSION=4.0
|
||||
- os: osx
|
||||
osx_image: xcode9.1
|
||||
env: SWIFT_VERSION=4.0
|
||||
- os: linux
|
||||
env: SWIFT_VERSION=3.1.1
|
||||
- os: linux
|
||||
env: SWIFT_VERSION=4.0
|
||||
language: generic
|
||||
sudo: required
|
||||
dist: trusty
|
||||
install:
|
||||
- eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
|
||||
script:
|
||||
- swift test
|
||||
312
CHANGELOG.md
Normal file
312
CHANGELOG.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# Stencil Changelog
|
||||
|
||||
## 0.12.1
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Updated the PathKit dependency to 0.9.0 in CocoaPods, to be in line with SPM.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#227](https://github.com/stencilproject/Stencil/pull/227)
|
||||
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Added an optional second parameter to the `include` tag for passing a sub context to the included file.
|
||||
[Yonas Kolb](https://github.com/yonaskolb)
|
||||
[#214](https://github.com/stencilproject/Stencil/pull/214)
|
||||
- Variables now support the subscript notation. For example, if you have a variable `key = "name"`, and an
|
||||
object `item = ["name": "John"]`, then `{{ item[key] }}` will evaluate to "John".
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#215](https://github.com/stencilproject/Stencil/pull/215)
|
||||
- Adds support for using spaces in filter expression.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#178](https://github.com/stencilproject/Stencil/pull/178)
|
||||
- Improvements in error reporting.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#167](https://github.com/stencilproject/Stencil/pull/167)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed using quote as a filter parameter.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#210](https://github.com/stencilproject/Stencil/pull/210)
|
||||
|
||||
|
||||
## 0.11.0 (2018-04-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Added support for resolving superclass properties for not-NSObject subclasses.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#152](https://github.com/stencilproject/Stencil/pull/152)
|
||||
- The `{% for %}` tag can now iterate over tuples, structures and classes via
|
||||
their stored properties.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#172](https://github.com/stencilproject/Stencil/pull/173)
|
||||
- Added `split` filter.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#187](https://github.com/stencilproject/Stencil/pull/187)
|
||||
- Allow default string filters to be applied to arrays.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#190](https://github.com/stencilproject/Stencil/pull/190)
|
||||
- Similar filters are suggested when unknown filter is used.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#186](https://github.com/stencilproject/Stencil/pull/186)
|
||||
- Added `indent` filter.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#188](https://github.com/stencilproject/Stencil/pull/188)
|
||||
- Allow using new lines inside tags.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#202](https://github.com/stencilproject/Stencil/pull/202)
|
||||
- Added support for iterating arrays of tuples.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#177](https://github.com/stencilproject/Stencil/pull/177)
|
||||
- Added support for ranges in if-in expression.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#193](https://github.com/stencilproject/Stencil/pull/193)
|
||||
- Added property `forloop.length` to get number of items in the loop.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#171](https://github.com/stencilproject/Stencil/pull/171)
|
||||
- Now you can construct ranges for loops using `a...b` syntax, i.e. `for i in 1...array.count`.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#192](https://github.com/stencilproject/Stencil/pull/192)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed rendering `{{ block.super }}` with several levels of inheritance.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#154](https://github.com/stencilproject/Stencil/pull/154)
|
||||
- Fixed checking dictionary values for nil in `default` filter.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#162](https://github.com/stencilproject/Stencil/pull/162)
|
||||
- Fixed comparing string variables with string literals, in Swift 4 string literals became `Substring` and thus couldn't be directly compared to strings.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#168](https://github.com/stencilproject/Stencil/pull/168)
|
||||
- Integer literals now resolve into Int values, not Float.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#181](https://github.com/stencilproject/Stencil/pull/181)
|
||||
- Fixed accessing properties of optional properties via reflection.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#204](https://github.com/stencilproject/Stencil/pull/204)
|
||||
- No longer render optional values in arrays as `Optional(..)`.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#205](https://github.com/stencilproject/Stencil/pull/205)
|
||||
- Fixed subscription tuples by value index, i.e. `{{ tuple.0 }}`.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#172](https://github.com/stencilproject/Stencil/pull/172)
|
||||
|
||||
|
||||
## 0.10.1
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add support for Xcode 9.1.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Adds `counter0` to for loop context allowing you to get the current index of
|
||||
the for loop 0 indexed.
|
||||
- Introduces a new `DictionaryLoader` for loading templates from a Swift
|
||||
Dictionary.
|
||||
- Added `in` expression in if tag for strings and arrays of hashable types
|
||||
- You can now access the amount of items in a dictionary using the `count`
|
||||
property.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixes a potential crash when using the `{% for %}` template tag with the
|
||||
incorrect amount of arguments.
|
||||
- Fixes a potential crash when using incomplete tokens in a template for
|
||||
example, `{%%}` or `{{}}`.
|
||||
- Fixes evaluating nil properties as true
|
||||
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Enhancements
|
||||
|
||||
- `for` block now can contain `where` expression to filter array items. For example `{% for item in items where item > 1 %}` is now supported.
|
||||
- `if` blocks may now contain else if (`elif`) conditions.
|
||||
|
||||
```html+django
|
||||
{% if one or two and not three %}
|
||||
one or two but not three
|
||||
{% elif four %}
|
||||
four
|
||||
{% else %}
|
||||
not one, two, or four
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
- `for` block now allows you to iterate over array of tuples or dictionaries.
|
||||
|
||||
```html+django
|
||||
{% for key,value in thing %}
|
||||
<li>{{ key }}: {{ value }}</li>
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- You can now use literal filter arguments which contain quotes.
|
||||
[#98](https://github.com/kylef/Stencil/pull/98)
|
||||
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Breaking
|
||||
|
||||
- It is no longer possible to create `Context` objects. Instead, you can pass a
|
||||
dictionary directly to a `Template`s `render` method.
|
||||
|
||||
```diff
|
||||
- try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||
+ try template.render(["name": "Kyle"])
|
||||
```
|
||||
|
||||
- Template loader are no longer passed into a `Context`, instead you will need
|
||||
to pass the `Loader` to an `Environment` and create a template from the
|
||||
`Environment`.
|
||||
|
||||
```diff
|
||||
let loader = FileSystemLoader(paths: ["templates/"])
|
||||
|
||||
- let template = loader.loadTemplate(name: "index.html")
|
||||
- try template.render(Context(dictionary: ["loader": loader]))
|
||||
+ let environment = Environment(loader: loader)
|
||||
+ try environment.renderTemplate(name: "index.html")
|
||||
```
|
||||
|
||||
- `Loader`s will now throw a `TemplateDoesNotExist` error when a template
|
||||
is not found.
|
||||
|
||||
- `Namespace` has been removed and replaced by extensions. You can create an
|
||||
extension including any custom template tags and filters. A collection of
|
||||
extensions can be passed to an `Environment`.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- `Environment` is a new way to load templates. You can configure an
|
||||
environment with custom template filters, tags and loaders and then create a
|
||||
template from an environment.
|
||||
|
||||
Environment also provides a convenience method to render a template directly.
|
||||
|
||||
- `FileSystemLoader` will now ensure that template paths are within the base
|
||||
path. Any template names that try to escape the base path will raise a
|
||||
`SuspiciousFileOperation` error.
|
||||
|
||||
- New `{% filter %}` tag allowing you to perform a filter across the contents
|
||||
of a block.
|
||||
|
||||
```html+django
|
||||
{% filter lowercase %}
|
||||
This Text Will Be Lowercased.
|
||||
{% endfilter %}
|
||||
```
|
||||
|
||||
- You can now use `{{ block.super }}` to render a super block from another `{%
|
||||
block %}`.
|
||||
|
||||
- `Environment` allows you to provide a custom `Template` subclass, allowing
|
||||
new template to use a specific subclass.
|
||||
|
||||
- If expressions may now contain filters on variables. For example
|
||||
`{% if name|uppercase == "TEST" %}` is now supported.
|
||||
|
||||
### Deprecations
|
||||
|
||||
- `Template` initialisers have been deprecated in favour of using a template
|
||||
loader such as `FileSystemLoader` inside an `Environment`.
|
||||
|
||||
- The use of whitespace inside variable filter expression is now deprecated.
|
||||
|
||||
```diff
|
||||
- {{ name | uppercase }}
|
||||
+ {{ name|uppercase }}
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Restores compatibility with ARM based platforms such as iOS. Stencil 0.7
|
||||
introduced compilation errors due to using the `Float80` type which is not
|
||||
available.
|
||||
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixes an issue where using `{% if %}` statements which use operators would
|
||||
throw a syntax error.
|
||||
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Breaking
|
||||
|
||||
- `TemplateLoader` has been renamed to `FileSystemLoader`. The
|
||||
`loadTemplate(s)` methods are now throwing and now take labels for the `name`
|
||||
and `names` arguments.
|
||||
|
||||
- Many internal classes are no longer public. Some APIs were previously
|
||||
accessible due to earlier versions of Swift requiring the types to be public
|
||||
to be able to test. Now we have access to `@testable` these can correctly be
|
||||
private.
|
||||
|
||||
- `{% ifnot %}` tag is now deprecated, please use `{% if not %}` instead.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Variable lookup now supports introspection of Swift types. You can now lookup
|
||||
values of Swift structures and classes inside a Context.
|
||||
|
||||
- If tags can now use prefix and infix operators such as `not`, `and`, `or`,
|
||||
`==`, `!=`, `>`, `>=`, `<` and `<=`.
|
||||
|
||||
```html+django
|
||||
{% if one or two and not three %}
|
||||
```
|
||||
|
||||
- You may now register custom template filters which make use of arguments.
|
||||
- There is now a `default` filter.
|
||||
|
||||
```html+django
|
||||
Hello {{ name|default:"World" }}
|
||||
```
|
||||
|
||||
- There is now a `join` filter.
|
||||
|
||||
```html+django
|
||||
{{ value|join:", " }}
|
||||
```
|
||||
|
||||
- `{% for %}` tag now supports filters.
|
||||
|
||||
```html+django
|
||||
{% for user in non_admins|default:admins %}
|
||||
{{ user }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Variables (`{{ variable.5 }}`) that reference an array index at an unknown
|
||||
index will now resolve to `nil` instead of causing a crash.
|
||||
[#72](https://github.com/kylef/Stencil/issues/72)
|
||||
|
||||
- Templates can now extend templates that extend other templates.
|
||||
[#60](https://github.com/kylef/Stencil/issues/60)
|
||||
|
||||
- If comparisons will now treat 0 and below numbers as negative.
|
||||
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Adds support for Swift 3.0.
|
||||
10
Package.swift
Normal file
10
Package.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
// swift-tools-version:3.1
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Stencil",
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 9),
|
||||
.Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 8),
|
||||
]
|
||||
)
|
||||
10
Package@swift-3.swift
Normal file
10
Package@swift-3.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
// swift-tools-version:3.1
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Stencil",
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 8),
|
||||
.Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 7),
|
||||
]
|
||||
)
|
||||
161
README.md
161
README.md
@@ -1,37 +1,42 @@
|
||||
Stencil
|
||||
=======
|
||||
# Stencil
|
||||
|
||||
[](https://travis-ci.org/stencilproject/Stencil)
|
||||
|
||||
Stencil is a simple and powerful template language for Swift. It provides a
|
||||
syntax similar to Django and Mustache. If you're familiar with these, you will
|
||||
feel right at home with Stencil.
|
||||
|
||||
### Example
|
||||
## Example
|
||||
|
||||
```html+django
|
||||
There are {{ articles.count }} articles.
|
||||
|
||||
{% for article in articles %}
|
||||
- {{ article.title }} by {{ article.author }}.
|
||||
{% endfor %}
|
||||
<ul>
|
||||
{% for article in articles %}
|
||||
<li>{{ article.title }} by {{ article.author }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
```
|
||||
|
||||
```swift
|
||||
let context = Context(dictionary: [
|
||||
"articles": [
|
||||
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
|
||||
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
|
||||
]
|
||||
])
|
||||
import Stencil
|
||||
|
||||
let template = Template(named: "template.stencil")
|
||||
let result = template.render(context)
|
||||
|
||||
switch result {
|
||||
case .Error(let error):
|
||||
println("There was an error rendering your template (\(error)).")
|
||||
case .Success(let string):
|
||||
println("\(string)")
|
||||
struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
|
||||
let context = [
|
||||
"articles": [
|
||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||
]
|
||||
]
|
||||
|
||||
let environment = Environment(loader: FileSystemLoader(paths: ["templates/"]))
|
||||
let rendered = try environment.renderTemplate(name: "article_list.html", context: context)
|
||||
|
||||
print(rendered)
|
||||
```
|
||||
|
||||
## Philosophy
|
||||
@@ -44,116 +49,28 @@ Stencil follows the same philosophy of Django:
|
||||
> design: the template system is meant to express presentation, not program
|
||||
> logic.
|
||||
|
||||
## Templates
|
||||
## The User Guide
|
||||
|
||||
### Variables
|
||||
Resources for Stencil template authors to write Stencil templates:
|
||||
|
||||
A variable can be defined in your template using the following:
|
||||
- [Language overview](http://stencil.fuller.li/en/latest/templates.html)
|
||||
- [Built-in template tags and filters](http://stencil.fuller.li/en/latest/builtins.html)
|
||||
|
||||
```html+django
|
||||
{{ variable }}
|
||||
```
|
||||
Resources to help you integrate Stencil into a Swift project:
|
||||
|
||||
Stencil will look up the variable inside the current variable context and
|
||||
evaluate it. When a variable contains a dot, it will try doing the
|
||||
following lookup:
|
||||
- [Installation](http://stencil.fuller.li/en/latest/installation.html)
|
||||
- [Getting Started](http://stencil.fuller.li/en/latest/getting-started.html)
|
||||
- [API Reference](http://stencil.fuller.li/en/latest/api.html)
|
||||
- [Custom Template Tags and Filters](http://stencil.fuller.li/en/latest/custom-template-tags-and-filters.html)
|
||||
|
||||
- Context lookup
|
||||
- Dictionary lookup
|
||||
- Array lookup (first, last, count, index)
|
||||
- Key value coding lookup
|
||||
## Projects that use Stencil
|
||||
|
||||
For example, if `people` was an array:
|
||||
|
||||
```html+django
|
||||
There are {{ people.count }} people, {{ people.first }} is first person.
|
||||
Followed by {{ people.1 }}.
|
||||
```
|
||||
|
||||
### Tags
|
||||
|
||||
Tags are a mechanism to execute a piece of code which can allows you to have
|
||||
control flow within your template.
|
||||
|
||||
```html+django
|
||||
{% if variable %}
|
||||
{{ variable }} was found.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
A tag can also effect the context and define variables as follows:
|
||||
|
||||
```html+django
|
||||
{% for item in items %}
|
||||
{{ item }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Stencil has a couple of built in tags which are listed below. You can also
|
||||
extend Stencil and provide your own tags.
|
||||
|
||||
#### for
|
||||
|
||||
A for loop allows you to iterate over an array found by variable lookup.
|
||||
|
||||
```html+django
|
||||
{% for item in items %}
|
||||
{{ item }}
|
||||
{% empty %}
|
||||
There we're no items.
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
#### if
|
||||
|
||||
```html+django
|
||||
{% if variable %}
|
||||
The variable was found in the current context.
|
||||
{% else %}
|
||||
The variable was not found.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
#### ifnot
|
||||
|
||||
```html+django
|
||||
{% ifnot variable %}
|
||||
The variable was NOT found in the current context.
|
||||
{% else %}
|
||||
The variable was found.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
#### Building custom tags
|
||||
|
||||
You can build a custom template tag. There are a couple of APIs to allow
|
||||
you to write your own custom tags. The following is the simplest form:
|
||||
|
||||
```swift
|
||||
template.parser.registerSimpleTag("custom") { context in
|
||||
return .Success("Hello World")
|
||||
}
|
||||
```
|
||||
|
||||
When your tag is used via `{% custom %}` it will execute the registered block
|
||||
of code allowing you to modify or retrieve a value from the context. Then
|
||||
return either a string rendered in your template, or an error.
|
||||
|
||||
If you want to accept arguments or to capture different tokens between two sets
|
||||
of template tags. You will need to the `registerTag` API which accepts a
|
||||
closure to handle the parsing. You can find examples of the `now`, `if` and
|
||||
`for` tags found inside `Node.swift`.
|
||||
|
||||
### Comments
|
||||
|
||||
To comment out part of your template you can use the following syntax:
|
||||
|
||||
```html+django
|
||||
{# My comment is completely hidden #}
|
||||
```
|
||||
[Sourcery](https://github.com/krzysztofzablocki/Sourcery),
|
||||
[SwiftGen](https://github.com/SwiftGen/SwiftGen),
|
||||
[Kitura](https://github.com/IBM-Swift/Kitura),
|
||||
[Weaver](https://github.com/scribd/Weaver)
|
||||
|
||||
## License
|
||||
|
||||
Stencil is licensed under the BSD license. See [LICENSE](LICENSE) for more
|
||||
info.
|
||||
|
||||
|
||||
69
Sources/Context.swift
Normal file
69
Sources/Context.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
/// A container for template variables.
|
||||
public class Context {
|
||||
var dictionaries: [[String: Any?]]
|
||||
|
||||
public let environment: Environment
|
||||
|
||||
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
||||
if let dictionary = dictionary {
|
||||
dictionaries = [dictionary]
|
||||
} else {
|
||||
dictionaries = []
|
||||
}
|
||||
|
||||
self.environment = environment ?? Environment()
|
||||
}
|
||||
|
||||
public subscript(key: String) -> Any? {
|
||||
/// Retrieves a variable's value, starting at the current context and going upwards
|
||||
get {
|
||||
for dictionary in Array(dictionaries.reversed()) {
|
||||
if let value = dictionary[key] {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Set a variable in the current context, deleting the variable if it's nil
|
||||
set(value) {
|
||||
if let dictionary = dictionaries.popLast() {
|
||||
var mutable_dictionary = dictionary
|
||||
mutable_dictionary[key] = value
|
||||
dictionaries.append(mutable_dictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new level into the Context
|
||||
fileprivate func push(_ dictionary: [String: Any]? = nil) {
|
||||
dictionaries.append(dictionary ?? [:])
|
||||
}
|
||||
|
||||
/// Pop the last level off of the Context
|
||||
fileprivate func pop() -> [String: Any?]? {
|
||||
return dictionaries.popLast()
|
||||
}
|
||||
|
||||
/// Push a new level onto the context for the duration of the execution of the given closure
|
||||
public func push<Result>(dictionary: [String: Any]? = nil, closure: (() throws -> Result)) rethrows -> Result {
|
||||
push(dictionary)
|
||||
defer { _ = pop() }
|
||||
return try closure()
|
||||
}
|
||||
|
||||
public func flatten() -> [String: Any] {
|
||||
var accumulator: [String: Any] = [:]
|
||||
|
||||
for dictionary in dictionaries {
|
||||
for (key, value) in dictionary {
|
||||
if let value = value {
|
||||
accumulator.updateValue(value, forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return accumulator
|
||||
}
|
||||
}
|
||||
48
Sources/Environment.swift
Normal file
48
Sources/Environment.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
public struct Environment {
|
||||
public let templateClass: Template.Type
|
||||
public var extensions: [Extension]
|
||||
|
||||
public var loader: Loader?
|
||||
|
||||
public init(loader: Loader? = nil,
|
||||
extensions: [Extension]? = nil,
|
||||
templateClass: Template.Type = Template.self) {
|
||||
|
||||
self.templateClass = templateClass
|
||||
self.loader = loader
|
||||
self.extensions = (extensions ?? []) + [DefaultExtension()]
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String) throws -> Template {
|
||||
if let loader = loader {
|
||||
return try loader.loadTemplate(name: name, environment: self)
|
||||
} else {
|
||||
throw TemplateDoesNotExist(templateNames: [name], loader: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadTemplate(names: [String]) throws -> Template {
|
||||
if let loader = loader {
|
||||
return try loader.loadTemplate(names: names, environment: self)
|
||||
} else {
|
||||
throw TemplateDoesNotExist(templateNames: names, loader: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
||||
let template = try loadTemplate(name: name)
|
||||
return try render(template: template, context: context)
|
||||
}
|
||||
|
||||
public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String {
|
||||
let template = templateClass.init(templateString: string, environment: self)
|
||||
return try render(template: template, context: context)
|
||||
}
|
||||
|
||||
func render(template: Template, context: [String: Any]?) throws -> String {
|
||||
// update template environment as it can be created from string literal with default environment
|
||||
template.environment = self
|
||||
return try template.render(context)
|
||||
}
|
||||
|
||||
}
|
||||
83
Sources/Errors.swift
Normal file
83
Sources/Errors.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
||||
let templateNames: [String]
|
||||
let loader: Loader?
|
||||
|
||||
public init(templateNames: [String], loader: Loader? = nil) {
|
||||
self.templateNames = templateNames
|
||||
self.loader = loader
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
let templates = templateNames.joined(separator: ", ")
|
||||
|
||||
if let loader = loader {
|
||||
return "Template named `\(templates)` does not exist in loader \(loader)"
|
||||
}
|
||||
|
||||
return "Template named `\(templates)` does not exist. No loaders found"
|
||||
}
|
||||
}
|
||||
|
||||
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||
public let reason: String
|
||||
public var description: String { return reason }
|
||||
public internal(set) var token: Token?
|
||||
public internal(set) var stackTrace: [Token]
|
||||
public var templateName: String? { return token?.sourceMap.filename }
|
||||
var allTokens: [Token] {
|
||||
return stackTrace + (token.map({ [$0] }) ?? [])
|
||||
}
|
||||
|
||||
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
||||
self.reason = reason
|
||||
self.stackTrace = stackTrace
|
||||
self.token = token
|
||||
}
|
||||
|
||||
public init(_ description: String) {
|
||||
self.init(reason: description)
|
||||
}
|
||||
|
||||
public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Error {
|
||||
func withToken(_ token: Token?) -> Error {
|
||||
if var error = self as? TemplateSyntaxError {
|
||||
error.token = error.token ?? token
|
||||
return error
|
||||
} else {
|
||||
return TemplateSyntaxError(reason: "\(self)", token: token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ErrorReporter: class {
|
||||
func renderError(_ error: Error) -> String
|
||||
}
|
||||
|
||||
open class SimpleErrorReporter: ErrorReporter {
|
||||
|
||||
open func renderError(_ error: Error) -> String {
|
||||
guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
|
||||
|
||||
func describe(token: Token) -> String {
|
||||
let templateName = token.sourceMap.filename ?? ""
|
||||
let line = token.sourceMap.line
|
||||
let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))"
|
||||
|
||||
return "\(templateName)\(line.number):\(line.offset): error: \(templateError.reason)\n"
|
||||
+ "\(line.content)\n"
|
||||
+ "\(highlight)\n"
|
||||
}
|
||||
|
||||
var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] }
|
||||
let description = templateError.token.map(describe(token:)) ?? templateError.reason
|
||||
descriptions.append(description)
|
||||
return descriptions.joined(separator: "\n")
|
||||
}
|
||||
|
||||
}
|
||||
335
Sources/Expression.swift
Normal file
335
Sources/Expression.swift
Normal file
@@ -0,0 +1,335 @@
|
||||
protocol Expression: CustomStringConvertible {
|
||||
func evaluate(context: Context) throws -> Bool
|
||||
}
|
||||
|
||||
|
||||
protocol InfixOperator: Expression {
|
||||
init(lhs: Expression, rhs: Expression)
|
||||
}
|
||||
|
||||
|
||||
protocol PrefixOperator: Expression {
|
||||
init(expression: Expression)
|
||||
}
|
||||
|
||||
|
||||
final class StaticExpression: Expression, CustomStringConvertible {
|
||||
let value: Bool
|
||||
|
||||
init(value: Bool) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
return value
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class VariableExpression: Expression, CustomStringConvertible {
|
||||
let variable: Resolvable
|
||||
|
||||
init(variable: Resolvable) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(variable: \(variable))"
|
||||
}
|
||||
|
||||
/// Resolves a variable in the given context as boolean
|
||||
func resolve(context: Context, variable: Resolvable) throws -> Bool {
|
||||
let result = try variable.resolve(context)
|
||||
var truthy = false
|
||||
|
||||
if let result = result as? [Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if let result = result as? [String:Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if let result = result as? Bool {
|
||||
truthy = result
|
||||
} else if let result = result as? String {
|
||||
truthy = !result.isEmpty
|
||||
} else if let value = result, let result = toNumber(value: value) {
|
||||
truthy = result > 0
|
||||
} else if result != nil {
|
||||
truthy = true
|
||||
}
|
||||
|
||||
return truthy
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
return try resolve(context: context, variable: variable)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||
let expression: Expression
|
||||
|
||||
init(expression: Expression) {
|
||||
self.expression = expression
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "not \(expression)"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
return try !expression.evaluate(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
|
||||
init(lhs: Expression, rhs: Expression) {
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) in \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression {
|
||||
let lhsValue = try lhs.variable.resolve(context)
|
||||
let rhsValue = try rhs.variable.resolve(context)
|
||||
|
||||
if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] {
|
||||
return rhs.contains(lhs)
|
||||
} else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableClosedRange<Int> {
|
||||
return rhs.contains(lhs)
|
||||
} else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableRange<Int> {
|
||||
return rhs.contains(lhs)
|
||||
} else if let lhs = lhsValue as? String, let rhs = rhsValue as? String {
|
||||
return rhs.contains(lhs)
|
||||
} else if lhsValue == nil && rhsValue == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
|
||||
init(lhs: Expression, rhs: Expression) {
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) or \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
let lhs = try self.lhs.evaluate(context: context)
|
||||
if lhs {
|
||||
return lhs
|
||||
}
|
||||
|
||||
return try rhs.evaluate(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
|
||||
init(lhs: Expression, rhs: Expression) {
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) and \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
let lhs = try self.lhs.evaluate(context: context)
|
||||
if !lhs {
|
||||
return lhs
|
||||
}
|
||||
|
||||
return try rhs.evaluate(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
|
||||
required init(lhs: Expression, rhs: Expression) {
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) == \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression {
|
||||
let lhsValue = try lhs.variable.resolve(context)
|
||||
let rhsValue = try rhs.variable.resolve(context)
|
||||
|
||||
if let lhs = lhsValue, let rhs = rhsValue {
|
||||
if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) {
|
||||
return lhs == rhs
|
||||
} else if let lhs = lhsValue as? String, let rhs = rhsValue as? String {
|
||||
return lhs == rhs
|
||||
} else if let lhs = lhsValue as? Bool, let rhs = rhsValue as? Bool {
|
||||
return lhs == rhs
|
||||
}
|
||||
} else if lhsValue == nil && rhsValue == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
|
||||
required init(lhs: Expression, rhs: Expression) {
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) \(op) \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression {
|
||||
let lhsValue = try lhs.variable.resolve(context)
|
||||
let rhsValue = try rhs.variable.resolve(context)
|
||||
|
||||
if let lhs = lhsValue, let rhs = rhsValue {
|
||||
if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) {
|
||||
return compare(lhs: lhs, rhs: rhs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var op: String {
|
||||
return ""
|
||||
}
|
||||
|
||||
func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MoreThanExpression: NumericExpression {
|
||||
override var op: String {
|
||||
return ">"
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs > rhs
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MoreThanEqualExpression: NumericExpression {
|
||||
override var op: String {
|
||||
return ">="
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs >= rhs
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LessThanExpression: NumericExpression {
|
||||
override var op: String {
|
||||
return "<"
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs < rhs
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LessThanEqualExpression: NumericExpression {
|
||||
override var op: String {
|
||||
return "<="
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs <= rhs
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class InequalityExpression: EqualityExpression {
|
||||
override var description: String {
|
||||
return "(\(lhs) != \(rhs))"
|
||||
}
|
||||
|
||||
override func evaluate(context: Context) throws -> Bool {
|
||||
return try !super.evaluate(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func toNumber(value: Any) -> Number? {
|
||||
if let value = value as? Float {
|
||||
return Number(value)
|
||||
} else if let value = value as? Double {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int8 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int16 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int32 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int64 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt8 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt16 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt32 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt64 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Number {
|
||||
return value
|
||||
} else if let value = value as? Float64 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Float32 {
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
86
Sources/Extension.swift
Normal file
86
Sources/Extension.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
open class Extension {
|
||||
typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
var tags = [String: TagParser]()
|
||||
|
||||
var filters = [String: Filter]()
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
/// Registers a new template tag
|
||||
public func registerTag(_ name: String, parser: @escaping (TokenParser, Token) throws -> NodeType) {
|
||||
tags[name] = parser
|
||||
}
|
||||
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
|
||||
registerTag(name, parser: { parser, token in
|
||||
return SimpleNode(token: token, handler: handler)
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers a template filter with the given name
|
||||
public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) {
|
||||
filters[name] = .simple(filter)
|
||||
}
|
||||
|
||||
/// Registers a template filter with the given name
|
||||
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) {
|
||||
filters[name] = .arguments(filter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DefaultExtension: Extension {
|
||||
override init() {
|
||||
super.init()
|
||||
registerDefaultTags()
|
||||
registerDefaultFilters()
|
||||
}
|
||||
|
||||
fileprivate func registerDefaultTags() {
|
||||
registerTag("for", parser: ForNode.parse)
|
||||
registerTag("if", parser: IfNode.parse)
|
||||
registerTag("ifnot", parser: IfNode.parse_ifnot)
|
||||
#if !os(Linux)
|
||||
registerTag("now", parser: NowNode.parse)
|
||||
#endif
|
||||
registerTag("include", parser: IncludeNode.parse)
|
||||
registerTag("extends", parser: ExtendsNode.parse)
|
||||
registerTag("block", parser: BlockNode.parse)
|
||||
registerTag("filter", parser: FilterNode.parse)
|
||||
}
|
||||
|
||||
fileprivate func registerDefaultFilters() {
|
||||
registerFilter("default", filter: defaultFilter)
|
||||
registerFilter("capitalize", filter: capitalise)
|
||||
registerFilter("uppercase", filter: uppercase)
|
||||
registerFilter("lowercase", filter: lowercase)
|
||||
registerFilter("join", filter: joinFilter)
|
||||
registerFilter("split", filter: splitFilter)
|
||||
registerFilter("indent", filter: indentFilter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protocol FilterType {
|
||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any?
|
||||
}
|
||||
|
||||
enum Filter: FilterType {
|
||||
case simple(((Any?) throws -> Any?))
|
||||
case arguments(((Any?, [Any?]) throws -> Any?))
|
||||
|
||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
switch self {
|
||||
case let .simple(filter):
|
||||
if !arguments.isEmpty {
|
||||
throw TemplateSyntaxError("cannot invoke filter with an argument")
|
||||
}
|
||||
|
||||
return try filter(value)
|
||||
case let .arguments(filter):
|
||||
return try filter(value, arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Sources/FilterTag.swift
Normal file
37
Sources/FilterTag.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
class FilterNode : NodeType {
|
||||
let resolvable: Resolvable
|
||||
let nodes: [NodeType]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'filter' tag takes one argument, the filter expression")
|
||||
}
|
||||
|
||||
let blocks = try parser.parse(until(["endfilter"]))
|
||||
|
||||
guard parser.nextToken() != nil else {
|
||||
throw TemplateSyntaxError("`endfilter` was not found.")
|
||||
}
|
||||
|
||||
let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token)
|
||||
return FilterNode(nodes: blocks, resolvable: resolvable, token: token)
|
||||
}
|
||||
|
||||
init(nodes: [NodeType], resolvable: Resolvable, token: Token) {
|
||||
self.nodes = nodes
|
||||
self.resolvable = resolvable
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
let value = try renderNodes(nodes, context)
|
||||
|
||||
return try context.push(dictionary: ["filter_value": value]) {
|
||||
return try VariableNode(variable: resolvable, token: token).render(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
Sources/Filters.swift
Normal file
113
Sources/Filters.swift
Normal file
@@ -0,0 +1,113 @@
|
||||
func capitalise(_ value: Any?) -> Any? {
|
||||
if let array = value as? [Any?] {
|
||||
return array.map { stringify($0).capitalized }
|
||||
} else {
|
||||
return stringify(value).capitalized
|
||||
}
|
||||
}
|
||||
|
||||
func uppercase(_ value: Any?) -> Any? {
|
||||
if let array = value as? [Any?] {
|
||||
return array.map { stringify($0).uppercased() }
|
||||
} else {
|
||||
return stringify(value).uppercased()
|
||||
}
|
||||
}
|
||||
|
||||
func lowercase(_ value: Any?) -> Any? {
|
||||
if let array = value as? [Any?] {
|
||||
return array.map { stringify($0).lowercased() }
|
||||
} else {
|
||||
return stringify(value).lowercased()
|
||||
}
|
||||
}
|
||||
|
||||
func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {
|
||||
// value can be optional wrapping nil, so this way we check for underlying value
|
||||
if let value = value, String(describing: value) != "nil" {
|
||||
return value
|
||||
}
|
||||
|
||||
for argument in arguments {
|
||||
if let argument = argument {
|
||||
return argument
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
guard arguments.count < 2 else {
|
||||
throw TemplateSyntaxError("'join' filter takes a single argument")
|
||||
}
|
||||
|
||||
let separator = stringify(arguments.first ?? "")
|
||||
|
||||
if let value = value as? [Any] {
|
||||
return value
|
||||
.map(stringify)
|
||||
.joined(separator: separator)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
guard arguments.count < 2 else {
|
||||
throw TemplateSyntaxError("'split' filter takes a single argument")
|
||||
}
|
||||
|
||||
let separator = stringify(arguments.first ?? " ")
|
||||
if let value = value as? String {
|
||||
return value.components(separatedBy: separator)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
guard arguments.count <= 3 else {
|
||||
throw TemplateSyntaxError("'indent' filter can take at most 3 arguments")
|
||||
}
|
||||
|
||||
var indentWidth = 4
|
||||
if arguments.count > 0 {
|
||||
guard let value = arguments[0] as? Int else {
|
||||
throw TemplateSyntaxError("'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))")
|
||||
}
|
||||
indentWidth = value
|
||||
}
|
||||
|
||||
var indentationChar = " "
|
||||
if arguments.count > 1 {
|
||||
guard let value = arguments[1] as? String else {
|
||||
throw TemplateSyntaxError("'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))")
|
||||
}
|
||||
indentationChar = value
|
||||
}
|
||||
|
||||
var indentFirst = false
|
||||
if arguments.count > 2 {
|
||||
guard let value = arguments[2] as? Bool else {
|
||||
throw TemplateSyntaxError("'indent' filter indentFirst argument must be a Bool")
|
||||
}
|
||||
indentFirst = value
|
||||
}
|
||||
|
||||
let indentation = [String](repeating: indentationChar, count: indentWidth).joined(separator: "")
|
||||
return indent(stringify(value), indentation: indentation, indentFirst: indentFirst)
|
||||
}
|
||||
|
||||
|
||||
func indent(_ content: String, indentation: String, indentFirst: Bool) -> String {
|
||||
guard !indentation.isEmpty else { return content }
|
||||
|
||||
var lines = content.components(separatedBy: .newlines)
|
||||
let firstLine = (indentFirst ? indentation : "") + lines.removeFirst()
|
||||
let result = lines.reduce([firstLine]) { (result, line) in
|
||||
return result + [(line.isEmpty ? "" : "\(indentation)\(line)")]
|
||||
}
|
||||
return result.joined(separator: "\n")
|
||||
}
|
||||
|
||||
155
Sources/ForTag.swift
Normal file
155
Sources/ForTag.swift
Normal file
@@ -0,0 +1,155 @@
|
||||
import Foundation
|
||||
|
||||
class ForNode : NodeType {
|
||||
let resolvable: Resolvable
|
||||
let loopVariables: [String]
|
||||
let nodes:[NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
let `where`: Expression?
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
return components.count > (index + 1) && components[index] == token
|
||||
}
|
||||
|
||||
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
|
||||
return components.count == index || hasToken(token, at: index)
|
||||
}
|
||||
|
||||
guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
|
||||
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.")
|
||||
}
|
||||
|
||||
let loopVariables = components[1].characters
|
||||
.split(separator: ",")
|
||||
.map(String.init)
|
||||
.map { $0.trim(character: " ") }
|
||||
|
||||
let resolvable = try parser.compileResolvable(components[3], containedIn: token)
|
||||
|
||||
let `where` = hasToken("where", at: 4)
|
||||
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token)
|
||||
: nil
|
||||
|
||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endfor` was not found.")
|
||||
}
|
||||
|
||||
var emptyNodes = [NodeType]()
|
||||
if token.contents == "empty" {
|
||||
emptyNodes = try parser.parse(until(["endfor"]))
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes: emptyNodes, where: `where`, token: token)
|
||||
}
|
||||
|
||||
init(resolvable: Resolvable, loopVariables: [String], nodes: [NodeType], emptyNodes: [NodeType], where: Expression? = nil, token: Token? = nil) {
|
||||
self.resolvable = resolvable
|
||||
self.loopVariables = loopVariables
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
self.where = `where`
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||
if loopVariables.isEmpty {
|
||||
return try context.push() {
|
||||
return try closure()
|
||||
}
|
||||
}
|
||||
|
||||
let valueMirror = Mirror(reflecting: value)
|
||||
if case .tuple? = valueMirror.displayStyle {
|
||||
if loopVariables.count > Int(valueMirror.children.count) {
|
||||
throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables")
|
||||
}
|
||||
var variablesContext = [String: Any]()
|
||||
valueMirror.children.prefix(loopVariables.count).enumerated().forEach({ (offset, element) in
|
||||
if loopVariables[offset] != "_" {
|
||||
variablesContext[loopVariables[offset]] = element.value
|
||||
}
|
||||
})
|
||||
|
||||
return try context.push(dictionary: variablesContext) {
|
||||
return try closure()
|
||||
}
|
||||
}
|
||||
|
||||
return try context.push(dictionary: [loopVariables.first!: value]) {
|
||||
return try closure()
|
||||
}
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
let resolved = try resolvable.resolve(context)
|
||||
|
||||
var values: [Any]
|
||||
|
||||
if let dictionary = resolved as? [String: Any], !dictionary.isEmpty {
|
||||
values = dictionary.map { ($0.key, $0.value) }
|
||||
} else if let array = resolved as? [Any] {
|
||||
values = array
|
||||
} else if let range = resolved as? CountableClosedRange<Int> {
|
||||
values = Array(range)
|
||||
} else if let range = resolved as? CountableRange<Int> {
|
||||
values = Array(range)
|
||||
} else if let resolved = resolved {
|
||||
let mirror = Mirror(reflecting: resolved)
|
||||
switch mirror.displayStyle {
|
||||
case .struct?, .tuple?:
|
||||
values = Array(mirror.children)
|
||||
case .class?:
|
||||
var children = Array(mirror.children)
|
||||
var currentMirror: Mirror? = mirror
|
||||
while let superclassMirror = currentMirror?.superclassMirror {
|
||||
children.append(contentsOf: superclassMirror.children)
|
||||
currentMirror = superclassMirror
|
||||
}
|
||||
values = Array(children)
|
||||
default:
|
||||
values = []
|
||||
}
|
||||
} else {
|
||||
values = []
|
||||
}
|
||||
|
||||
if let `where` = self.where {
|
||||
values = try values.filter({ item -> Bool in
|
||||
return try push(value: item, context: context) {
|
||||
try `where`.evaluate(context: context)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if !values.isEmpty {
|
||||
let count = values.count
|
||||
|
||||
return try values.enumerated().map { index, item in
|
||||
let forContext: [String: Any] = [
|
||||
"first": index == 0,
|
||||
"last": index == (count - 1),
|
||||
"counter": index + 1,
|
||||
"counter0": index,
|
||||
"length": count
|
||||
]
|
||||
|
||||
return try context.push(dictionary: ["forloop": forContext]) {
|
||||
return try push(value: item, context: context) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}.joined(separator: "")
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
try renderNodes(emptyNodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
267
Sources/IfTag.swift
Normal file
267
Sources/IfTag.swift
Normal file
@@ -0,0 +1,267 @@
|
||||
enum Operator {
|
||||
case infix(String, Int, InfixOperator.Type)
|
||||
case prefix(String, Int, PrefixOperator.Type)
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .infix(let name, _, _):
|
||||
return name
|
||||
case .prefix(let name, _, _):
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let operators: [Operator] = [
|
||||
.infix("in", 5, InExpression.self),
|
||||
.infix("or", 6, OrExpression.self),
|
||||
.infix("and", 7, AndExpression.self),
|
||||
.prefix("not", 8, NotExpression.self),
|
||||
.infix("==", 10, EqualityExpression.self),
|
||||
.infix("!=", 10, InequalityExpression.self),
|
||||
.infix(">", 10, MoreThanExpression.self),
|
||||
.infix(">=", 10, MoreThanEqualExpression.self),
|
||||
.infix("<", 10, LessThanExpression.self),
|
||||
.infix("<=", 10, LessThanEqualExpression.self),
|
||||
]
|
||||
|
||||
|
||||
func findOperator(name: String) -> Operator? {
|
||||
for op in operators {
|
||||
if op.name == name {
|
||||
return op
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
enum IfToken {
|
||||
case infix(name: String, bindingPower: Int, op: InfixOperator.Type)
|
||||
case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type)
|
||||
case variable(Resolvable)
|
||||
case end
|
||||
|
||||
var bindingPower: Int {
|
||||
switch self {
|
||||
case .infix(_, let bindingPower, _):
|
||||
return bindingPower
|
||||
case .prefix(_, let bindingPower, _):
|
||||
return bindingPower
|
||||
case .variable(_):
|
||||
return 0
|
||||
case .end:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func nullDenotation(parser: IfExpressionParser) throws -> Expression {
|
||||
switch self {
|
||||
case .infix(let name, _, _):
|
||||
throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side")
|
||||
case .prefix(_, let bindingPower, let op):
|
||||
let expression = try parser.expression(bindingPower: bindingPower)
|
||||
return op.init(expression: expression)
|
||||
case .variable(let variable):
|
||||
return VariableExpression(variable: variable)
|
||||
case .end:
|
||||
throw TemplateSyntaxError("'if' expression error: end")
|
||||
}
|
||||
}
|
||||
|
||||
func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression {
|
||||
switch self {
|
||||
case .infix(_, let bindingPower, let op):
|
||||
let right = try parser.expression(bindingPower: bindingPower)
|
||||
return op.init(lhs: left, rhs: right)
|
||||
case .prefix(let name, _, _):
|
||||
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
|
||||
case .variable(let variable):
|
||||
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
|
||||
case .end:
|
||||
throw TemplateSyntaxError("'if' expression error: end")
|
||||
}
|
||||
}
|
||||
|
||||
var isEnd: Bool {
|
||||
switch self {
|
||||
case .end:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class IfExpressionParser {
|
||||
let tokens: [IfToken]
|
||||
var position: Int = 0
|
||||
|
||||
init(components: [String], tokenParser: TokenParser, token: Token) throws {
|
||||
self.tokens = try components.map { component in
|
||||
if let op = findOperator(name: component) {
|
||||
switch op {
|
||||
case .infix(let name, let bindingPower, let cls):
|
||||
return .infix(name: name, bindingPower: bindingPower, op: cls)
|
||||
case .prefix(let name, let bindingPower, let cls):
|
||||
return .prefix(name: name, bindingPower: bindingPower, op: cls)
|
||||
}
|
||||
}
|
||||
|
||||
return .variable(try tokenParser.compileResolvable(component, containedIn: token))
|
||||
}
|
||||
}
|
||||
|
||||
var currentToken: IfToken {
|
||||
if tokens.count > position {
|
||||
return tokens[position]
|
||||
}
|
||||
|
||||
return .end
|
||||
}
|
||||
|
||||
var nextToken: IfToken {
|
||||
position += 1
|
||||
return currentToken
|
||||
}
|
||||
|
||||
func parse() throws -> Expression {
|
||||
let expression = try self.expression()
|
||||
|
||||
if !currentToken.isEnd {
|
||||
throw TemplateSyntaxError("'if' expression error: dangling token")
|
||||
}
|
||||
|
||||
return expression
|
||||
}
|
||||
|
||||
func expression(bindingPower: Int = 0) throws -> Expression {
|
||||
var token = currentToken
|
||||
position += 1
|
||||
|
||||
var left = try token.nullDenotation(parser: self)
|
||||
|
||||
while bindingPower < currentToken.bindingPower {
|
||||
token = currentToken
|
||||
position += 1
|
||||
left = try token.leftDenotation(left: left, parser: self)
|
||||
}
|
||||
|
||||
return left
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func parseExpression(components: [String], tokenParser: TokenParser, token: Token) throws -> Expression {
|
||||
let parser = try IfExpressionParser(components: components, tokenParser: tokenParser, token: token)
|
||||
return try parser.parse()
|
||||
}
|
||||
|
||||
|
||||
/// Represents an if condition and the associated nodes when the condition
|
||||
/// evaluates
|
||||
final class IfCondition {
|
||||
let expression: Expression?
|
||||
let nodes: [NodeType]
|
||||
|
||||
init(expression: Expression?, nodes: [NodeType]) {
|
||||
self.expression = expression
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
return try context.push {
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class IfNode : NodeType {
|
||||
let conditions: [IfCondition]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components()
|
||||
components.removeFirst()
|
||||
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||
var conditions: [IfCondition] = [
|
||||
IfCondition(expression: expression, nodes: nodes)
|
||||
]
|
||||
|
||||
var nextToken = parser.nextToken()
|
||||
while let current = nextToken, current.contents.hasPrefix("elif") {
|
||||
var components = current.components()
|
||||
components.removeFirst()
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: current)
|
||||
|
||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||
nextToken = parser.nextToken()
|
||||
conditions.append(IfCondition(expression: expression, nodes: nodes))
|
||||
}
|
||||
|
||||
if let current = nextToken, current.contents == "else" {
|
||||
conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"]))))
|
||||
nextToken = parser.nextToken()
|
||||
}
|
||||
|
||||
guard let current = nextToken, current.contents == "endif" else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
return IfNode(conditions: conditions, token: token)
|
||||
}
|
||||
|
||||
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.")
|
||||
}
|
||||
components.removeFirst()
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
if token.contents == "else" {
|
||||
trueNodes = try parser.parse(until(["endif"]))
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
return IfNode(conditions: [
|
||||
IfCondition(expression: expression, nodes: trueNodes),
|
||||
IfCondition(expression: nil, nodes: falseNodes),
|
||||
], token: token)
|
||||
}
|
||||
|
||||
init(conditions: [IfCondition], token: Token? = nil) {
|
||||
self.conditions = conditions
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
for condition in conditions {
|
||||
if let expression = condition.expression {
|
||||
let truthy = try expression.evaluate(context: context)
|
||||
|
||||
if truthy {
|
||||
return try condition.render(context)
|
||||
}
|
||||
} else {
|
||||
return try condition.render(context)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
46
Sources/Include.swift
Normal file
46
Sources/Include.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
import PathKit
|
||||
|
||||
|
||||
class IncludeNode : NodeType {
|
||||
let templateName: Variable
|
||||
let includeContext: String?
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 || bits.count == 3 else {
|
||||
throw TemplateSyntaxError("'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file")
|
||||
}
|
||||
|
||||
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token)
|
||||
}
|
||||
|
||||
init(templateName: Variable, includeContext: String? = nil, token: Token) {
|
||||
self.templateName = templateName
|
||||
self.includeContext = includeContext
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
guard let templateName = try self.templateName.resolve(context) as? String else {
|
||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||
}
|
||||
|
||||
let template = try context.environment.loadTemplate(name: templateName)
|
||||
|
||||
do {
|
||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
||||
return try context.push(dictionary: subContext) {
|
||||
return try template.render(context)
|
||||
}
|
||||
} catch {
|
||||
if let error = error as? TemplateSyntaxError {
|
||||
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
189
Sources/Inheritence.swift
Normal file
189
Sources/Inheritence.swift
Normal file
@@ -0,0 +1,189 @@
|
||||
class BlockContext {
|
||||
class var contextKey: String { return "block_context" }
|
||||
|
||||
// contains mapping of block names to their nodes and templates where they are defined
|
||||
var blocks: [String: [BlockNode]]
|
||||
|
||||
init(blocks: [String: BlockNode]) {
|
||||
self.blocks = [:]
|
||||
blocks.forEach { self.blocks[$0.key] = [$0.value] }
|
||||
}
|
||||
|
||||
func push(_ block: BlockNode, forKey blockName: String) {
|
||||
if var blocks = blocks[blockName] {
|
||||
blocks.append(block)
|
||||
self.blocks[blockName] = blocks
|
||||
} else {
|
||||
self.blocks[blockName] = [block]
|
||||
}
|
||||
}
|
||||
|
||||
func pop(_ blockName: String) -> BlockNode? {
|
||||
if var blocks = blocks[blockName] {
|
||||
let block = blocks.removeFirst()
|
||||
if blocks.isEmpty {
|
||||
self.blocks.removeValue(forKey: blockName)
|
||||
} else {
|
||||
self.blocks[blockName] = blocks
|
||||
}
|
||||
return block
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Collection {
|
||||
func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? {
|
||||
for element in self {
|
||||
if closure(element) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExtendsNode : NodeType {
|
||||
let templateName: Variable
|
||||
let blocks: [String:BlockNode]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
|
||||
}
|
||||
|
||||
let parsedNodes = try parser.parse()
|
||||
guard (parsedNodes.any { $0 is ExtendsNode }) == nil else {
|
||||
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
|
||||
}
|
||||
|
||||
let blockNodes = parsedNodes.flatMap { $0 as? BlockNode }
|
||||
|
||||
let nodes = blockNodes.reduce([String: BlockNode]()) { (accumulator, node) -> [String: BlockNode] in
|
||||
var dict = accumulator
|
||||
dict[node.name] = node
|
||||
return dict
|
||||
}
|
||||
|
||||
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes, token: token)
|
||||
}
|
||||
|
||||
init(templateName: Variable, blocks: [String: BlockNode], token: Token) {
|
||||
self.templateName = templateName
|
||||
self.blocks = blocks
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
guard let templateName = try self.templateName.resolve(context) as? String else {
|
||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||
}
|
||||
|
||||
let baseTemplate = try context.environment.loadTemplate(name: templateName)
|
||||
|
||||
let blockContext: BlockContext
|
||||
if let currentBlockContext = context[BlockContext.contextKey] as? BlockContext {
|
||||
blockContext = currentBlockContext
|
||||
for (name, block) in blocks {
|
||||
blockContext.push(block, forKey: name)
|
||||
}
|
||||
} else {
|
||||
blockContext = BlockContext(blocks: blocks)
|
||||
}
|
||||
|
||||
do {
|
||||
// pushes base template and renders it's content
|
||||
// block_context contains all blocks from child templates
|
||||
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||
return try baseTemplate.render(context)
|
||||
}
|
||||
} catch {
|
||||
// if error template is already set (see catch in BlockNode)
|
||||
// and it happend in the same template as current template
|
||||
// there is no need to wrap it in another error
|
||||
if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename {
|
||||
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BlockNode : NodeType {
|
||||
let name: String
|
||||
let nodes: [NodeType]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'block' tag takes one argument, the block name")
|
||||
}
|
||||
|
||||
let blockName = bits[1]
|
||||
let nodes = try parser.parse(until(["endblock"]))
|
||||
_ = parser.nextToken()
|
||||
return BlockNode(name:blockName, nodes:nodes, token: token)
|
||||
}
|
||||
|
||||
init(name: String, nodes: [NodeType], token: Token) {
|
||||
self.name = name
|
||||
self.nodes = nodes
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.pop(name) {
|
||||
let childContext = try self.childContext(child, blockContext: blockContext, context: context)
|
||||
// render extension node
|
||||
do {
|
||||
return try context.push(dictionary: childContext) {
|
||||
return try child.render(context)
|
||||
}
|
||||
} catch {
|
||||
throw error.withToken(child.token)
|
||||
}
|
||||
}
|
||||
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
|
||||
// child node is a block node from child template that extends this node (has the same name)
|
||||
func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any?] {
|
||||
var childContext: [String: Any?] = [BlockContext.contextKey: blockContext]
|
||||
|
||||
if let blockSuperNode = child.nodes.first(where: {
|
||||
if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true }
|
||||
else { return false}
|
||||
}) {
|
||||
do {
|
||||
// render base node so that its content can be used as part of child node that extends it
|
||||
childContext["block"] = ["super": try self.render(context)]
|
||||
} catch {
|
||||
if let error = error as? TemplateSyntaxError {
|
||||
throw TemplateSyntaxError(
|
||||
reason: error.reason,
|
||||
token: blockSuperNode.token,
|
||||
stackTrace: error.allTokens)
|
||||
} else {
|
||||
throw TemplateSyntaxError(
|
||||
reason: "\(error)",
|
||||
token: blockSuperNode.token,
|
||||
stackTrace: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
return childContext
|
||||
}
|
||||
|
||||
}
|
||||
112
Sources/KeyPath.swift
Normal file
112
Sources/KeyPath.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
import Foundation
|
||||
|
||||
/// A structure used to represent a template variable, and to resolve it in a given context.
|
||||
final class KeyPath {
|
||||
private var components = [String]()
|
||||
private var current = ""
|
||||
private var partialComponents = [String]()
|
||||
private var subscriptLevel = 0
|
||||
|
||||
let variable: String
|
||||
let context: Context
|
||||
|
||||
// Split the keypath string and resolve references if possible
|
||||
init(_ variable: String, in context: Context) {
|
||||
self.variable = variable
|
||||
self.context = context
|
||||
}
|
||||
|
||||
func parse() throws -> [String] {
|
||||
defer {
|
||||
components = []
|
||||
current = ""
|
||||
partialComponents = []
|
||||
subscriptLevel = 0
|
||||
}
|
||||
|
||||
for c in variable.characters {
|
||||
switch c {
|
||||
case "." where subscriptLevel == 0:
|
||||
try foundSeparator()
|
||||
case "[":
|
||||
try openBracket()
|
||||
case "]":
|
||||
try closeBracket()
|
||||
default:
|
||||
try addCharacter(c)
|
||||
}
|
||||
}
|
||||
try finish()
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
private func foundSeparator() throws {
|
||||
if !current.isEmpty {
|
||||
partialComponents.append(current)
|
||||
}
|
||||
|
||||
guard !partialComponents.isEmpty else {
|
||||
throw TemplateSyntaxError("Unexpected '.' in variable '\(variable)'")
|
||||
}
|
||||
|
||||
components += partialComponents
|
||||
current = ""
|
||||
partialComponents = []
|
||||
}
|
||||
|
||||
// when opening the first bracket, we must have a partial component
|
||||
private func openBracket() throws {
|
||||
guard !partialComponents.isEmpty || !current.isEmpty else {
|
||||
throw TemplateSyntaxError("Unexpected '[' in variable '\(variable)'")
|
||||
}
|
||||
|
||||
if subscriptLevel > 0 {
|
||||
current.append("[")
|
||||
} else if !current.isEmpty {
|
||||
partialComponents.append(current)
|
||||
current = ""
|
||||
}
|
||||
|
||||
subscriptLevel += 1
|
||||
}
|
||||
|
||||
// for a closing bracket at root level, try to resolve the reference
|
||||
private func closeBracket() throws {
|
||||
guard subscriptLevel > 0 else {
|
||||
throw TemplateSyntaxError("Unbalanced ']' in variable '\(variable)'")
|
||||
}
|
||||
|
||||
if subscriptLevel > 1 {
|
||||
current.append("]")
|
||||
} else if !current.isEmpty,
|
||||
let value = try Variable(current).resolve(context) {
|
||||
partialComponents.append("\(value)")
|
||||
current = ""
|
||||
} else {
|
||||
throw TemplateSyntaxError("Unable to resolve subscript '\(current)' in variable '\(variable)'")
|
||||
}
|
||||
|
||||
subscriptLevel -= 1
|
||||
}
|
||||
|
||||
private func addCharacter(_ c: Character) throws {
|
||||
guard partialComponents.isEmpty || subscriptLevel > 0 else {
|
||||
throw TemplateSyntaxError("Unexpected character '\(c)' in variable '\(variable)'")
|
||||
}
|
||||
|
||||
current.append(c)
|
||||
}
|
||||
|
||||
private func finish() throws {
|
||||
// check if we have a last piece
|
||||
if !current.isEmpty {
|
||||
partialComponents.append(current)
|
||||
}
|
||||
components += partialComponents
|
||||
|
||||
guard subscriptLevel == 0 else {
|
||||
throw TemplateSyntaxError("Unbalanced subscript brackets in variable '\(variable)'")
|
||||
}
|
||||
}
|
||||
}
|
||||
201
Sources/Lexer.swift
Normal file
201
Sources/Lexer.swift
Normal file
@@ -0,0 +1,201 @@
|
||||
import Foundation
|
||||
|
||||
struct Lexer {
|
||||
let templateName: String?
|
||||
let templateString: String
|
||||
|
||||
init(templateName: String? = nil, templateString: String) {
|
||||
self.templateName = templateName
|
||||
self.templateString = templateString
|
||||
}
|
||||
|
||||
func createToken(string: String, at range: Range<String.Index>) -> Token {
|
||||
func strip() -> String {
|
||||
guard string.characters.count > 4 else { return "" }
|
||||
let start = string.index(string.startIndex, offsetBy: 2)
|
||||
let end = string.index(string.endIndex, offsetBy: -2)
|
||||
let trimmed = String(string[start..<end])
|
||||
.components(separatedBy: "\n")
|
||||
.filter({ !$0.isEmpty })
|
||||
.map({ $0.trim(character: " ") })
|
||||
.joined(separator: " ")
|
||||
return trimmed
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") {
|
||||
let value = strip()
|
||||
let range = templateString.range(of: value, range: range) ?? range
|
||||
let line = templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: templateName, line: line)
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return .variable(value: value, at: sourceMap)
|
||||
} else if string.hasPrefix("{%") {
|
||||
return .block(value: value, at: sourceMap)
|
||||
} else if string.hasPrefix("{#") {
|
||||
return .comment(value: value, at: sourceMap)
|
||||
}
|
||||
}
|
||||
|
||||
let line = templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: templateName, line: line)
|
||||
return .text(value: string, at: sourceMap)
|
||||
}
|
||||
|
||||
/// Returns an array of tokens from a given template string.
|
||||
func tokenize() -> [Token] {
|
||||
var tokens: [Token] = []
|
||||
|
||||
let scanner = Scanner(templateString)
|
||||
|
||||
let map = [
|
||||
"{{": "}}",
|
||||
"{%": "%}",
|
||||
"{#": "#}",
|
||||
]
|
||||
|
||||
while !scanner.isEmpty {
|
||||
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
|
||||
if !text.1.isEmpty {
|
||||
tokens.append(createToken(string: text.1, at: scanner.range))
|
||||
}
|
||||
|
||||
let end = map[text.0]!
|
||||
let result = scanner.scan(until: end, returnUntil: true)
|
||||
tokens.append(createToken(string: result, at: scanner.range))
|
||||
} else {
|
||||
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
||||
scanner.content = ""
|
||||
}
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Scanner {
|
||||
let originalContent: String
|
||||
var content: String
|
||||
var range: Range<String.Index>
|
||||
|
||||
init(_ content: String) {
|
||||
self.originalContent = content
|
||||
self.content = content
|
||||
range = content.startIndex..<content.startIndex
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return content.isEmpty
|
||||
}
|
||||
|
||||
func scan(until: String, returnUntil: Bool = false) -> String {
|
||||
var index = content.startIndex
|
||||
|
||||
if until.isEmpty {
|
||||
return ""
|
||||
}
|
||||
|
||||
range = range.upperBound..<range.upperBound
|
||||
while index != content.endIndex {
|
||||
let substring = content.substring(from: index)
|
||||
|
||||
if substring.hasPrefix(until) {
|
||||
let result = content.substring(to: index)
|
||||
|
||||
if returnUntil {
|
||||
range = range.lowerBound..<originalContent.index(range.upperBound, offsetBy: until.characters.count)
|
||||
content = substring.substring(from: until.endIndex)
|
||||
return result + until
|
||||
}
|
||||
|
||||
content = substring
|
||||
return result
|
||||
}
|
||||
|
||||
index = content.index(after: index)
|
||||
range = range.lowerBound..<originalContent.index(after: range.upperBound)
|
||||
}
|
||||
|
||||
content = ""
|
||||
return ""
|
||||
}
|
||||
|
||||
func scan(until: [String]) -> (String, String)? {
|
||||
if until.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
var index = content.startIndex
|
||||
range = range.upperBound..<range.upperBound
|
||||
while index != content.endIndex {
|
||||
let substring = content.substring(from: index)
|
||||
for string in until {
|
||||
if substring.hasPrefix(string) {
|
||||
let result = content.substring(to: index)
|
||||
content = substring
|
||||
return (string, result)
|
||||
}
|
||||
}
|
||||
|
||||
index = content.index(after: index)
|
||||
range = range.lowerBound..<originalContent.index(after: range.upperBound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension String {
|
||||
func findFirstNot(character: Character) -> String.Index? {
|
||||
var index = startIndex
|
||||
|
||||
while index != endIndex {
|
||||
if character != self[index] {
|
||||
return index
|
||||
}
|
||||
index = self.index(after: index)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findLastNot(character: Character) -> String.Index? {
|
||||
var index = self.index(before: endIndex)
|
||||
|
||||
while index != startIndex {
|
||||
if character != self[index] {
|
||||
return self.index(after: index)
|
||||
}
|
||||
index = self.index(before: index)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func trim(character: Character) -> String {
|
||||
let first = findFirstNot(character: character) ?? startIndex
|
||||
let last = findLastNot(character: character) ?? endIndex
|
||||
return String(self[first..<last])
|
||||
}
|
||||
|
||||
public func rangeLine(_ range: Range<String.Index>) -> RangeLine {
|
||||
var lineNumber: UInt = 0
|
||||
var offset: Int = 0
|
||||
var lineContent = ""
|
||||
|
||||
for line in components(separatedBy: CharacterSet.newlines) {
|
||||
lineNumber += 1
|
||||
lineContent = line
|
||||
if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) {
|
||||
offset = distance(from: rangeOfLine.lowerBound, to: range.lowerBound)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (lineContent, lineNumber, offset)
|
||||
}
|
||||
}
|
||||
|
||||
public typealias RangeLine = (content: String, number: UInt, offset: Int)
|
||||
130
Sources/Loader.swift
Normal file
130
Sources/Loader.swift
Normal file
@@ -0,0 +1,130 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
|
||||
public protocol Loader {
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template
|
||||
func loadTemplate(names: [String], environment: Environment) throws -> Template
|
||||
}
|
||||
|
||||
|
||||
extension Loader {
|
||||
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||
for name in names {
|
||||
do {
|
||||
return try loadTemplate(name: name, environment: environment)
|
||||
} catch is TemplateDoesNotExist {
|
||||
continue
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: names, loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// A class for loading a template from disk
|
||||
public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
public let paths: [Path]
|
||||
|
||||
public init(paths: [Path]) {
|
||||
self.paths = paths
|
||||
}
|
||||
|
||||
public init(bundle: [Bundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "FileSystemLoader(\(paths))"
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
for path in paths {
|
||||
let templatePath = try path.safeJoin(path: Path(name))
|
||||
|
||||
if !templatePath.exists {
|
||||
continue
|
||||
}
|
||||
|
||||
let content: String = try templatePath.read()
|
||||
return environment.templateClass.init(templateString: content, environment: environment, name: name)
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: [name], loader: self)
|
||||
}
|
||||
|
||||
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||
for path in paths {
|
||||
for templateName in names {
|
||||
let templatePath = try path.safeJoin(path: Path(templateName))
|
||||
|
||||
if templatePath.exists {
|
||||
let content: String = try templatePath.read()
|
||||
return environment.templateClass.init(templateString: content, environment: environment, name: templateName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: names, loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class DictionaryLoader: Loader {
|
||||
public let templates: [String: String]
|
||||
|
||||
public init(templates: [String: String]) {
|
||||
self.templates = templates
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
if let content = templates[name] {
|
||||
return environment.templateClass.init(templateString: content, environment: environment, name: name)
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: [name], loader: self)
|
||||
}
|
||||
|
||||
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||
for name in names {
|
||||
if let content = templates[name] {
|
||||
return environment.templateClass.init(templateString: content, environment: environment, name: name)
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: names, loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Path {
|
||||
func safeJoin(path: Path) throws -> Path {
|
||||
let newPath = self + path
|
||||
|
||||
if !newPath.absolute().description.hasPrefix(absolute().description) {
|
||||
throw SuspiciousFileOperation(basePath: self, path: newPath)
|
||||
}
|
||||
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SuspiciousFileOperation: Error {
|
||||
let basePath: Path
|
||||
let path: Path
|
||||
|
||||
init(basePath: Path, path: Path) {
|
||||
self.basePath = basePath
|
||||
self.path = path
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "Path `\(path)` is located outside of base path `\(basePath)`"
|
||||
}
|
||||
}
|
||||
104
Sources/Node.swift
Normal file
104
Sources/Node.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import Foundation
|
||||
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
func render(_ context:Context) throws -> String
|
||||
|
||||
/// Reference to this node's token
|
||||
var token: Token? { get }
|
||||
}
|
||||
|
||||
|
||||
/// Render the collection of nodes in the given context
|
||||
public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String {
|
||||
return try nodes.map {
|
||||
do {
|
||||
return try $0.render(context)
|
||||
} catch {
|
||||
throw error.withToken($0.token)
|
||||
}
|
||||
}.joined(separator: "")
|
||||
}
|
||||
|
||||
public class SimpleNode : NodeType {
|
||||
public let handler:(Context) throws -> String
|
||||
public let token: Token?
|
||||
|
||||
public init(token: Token, handler: @escaping (Context) throws -> String) {
|
||||
self.token = token
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
return try handler(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class TextNode : NodeType {
|
||||
public let text:String
|
||||
public let token: Token?
|
||||
|
||||
public init(text:String) {
|
||||
self.text = text
|
||||
self.token = nil
|
||||
}
|
||||
|
||||
public func render(_ context:Context) throws -> String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public protocol Resolvable {
|
||||
func resolve(_ context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
|
||||
public class VariableNode : NodeType {
|
||||
public let variable: Resolvable
|
||||
public var token: Token?
|
||||
|
||||
public init(variable: Resolvable, token: Token? = nil) {
|
||||
self.variable = variable
|
||||
self.token = token
|
||||
}
|
||||
|
||||
public init(variable: String, token: Token? = nil) {
|
||||
self.variable = Variable(variable)
|
||||
self.token = token
|
||||
}
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
let result = try variable.resolve(context)
|
||||
return stringify(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func stringify(_ result: Any?) -> String {
|
||||
if let result = result as? String {
|
||||
return result
|
||||
} else if let array = result as? [Any?] {
|
||||
return unwrap(array).description
|
||||
} else if let result = result as? CustomStringConvertible {
|
||||
return result.description
|
||||
} else if let result = result as? NSObject {
|
||||
return result.description
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func unwrap(_ array: [Any?]) -> [Any] {
|
||||
return array.map { (item: Any?) -> Any in
|
||||
if let item = item {
|
||||
if let items = item as? [Any?] {
|
||||
return unwrap(items)
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
else { return item as Any }
|
||||
}
|
||||
}
|
||||
45
Sources/NowTag.swift
Normal file
45
Sources/NowTag.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
#if !os(Linux)
|
||||
import Foundation
|
||||
|
||||
|
||||
class NowNode : NodeType {
|
||||
let format:Variable
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
|
||||
let components = token.components()
|
||||
guard components.count <= 2 else {
|
||||
throw TemplateSyntaxError("'now' tags may only have one argument: the format string.")
|
||||
}
|
||||
if components.count == 2 {
|
||||
format = Variable(components[1])
|
||||
}
|
||||
|
||||
return NowNode(format:format, token: token)
|
||||
}
|
||||
|
||||
init(format:Variable?, token: Token? = nil) {
|
||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
let date = Date()
|
||||
let format = try self.format.resolve(context)
|
||||
var formatter:DateFormatter?
|
||||
|
||||
if let format = format as? DateFormatter {
|
||||
formatter = format
|
||||
} else if let format = format as? String {
|
||||
formatter = DateFormatter()
|
||||
formatter!.dateFormat = format
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatter!.string(from: date)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
196
Sources/Parser.swift
Normal file
196
Sources/Parser.swift
Normal file
@@ -0,0 +1,196 @@
|
||||
public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||
return { parser, token in
|
||||
if let name = token.components().first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A class for parsing an array of tokens and converts them into a collection of Node's
|
||||
public class TokenParser {
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
fileprivate var tokens: [Token]
|
||||
fileprivate let environment: Environment
|
||||
|
||||
public init(tokens: [Token], environment: Environment) {
|
||||
self.tokens = tokens
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
/// Parse the given tokens into nodes
|
||||
public func parse() throws -> [NodeType] {
|
||||
return try parse(nil)
|
||||
}
|
||||
|
||||
public func parse(_ parse_until:((_ parser:TokenParser, _ token:Token) -> (Bool))?) throws -> [NodeType] {
|
||||
var nodes = [NodeType]()
|
||||
|
||||
while tokens.count > 0 {
|
||||
let token = nextToken()!
|
||||
|
||||
switch token {
|
||||
case .text(let text, _):
|
||||
nodes.append(TextNode(text: text))
|
||||
case .variable:
|
||||
let filter = try compileResolvable(token.contents, containedIn: token)
|
||||
nodes.append(VariableNode(variable: filter, token: token))
|
||||
case .block:
|
||||
if let parse_until = parse_until , parse_until(self, token) {
|
||||
prependToken(token)
|
||||
return nodes
|
||||
}
|
||||
|
||||
if let tag = token.components().first {
|
||||
do {
|
||||
let parser = try findTag(name: tag)
|
||||
let node = try parser(self, token)
|
||||
nodes.append(node)
|
||||
} catch {
|
||||
throw error.withToken(token)
|
||||
}
|
||||
}
|
||||
case .comment:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
public func nextToken() -> Token? {
|
||||
if tokens.count > 0 {
|
||||
return tokens.remove(at: 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func prependToken(_ token:Token) {
|
||||
tokens.insert(token, at: 0)
|
||||
}
|
||||
|
||||
func findTag(name: String) throws -> Extension.TagParser {
|
||||
for ext in environment.extensions {
|
||||
if let filter = ext.tags[name] {
|
||||
return filter
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateSyntaxError("Unknown template tag '\(name)'")
|
||||
}
|
||||
|
||||
func findFilter(_ name: String) throws -> FilterType {
|
||||
for ext in environment.extensions {
|
||||
if let filter = ext.filters[name] {
|
||||
return filter
|
||||
}
|
||||
}
|
||||
|
||||
let suggestedFilters = self.suggestedFilters(for: name)
|
||||
if suggestedFilters.isEmpty {
|
||||
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
||||
} else {
|
||||
throw TemplateSyntaxError("Unknown filter '\(name)'. Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")).")
|
||||
}
|
||||
}
|
||||
|
||||
private func suggestedFilters(for name: String) -> [String] {
|
||||
let allFilters = environment.extensions.flatMap({ $0.filters.keys })
|
||||
|
||||
let filtersWithDistance = allFilters
|
||||
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
|
||||
// do not suggest filters which names are shorter than the distance
|
||||
.filter({ $0.filterName.characters.count > $0.distance })
|
||||
guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else {
|
||||
return []
|
||||
}
|
||||
// suggest all filters with the same distance
|
||||
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
||||
}
|
||||
|
||||
public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
do {
|
||||
return try FilterExpression(token: filterToken, parser: self)
|
||||
} catch {
|
||||
guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else {
|
||||
throw error
|
||||
}
|
||||
// find offset of filter in the containing token so that only filter is highligted, not the whole token
|
||||
if let filterTokenRange = containingToken.contents.range(of: filterToken) {
|
||||
var rangeLine = containingToken.sourceMap.line
|
||||
rangeLine.offset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound)
|
||||
syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine))
|
||||
} else {
|
||||
syntaxError.token = containingToken
|
||||
}
|
||||
throw syntaxError
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use compileFilter(_:containedIn:)")
|
||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||
return try FilterExpression(token: token, parser: self)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use compileResolvable(_:containedIn:)")
|
||||
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||
return try RangeVariable(token, parser: self)
|
||||
?? compileFilter(token)
|
||||
}
|
||||
|
||||
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
return try RangeVariable(token, parser: self, containedIn: containingToken)
|
||||
?? compileFilter(token, containedIn: containingToken)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||
extension String {
|
||||
|
||||
subscript(_ i: Int) -> Character {
|
||||
return self[self.index(self.startIndex, offsetBy: i)]
|
||||
}
|
||||
|
||||
func levenshteinDistance(_ target: String) -> Int {
|
||||
// create two work vectors of integer distances
|
||||
var last, current: [Int]
|
||||
|
||||
// initialize v0 (the previous row of distances)
|
||||
// this row is A[0][i]: edit distance for an empty s
|
||||
// the distance is just the number of characters to delete from t
|
||||
last = [Int](0...target.characters.count)
|
||||
current = [Int](repeating: 0, count: target.characters.count + 1)
|
||||
|
||||
for i in 0..<self.characters.count {
|
||||
// calculate v1 (current row distances) from the previous row v0
|
||||
|
||||
// first element of v1 is A[i+1][0]
|
||||
// edit distance is delete (i+1) chars from s to match empty t
|
||||
current[0] = i + 1
|
||||
|
||||
// use formula to fill in the rest of the row
|
||||
for j in 0..<target.characters.count {
|
||||
current[j+1] = Swift.min(
|
||||
last[j+1] + 1,
|
||||
current[j] + 1,
|
||||
last[j] + (self[i] == target[j] ? 0 : 1)
|
||||
)
|
||||
}
|
||||
|
||||
// copy v1 (current row) to v0 (previous row) for next iteration
|
||||
last = current
|
||||
}
|
||||
|
||||
return current[target.characters.count]
|
||||
}
|
||||
|
||||
}
|
||||
79
Sources/Template.swift
Normal file
79
Sources/Template.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
#if os(Linux)
|
||||
let NSFileNoSuchFileError = 4
|
||||
#endif
|
||||
|
||||
/// A class representing a template
|
||||
open class Template: ExpressibleByStringLiteral {
|
||||
let templateString: String
|
||||
internal(set) var environment: Environment
|
||||
let tokens: [Token]
|
||||
|
||||
/// The name of the loaded Template if the Template was loaded from a Loader
|
||||
public let name: String?
|
||||
|
||||
/// Create a template with a template string
|
||||
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
||||
self.environment = environment ?? Environment()
|
||||
self.name = name
|
||||
self.templateString = templateString
|
||||
|
||||
let lexer = Lexer(templateName: name, templateString: templateString)
|
||||
tokens = lexer.tokenize()
|
||||
}
|
||||
|
||||
/// Create a template with the given name inside the given bundle
|
||||
@available(*, deprecated, message: "Use Environment/FileSystemLoader instead")
|
||||
public convenience init(named:String, inBundle bundle:Bundle? = nil) throws {
|
||||
let useBundle = bundle ?? Bundle.main
|
||||
guard let url = useBundle.url(forResource: named, withExtension: nil) else {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
|
||||
}
|
||||
|
||||
try self.init(URL:url)
|
||||
}
|
||||
|
||||
/// Create a template with a file found at the given URL
|
||||
@available(*, deprecated, message: "Use Environment/FileSystemLoader instead")
|
||||
public convenience init(URL:Foundation.URL) throws {
|
||||
try self.init(path: Path(URL.path))
|
||||
}
|
||||
|
||||
/// Create a template with a file found at the given path
|
||||
@available(*, deprecated, message: "Use Environment/FileSystemLoader instead")
|
||||
public convenience init(path: Path, environment: Environment? = nil, name: String? = nil) throws {
|
||||
self.init(templateString: try path.read(), environment: environment, name: name)
|
||||
}
|
||||
|
||||
// MARK: ExpressibleByStringLiteral
|
||||
|
||||
// Create a templaVte with a template string literal
|
||||
public convenience required init(stringLiteral value: String) {
|
||||
self.init(templateString: value)
|
||||
}
|
||||
|
||||
// Create a template with a template string literal
|
||||
public convenience required init(extendedGraphemeClusterLiteral value: StringLiteralType) {
|
||||
self.init(stringLiteral: value)
|
||||
}
|
||||
|
||||
// Create a template with a template string literal
|
||||
public convenience required init(unicodeScalarLiteral value: StringLiteralType) {
|
||||
self.init(stringLiteral: value)
|
||||
}
|
||||
|
||||
/// Render the given template with a context
|
||||
func render(_ context: Context) throws -> String {
|
||||
let context = context
|
||||
let parser = TokenParser(tokens: tokens, environment: context.environment)
|
||||
let nodes = try parser.parse()
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
|
||||
/// Render the given template
|
||||
open func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||
return try render(Context(dictionary: dictionary, environment: environment))
|
||||
}
|
||||
}
|
||||
134
Sources/Tokenizer.swift
Normal file
134
Sources/Tokenizer.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
extension String {
|
||||
/// Split a string by a separator leaving quoted phrases together
|
||||
func smartSplit(separator: Character = " ") -> [String] {
|
||||
var word = ""
|
||||
var components: [String] = []
|
||||
var separate: Character = separator
|
||||
var singleQuoteCount = 0
|
||||
var doubleQuoteCount = 0
|
||||
|
||||
let specialCharacters = ",|:"
|
||||
func appendWord(_ word: String) {
|
||||
if components.count > 0 {
|
||||
if let precedingChar = components.last?.characters.last, specialCharacters.characters.contains(precedingChar) {
|
||||
components[components.count-1] += word
|
||||
} else if specialCharacters.contains(word) {
|
||||
components[components.count-1] += word
|
||||
} else {
|
||||
components.append(word)
|
||||
}
|
||||
} else {
|
||||
components.append(word)
|
||||
}
|
||||
}
|
||||
|
||||
for character in self.characters {
|
||||
if character == "'" { singleQuoteCount += 1 }
|
||||
else if character == "\"" { doubleQuoteCount += 1 }
|
||||
|
||||
if character == separate {
|
||||
|
||||
if separate != separator {
|
||||
word.append(separate)
|
||||
} else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty {
|
||||
appendWord(word)
|
||||
word = ""
|
||||
}
|
||||
|
||||
separate = separator
|
||||
} else {
|
||||
if separate == separator && (character == "'" || character == "\"") {
|
||||
separate = character
|
||||
}
|
||||
word.append(character)
|
||||
}
|
||||
}
|
||||
|
||||
if !word.isEmpty {
|
||||
appendWord(word)
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
}
|
||||
|
||||
public struct SourceMap: Equatable {
|
||||
public let filename: String?
|
||||
public let line: RangeLine
|
||||
|
||||
init(filename: String? = nil, line: RangeLine = ("", 0, 0)) {
|
||||
self.filename = filename
|
||||
self.line = line
|
||||
}
|
||||
|
||||
static let unknown = SourceMap()
|
||||
|
||||
public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool {
|
||||
return lhs.filename == rhs.filename && lhs.line == rhs.line
|
||||
}
|
||||
}
|
||||
|
||||
public enum Token : Equatable {
|
||||
/// A token representing a piece of text.
|
||||
case text(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a variable.
|
||||
case variable(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a comment.
|
||||
case comment(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a template block.
|
||||
case block(value: String, at: SourceMap)
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public func components() -> [String] {
|
||||
switch self {
|
||||
case .block(let value, _),
|
||||
.variable(let value, _),
|
||||
.text(let value, _),
|
||||
.comment(let value, _):
|
||||
return value.smartSplit()
|
||||
}
|
||||
}
|
||||
|
||||
public var contents: String {
|
||||
switch self {
|
||||
case .block(let value, _),
|
||||
.variable(let value, _),
|
||||
.text(let value, _),
|
||||
.comment(let value, _):
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public var sourceMap: SourceMap {
|
||||
switch self {
|
||||
case .block(_, let sourceMap),
|
||||
.variable(_, let sourceMap),
|
||||
.text(_, let sourceMap),
|
||||
.comment(_, let sourceMap):
|
||||
return sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public func == (lhs: Token, rhs: Token) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.text(lhsValue, lhsAt), .text(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
case let (.variable(lhsValue, lhsAt), .variable(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
case let (.block(lhsValue, lhsAt), .block(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
case let (.comment(lhsValue, lhsAt), .comment(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
266
Sources/Variable.swift
Normal file
266
Sources/Variable.swift
Normal file
@@ -0,0 +1,266 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
typealias Number = Float
|
||||
|
||||
|
||||
class FilterExpression : Resolvable {
|
||||
let filters: [(FilterType, [Variable])]
|
||||
let variable: Variable
|
||||
|
||||
init(token: String, parser: TokenParser) throws {
|
||||
let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") })
|
||||
if bits.isEmpty {
|
||||
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
||||
}
|
||||
|
||||
variable = Variable(bits[0])
|
||||
let filterBits = bits[bits.indices.suffix(from: 1)]
|
||||
|
||||
do {
|
||||
filters = try filterBits.map {
|
||||
let (name, arguments) = parseFilterComponents(token: $0)
|
||||
let filter = try parser.findFilter(name)
|
||||
return (filter, arguments)
|
||||
}
|
||||
} catch {
|
||||
filters = []
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func resolve(_ context: Context) throws -> Any? {
|
||||
let result = try variable.resolve(context)
|
||||
|
||||
return try filters.reduce(result) { x, y in
|
||||
let arguments = try y.1.map { try $0.resolve(context) }
|
||||
return try y.0.invoke(value: x, arguments: arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure used to represent a template variable, and to resolve it in a given context.
|
||||
public struct Variable : Equatable, Resolvable {
|
||||
public let variable: String
|
||||
|
||||
/// Create a variable with a string representing the variable
|
||||
public init(_ variable: String) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
// Split the lookup string and resolve references if possible
|
||||
fileprivate func lookup(_ context: Context) throws -> [String] {
|
||||
let keyPath = KeyPath(variable, in: context)
|
||||
return try keyPath.parse()
|
||||
}
|
||||
|
||||
/// Resolve the variable in the given context
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
var current: Any? = context
|
||||
|
||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||
// String literal
|
||||
return String(variable[variable.characters.index(after: variable.startIndex) ..< variable.characters.index(before: variable.endIndex)])
|
||||
}
|
||||
|
||||
// Number literal
|
||||
if let int = Int(variable) {
|
||||
return int
|
||||
}
|
||||
if let number = Number(variable) {
|
||||
return number
|
||||
}
|
||||
// Boolean literal
|
||||
if let bool = Bool(variable) {
|
||||
return bool
|
||||
}
|
||||
|
||||
for bit in try lookup(context) {
|
||||
current = normalize(current)
|
||||
|
||||
if let context = current as? Context {
|
||||
current = context[bit]
|
||||
} else if let dictionary = current as? [String: Any] {
|
||||
if bit == "count" {
|
||||
current = dictionary.count
|
||||
} else {
|
||||
current = dictionary[bit]
|
||||
}
|
||||
} else if let array = current as? [Any] {
|
||||
if let index = Int(bit) {
|
||||
if index >= 0 && index < array.count {
|
||||
current = array[index]
|
||||
} else {
|
||||
current = nil
|
||||
}
|
||||
} else if bit == "first" {
|
||||
current = array.first
|
||||
} else if bit == "last" {
|
||||
current = array.last
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
} else if let object = current as? NSObject { // NSKeyValueCoding
|
||||
#if os(Linux)
|
||||
return nil
|
||||
#else
|
||||
current = object.value(forKey: bit)
|
||||
#endif
|
||||
} else if let value = current {
|
||||
current = Mirror(reflecting: value).getValue(for: bit)
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if let resolvable = current as? Resolvable {
|
||||
current = try resolvable.resolve(context)
|
||||
} else if let node = current as? NodeType {
|
||||
current = try node.render(context)
|
||||
}
|
||||
|
||||
return normalize(current)
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs: Variable, rhs: Variable) -> Bool {
|
||||
return lhs.variable == rhs.variable
|
||||
}
|
||||
|
||||
/// A structure used to represet range of two integer values expressed as `from...to`.
|
||||
/// Values should be numbers (they will be converted to integers).
|
||||
/// Rendering this variable produces array from range `from...to`.
|
||||
/// If `from` is more than `to` array will contain values of reversed range.
|
||||
public struct RangeVariable: Resolvable {
|
||||
public let from: Resolvable
|
||||
public let to: Resolvable
|
||||
|
||||
@available(*, deprecated, message: "Use init?(_:parser:containedIn:)")
|
||||
public init?(_ token: String, parser: TokenParser) throws {
|
||||
let components = token.components(separatedBy: "...")
|
||||
guard components.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.from = try parser.compileFilter(components[0])
|
||||
self.to = try parser.compileFilter(components[1])
|
||||
}
|
||||
|
||||
public init?(_ token: String, parser: TokenParser, containedIn containingToken: Token) throws {
|
||||
let components = token.components(separatedBy: "...")
|
||||
guard components.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.from = try parser.compileFilter(components[0], containedIn: containingToken)
|
||||
self.to = try parser.compileFilter(components[1], containedIn: containingToken)
|
||||
}
|
||||
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
let fromResolved = try from.resolve(context)
|
||||
let toResolved = try to.resolve(context)
|
||||
|
||||
guard let from = fromResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'from' value is not an Integer (\(fromResolved ?? "nil"))")
|
||||
}
|
||||
|
||||
guard let to = toResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'to' value is not an Integer (\(toResolved ?? "nil") )")
|
||||
}
|
||||
|
||||
let range = min(from, to)...max(from, to)
|
||||
return from > to ? Array(range.reversed()) : Array(range)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func normalize(_ current: Any?) -> Any? {
|
||||
if let current = current as? Normalizable {
|
||||
return current.normalize()
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
protocol Normalizable {
|
||||
func normalize() -> Any?
|
||||
}
|
||||
|
||||
extension Array : Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
extension NSArray : Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary : Normalizable {
|
||||
func normalize() -> Any? {
|
||||
var dictionary: [String: Any] = [:]
|
||||
|
||||
for (key, value) in self {
|
||||
if let key = key as? String {
|
||||
dictionary[key] = Stencil.normalize(value)
|
||||
} else if let key = key as? CustomStringConvertible {
|
||||
dictionary[key.description] = Stencil.normalize(value)
|
||||
}
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
}
|
||||
|
||||
func parseFilterComponents(token: String) -> (String, [Variable]) {
|
||||
var components = token.smartSplit(separator: ":")
|
||||
let name = components.removeFirst().trim(character: " ")
|
||||
let variables = components
|
||||
.joined(separator: ":")
|
||||
.smartSplit(separator: ",")
|
||||
.map { Variable($0.trim(character: " ")) }
|
||||
return (name, variables)
|
||||
}
|
||||
|
||||
extension Mirror {
|
||||
func getValue(for key: String) -> Any? {
|
||||
let result = descendant(key) ?? Int(key).flatMap({ descendant($0) })
|
||||
if result == nil {
|
||||
// go through inheritance chain to reach superclass properties
|
||||
return superclassMirror?.getValue(for: key)
|
||||
} else if let result = result {
|
||||
guard String(describing: result) != "nil" else {
|
||||
// mirror returns non-nil value even for nil-containing properties
|
||||
// so we have to check if its value is actually nil or not
|
||||
return nil
|
||||
}
|
||||
if let result = (result as? AnyOptional)?.wrapped {
|
||||
return result
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
protocol AnyOptional {
|
||||
var wrapped: Any? { get }
|
||||
}
|
||||
|
||||
extension Optional: AnyOptional {
|
||||
var wrapped: Any? {
|
||||
switch self {
|
||||
case let .some(value): return value
|
||||
case .none: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = 'Stencil'
|
||||
spec.version = '0.1.0'
|
||||
spec.summary = 'Stencil is a simple and powerful template language for Swift.'
|
||||
spec.homepage = 'https://github.com/kylef/Stencil'
|
||||
spec.license = { :type => 'BSD', :file => 'LICENSE' }
|
||||
spec.author = { 'Kyle Fuller' => 'inbox@kylefuller.co.uk' }
|
||||
spec.social_media_url = 'http://twitter.com/kylefuller'
|
||||
spec.source = { :git => 'https://github.com/kylef/Stencil.git', :tag => "#{spec.version}" }
|
||||
spec.source_files = 'Stencil/*.{h,swift}'
|
||||
spec.requires_arc = true
|
||||
end
|
||||
|
||||
32
Stencil.podspec.json
Normal file
32
Stencil.podspec.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Stencil",
|
||||
"version": "0.12.1",
|
||||
"summary": "Stencil is a simple and powerful template language for Swift.",
|
||||
"homepage": "https://stencil.fuller.li",
|
||||
"license": {
|
||||
"type": "BSD",
|
||||
"file": "LICENSE"
|
||||
},
|
||||
"authors": {
|
||||
"Kyle Fuller": "kyle@fuller.li"
|
||||
},
|
||||
"social_media_url": "https://twitter.com/kylefuller",
|
||||
"source": {
|
||||
"git": "https://github.com/stencilproject/Stencil.git",
|
||||
"tag": "0.12.1"
|
||||
},
|
||||
"source_files": [
|
||||
"Sources/*.swift"
|
||||
],
|
||||
"platforms": {
|
||||
"ios": "8.0",
|
||||
"osx": "10.9",
|
||||
"tvos": "9.0"
|
||||
},
|
||||
"requires_arc": true,
|
||||
"dependencies": {
|
||||
"PathKit": [
|
||||
"~> 0.9.0"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71CE4C0919FD29D000B9E0C5 /* Result.swift */; };
|
||||
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CA19F92B4F002CF74B /* VariableTests.swift */; };
|
||||
7725B3CD19F92B61002CF74B /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CC19F92B61002CF74B /* Variable.swift */; };
|
||||
7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3CE19F94214002CF74B /* Tokenizer.swift */; };
|
||||
7725B3D319F9437F002CF74B /* NodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D219F9437F002CF74B /* NodeTests.swift */; };
|
||||
7725B3D519F9438F002CF74B /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D419F9438F002CF74B /* Node.swift */; };
|
||||
7725B3D719F94A43002CF74B /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D619F94A43002CF74B /* Parser.swift */; };
|
||||
7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725B3D819F94A61002CF74B /* ParserTests.swift */; };
|
||||
77EB082519F96E88001870F1 /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082419F96E88001870F1 /* Template.swift */; };
|
||||
77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082619F96E9C001870F1 /* TemplateTests.swift */; };
|
||||
77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082819FA85F2001870F1 /* LexerTests.swift */; };
|
||||
77EB082B19FA8600001870F1 /* Lexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EB082A19FA8600001870F1 /* Lexer.swift */; };
|
||||
77FAAE5819F91E480029DC5E /* Stencil.h in Headers */ = {isa = PBXBuildFile; fileRef = 77FAAE5719F91E480029DC5E /* Stencil.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
77FAAE5E19F91E480029DC5E /* Stencil.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77FAAE5219F91E480029DC5E /* Stencil.framework */; };
|
||||
77FAAE6519F91E480029DC5E /* StencilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAAE6419F91E480029DC5E /* StencilTests.swift */; };
|
||||
77FAAE6F19F920750029DC5E /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAAE6E19F920750029DC5E /* Context.swift */; };
|
||||
77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77FAAE7019F9208C0029DC5E /* ContextTests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
77FAAE5F19F91E480029DC5E /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 77FAAE4919F91E480029DC5E /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 77FAAE5119F91E480029DC5E;
|
||||
remoteInfo = Stencil;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
71CE4C0919FD29D000B9E0C5 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = "<group>"; };
|
||||
7725B3CA19F92B4F002CF74B /* VariableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariableTests.swift; sourceTree = "<group>"; };
|
||||
7725B3CC19F92B61002CF74B /* Variable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = "<group>"; };
|
||||
7725B3CE19F94214002CF74B /* Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokenizer.swift; sourceTree = "<group>"; };
|
||||
7725B3D219F9437F002CF74B /* NodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeTests.swift; sourceTree = "<group>"; };
|
||||
7725B3D419F9438F002CF74B /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = "<group>"; };
|
||||
7725B3D619F94A43002CF74B /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
|
||||
7725B3D819F94A61002CF74B /* ParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParserTests.swift; sourceTree = "<group>"; };
|
||||
77EB082419F96E88001870F1 /* Template.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Template.swift; sourceTree = "<group>"; };
|
||||
77EB082619F96E9C001870F1 /* TemplateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateTests.swift; sourceTree = "<group>"; };
|
||||
77EB082819FA85F2001870F1 /* LexerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LexerTests.swift; sourceTree = "<group>"; };
|
||||
77EB082A19FA8600001870F1 /* Lexer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lexer.swift; sourceTree = "<group>"; };
|
||||
77FAAE5219F91E480029DC5E /* Stencil.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Stencil.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
77FAAE5619F91E480029DC5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
77FAAE5719F91E480029DC5E /* Stencil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Stencil.h; sourceTree = "<group>"; };
|
||||
77FAAE5D19F91E480029DC5E /* StencilTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StencilTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
77FAAE6319F91E480029DC5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
77FAAE6419F91E480029DC5E /* StencilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StencilTests.swift; sourceTree = "<group>"; };
|
||||
77FAAE6E19F920750029DC5E /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = "<group>"; };
|
||||
77FAAE7019F9208C0029DC5E /* ContextTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
77FAAE4E19F91E480029DC5E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
77FAAE5A19F91E480029DC5E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE5E19F91E480029DC5E /* Stencil.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
77FAAE4819F91E480029DC5E = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE5419F91E480029DC5E /* Stencil */,
|
||||
77FAAE6119F91E480029DC5E /* StencilTests */,
|
||||
77FAAE5319F91E480029DC5E /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE5319F91E480029DC5E /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE5219F91E480029DC5E /* Stencil.framework */,
|
||||
77FAAE5D19F91E480029DC5E /* StencilTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE5419F91E480029DC5E /* Stencil */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE5719F91E480029DC5E /* Stencil.h */,
|
||||
77FAAE6E19F920750029DC5E /* Context.swift */,
|
||||
77EB082A19FA8600001870F1 /* Lexer.swift */,
|
||||
7725B3D419F9438F002CF74B /* Node.swift */,
|
||||
7725B3D619F94A43002CF74B /* Parser.swift */,
|
||||
71CE4C0919FD29D000B9E0C5 /* Result.swift */,
|
||||
77EB082419F96E88001870F1 /* Template.swift */,
|
||||
7725B3CE19F94214002CF74B /* Tokenizer.swift */,
|
||||
7725B3CC19F92B61002CF74B /* Variable.swift */,
|
||||
77FAAE5519F91E480029DC5E /* Supporting Files */,
|
||||
);
|
||||
path = Stencil;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE5519F91E480029DC5E /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE5619F91E480029DC5E /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE6119F91E480029DC5E /* StencilTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE6419F91E480029DC5E /* StencilTests.swift */,
|
||||
77FAAE7019F9208C0029DC5E /* ContextTests.swift */,
|
||||
7725B3CA19F92B4F002CF74B /* VariableTests.swift */,
|
||||
7725B3D219F9437F002CF74B /* NodeTests.swift */,
|
||||
7725B3D819F94A61002CF74B /* ParserTests.swift */,
|
||||
77EB082819FA85F2001870F1 /* LexerTests.swift */,
|
||||
77EB082619F96E9C001870F1 /* TemplateTests.swift */,
|
||||
77FAAE6219F91E480029DC5E /* Supporting Files */,
|
||||
);
|
||||
path = StencilTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE6219F91E480029DC5E /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE6319F91E480029DC5E /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
77FAAE4F19F91E480029DC5E /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE5819F91E480029DC5E /* Stencil.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXHeadersBuildPhase section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
77FAAE5119F91E480029DC5E /* Stencil */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 77FAAE6819F91E480029DC5E /* Build configuration list for PBXNativeTarget "Stencil" */;
|
||||
buildPhases = (
|
||||
77FAAE4D19F91E480029DC5E /* Sources */,
|
||||
77FAAE4E19F91E480029DC5E /* Frameworks */,
|
||||
77FAAE4F19F91E480029DC5E /* Headers */,
|
||||
77FAAE5019F91E480029DC5E /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Stencil;
|
||||
productName = Stencil;
|
||||
productReference = 77FAAE5219F91E480029DC5E /* Stencil.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
77FAAE5C19F91E480029DC5E /* StencilTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 77FAAE6B19F91E480029DC5E /* Build configuration list for PBXNativeTarget "StencilTests" */;
|
||||
buildPhases = (
|
||||
77FAAE5919F91E480029DC5E /* Sources */,
|
||||
77FAAE5A19F91E480029DC5E /* Frameworks */,
|
||||
77FAAE5B19F91E480029DC5E /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
77FAAE6019F91E480029DC5E /* PBXTargetDependency */,
|
||||
);
|
||||
name = StencilTests;
|
||||
productName = StencilTests;
|
||||
productReference = 77FAAE5D19F91E480029DC5E /* StencilTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
77FAAE4919F91E480029DC5E /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0600;
|
||||
ORGANIZATIONNAME = Cocode;
|
||||
TargetAttributes = {
|
||||
77FAAE5119F91E480029DC5E = {
|
||||
CreatedOnToolsVersion = 6.1;
|
||||
};
|
||||
77FAAE5C19F91E480029DC5E = {
|
||||
CreatedOnToolsVersion = 6.1;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 77FAAE4C19F91E480029DC5E /* Build configuration list for PBXProject "Stencil" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
);
|
||||
mainGroup = 77FAAE4819F91E480029DC5E;
|
||||
productRefGroup = 77FAAE5319F91E480029DC5E /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
77FAAE5119F91E480029DC5E /* Stencil */,
|
||||
77FAAE5C19F91E480029DC5E /* StencilTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
77FAAE5019F91E480029DC5E /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
77FAAE5B19F91E480029DC5E /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
77FAAE4D19F91E480029DC5E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE6F19F920750029DC5E /* Context.swift in Sources */,
|
||||
77EB082B19FA8600001870F1 /* Lexer.swift in Sources */,
|
||||
7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */,
|
||||
7725B3D719F94A43002CF74B /* Parser.swift in Sources */,
|
||||
77EB082519F96E88001870F1 /* Template.swift in Sources */,
|
||||
7725B3CD19F92B61002CF74B /* Variable.swift in Sources */,
|
||||
71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */,
|
||||
7725B3D519F9438F002CF74B /* Node.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
77FAAE5919F91E480029DC5E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE6519F91E480029DC5E /* StencilTests.swift in Sources */,
|
||||
7725B3D319F9437F002CF74B /* NodeTests.swift in Sources */,
|
||||
7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */,
|
||||
77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */,
|
||||
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */,
|
||||
77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */,
|
||||
77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
77FAAE6019F91E480029DC5E /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 77FAAE5119F91E480029DC5E /* Stencil */;
|
||||
targetProxy = 77FAAE5F19F91E480029DC5E /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
77FAAE6619F91E480029DC5E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
77FAAE6719F91E480029DC5E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.10;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
77FAAE6919F91E480029DC5E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
FRAMEWORK_VERSION = A;
|
||||
INFOPLIST_FILE = Stencil/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
77FAAE6A19F91E480029DC5E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
FRAMEWORK_VERSION = A;
|
||||
INFOPLIST_FILE = Stencil/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
77FAAE6C19F91E480029DC5E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(DEVELOPER_FRAMEWORKS_DIR)",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = StencilTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
77FAAE6D19F91E480029DC5E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(DEVELOPER_FRAMEWORKS_DIR)",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = StencilTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
77FAAE4C19F91E480029DC5E /* Build configuration list for PBXProject "Stencil" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
77FAAE6619F91E480029DC5E /* Debug */,
|
||||
77FAAE6719F91E480029DC5E /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
77FAAE6819F91E480029DC5E /* Build configuration list for PBXNativeTarget "Stencil" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
77FAAE6919F91E480029DC5E /* Debug */,
|
||||
77FAAE6A19F91E480029DC5E /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
77FAAE6B19F91E480029DC5E /* Build configuration list for PBXNativeTarget "StencilTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
77FAAE6C19F91E480029DC5E /* Debug */,
|
||||
77FAAE6D19F91E480029DC5E /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 77FAAE4919F91E480029DC5E /* Project object */;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:Stencil.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,50 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// A container for template variables.
|
||||
public class Context : Equatable {
|
||||
var dictionaries:[Dictionary<String, AnyObject>]
|
||||
|
||||
public init(dictionary:Dictionary<String, AnyObject>) {
|
||||
dictionaries = [dictionary]
|
||||
}
|
||||
|
||||
public init() {
|
||||
dictionaries = []
|
||||
}
|
||||
|
||||
public subscript(key: String) -> AnyObject? {
|
||||
get {
|
||||
for dictionary in reverse(dictionaries) {
|
||||
if let value:AnyObject = dictionary[key] {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
set(value) {
|
||||
if dictionaries.count > 0 {
|
||||
var dictionary = dictionaries.removeLast()
|
||||
dictionary[key] = value
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func push() {
|
||||
push(Dictionary<String, String>())
|
||||
}
|
||||
|
||||
public func push(dictionary:Dictionary<String, String>) {
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
|
||||
public func pop() {
|
||||
dictionaries.removeLast()
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:Context, rhs:Context) -> Bool {
|
||||
return lhs.dictionaries == rhs.dictionaries
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.cocode.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2014 Cocode. All rights reserved.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,56 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct Lexer {
|
||||
public let templateString:String
|
||||
let regex = NSRegularExpression(pattern: "(\\{\\{.*?\\}\\}|\\{%.*?%\\}|\\{#.*?#\\})", options: nil, error: nil)!
|
||||
|
||||
public init(templateString:String) {
|
||||
self.templateString = templateString
|
||||
}
|
||||
|
||||
func createToken(string:String) -> Token {
|
||||
func strip() -> String {
|
||||
return string[string.startIndex.successor().successor()..<string.endIndex.predecessor().predecessor()].stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return Token.Variable(value: strip())
|
||||
} else if string.hasPrefix("{%") {
|
||||
return Token.Block(value: strip())
|
||||
} else if string.hasPrefix("{#") {
|
||||
return Token.Comment(value: strip())
|
||||
}
|
||||
|
||||
return Token.Text(value: string)
|
||||
}
|
||||
|
||||
public func tokenize() -> [Token] {
|
||||
// Unfortunately NSRegularExpression doesn't have a split.
|
||||
// So here's a really terrible implementation
|
||||
|
||||
var tokens = [Token]()
|
||||
|
||||
let range = NSMakeRange(0, countElements(templateString))
|
||||
var lastIndex = 0
|
||||
let nsTemplateString = templateString as NSString
|
||||
let options = NSMatchingOptions(0)
|
||||
regex.enumerateMatchesInString(templateString, options: options, range: range) { (result, flags, b) in
|
||||
if result.range.location != lastIndex {
|
||||
let previousMatch = nsTemplateString.substringWithRange(NSMakeRange(lastIndex, result.range.location - lastIndex))
|
||||
tokens.append(self.createToken(previousMatch))
|
||||
}
|
||||
|
||||
let match = nsTemplateString.substringWithRange(result.range)
|
||||
tokens.append(self.createToken(match))
|
||||
|
||||
lastIndex = result.range.location + result.range.length
|
||||
}
|
||||
|
||||
if lastIndex < countElements(templateString) {
|
||||
let substring = (templateString as NSString).substringFromIndex(lastIndex)
|
||||
tokens.append(Token.Text(value: substring))
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct NodeError : Error {
|
||||
let token:Token
|
||||
let message:String
|
||||
|
||||
init(token:Token, message:String) {
|
||||
self.token = token
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var description:String {
|
||||
return "\(token.components().first!): \(message)"
|
||||
}
|
||||
}
|
||||
|
||||
public protocol Node {
|
||||
func render(context:Context) -> Result
|
||||
}
|
||||
|
||||
extension Array {
|
||||
func map<U>(block:((Element) -> (U?, Error?))) -> ([U]?, Error?) {
|
||||
var results = [U]()
|
||||
|
||||
for item in self {
|
||||
let (result, error) = block(item)
|
||||
|
||||
if let error = error {
|
||||
return (nil, error)
|
||||
} else if (result != nil) {
|
||||
// let result = result exposing a bug in the Swift compier :(
|
||||
results.append(result!)
|
||||
}
|
||||
}
|
||||
|
||||
return (results, nil)
|
||||
}
|
||||
}
|
||||
|
||||
public func renderNodes(nodes:[Node], context:Context) -> Result {
|
||||
var result = ""
|
||||
|
||||
for item in nodes {
|
||||
switch item.render(context) {
|
||||
case .Success(let string):
|
||||
result += string
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return .Success(result)
|
||||
}
|
||||
|
||||
public class SimpleNode : Node {
|
||||
let handler:(Context) -> (Result)
|
||||
|
||||
public init(handler:((Context) -> (Result))) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
public func render(context:Context) -> Result {
|
||||
return handler(context)
|
||||
}
|
||||
}
|
||||
|
||||
public class TextNode : Node {
|
||||
public let text:String
|
||||
|
||||
public init(text:String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
public func render(context:Context) -> Result {
|
||||
return .Success(self.text)
|
||||
}
|
||||
}
|
||||
|
||||
public class VariableNode : Node {
|
||||
public let variable:Variable
|
||||
|
||||
public init(variable:Variable) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
public init(variable:String) {
|
||||
self.variable = Variable(variable)
|
||||
}
|
||||
|
||||
public func render(context:Context) -> Result {
|
||||
let result:AnyObject? = variable.resolve(context)
|
||||
|
||||
if let result = result as? String {
|
||||
return .Success(result)
|
||||
} else if let result = result as? NSObject {
|
||||
return .Success(result.description)
|
||||
}
|
||||
|
||||
return .Success("")
|
||||
}
|
||||
}
|
||||
|
||||
public class NowNode : Node {
|
||||
public let format:Variable
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
var format:Variable?
|
||||
|
||||
let components = token.components()
|
||||
if components.count == 2 {
|
||||
format = Variable(components[1])
|
||||
}
|
||||
|
||||
return .Success(node:NowNode(format:format))
|
||||
}
|
||||
|
||||
public init(format:Variable?) {
|
||||
if let format = format {
|
||||
self.format = format
|
||||
} else {
|
||||
self.format = Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
}
|
||||
}
|
||||
|
||||
public func render(context: Context) -> Result {
|
||||
let date = NSDate()
|
||||
let format: AnyObject? = self.format.resolve(context)
|
||||
var formatter:NSDateFormatter?
|
||||
|
||||
if let format = format as? NSDateFormatter {
|
||||
formatter = format
|
||||
} else if let format = format as? String {
|
||||
formatter = NSDateFormatter()
|
||||
formatter!.dateFormat = format
|
||||
} else {
|
||||
return .Success("")
|
||||
}
|
||||
|
||||
return .Success(formatter!.stringFromDate(date))
|
||||
}
|
||||
}
|
||||
|
||||
public class ForNode : Node {
|
||||
let variable:Variable
|
||||
let loopVariable:String
|
||||
let nodes:[Node]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
let components = token.components()
|
||||
let count = countElements(components)
|
||||
|
||||
if count == 4 && components[2] == "in" {
|
||||
let loopVariable = components[1]
|
||||
let variable = components[3]
|
||||
|
||||
var forNodes:[Node]!
|
||||
var emptyNodes = [Node]()
|
||||
|
||||
switch parser.parse(until(["endfor", "empty"])) {
|
||||
case .Success(let nodes):
|
||||
forNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
|
||||
if let token = parser.nextToken() {
|
||||
if token.contents == "empty" {
|
||||
switch parser.parse(until(["endfor"])) {
|
||||
case .Success(let nodes):
|
||||
emptyNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
|
||||
parser.nextToken()
|
||||
}
|
||||
} else {
|
||||
return .Error(error: NodeError(token: token, message: "`endfor` was not found."))
|
||||
}
|
||||
|
||||
return .Success(node:ForNode(variable: variable, loopVariable: loopVariable, nodes: forNodes, emptyNodes:emptyNodes))
|
||||
}
|
||||
|
||||
return .Error(error: NodeError(token: token, message: "Invalid syntax. Expected `for x in y`."))
|
||||
}
|
||||
|
||||
public init(variable:String, loopVariable:String, nodes:[Node], emptyNodes:[Node]) {
|
||||
self.variable = Variable(variable)
|
||||
self.loopVariable = loopVariable
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
public func render(context: Context) -> Result {
|
||||
let values = variable.resolve(context) as? [AnyObject]
|
||||
var output = ""
|
||||
|
||||
if let values = values {
|
||||
for item in values {
|
||||
context.push()
|
||||
context[loopVariable] = item
|
||||
let result = renderNodes(nodes, context)
|
||||
context.pop()
|
||||
|
||||
switch result {
|
||||
case .Success(let string):
|
||||
output += string
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .Success(output)
|
||||
}
|
||||
}
|
||||
|
||||
public class IfNode : Node {
|
||||
public let variable:Variable
|
||||
public let trueNodes:[Node]
|
||||
public let falseNodes:[Node]
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
let variable = token.components()[1]
|
||||
var trueNodes = [Node]()
|
||||
var falseNodes = [Node]()
|
||||
|
||||
switch parser.parse(until(["endif", "else"])) {
|
||||
case .Success(let nodes):
|
||||
trueNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
|
||||
if let token = parser.nextToken() {
|
||||
if token.contents == "else" {
|
||||
switch parser.parse(until(["endif"])) {
|
||||
case .Success(let nodes):
|
||||
falseNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
parser.nextToken()
|
||||
}
|
||||
} else {
|
||||
return .Error(error:NodeError(token: token, message: "`endif` was not found."))
|
||||
}
|
||||
|
||||
return .Success(node:IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes))
|
||||
}
|
||||
|
||||
public class func parse_ifnot(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
let variable = token.components()[1]
|
||||
var trueNodes = [Node]()
|
||||
var falseNodes = [Node]()
|
||||
|
||||
switch parser.parse(until(["endif", "else"])) {
|
||||
case .Success(let nodes):
|
||||
falseNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
|
||||
if let token = parser.nextToken() {
|
||||
if token.contents == "else" {
|
||||
switch parser.parse(until(["endif"])) {
|
||||
case .Success(let nodes):
|
||||
trueNodes = nodes
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
parser.nextToken()
|
||||
}
|
||||
} else {
|
||||
return .Error(error:NodeError(token: token, message: "`endif` was not found."))
|
||||
}
|
||||
|
||||
return .Success(node:IfNode(variable: variable, trueNodes: trueNodes, falseNodes: falseNodes))
|
||||
}
|
||||
|
||||
public init(variable:String, trueNodes:[Node], falseNodes:[Node]) {
|
||||
self.variable = Variable(variable)
|
||||
self.trueNodes = trueNodes
|
||||
self.falseNodes = falseNodes
|
||||
}
|
||||
|
||||
public func render(context: Context) -> Result {
|
||||
let result: AnyObject? = variable.resolve(context)
|
||||
var truthy = false
|
||||
|
||||
if let result = result as? [AnyObject] {
|
||||
if result.count > 0 {
|
||||
truthy = true
|
||||
}
|
||||
} else if let result: AnyObject = result {
|
||||
truthy = true
|
||||
}
|
||||
|
||||
context.push()
|
||||
let output = renderNodes(truthy ? trueNodes : falseNodes, context)
|
||||
context.pop()
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public func until(tags:[String])(parser:TokenParser, token:Token) -> Bool {
|
||||
if let name = token.components().first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public class TokenParser {
|
||||
public typealias TagParser = (TokenParser, Token) -> Result
|
||||
public typealias NodeList = [Node]
|
||||
|
||||
public enum Result {
|
||||
case Success(node: Node)
|
||||
case Error(error: Stencil.Error)
|
||||
}
|
||||
|
||||
public enum Results {
|
||||
case Success(nodes: NodeList)
|
||||
case Error(error: Stencil.Error)
|
||||
}
|
||||
|
||||
private var tokens:[Token]
|
||||
private var tags = Dictionary<String, TagParser>()
|
||||
|
||||
public init(tokens:[Token]) {
|
||||
self.tokens = tokens
|
||||
registerTag("for", ForNode.parse)
|
||||
registerTag("if", IfNode.parse)
|
||||
registerTag("ifnot", IfNode.parse_ifnot)
|
||||
registerTag("now", NowNode.parse)
|
||||
}
|
||||
|
||||
public func registerTag(name:String, parser:TagParser) {
|
||||
tags[name] = parser
|
||||
}
|
||||
|
||||
public func registerSimpleTag(name:String, handler:((Context) -> (Stencil.Result))) {
|
||||
registerTag(name, parser: { (parser, token) -> TokenParser.Result in
|
||||
return .Success(node:SimpleNode(handler: handler))
|
||||
})
|
||||
}
|
||||
|
||||
public func parse() -> Results {
|
||||
return parse(nil)
|
||||
}
|
||||
|
||||
public func parse(parse_until:((parser:TokenParser, token:Token) -> (Bool))?) -> TokenParser.Results {
|
||||
var nodes = NodeList()
|
||||
|
||||
while tokens.count > 0 {
|
||||
let token = nextToken()!
|
||||
|
||||
switch token {
|
||||
case .Text(let text):
|
||||
nodes.append(TextNode(text: text))
|
||||
case .Variable(let variable):
|
||||
nodes.append(VariableNode(variable: variable))
|
||||
case .Block(let value):
|
||||
let tag = token.components().first
|
||||
|
||||
if let parse_until = parse_until {
|
||||
if parse_until(parser: self, token: token) {
|
||||
prependToken(token)
|
||||
return .Success(nodes:nodes)
|
||||
}
|
||||
}
|
||||
|
||||
if let tag = tag {
|
||||
if let parser = self.tags[tag] {
|
||||
switch parser(self, token) {
|
||||
case .Success(let node):
|
||||
nodes.append(node)
|
||||
case .Error(let error):
|
||||
return .Error(error:error)
|
||||
}
|
||||
}
|
||||
}
|
||||
case .Comment(let value):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return .Success(nodes:nodes)
|
||||
}
|
||||
|
||||
public func nextToken() -> Token? {
|
||||
if tokens.count > 0 {
|
||||
return tokens.removeAtIndex(0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func prependToken(token:Token) {
|
||||
tokens.insert(token, atIndex: 0)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public protocol Error : Printable {
|
||||
|
||||
}
|
||||
|
||||
public func ==(lhs:Error, rhs:Error) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
}
|
||||
|
||||
public enum Result : Equatable {
|
||||
case Success(String)
|
||||
case Error(Stencil.Error)
|
||||
}
|
||||
|
||||
public func ==(lhs:Result, rhs:Result) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.Success(let lhsValue), .Success(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Error(let lhsValue), .Error(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// Stencil.h
|
||||
// Stencil
|
||||
//
|
||||
// Created by Kyle Fuller on 23/10/2014.
|
||||
// Copyright (c) 2014 Cocode. All rights reserved.
|
||||
// See LICENSE for more details.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
//! Project version number for Stencil.
|
||||
FOUNDATION_EXPORT double StencilVersionNumber;
|
||||
|
||||
//! Project version string for Stencil.
|
||||
FOUNDATION_EXPORT const unsigned char StencilVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <Stencil/PublicHeader.h>
|
||||
@@ -1,53 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public class Template {
|
||||
public let parser:TokenParser
|
||||
|
||||
public convenience init?(named:String) {
|
||||
self.init(named:named, inBundle:nil)
|
||||
}
|
||||
|
||||
public convenience init?(named:String, inBundle bundle:NSBundle?) {
|
||||
var url:NSURL?
|
||||
|
||||
if let bundle = bundle {
|
||||
url = bundle.URLForResource(named, withExtension: nil)
|
||||
} else {
|
||||
url = NSBundle.mainBundle().URLForResource(named, withExtension: nil)
|
||||
}
|
||||
|
||||
self.init(URL:url!)
|
||||
}
|
||||
|
||||
public convenience init?(URL:NSURL) {
|
||||
var error:NSError?
|
||||
let maybeTemplateString = NSString(contentsOfURL: URL, encoding: NSUTF8StringEncoding, error: &error)
|
||||
if let templateString = maybeTemplateString {
|
||||
self.init(templateString:templateString)
|
||||
} else {
|
||||
self.init(templateString:"")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public init(templateString:String) {
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
parser = TokenParser(tokens: tokens)
|
||||
}
|
||||
|
||||
public func render(context:Context) -> Result {
|
||||
switch parser.parse() {
|
||||
case .Success(let nodes):
|
||||
return renderNodes(nodes, context)
|
||||
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
public func render() -> Result {
|
||||
let context = Context()
|
||||
return render(context)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public enum Token : Equatable {
|
||||
case Text(value:String)
|
||||
case Variable(value:String)
|
||||
case Comment(value:String)
|
||||
case Block(value:String)
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
func components() -> [String] {
|
||||
// TODO: Make this smarter and treat quoted strings as a single component
|
||||
let characterSet = NSCharacterSet.whitespaceAndNewlineCharacterSet()
|
||||
|
||||
func strip(value: String) -> [String] {
|
||||
return value.stringByTrimmingCharactersInSet(characterSet).componentsSeparatedByCharactersInSet(characterSet)
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return strip(value)
|
||||
case .Variable(let value):
|
||||
return strip(value)
|
||||
case .Text(let value):
|
||||
return strip(value)
|
||||
case .Comment(let value):
|
||||
return strip(value)
|
||||
}
|
||||
}
|
||||
|
||||
var contents:String {
|
||||
switch self {
|
||||
case .Block(let value):
|
||||
return value
|
||||
case .Variable(let value):
|
||||
return value
|
||||
case .Text(let value):
|
||||
return value
|
||||
case .Comment(let value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:Token, rhs:Token) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.Text(let lhsValue), .Text(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Variable(let lhsValue), .Variable(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Block(let lhsValue), .Block(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.Comment(let lhsValue), .Comment(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
public struct Variable : Equatable {
|
||||
public let variable:String
|
||||
|
||||
public init(_ variable:String) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
private func lookup() -> [String] {
|
||||
return variable.componentsSeparatedByString(".")
|
||||
}
|
||||
|
||||
public func resolve(context:Context) -> AnyObject? {
|
||||
var current:AnyObject? = context
|
||||
|
||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||
return variable.substringWithRange(variable.startIndex.successor() ..< variable.endIndex.predecessor())
|
||||
}
|
||||
|
||||
for bit in lookup() {
|
||||
if let context = current as? Context {
|
||||
current = context[bit]
|
||||
} else if let dictionary = current as? Dictionary<String, AnyObject> {
|
||||
current = dictionary[bit]
|
||||
} else if let array = current as? [AnyObject] {
|
||||
if let index = bit.toInt() {
|
||||
current = array[index]
|
||||
} else if bit == "first" {
|
||||
current = array.first
|
||||
} else if bit == "last" {
|
||||
current = array.last
|
||||
} else if bit == "count" {
|
||||
current = countElements(array)
|
||||
}
|
||||
} else if let object = current as? NSObject {
|
||||
current = object.valueForKey(bit)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs:Variable, rhs:Variable) -> Bool {
|
||||
return lhs.variable == rhs.variable
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import Cocoa
|
||||
import XCTest
|
||||
import Stencil
|
||||
|
||||
class ContextTests: XCTestCase {
|
||||
var context:Context!
|
||||
|
||||
override func setUp() {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
func testItAllowsYouToRetrieveAValue() {
|
||||
let name = context["name"] as String!
|
||||
XCTAssertEqual(name, "Kyle")
|
||||
}
|
||||
|
||||
func testItAllowsYouToSetValue() {
|
||||
context["name"] = "Katie"
|
||||
|
||||
let name = context["name"] as String!
|
||||
XCTAssertEqual(name, "Katie")
|
||||
}
|
||||
|
||||
func testItAllowsYouToRemoveAValue() {
|
||||
context["name"] = nil
|
||||
XCTAssertNil(context["name"])
|
||||
}
|
||||
|
||||
func testItAllowsYouToRetrieveAValueFromParent() {
|
||||
context.push()
|
||||
|
||||
let name = context["name"] as String!
|
||||
XCTAssertEqual(name, "Kyle")
|
||||
}
|
||||
|
||||
func testItAllowsYouToOverideAParentVariable() {
|
||||
context.push()
|
||||
context["name"] = "Katie"
|
||||
|
||||
let name = context["name"] as String!
|
||||
XCTAssertEqual(name, "Katie")
|
||||
}
|
||||
|
||||
func testShowAllowYouToPopVariablesRestoringPreviousState() {
|
||||
context.push()
|
||||
context["name"] = "Katie"
|
||||
context.pop()
|
||||
|
||||
let name = context["name"] as String!
|
||||
XCTAssertEqual(name, "Kyle")
|
||||
}
|
||||
|
||||
func testItAllowsYouToPushADictionaryToTheStack() {
|
||||
context.push(["name": "Katie"])
|
||||
|
||||
let name = context["name"] as String!
|
||||
XCTAssertEqual(name, "Katie")
|
||||
}
|
||||
|
||||
func testItAllowsYouToCompareTwoContextsForEquality() {
|
||||
let otherContext = Context(dictionary: ["name": "Kyle"])
|
||||
|
||||
XCTAssertEqual(otherContext, context )
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.cocode.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,50 +0,0 @@
|
||||
import Cocoa
|
||||
import XCTest
|
||||
import Stencil
|
||||
|
||||
class LexerTests: XCTestCase {
|
||||
|
||||
func testTokenizeText() {
|
||||
let lexer = Lexer(templateString:"Hello World")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
XCTAssertEqual(tokens.count, 1)
|
||||
XCTAssertEqual(tokens.first!, Token.Text(value: "Hello World"))
|
||||
}
|
||||
|
||||
func testTokenizeComment() {
|
||||
let lexer = Lexer(templateString:"{# Comment #}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
XCTAssertEqual(tokens.count, 1)
|
||||
XCTAssertEqual(tokens.first!, Token.Comment(value: "Comment"))
|
||||
}
|
||||
|
||||
func testTokenizeVariable() {
|
||||
let lexer = Lexer(templateString:"{{ Variable }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
XCTAssertEqual(tokens.count, 1)
|
||||
XCTAssertEqual(tokens.first!, Token.Variable(value: "Variable"))
|
||||
}
|
||||
|
||||
func testTokenizeMixture() {
|
||||
let lexer = Lexer(templateString:"My name is {{ name }}.")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
XCTAssertEqual(tokens.count, 3)
|
||||
XCTAssertEqual(tokens[0], Token.Text(value: "My name is "))
|
||||
XCTAssertEqual(tokens[1], Token.Variable(value: "name"))
|
||||
XCTAssertEqual(tokens[2], Token.Text(value: "."))
|
||||
}
|
||||
|
||||
func testTokenizeTwoVariables() { // Don't be greedy
|
||||
let lexer = Lexer(templateString:"{{ thing }}{{ name }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
XCTAssertEqual(tokens.count, 2)
|
||||
XCTAssertEqual(tokens[0], Token.Variable(value: "thing"))
|
||||
XCTAssertEqual(tokens[1], Token.Variable(value: "name"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
import Cocoa
|
||||
import XCTest
|
||||
import Stencil
|
||||
|
||||
class ErrorNodeError : Error {
|
||||
var description: String {
|
||||
return "Node Error"
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorNode : Node {
|
||||
func render(context: Context) -> Result {
|
||||
|
||||
return .Error(ErrorNodeError())
|
||||
}
|
||||
}
|
||||
|
||||
class NodeTests: XCTestCase {
|
||||
var context:Context!
|
||||
|
||||
override func setUp() {
|
||||
context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"age": 27,
|
||||
"items": [1,2,3],
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
class TextNodeTests: NodeTests {
|
||||
func testTextNodeResolvesText() {
|
||||
let node = TextNode(text:"Hello World")
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "Hello World")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VariableNodeTests: NodeTests {
|
||||
func testVariableNodeResolvesVariable() {
|
||||
let node = VariableNode(variable:Variable("name"))
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "Kyle")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func testVariableNodeResolvesNonStringVariable() {
|
||||
let node = VariableNode(variable:Variable("age"))
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "27")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RenderNodeTests: NodeTests {
|
||||
func testRenderingNodes() {
|
||||
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name")] as [Node]
|
||||
switch renderNodes(nodes, context) {
|
||||
case .Success(let result):
|
||||
XCTAssertEqual(result, "Hello Kyle")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func testRenderingNodesWithFailure() {
|
||||
let nodes = [TextNode(text:"Hello "), VariableNode(variable: "name"), ErrorNode()] as [Node]
|
||||
|
||||
switch renderNodes(nodes, context) {
|
||||
case .Success(let result):
|
||||
XCTAssert(false, "Unexpected success")
|
||||
case .Error(let error):
|
||||
XCTAssertEqual("\(error)", "Node Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ForNodeTests: NodeTests {
|
||||
func testForNodeRender() {
|
||||
let node = ForNode(variable: "items", loopVariable: "item", nodes: [VariableNode(variable: "item")], emptyNodes:[])
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "123")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IfNodeTests: NodeTests {
|
||||
|
||||
// MARK: Parsing
|
||||
|
||||
func testParseIf() {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first! as IfNode
|
||||
let trueNode = node.trueNodes.first! as TextNode
|
||||
let falseNode = node.falseNodes.first! as TextNode
|
||||
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.variable.variable, "value")
|
||||
XCTAssertEqual(node.trueNodes.count, 1)
|
||||
XCTAssertEqual(trueNode.text, "true")
|
||||
XCTAssertEqual(node.falseNodes.count, 1)
|
||||
XCTAssertEqual(falseNode.text, "false")
|
||||
}
|
||||
}
|
||||
|
||||
func testParseIfNot() {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
Token.Text(value: "false"),
|
||||
Token.Block(value: "else"),
|
||||
Token.Text(value: "true"),
|
||||
Token.Block(value: "endif")
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first! as IfNode
|
||||
let trueNode = node.trueNodes.first! as TextNode
|
||||
let falseNode = node.falseNodes.first! as TextNode
|
||||
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.variable.variable, "value")
|
||||
XCTAssertEqual(node.trueNodes.count, 1)
|
||||
XCTAssertEqual(trueNode.text, "true")
|
||||
XCTAssertEqual(node.falseNodes.count, 1)
|
||||
XCTAssertEqual(falseNode.text, "false")
|
||||
}
|
||||
}
|
||||
|
||||
func testParseIfWithoutEndIfError() {
|
||||
let tokens = [
|
||||
Token.Block(value: "if value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
assertFailure(parser.parse(), "if: `endif` was not found.")
|
||||
}
|
||||
|
||||
func testParseIfNotWithoutEndIfError() {
|
||||
let tokens = [
|
||||
Token.Block(value: "ifnot value"),
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
assertFailure(parser.parse(), "ifnot: `endif` was not found.")
|
||||
}
|
||||
|
||||
// MARK: Rendering
|
||||
|
||||
func testIfNodeRenderTruth() {
|
||||
let node = IfNode(variable: "items", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "true")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func testIfNodeRenderFalse() {
|
||||
let node = IfNode(variable: "unknown", trueNodes: [TextNode(text: "true")], falseNodes: [TextNode(text: "false")])
|
||||
let result = node.render(context)
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "false")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NowNodeTests: NodeTests {
|
||||
|
||||
// MARK: Parsing
|
||||
|
||||
func testParseDefaultNow() {
|
||||
let tokens = [ Token.Block(value: "now") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first! as NowNode
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.format.variable, "\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
}
|
||||
}
|
||||
|
||||
func testParseNowWithFormat() {
|
||||
let tokens = [ Token.Block(value: "now \"HH:mm\"") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first! as NowNode
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.format.variable, "\"HH:mm\"")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Rendering
|
||||
|
||||
func testRenderNowNode() {
|
||||
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
||||
let result = node.render(context)
|
||||
|
||||
let formatter = NSDateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let date = formatter.stringFromDate(NSDate())
|
||||
|
||||
switch node.render(context) {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, date)
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import Cocoa
|
||||
import XCTest
|
||||
import Stencil
|
||||
|
||||
class TokenParserTests: XCTestCase {
|
||||
func testParsingTextToken() {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Text(value: "Hello World")
|
||||
])
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first as TextNode!
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.text, "Hello World")
|
||||
}
|
||||
}
|
||||
|
||||
func testParsingVariableToken() {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Variable(value: "name")
|
||||
])
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first as VariableNode!
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.variable, Variable("name"))
|
||||
}
|
||||
}
|
||||
|
||||
func testParsingCommentToken() {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Comment(value: "Secret stuff!")
|
||||
])
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
XCTAssertEqual(nodes.count, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func testParsingTagToken() {
|
||||
let parser = TokenParser(tokens: [
|
||||
Token.Block(value: "now"),
|
||||
])
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import Cocoa
|
||||
import XCTest
|
||||
import Stencil
|
||||
|
||||
func assertSuccess(result:TokenParser.Results, block:(([Node]) -> ())) {
|
||||
switch result {
|
||||
case .Success(let nodes):
|
||||
block(nodes)
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
func assertFailure(result:TokenParser.Results, description:String) {
|
||||
switch result {
|
||||
case .Success(let nodes):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
case .Error(let error):
|
||||
XCTAssertEqual("\(error)", description)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomNode : Node {
|
||||
func render(context:Context) -> Result {
|
||||
return .Success("Hello World")
|
||||
}
|
||||
}
|
||||
|
||||
class StencilTests: XCTestCase {
|
||||
func testReadmeExample() {
|
||||
let templateString = "There are {{ articles.count }} articles.\n" +
|
||||
"\n" +
|
||||
"{% for article in articles %}" +
|
||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"articles": [
|
||||
[ "title": "Migrating from OCUnit to XCTest", "author": "Kyle Fuller" ],
|
||||
[ "title": "Memory Management with ARC", "author": "Kyle Fuller" ],
|
||||
]
|
||||
])
|
||||
|
||||
let template = Template(templateString:templateString)
|
||||
let result = template.render(context)
|
||||
|
||||
let fixture = "There are 2 articles.\n" +
|
||||
"\n" +
|
||||
" - Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||
" - Memory Management with ARC by Kyle Fuller.\n" +
|
||||
"\n"
|
||||
|
||||
XCTAssertEqual(result, Result.Success(fixture))
|
||||
}
|
||||
|
||||
func testCustomTag() {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
|
||||
template.parser.registerTag("custom") { parser, token in
|
||||
return .Success(node:CustomNode())
|
||||
}
|
||||
|
||||
let result = template.render()
|
||||
XCTAssertEqual(result, Result.Success("Hello World"))
|
||||
}
|
||||
|
||||
func testSimpleCustomTag() {
|
||||
let templateString = "{% custom %}"
|
||||
let template = Template(templateString:templateString)
|
||||
|
||||
template.parser.registerSimpleTag("custom") { context in
|
||||
return .Success("Hello World")
|
||||
}
|
||||
|
||||
let result = template.render()
|
||||
XCTAssertEqual(result, Result.Success("Hello World"))
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import Cocoa
|
||||
import XCTest
|
||||
import Stencil
|
||||
|
||||
class TemplateTests: XCTestCase {
|
||||
|
||||
func testTemplate() {
|
||||
let context = Context(dictionary: [ "name": "Kyle" ])
|
||||
let template = Template(templateString: "Hello World")
|
||||
let result = template.render(context)
|
||||
XCTAssertEqual(result, Result.Success("Hello World"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import Cocoa
|
||||
import XCTest
|
||||
import Stencil
|
||||
|
||||
@objc class Object : NSObject {
|
||||
let title = "Hello World"
|
||||
}
|
||||
|
||||
class VariableTests: XCTestCase {
|
||||
var context:Context!
|
||||
|
||||
override func setUp() {
|
||||
context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"contacts": [ "Katie", "Orta", ],
|
||||
"profiles": [ "github": "kylef", ],
|
||||
"object": Object(),
|
||||
])
|
||||
}
|
||||
|
||||
func testResolvingStringLiteral() {
|
||||
let variable = Variable("\"name\"")
|
||||
let result = variable.resolve(context) as String!
|
||||
XCTAssertEqual(result, "name")
|
||||
}
|
||||
|
||||
func testResolvingVariable() {
|
||||
let variable = Variable("name")
|
||||
let result = variable.resolve(context) as String!
|
||||
XCTAssertEqual(result, "Kyle")
|
||||
}
|
||||
|
||||
func testResolvingItemFromDictionary() {
|
||||
let variable = Variable("profiles.github")
|
||||
let result = variable.resolve(context) as String!
|
||||
XCTAssertEqual(result, "kylef")
|
||||
}
|
||||
|
||||
func testResolvingItemFromArrayWithIndex() {
|
||||
let variable = Variable("contacts.0")
|
||||
let result = variable.resolve(context) as String!
|
||||
XCTAssertEqual(result, "Katie")
|
||||
}
|
||||
|
||||
func testResolvingFirstItemFromArray() {
|
||||
let variable = Variable("contacts.first")
|
||||
let result = variable.resolve(context) as String!
|
||||
XCTAssertEqual(result, "Katie")
|
||||
}
|
||||
|
||||
func testResolvingLastItemFromArray() {
|
||||
let variable = Variable("contacts.last")
|
||||
let result = variable.resolve(context) as String!
|
||||
XCTAssertEqual(result, "Orta")
|
||||
}
|
||||
|
||||
func testResolvingValueViaKVO() {
|
||||
let variable = Variable("object.title")
|
||||
let result = variable.resolve(context) as String!
|
||||
XCTAssertEqual(result, "Hello World")
|
||||
}
|
||||
}
|
||||
3
Tests/LinuxMain.swift
Normal file
3
Tests/LinuxMain.swift
Normal file
@@ -0,0 +1,3 @@
|
||||
import StencilTests
|
||||
|
||||
stencilTests()
|
||||
81
Tests/StencilTests/ContextSpec.swift
Normal file
81
Tests/StencilTests/ContextSpec.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testContext() {
|
||||
describe("Context") {
|
||||
var context: Context!
|
||||
|
||||
$0.before {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
$0.it("allows you to get a value via subscripting") {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to set a value via subscripting") {
|
||||
context["name"] = "Katie"
|
||||
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("allows you to remove a value via subscripting") {
|
||||
context["name"] = nil
|
||||
|
||||
try expect(context["name"]).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("allows you to retrieve a value from a parent") {
|
||||
try context.push {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("allows you to override a parent's value") {
|
||||
try context.push {
|
||||
context["name"] = "Katie"
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("allows you to pop to restore previous state") {
|
||||
context.push {
|
||||
context["name"] = "Katie"
|
||||
}
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to remove a parent's value in a level") {
|
||||
try context.push {
|
||||
context["name"] = nil
|
||||
try expect(context["name"]).to.beNil()
|
||||
}
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to push a dictionary and run a closure then restoring previous state") {
|
||||
var didRun = false
|
||||
|
||||
try context.push(dictionary: ["name": "Katie"]) {
|
||||
didRun = true
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
try expect(didRun).to.beTrue()
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to flatten the context contents") {
|
||||
try context.push(dictionary: ["test": "abc"]) {
|
||||
let flattened = context.flatten()
|
||||
|
||||
try expect(flattened.count) == 2
|
||||
try expect(flattened["name"] as? String) == "Kyle"
|
||||
try expect(flattened["test"] as? String) == "abc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
361
Tests/StencilTests/EnvironmentSpec.swift
Normal file
361
Tests/StencilTests/EnvironmentSpec.swift
Normal file
@@ -0,0 +1,361 @@
|
||||
import Spectre
|
||||
import PathKit
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testEnvironment() {
|
||||
describe("Environment") {
|
||||
var environment: Environment!
|
||||
var template: Template!
|
||||
|
||||
$0.before {
|
||||
environment = Environment(loader: ExampleLoader())
|
||||
template = nil
|
||||
}
|
||||
|
||||
$0.it("can load a template from a name") {
|
||||
let template = try environment.loadTemplate(name: "example.html")
|
||||
try expect(template.name) == "example.html"
|
||||
}
|
||||
|
||||
$0.it("can load a template from a names") {
|
||||
let template = try environment.loadTemplate(names: ["first.html", "example.html"])
|
||||
try expect(template.name) == "example.html"
|
||||
}
|
||||
|
||||
$0.it("can render a template from a string") {
|
||||
let result = try environment.renderTemplate(string: "Hello World")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a template from a file") {
|
||||
let result = try environment.renderTemplate(name: "example.html")
|
||||
try expect(result) == "Hello World!"
|
||||
}
|
||||
|
||||
$0.it("allows you to provide a custom template class") {
|
||||
let environment = Environment(loader: ExampleLoader(), templateClass: CustomTemplate.self)
|
||||
let result = try environment.renderTemplate(string: "Hello World")
|
||||
|
||||
try expect(result) == "here"
|
||||
}
|
||||
|
||||
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||
guard let range = template.templateString.range(of: token) else {
|
||||
fatalError("Can't find '\(token)' in '\(template)'")
|
||||
}
|
||||
let rangeLine = template.templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: template.name, line: rangeLine)
|
||||
let token = Token.block(value: token, at: sourceMap)
|
||||
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||
}
|
||||
|
||||
func expectError(reason: String, token: String,
|
||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||
|
||||
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]),
|
||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||
let reporter = SimpleErrorReporter()
|
||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
$0.context("given syntax error") {
|
||||
|
||||
$0.it("reports syntax error on invalid for tag syntax") {
|
||||
template = "Hello {% for name in %}{{ name }}, {% endfor %}!"
|
||||
try expectError(reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.", token: "for name in")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error on missing endfor") {
|
||||
template = "{% for name in names %}{{ name }}"
|
||||
try expectError(reason: "`endfor` was not found.", token: "for name in names")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error on unknown tag") {
|
||||
template = "{% for name in names %}{{ name }}{% end %}"
|
||||
try expectError(reason: "Unknown template tag 'end'", token: "end")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$0.context("given unknown filter") {
|
||||
|
||||
$0.it("reports syntax error in for tag") {
|
||||
template = "{% for name in names|unknown %}{{ name }}{% endfor %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "names|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in for-where tag") {
|
||||
template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in if tag") {
|
||||
template = "{% if name|unknown %}{{ name }}{% endif %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in elif tag") {
|
||||
template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in ifnot tag") {
|
||||
template = "{% ifnot name|unknown %}{{ name }}{% endif %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in filter tag") {
|
||||
template = "{% filter unknown %}Text{% endfilter %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "filter unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in variable tag") {
|
||||
template = "{{ name|unknown }}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$0.context("given rendering error") {
|
||||
|
||||
$0.it("reports rendering error in variable filter") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("throw") { (value: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
}
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
template = Template(templateString: "{{ name|throw }}", environment: environment)
|
||||
try expectError(reason: "filter error", token: "name|throw")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in filter tag") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("throw") { (value: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
}
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment)
|
||||
try expectError(reason: "filter error", token: "filter throw")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in simple tag") {
|
||||
let tagExtension = Extension()
|
||||
tagExtension.registerSimpleTag("simpletag") { context in
|
||||
throw TemplateSyntaxError("simpletag error")
|
||||
}
|
||||
environment.extensions += [tagExtension]
|
||||
|
||||
template = Template(templateString: "{% simpletag %}", environment: environment)
|
||||
try expectError(reason: "simpletag error", token: "simpletag")
|
||||
}
|
||||
|
||||
$0.it("reporsts passing argument to simple filter") {
|
||||
template = "{{ name|uppercase:5 }}"
|
||||
try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in custom tag") {
|
||||
let tagExtension = Extension()
|
||||
tagExtension.registerTag("customtag") { parser, token in
|
||||
return ErrorNode(token: token)
|
||||
}
|
||||
environment.extensions += [tagExtension]
|
||||
|
||||
template = Template(templateString: "{% customtag %}", environment: environment)
|
||||
try expectError(reason: "Custom Error", token: "customtag")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in for body") {
|
||||
let tagExtension = Extension()
|
||||
tagExtension.registerTag("customtag") { parser, token in
|
||||
return ErrorNode(token: token)
|
||||
}
|
||||
environment.extensions += [tagExtension]
|
||||
|
||||
template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment)
|
||||
try expectError(reason: "Custom Error", token: "customtag")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in block") {
|
||||
let tagExtension = Extension()
|
||||
tagExtension.registerTag("customtag") { parser, token in
|
||||
return ErrorNode(token: token)
|
||||
}
|
||||
environment.extensions += [tagExtension]
|
||||
|
||||
template = Template(templateString: "{% block some %}{% customtag %}{% endblock %}", environment: environment)
|
||||
try expectError(reason: "Custom Error", token: "customtag")
|
||||
}
|
||||
}
|
||||
|
||||
$0.context("given included template") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
var environment = Environment(loader: loader)
|
||||
var template: Template!
|
||||
var includedTemplate: Template!
|
||||
|
||||
$0.before {
|
||||
environment = Environment(loader: loader)
|
||||
template = nil
|
||||
includedTemplate = nil
|
||||
}
|
||||
|
||||
func expectError(reason: String, token: String, includedToken: String,
|
||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||
var expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||
expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!]
|
||||
|
||||
let error = try expect(environment.render(template: template, context: ["target": "World"]),
|
||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||
let reporter = SimpleErrorReporter()
|
||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in included template") {
|
||||
template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment)
|
||||
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
token: "include \"invalid-include.html\"",
|
||||
includedToken: "target|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports runtime error in included template") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
})
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment)
|
||||
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||
|
||||
try expectError(reason: "filter error",
|
||||
token: "include \"invalid-include.html\"",
|
||||
includedToken: "target|unknown")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$0.context("given base and child templates") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
var environment: Environment!
|
||||
var childTemplate: Template!
|
||||
var baseTemplate: Template!
|
||||
|
||||
$0.before {
|
||||
environment = Environment(loader: loader)
|
||||
childTemplate = nil
|
||||
baseTemplate = nil
|
||||
}
|
||||
|
||||
func expectError(reason: String, childToken: String, baseToken: String?,
|
||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
|
||||
if let baseToken = baseToken {
|
||||
expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!]
|
||||
}
|
||||
let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]),
|
||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||
let reporter = SimpleErrorReporter()
|
||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in base template") {
|
||||
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
childToken: "extends \"invalid-base.html\"",
|
||||
baseToken: "target|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports runtime error in base template") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
})
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||
|
||||
try expectError(reason: "filter error",
|
||||
childToken: "block.super",
|
||||
baseToken: "target|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in child template") {
|
||||
childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" +
|
||||
"{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil)
|
||||
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
childToken: "target|unknown",
|
||||
baseToken: nil)
|
||||
}
|
||||
|
||||
$0.it("reports runtime error in child template") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
})
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" +
|
||||
"{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil)
|
||||
|
||||
try expectError(reason: "filter error",
|
||||
childToken: "target|unknown",
|
||||
baseToken: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension Expectation {
|
||||
@discardableResult
|
||||
func toThrow<T: Error>() throws -> T {
|
||||
var thrownError: Error? = nil
|
||||
|
||||
do {
|
||||
_ = try expression()
|
||||
} catch {
|
||||
thrownError = error
|
||||
}
|
||||
|
||||
if let thrownError = thrownError {
|
||||
if let thrownError = thrownError as? T {
|
||||
return thrownError
|
||||
} else {
|
||||
throw failure("\(thrownError) is not \(T.self)")
|
||||
}
|
||||
} else {
|
||||
throw failure("expression did not throw an error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class ExampleLoader: Loader {
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
if name == "example.html" {
|
||||
return Template(templateString: "Hello World!", environment: environment, name: name)
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(templateNames: [name], loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CustomTemplate: Template {
|
||||
override func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||
return "here"
|
||||
}
|
||||
}
|
||||
304
Tests/StencilTests/ExpressionSpec.swift
Normal file
304
Tests/StencilTests/ExpressionSpec.swift
Normal file
@@ -0,0 +1,304 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testExpressions() {
|
||||
describe("Expression") {
|
||||
let parser = TokenParser(tokens: [], environment: Environment())
|
||||
|
||||
$0.describe("VariableExpression") {
|
||||
let expression = VariableExpression(variable: Variable("value"))
|
||||
|
||||
$0.it("evaluates to true when value is not nil") {
|
||||
let context = Context(dictionary: ["value": "known"])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when value is unset") {
|
||||
let context = Context()
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true when array variable is not empty") {
|
||||
let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]]
|
||||
let context = Context(dictionary: ["value": [items]])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when array value is empty") {
|
||||
let emptyItems = [[String: Any]]()
|
||||
let context = Context(dictionary: ["value": emptyItems])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when dictionary value is empty") {
|
||||
let emptyItems = [String:Any]()
|
||||
let context = Context(dictionary: ["value": emptyItems])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when Array<Any> value is empty") {
|
||||
let context = Context(dictionary: ["value": ([] as [Any])])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true when integer value is above 0") {
|
||||
let context = Context(dictionary: ["value": 1])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with string") {
|
||||
let context = Context(dictionary: ["value": "test"])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when empty string") {
|
||||
let context = Context(dictionary: ["value": ""])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when integer value is below 0 or below") {
|
||||
let context = Context(dictionary: ["value": 0])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
|
||||
let negativeContext = Context(dictionary: ["value": 0])
|
||||
try expect(try expression.evaluate(context: negativeContext)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true when float value is above 0") {
|
||||
let context = Context(dictionary: ["value": Float(0.5)])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when float is 0 or below") {
|
||||
let context = Context(dictionary: ["value": Float(0)])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true when double value is above 0") {
|
||||
let context = Context(dictionary: ["value": Double(0.5)])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when double is 0 or below") {
|
||||
let context = Context(dictionary: ["value": Double(0)])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when uint is 0") {
|
||||
let context = Context(dictionary: ["value": UInt(0)])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("NotExpression") {
|
||||
$0.it("returns truthy for positive expressions") {
|
||||
let expression = NotExpression(expression: StaticExpression(value: true))
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("returns falsy for negative expressions") {
|
||||
let expression = NotExpression(expression: StaticExpression(value: false))
|
||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("expression parsing") {
|
||||
$0.it("can parse a variable expression") {
|
||||
let expression = try parseExpression(components: ["value"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("can parse a not expression") {
|
||||
let expression = try parseExpression(components: ["not", "value"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.describe("and expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to false with lhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with rhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs and rhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with lhs and rhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("or expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with rhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with lhs and rhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs and rhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("equality expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with non equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with nils") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with numbers") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with non equal numbers") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with booleans") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with falsy booleans") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with different types") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("inequality expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with inequal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("more than expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs > rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("more than equal expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs < rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("less than expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs < rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("less than equal expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs > rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("multiple expression") {
|
||||
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with one") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with one and three") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with two") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with two and three") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with two and three") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with nothing") {
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("in expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true when rhs contains lhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1...3]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1..<3]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when rhs does not contain lhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 1...3]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
293
Tests/StencilTests/FilterSpec.swift
Normal file
293
Tests/StencilTests/FilterSpec.swift
Normal file
@@ -0,0 +1,293 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testFilter() {
|
||||
describe("template filters") {
|
||||
let context: [String: Any] = ["name": "Kyle"]
|
||||
|
||||
$0.it("allows you to register a custom filter") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { (value: Any?) in
|
||||
if let value = value as? String {
|
||||
return "\(value) \(value)"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "Kyle Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom filter which accepts single argument") {
|
||||
let template = Template(templateString: "{{ name|repeat:'value1, \"value2\"' }}")
|
||||
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { value, arguments in
|
||||
if !arguments.isEmpty {
|
||||
return "\(value!) \(value!) with args \(arguments.first!!)"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "Kyle Kyle with args value1, \"value2\""
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom filter which accepts several arguments") {
|
||||
let template = Template(templateString: "{{ name|repeat:'value\"1\"',\"value'2'\",'(key, value)' }}")
|
||||
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { value, arguments in
|
||||
if !arguments.isEmpty {
|
||||
return "\(value!) \(value!) with args 0: \(arguments[0]!), 1: \(arguments[1]!), 2: \(arguments[2]!)"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "Kyle Kyle with args 0: value\"1\", 1: value'2', 2: (key, value)"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom which throws") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { (value: Any?) in
|
||||
throw TemplateSyntaxError("No Repeat")
|
||||
}
|
||||
|
||||
let context = Context(dictionary: context, environment: Environment(extensions: [repeatExtension]))
|
||||
try expect(try template.render(context)).toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first))
|
||||
}
|
||||
|
||||
$0.it("allows you to override a default filter") {
|
||||
let template = Template(templateString: "{{ name|join }}")
|
||||
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("join") { (value: Any?) in
|
||||
return "joined"
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "joined"
|
||||
}
|
||||
|
||||
$0.it("allows whitespace in expression") {
|
||||
let template = Template(templateString: "{{ value | join : \", \" }}")
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||
try expect(result) == "One, Two"
|
||||
}
|
||||
|
||||
$0.it("throws when you pass arguments to simple filter") {
|
||||
let template = Template(templateString: "{{ name|uppercase:5 }}")
|
||||
try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
describe("string filters") {
|
||||
$0.context("given string") {
|
||||
$0.it("transforms a string to be capitalized") {
|
||||
let template = Template(templateString: "{{ name|capitalize }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("transforms a string to be uppercase") {
|
||||
let template = Template(templateString: "{{ name|uppercase }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "KYLE"
|
||||
}
|
||||
|
||||
$0.it("transforms a string to be lowercase") {
|
||||
let template = Template(templateString: "{{ name|lowercase }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||
try expect(result) == "kyle"
|
||||
}
|
||||
}
|
||||
|
||||
$0.context("given array of strings") {
|
||||
$0.it("transforms a string to be capitalized") {
|
||||
let template = Template(templateString: "{{ names|capitalize }}")
|
||||
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||
try expect(result) == "[\"Kyle\", \"Kyle\"]"
|
||||
}
|
||||
|
||||
$0.it("transforms a string to be uppercase") {
|
||||
let template = Template(templateString: "{{ names|uppercase }}")
|
||||
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||
try expect(result) == "[\"KYLE\", \"KYLE\"]"
|
||||
}
|
||||
|
||||
$0.it("transforms a string to be lowercase") {
|
||||
let template = Template(templateString: "{{ names|lowercase }}")
|
||||
let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]]))
|
||||
try expect(result) == "[\"kyle\", \"kyle\"]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("default filter") {
|
||||
let template = Template(templateString: "Hello {{ name|default:\"World\" }}")
|
||||
|
||||
$0.it("shows the variable value") {
|
||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||
try expect(result) == "Hello Kyle"
|
||||
}
|
||||
|
||||
$0.it("shows the default value") {
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("supports multiple defaults") {
|
||||
let template = Template(templateString: "Hello {{ name|default:a,b,c,\"World\" }}")
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can use int as default") {
|
||||
let template = Template(templateString: "{{ value|default:1 }}")
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "1"
|
||||
}
|
||||
|
||||
$0.it("can use float as default") {
|
||||
let template = Template(templateString: "{{ value|default:1.5 }}")
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "1.5"
|
||||
}
|
||||
|
||||
$0.it("checks for underlying nil value correctly") {
|
||||
let template = Template(templateString: "Hello {{ user.name|default:\"anonymous\" }}")
|
||||
let nilName: String? = nil
|
||||
let user: [String: Any?] = ["name": nilName]
|
||||
let result = try template.render(Context(dictionary: ["user": user]))
|
||||
try expect(result) == "Hello anonymous"
|
||||
}
|
||||
}
|
||||
|
||||
describe("join filter") {
|
||||
let template = Template(templateString: "{{ value|join:\", \" }}")
|
||||
|
||||
$0.it("joins a collection of strings") {
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||
try expect(result) == "One, Two"
|
||||
}
|
||||
|
||||
$0.it("joins a mixed-type collection") {
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 10.5, "Five"]]))
|
||||
try expect(result) == "One, 2, true, 10.5, Five"
|
||||
}
|
||||
|
||||
$0.it("can join by non string") {
|
||||
let template = Template(templateString: "{{ value|join:separator }}")
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true]))
|
||||
try expect(result) == "OnetrueTwo"
|
||||
}
|
||||
|
||||
$0.it("can join without arguments") {
|
||||
let template = Template(templateString: "{{ value|join }}")
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||
try expect(result) == "OneTwo"
|
||||
}
|
||||
}
|
||||
|
||||
describe("split filter") {
|
||||
let template = Template(templateString: "{{ value|split:\", \" }}")
|
||||
|
||||
$0.it("split a string into array") {
|
||||
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
||||
try expect(result) == "[\"One\", \"Two\"]"
|
||||
}
|
||||
|
||||
$0.it("can split without arguments") {
|
||||
let template = Template(templateString: "{{ value|split }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
||||
try expect(result) == "[\"One,\", \"Two\"]"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("filter suggestion") {
|
||||
var template: Template!
|
||||
var filterExtension: Extension!
|
||||
|
||||
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||
guard let range = template.templateString.range(of: token) else {
|
||||
fatalError("Can't find '\(token)' in '\(template)'")
|
||||
}
|
||||
let rangeLine = template.templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: template.name, line: rangeLine)
|
||||
let token = Token.block(value: token, at: sourceMap)
|
||||
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||
}
|
||||
|
||||
func expectError(reason: String, token: String,
|
||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||
let environment = Environment(extensions: [filterExtension])
|
||||
|
||||
let error = try expect(environment.render(template: template, context: [:]),
|
||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||
let reporter = SimpleErrorReporter()
|
||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
$0.it("made for unknown filter") {
|
||||
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||
|
||||
filterExtension = Extension()
|
||||
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
||||
|
||||
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", token: "value|unknownFilter")
|
||||
}
|
||||
|
||||
$0.it("made for multiple similar filters") {
|
||||
template = Template(templateString: "{{ value|lowerFirst }}")
|
||||
|
||||
filterExtension = Extension()
|
||||
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
||||
filterExtension.registerFilter("lowerFirstLetter") { value, _ in value }
|
||||
|
||||
try expectError(reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", token: "value|lowerFirst")
|
||||
}
|
||||
|
||||
$0.it("not made when can't find similar filter") {
|
||||
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", token: "value|unknownFilter")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
describe("indent filter") {
|
||||
$0.it("indents content") {
|
||||
let template = Template(templateString: "{{ value|indent:2 }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
||||
try expect(result) == "One\n Two"
|
||||
}
|
||||
|
||||
$0.it("can indent with arbitrary character") {
|
||||
let template = Template(templateString: "{{ value|indent:2,\"\t\" }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
||||
try expect(result) == "One\n\t\tTwo"
|
||||
}
|
||||
|
||||
$0.it("can indent first line") {
|
||||
let template = Template(templateString: "{{ value|indent:2,\" \",true }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
||||
try expect(result) == " One\n Two"
|
||||
}
|
||||
|
||||
$0.it("does not indent empty lines") {
|
||||
let template = Template(templateString: "{{ value|indent }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One\n\n\nTwo\n\n"]))
|
||||
try expect(result) == "One\n\n\n Two\n\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Tests/StencilTests/FilterTagSpec.swift
Normal file
45
Tests/StencilTests/FilterTagSpec.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
func testFilterTag() {
|
||||
describe("Filter Tag") {
|
||||
$0.it("allows you to use a filter") {
|
||||
let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}")
|
||||
let result = try template.render()
|
||||
try expect(result) == "TEST"
|
||||
}
|
||||
|
||||
$0.it("allows you to chain filters") {
|
||||
let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}")
|
||||
let result = try template.render()
|
||||
try expect(result) == "Test"
|
||||
}
|
||||
|
||||
$0.it("errors without a filter") {
|
||||
let template = Template(templateString: "Some {% filter %}Test{% endfilter %}")
|
||||
try expect(try template.render()).toThrow()
|
||||
}
|
||||
|
||||
$0.it("can render filters with arguments") {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("split", filter: {
|
||||
return ($0 as! String).components(separatedBy: $1[0] as! String)
|
||||
})
|
||||
let env = Environment(extensions: [ext])
|
||||
let result = try env.renderTemplate(string: "{% filter split:\",\"|join:\";\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": [1, 2]])
|
||||
try expect(result) == "1;2"
|
||||
}
|
||||
|
||||
$0.it("can render filters with quote as an argument") {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("replace", filter: {
|
||||
print($1[0] as! String)
|
||||
return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String)
|
||||
})
|
||||
let env = Environment(extensions: [ext])
|
||||
let result = try env.renderTemplate(string: "{% filter replace:'\"',\"\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": ["\"1\"", "\"2\""]])
|
||||
try expect(result) == "1,2"
|
||||
}
|
||||
}
|
||||
}
|
||||
320
Tests/StencilTests/ForNodeSpec.swift
Normal file
320
Tests/StencilTests/ForNodeSpec.swift
Normal file
@@ -0,0 +1,320 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import Foundation
|
||||
|
||||
|
||||
func testForNode() {
|
||||
describe("ForNode") {
|
||||
let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"emptyItems": [Int](),
|
||||
"dict": [
|
||||
"one": "I",
|
||||
"two": "II",
|
||||
],
|
||||
"tuples": [(1, 2, 3), (4, 5, 6)]
|
||||
])
|
||||
|
||||
$0.it("renders the given nodes for each item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when no items found item") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type Array<Any>") {
|
||||
let any_context = Context(dictionary: [
|
||||
"items": ([1, 2, 3] as [Any])
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(any_context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type CountableClosedRange<Int>") {
|
||||
let context = Context(dictionary: ["range": 1...3])
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type CountableRange<Int>") {
|
||||
let context = Context(dictionary: ["range": 1..<4])
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("range"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
$0.it("renders a context variable of type NSArray") {
|
||||
let nsarray_context = Context(dictionary: [
|
||||
"items": NSArray(array: [1, 2, 3])
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(nsarray_context)) == "123"
|
||||
}
|
||||
#endif
|
||||
|
||||
$0.it("renders the given nodes while providing if the item is first in the context") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "1true2false3false"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing if the item is last in the context") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "1false2false3true"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing item counter") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "112233"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing item counter") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter0")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "102132"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing loop length") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "132333"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while filtering items using where expression") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
||||
try expect(try node.render(context)) == "2132"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
|
||||
$0.it("can render a filter with spaces") {
|
||||
let templateString = "{% for article in ars | default: a, b , articles %}" +
|
||||
"- {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"articles": [
|
||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||
]
|
||||
])
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "" +
|
||||
"- Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||
"- Memory Management with ARC by Kyle Fuller.\n" +
|
||||
"\n"
|
||||
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.context("given array of tuples") {
|
||||
$0.it("can iterate over all tuple values") {
|
||||
let templateString = "{% for first,second,third in tuples %}" +
|
||||
"{{ first }}, {{ second }}, {{ third }}\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "1, 2, 3\n4, 5, 6\n\n"
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can iterate with less number of variables") {
|
||||
let templateString = "{% for first,second in tuples %}" +
|
||||
"{{ first }}, {{ second }}\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "1, 2\n4, 5\n\n"
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can use _ to skip variables") {
|
||||
let templateString = "{% for first,_,third in tuples %}" +
|
||||
"{{ first }}, {{ third }}\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "1, 3\n4, 6\n\n"
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("throws when number of variables is more than number of tuple values") {
|
||||
let templateString = "{% for key,value,smth in dict %}" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
try expect(template.render(context)).toThrow()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$0.it("can iterate over dictionary") {
|
||||
let templateString = "{% for key, value in dict %}" +
|
||||
"{{ key }}: {{ value }}," +
|
||||
"{% endfor %}"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
||||
try expect(sortedResult) == ["one: I", "two: II"]
|
||||
}
|
||||
|
||||
$0.it("renders supports iterating over dictionary") {
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "key"),
|
||||
TextNode(text: ","),
|
||||
]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||
let result = try node.render(context)
|
||||
|
||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
||||
try expect(sortedResult) == ["one", "two"]
|
||||
}
|
||||
|
||||
$0.it("renders supports iterating over dictionary") {
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "key"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: ","),
|
||||
]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||
|
||||
let result = try node.render(context)
|
||||
|
||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
||||
try expect(sortedResult) == ["one=I", "two=II"]
|
||||
}
|
||||
|
||||
$0.it("handles invalid input") {
|
||||
let token = Token.block(value: "for i", at: .unknown)
|
||||
let parser = TokenParser(tokens: [token], environment: Environment())
|
||||
let error = TemplateSyntaxError(reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.", token: token)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("can iterate over struct properties") {
|
||||
struct MyStruct {
|
||||
let string: String
|
||||
let number: Int
|
||||
}
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"struct": MyStruct(string: "abc", number: 123)
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "property"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n"),
|
||||
]
|
||||
let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: [])
|
||||
let result = try node.render(context)
|
||||
|
||||
try expect(result) == "string=abc\nnumber=123\n"
|
||||
}
|
||||
|
||||
$0.it("can iterate tuple items") {
|
||||
let context = Context(dictionary: [
|
||||
"tuple": (one: 1, two: "dva"),
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "label"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n"),
|
||||
]
|
||||
|
||||
let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||
let result = try node.render(context)
|
||||
|
||||
try expect(result) == "one=1\ntwo=dva\n"
|
||||
}
|
||||
|
||||
$0.it("can iterate over class properties") {
|
||||
class MyClass {
|
||||
var baseString: String
|
||||
var baseInt: Int
|
||||
init(_ string: String, _ int: Int) {
|
||||
baseString = string
|
||||
baseInt = int
|
||||
}
|
||||
}
|
||||
|
||||
class MySubclass: MyClass {
|
||||
var childString: String
|
||||
init(_ childString: String, _ string: String, _ int: Int) {
|
||||
self.childString = childString
|
||||
super.init(string, int)
|
||||
}
|
||||
}
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"class": MySubclass("child", "base", 1)
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "label"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n"),
|
||||
]
|
||||
|
||||
let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||
let result = try node.render(context)
|
||||
|
||||
try expect(result) == "childString=child\nbaseString=base\nbaseInt=1\n"
|
||||
}
|
||||
|
||||
$0.it("can iterate in range of variables") {
|
||||
let template: Template = "{% for i in 1...j %}{{ i }}{% endfor %}"
|
||||
try expect(try template.render(Context(dictionary: ["j": 3]))) == "123"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
287
Tests/StencilTests/IfNodeSpec.swift
Normal file
287
Tests/StencilTests/IfNodeSpec.swift
Normal file
@@ -0,0 +1,287 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testIfNode() {
|
||||
describe("IfNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("can parse an if block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 1
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with else block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "else", at: .unknown),
|
||||
.text(value: "false", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 2
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let falseNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with elif block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "elif something", at: .unknown),
|
||||
.text(value: "some", at: .unknown),
|
||||
.block(value: "else", at: .unknown),
|
||||
.text(value: "false", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 3
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let elifNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(elifNode?.text) == "some"
|
||||
|
||||
try expect(conditions?[2].nodes.count) == 1
|
||||
let falseNode = conditions?[2].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with elif block without else") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "elif something", at: .unknown),
|
||||
.text(value: "some", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 2
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let elifNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(elifNode?.text) == "some"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with multiple elif block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "elif something1", at: .unknown),
|
||||
.text(value: "some1", at: .unknown),
|
||||
.block(value: "elif something2", at: .unknown),
|
||||
.text(value: "some2", at: .unknown),
|
||||
.block(value: "else", at: .unknown),
|
||||
.text(value: "false", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 4
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let elifNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(elifNode?.text) == "some1"
|
||||
|
||||
try expect(conditions?[2].nodes.count) == 1
|
||||
let elif2Node = conditions?[2].nodes.first as? TextNode
|
||||
try expect(elif2Node?.text) == "some2"
|
||||
|
||||
try expect(conditions?[3].nodes.count) == 1
|
||||
let falseNode = conditions?[3].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
|
||||
$0.it("can parse an if with complex expression") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value == \"test\" and not name", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.first is IfNode).beTrue()
|
||||
}
|
||||
|
||||
$0.it("can parse an ifnot block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "ifnot value", at: .unknown),
|
||||
.text(value: "false", at: .unknown),
|
||||
.block(value: "else", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 2
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let falseNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an if block without an endif") {
|
||||
let tokens: [Token] = [.block(value: "if value", at: .unknown)]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an ifnot without an endif") {
|
||||
let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("renders a true expression") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "1"
|
||||
}
|
||||
|
||||
$0.it("renders the first true expression") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "2"
|
||||
}
|
||||
|
||||
$0.it("renders the empty expression when other conditions are falsy") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "3"
|
||||
}
|
||||
|
||||
$0.it("renders empty when no truthy conditions") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == ""
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("supports variable filters in the if expression") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value|uppercase == \"TEST\"", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
|
||||
let result = try renderNodes(nodes, Context(dictionary: ["value": "test"]))
|
||||
try expect(result) == "true"
|
||||
}
|
||||
|
||||
$0.it("evaluates nil properties as false") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if instance.value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
|
||||
struct SomeType {
|
||||
let value: String? = nil
|
||||
}
|
||||
let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()]))
|
||||
try expect(result) == ""
|
||||
}
|
||||
|
||||
$0.it("supports closed range variables") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value in 1...3", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "else", at: .unknown),
|
||||
.text(value: "false", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
|
||||
try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true"
|
||||
try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
68
Tests/StencilTests/IncludeSpec.swift
Normal file
68
Tests/StencilTests/IncludeSpec.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
func testInclude() {
|
||||
describe("Include") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.describe("parsing") {
|
||||
$0.it("throws an error when no template is given") {
|
||||
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
let error = TemplateSyntaxError(reason: "'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file", token: tokens.first)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("can parse a valid include block") {
|
||||
let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IncludeNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.templateName) == Variable("\"test.html\"")
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("throws an error when rendering without a loader") {
|
||||
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
||||
|
||||
do {
|
||||
_ = try node.render(Context())
|
||||
} catch {
|
||||
try expect("\(error)") == "Template named `test.html` does not exist. No loaders found"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("throws an error when it cannot find the included template") {
|
||||
let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown))
|
||||
|
||||
do {
|
||||
_ = try node.render(Context(environment: environment))
|
||||
} catch {
|
||||
try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("successfully renders a found included template") {
|
||||
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
||||
let context = Context(dictionary: ["target": "World"], environment: environment)
|
||||
let value = try node.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
|
||||
$0.it("successfully passes context") {
|
||||
let template = Template(templateString: "{% include \"test.html\" child %}")
|
||||
let context = Context(dictionary: ["child": ["target": "World"]], environment: environment)
|
||||
let value = try template.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Tests/StencilTests/InheritenceSpec.swift
Normal file
27
Tests/StencilTests/InheritenceSpec.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
func testInheritence() {
|
||||
describe("Inheritence") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.it("can inherit from another template") {
|
||||
let template = try environment.loadTemplate(name: "child.html")
|
||||
try expect(try template.render()) == "Super_Header Child_Header\nChild_Body"
|
||||
}
|
||||
|
||||
$0.it("can inherit from another template inheriting from another template") {
|
||||
let template = try environment.loadTemplate(name: "child-child.html")
|
||||
try expect(try template.render()) == "Super_Header Child_Header Child_Child_Header\nChild_Body"
|
||||
}
|
||||
|
||||
$0.it("can inherit from a template that calls a super block") {
|
||||
let template = try environment.loadTemplate(name: "child-super.html")
|
||||
try expect(try template.render()) == "Header\nChild_Body"
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Tests/StencilTests/LexerSpec.swift
Normal file
94
Tests/StencilTests/LexerSpec.swift
Normal file
@@ -0,0 +1,94 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testLexer() {
|
||||
describe("Lexer") {
|
||||
$0.it("can tokenize text") {
|
||||
let lexer = Lexer(templateString: "Hello World")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "Hello World", at: SourceMap(line: ("Hello World", 1, 0)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize a comment") {
|
||||
let lexer = Lexer(templateString: "{# Comment #}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .comment(value: "Comment", at: SourceMap(line: ("{# Comment #}", 1, 3)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize a variable") {
|
||||
let lexer = Lexer(templateString: "{{ Variable }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(line: ("{{ Variable }}", 1, 3)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize unclosed tag by ignoring it") {
|
||||
let templateString = "{{ thing"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "", at: SourceMap(line: ("{{ thing", 1, 0)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize a mixture of content") {
|
||||
let templateString = "My name is {{ myname }}."
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 3
|
||||
try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "My name is ")!)))
|
||||
try expect(tokens[1]) == Token.variable(value: "myname", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "myname")!)))
|
||||
try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize two variables without being greedy") {
|
||||
let templateString = "{{ thing }}{{ name }}"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == Token.variable(value: "thing", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "thing")!)))
|
||||
try expect(tokens[1]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name")!)))
|
||||
}
|
||||
|
||||
$0.it("can tokenize an unclosed block") {
|
||||
let lexer = Lexer(templateString: "{%}")
|
||||
let _ = lexer.tokenize()
|
||||
}
|
||||
|
||||
$0.it("can tokenize an empty variable") {
|
||||
let lexer = Lexer(templateString: "{{}}")
|
||||
let _ = lexer.tokenize()
|
||||
}
|
||||
|
||||
$0.it("can tokenize with new lines") {
|
||||
let templateString =
|
||||
"My name is {%\n" +
|
||||
" if name\n" +
|
||||
" and\n" +
|
||||
" name\n" +
|
||||
"%}{{\n" +
|
||||
"name\n" +
|
||||
"}}{%\n" +
|
||||
"endif %}."
|
||||
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 5
|
||||
try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "My name is")!)))
|
||||
try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "{%")!)))
|
||||
try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name", options: [.backwards])!)))
|
||||
try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "endif")!)))
|
||||
try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!)))
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Tests/StencilTests/LoaderSpec.swift
Normal file
55
Tests/StencilTests/LoaderSpec.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
func testTemplateLoader() {
|
||||
describe("FileSystemLoader") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.it("errors when a template cannot be found") {
|
||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("errors when an array of templates cannot be found") {
|
||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a file") {
|
||||
_ = try environment.loadTemplate(name: "test.html")
|
||||
}
|
||||
|
||||
$0.it("errors when loading absolute file outside of the selected path") {
|
||||
try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("errors when loading relative file outside of the selected path") {
|
||||
try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
describe("DictionaryLoader") {
|
||||
let loader = DictionaryLoader(templates: [
|
||||
"index.html": "Hello World"
|
||||
])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.it("errors when a template cannot be found") {
|
||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("errors when an array of templates cannot be found") {
|
||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a known templates") {
|
||||
_ = try environment.loadTemplate(name: "index.html")
|
||||
}
|
||||
|
||||
$0.it("can load a known template from a collection of templates") {
|
||||
_ = try environment.loadTemplate(names: ["unknown.html", "index.html"])
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Tests/StencilTests/NodeSpec.swift
Normal file
65
Tests/StencilTests/NodeSpec.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
class ErrorNode : NodeType {
|
||||
let token: Token?
|
||||
init(token: Token? = nil) {
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
throw TemplateSyntaxError("Custom Error")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func testNode() {
|
||||
describe("Node") {
|
||||
let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"age": 27,
|
||||
"items": [1, 2, 3],
|
||||
])
|
||||
|
||||
$0.describe("TextNode") {
|
||||
$0.it("renders the given text") {
|
||||
let node = TextNode(text: "Hello World")
|
||||
try expect(try node.render(context)) == "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("VariableNode") {
|
||||
$0.it("resolves and renders the variable") {
|
||||
let node = VariableNode(variable: Variable("name"))
|
||||
try expect(try node.render(context)) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("resolves and renders a non string variable") {
|
||||
let node = VariableNode(variable: Variable("age"))
|
||||
try expect(try node.render(context)) == "27"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering nodes") {
|
||||
$0.it("renders the nodes") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)) == "Hello Kyle"
|
||||
}
|
||||
|
||||
$0.it("correctly throws a nodes failure") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
ErrorNode(),
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)).toThrow(TemplateSyntaxError("Custom Error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Tests/StencilTests/NowNodeSpec.swift
Normal file
43
Tests/StencilTests/NowNodeSpec.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testNowNode() {
|
||||
#if !os(Linux)
|
||||
describe("NowNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("parses default format without any now arguments") {
|
||||
let tokens: [Token] = [ .block(value: "now", at: .unknown) ]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\""
|
||||
}
|
||||
|
||||
$0.it("parses now with a format") {
|
||||
let tokens: [Token] = [ .block(value: "now \"HH:mm\"", at: .unknown) ]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"HH:mm\""
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("renders the date") {
|
||||
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let date = formatter.string(from: NSDate() as Date)
|
||||
|
||||
try expect(try node.render(Context())) == date
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
61
Tests/StencilTests/ParserSpec.swift
Normal file
61
Tests/StencilTests/ParserSpec.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testTokenParser() {
|
||||
describe("TokenParser") {
|
||||
$0.it("can parse a text token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.text(value: "Hello World", at: .unknown)
|
||||
], environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? TextNode
|
||||
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.text) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can parse a variable token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.variable(value: "'name'", at: .unknown)
|
||||
], environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? VariableNode
|
||||
try expect(nodes.count) == 1
|
||||
let result = try node?.render(Context())
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can parse a comment token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.comment(value: "Secret stuff!", at: .unknown)
|
||||
], environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 0
|
||||
}
|
||||
|
||||
$0.it("can parse a tag token") {
|
||||
let simpleExtension = Extension()
|
||||
simpleExtension.registerSimpleTag("known") { _ in
|
||||
return ""
|
||||
}
|
||||
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "known", at: .unknown),
|
||||
], environment: Environment(extensions: [simpleExtension]))
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 1
|
||||
}
|
||||
|
||||
$0.it("errors when parsing an unknown tag") {
|
||||
let tokens: [Token] = [.block(value: "unknown", at: .unknown)]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError(reason: "Unknown template tag 'unknown'", token: tokens.first))
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Tests/StencilTests/StencilSpec.swift
Normal file
70
Tests/StencilTests/StencilSpec.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
|
||||
|
||||
fileprivate struct CustomNode : NodeType {
|
||||
let token: Token?
|
||||
func render(_ context:Context) throws -> String {
|
||||
return "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
|
||||
|
||||
func testStencil() {
|
||||
describe("Stencil") {
|
||||
let exampleExtension = Extension()
|
||||
|
||||
exampleExtension.registerSimpleTag("simpletag") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
|
||||
exampleExtension.registerTag("customtag") { parser, token in
|
||||
return CustomNode(token: token)
|
||||
}
|
||||
|
||||
let environment = Environment(extensions: [exampleExtension])
|
||||
|
||||
$0.it("can render the README example") {
|
||||
|
||||
let templateString = "There are {{ articles.count }} articles.\n" +
|
||||
"\n" +
|
||||
"{% for article in articles %}" +
|
||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let context = [
|
||||
"articles": [
|
||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||
]
|
||||
]
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "There are 2 articles.\n" +
|
||||
"\n" +
|
||||
" - Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||
" - Memory Management with ARC by Kyle Fuller.\n" +
|
||||
"\n"
|
||||
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can render a custom template tag") {
|
||||
let result = try environment.renderTemplate(string: "{% customtag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a simple custom tag") {
|
||||
let result = try environment.renderTemplate(string: "{% simpletag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Tests/StencilTests/TemplateSpec.swift
Normal file
20
Tests/StencilTests/TemplateSpec.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testTemplate() {
|
||||
describe("Template") {
|
||||
$0.it("can render a template from a string") {
|
||||
let template = Template(templateString: "Hello World")
|
||||
let result = try template.render([ "name": "Kyle" ])
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a template from a string literal") {
|
||||
let template: Template = "Hello World"
|
||||
let result = try template.render([ "name": "Kyle" ])
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
34
Tests/StencilTests/TokenSpec.swift
Normal file
34
Tests/StencilTests/TokenSpec.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testToken() {
|
||||
describe("Token") {
|
||||
$0.it("can split the contents into components") {
|
||||
let token = Token.text(value: "hello world", at: .unknown)
|
||||
let components = token.components()
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "world"
|
||||
}
|
||||
|
||||
$0.it("can split the contents into components with single quoted strings") {
|
||||
let token = Token.text(value: "hello 'kyle fuller'", at: .unknown)
|
||||
let components = token.components()
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "'kyle fuller'"
|
||||
}
|
||||
|
||||
$0.it("can split the contents into components with double quoted strings") {
|
||||
let token = Token.text(value: "hello \"kyle fuller\"", at: .unknown)
|
||||
let components = token.components()
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "\"kyle fuller\""
|
||||
}
|
||||
}
|
||||
}
|
||||
334
Tests/StencilTests/VariableSpec.swift
Normal file
334
Tests/StencilTests/VariableSpec.swift
Normal file
@@ -0,0 +1,334 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
#if os(OSX)
|
||||
@objc class Superclass: NSObject {
|
||||
@objc let name = "Foo"
|
||||
}
|
||||
@objc class Object : Superclass {
|
||||
@objc let title = "Hello World"
|
||||
}
|
||||
#endif
|
||||
|
||||
fileprivate struct Person {
|
||||
let name: String
|
||||
}
|
||||
|
||||
fileprivate struct Article {
|
||||
let author: Person
|
||||
}
|
||||
|
||||
fileprivate class WebSite {
|
||||
let url: String = "blog.com"
|
||||
}
|
||||
|
||||
fileprivate class Blog: WebSite {
|
||||
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
|
||||
let featuring: Article? = Article(author: Person(name: "Jhon"))
|
||||
}
|
||||
|
||||
func testVariable() {
|
||||
describe("Variable") {
|
||||
let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"contacts": ["Katie", "Carlton"],
|
||||
"profiles": [
|
||||
"github": "kylef",
|
||||
],
|
||||
"counter": [
|
||||
"count": "kylef",
|
||||
],
|
||||
"article": Article(author: Person(name: "Kyle")),
|
||||
"tuple": (one: 1, two: 2)
|
||||
])
|
||||
|
||||
#if os(OSX)
|
||||
context["object"] = Object()
|
||||
#endif
|
||||
context["blog"] = Blog()
|
||||
|
||||
$0.it("can resolve a string literal with double quotes") {
|
||||
let variable = Variable("\"name\"")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve a string literal with single quotes") {
|
||||
let variable = Variable("'name'")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve an integer literal") {
|
||||
let variable = Variable("5")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 5
|
||||
}
|
||||
|
||||
$0.it("can resolve an float literal") {
|
||||
let variable = Variable("3.14")
|
||||
let result = try variable.resolve(context) as? Number
|
||||
try expect(result) == 3.14
|
||||
}
|
||||
|
||||
$0.it("can resolve boolean literal") {
|
||||
try expect(Variable("true").resolve(context) as? Bool) == true
|
||||
try expect(Variable("false").resolve(context) as? Bool) == false
|
||||
try expect(Variable("0").resolve(context) as? Int) == 0
|
||||
try expect(Variable("1").resolve(context) as? Int) == 1
|
||||
}
|
||||
|
||||
$0.it("can resolve a string variable") {
|
||||
let variable = Variable("name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from a dictionary") {
|
||||
let variable = Variable("profiles.github")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "kylef"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from an array via it's index") {
|
||||
let variable = Variable("contacts.0")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
|
||||
let variable1 = Variable("contacts.1")
|
||||
let result1 = try variable1.resolve(context) as? String
|
||||
try expect(result1) == "Carlton"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from an array via unknown index") {
|
||||
let variable = Variable("contacts.5")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result).to.beNil()
|
||||
|
||||
let variable1 = Variable("contacts.-5")
|
||||
let result1 = try variable1.resolve(context) as? String
|
||||
try expect(result1).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("can resolve the first item from an array") {
|
||||
let variable = Variable("contacts.first")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("can resolve the last item from an array") {
|
||||
let variable = Variable("contacts.last")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Carlton"
|
||||
}
|
||||
|
||||
$0.it("can resolve a property with reflection") {
|
||||
let variable = Variable("article.author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can get the count of a dictionary") {
|
||||
let variable = Variable("profiles.count")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 1
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
$0.it("can resolve a value via KVO") {
|
||||
let variable = Variable("object.title")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can resolve a superclass value via KVO") {
|
||||
let variable = Variable("object.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Foo"
|
||||
}
|
||||
#endif
|
||||
|
||||
$0.it("can resolve a value via reflection") {
|
||||
let variable = Variable("blog.articles.0.author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can resolve a superclass value via reflection") {
|
||||
let variable = Variable("blog.url")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "blog.com"
|
||||
}
|
||||
|
||||
$0.it("can resolve optional variable property using reflection") {
|
||||
let variable = Variable("blog.featuring.author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Jhon"
|
||||
}
|
||||
|
||||
$0.it("does not render Optional") {
|
||||
var array: [Any?] = [1, nil]
|
||||
array.append(array)
|
||||
let context = Context(dictionary: ["values": array])
|
||||
|
||||
try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]"
|
||||
try expect(VariableNode(variable: "values.1").render(context)) == ""
|
||||
}
|
||||
|
||||
$0.it("can subscript tuple by index") {
|
||||
let variable = Variable("tuple.0")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 1
|
||||
}
|
||||
|
||||
$0.it("can subscript tuple by label") {
|
||||
let variable = Variable("tuple.two")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 2
|
||||
}
|
||||
|
||||
$0.describe("Subrscripting") {
|
||||
$0.it("can resolve a property subscript via reflection") {
|
||||
try context.push(dictionary: ["property": "name"]) {
|
||||
let variable = Variable("article.author[property]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can subscript an array with a valid index") {
|
||||
try context.push(dictionary: ["property": 0]) {
|
||||
let variable = Variable("contacts[property]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can subscript an array with an unknown index") {
|
||||
try context.push(dictionary: ["property": 5]) {
|
||||
let variable = Variable("contacts[property]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result).to.beNil()
|
||||
}
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
$0.it("can resolve a subscript via KVO") {
|
||||
try context.push(dictionary: ["property": "name"]) {
|
||||
let variable = Variable("object[property]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Foo"
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
$0.it("can resolve an optional subscript via reflection") {
|
||||
try context.push(dictionary: ["property": "featuring"]) {
|
||||
let variable = Variable("blog[property].author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Jhon"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can resolve multiple subscripts") {
|
||||
try context.push(dictionary: [
|
||||
"prop1": "articles",
|
||||
"prop2": 0,
|
||||
"prop3": "name"
|
||||
]) {
|
||||
let variable = Variable("blog[prop1][prop2].author[prop3]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can resolve nested subscripts") {
|
||||
try context.push(dictionary: [
|
||||
"prop1": "prop2",
|
||||
"ref": ["prop2": "name"]
|
||||
]) {
|
||||
let variable = Variable("article.author[ref[prop1]]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("throws for invalid keypath syntax") {
|
||||
try context.push(dictionary: ["prop": "name"]) {
|
||||
let samples = [
|
||||
".",
|
||||
"..",
|
||||
".test",
|
||||
"test..test",
|
||||
"[prop]",
|
||||
"article.author[prop",
|
||||
"article.author[[prop]",
|
||||
"article.author[prop]]",
|
||||
"article.author[]",
|
||||
"article.author[[]]",
|
||||
"article.author[prop][]",
|
||||
"article.author[prop]comments",
|
||||
"article.author[.]"
|
||||
]
|
||||
|
||||
for lookup in samples {
|
||||
let variable = Variable(lookup)
|
||||
try expect(variable.resolve(context)).toThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("RangeVariable") {
|
||||
|
||||
let context: Context = {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("incr", filter: { (arg: Any?) in toNumber(value: arg!)! + 1 })
|
||||
let environment = Environment(extensions: [ext])
|
||||
return Context(dictionary: [:], environment: environment)
|
||||
}()
|
||||
|
||||
func makeVariable(_ token: String) throws -> RangeVariable? {
|
||||
let token = Token.variable(value: token, at: .unknown)
|
||||
let parser = TokenParser(tokens: [token], environment: context.environment)
|
||||
return try RangeVariable(token.contents, parser: parser, containedIn: token)
|
||||
}
|
||||
|
||||
$0.it("can resolve closed range as array") {
|
||||
let result = try makeVariable("1...3")?.resolve(context) as? [Int]
|
||||
try expect(result) == [1, 2, 3]
|
||||
}
|
||||
|
||||
$0.it("can resolve decreasing closed range as reversed array") {
|
||||
let result = try makeVariable("3...1")?.resolve(context) as? [Int]
|
||||
try expect(result) == [3, 2, 1]
|
||||
}
|
||||
|
||||
$0.it("can use filter on range variables") {
|
||||
let result = try makeVariable("1|incr...3|incr")?.resolve(context) as? [Int]
|
||||
try expect(result) == [2, 3, 4]
|
||||
}
|
||||
|
||||
$0.it("throws when left value is not int") {
|
||||
let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}"
|
||||
try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow()
|
||||
}
|
||||
|
||||
$0.it("throws when right value is not int") {
|
||||
let variable = try makeVariable("k...j")
|
||||
try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow()
|
||||
}
|
||||
|
||||
$0.it("throws is left range value is missing") {
|
||||
try expect(makeVariable("...1")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("throws is right range value is missing") {
|
||||
try expect(makeVariable("1...")).toThrow()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
30
Tests/StencilTests/XCTest.swift
Normal file
30
Tests/StencilTests/XCTest.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
import XCTest
|
||||
|
||||
|
||||
public func stencilTests() {
|
||||
testContext()
|
||||
testFilter()
|
||||
testLexer()
|
||||
testToken()
|
||||
testTokenParser()
|
||||
testTemplateLoader()
|
||||
testTemplate()
|
||||
testVariable()
|
||||
testNode()
|
||||
testForNode()
|
||||
testExpressions()
|
||||
testIfNode()
|
||||
testNowNode()
|
||||
testInclude()
|
||||
testInheritence()
|
||||
testFilterTag()
|
||||
testEnvironment()
|
||||
testStencil()
|
||||
}
|
||||
|
||||
|
||||
class StencilTests: XCTestCase {
|
||||
func testRunStencilTests() {
|
||||
stencilTests()
|
||||
}
|
||||
}
|
||||
2
Tests/StencilTests/fixtures/base.html
Normal file
2
Tests/StencilTests/fixtures/base.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% block header %}Header{% endblock %}
|
||||
{% block body %}Body{% endblock %}
|
||||
2
Tests/StencilTests/fixtures/child-child.html
Normal file
2
Tests/StencilTests/fixtures/child-child.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% extends "child.html" %}
|
||||
{% block header %}{{ block.super }} Child_Child_Header{% endblock %}
|
||||
3
Tests/StencilTests/fixtures/child-super.html
Normal file
3
Tests/StencilTests/fixtures/child-super.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body %}Child_{{ block.super }}{% endblock %}
|
||||
|
||||
3
Tests/StencilTests/fixtures/child.html
Normal file
3
Tests/StencilTests/fixtures/child.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block header %}Super_{{ block.super }} Child_Header{% endblock %}
|
||||
{% block body %}Child_Body{% endblock %}
|
||||
2
Tests/StencilTests/fixtures/invalid-base.html
Normal file
2
Tests/StencilTests/fixtures/invalid-base.html
Normal file
@@ -0,0 +1,2 @@
|
||||
{% block header %}Header{% endblock %}
|
||||
{% block body %}Body {{ target|unknown }} {% endblock %}
|
||||
3
Tests/StencilTests/fixtures/invalid-child-super.html
Normal file
3
Tests/StencilTests/fixtures/invalid-child-super.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{% extends "invalid-base.html" %}
|
||||
{% block body %}Child {{ block.super }}{% endblock %}
|
||||
|
||||
1
Tests/StencilTests/fixtures/invalid-include.html
Normal file
1
Tests/StencilTests/fixtures/invalid-include.html
Normal file
@@ -0,0 +1 @@
|
||||
Hello {{ target|unknown }}!
|
||||
1
Tests/StencilTests/fixtures/test.html
Normal file
1
Tests/StencilTests/fixtures/test.html
Normal file
@@ -0,0 +1 @@
|
||||
Hello {{ target }}!
|
||||
225
docs/Makefile
Normal file
225
docs/Makefile
Normal file
@@ -0,0 +1,225 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " epub3 to make an epub3"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
@echo " dummy to check syntax errors of document sources"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
.PHONY: html
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
.PHONY: dirhtml
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
.PHONY: singlehtml
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
.PHONY: pickle
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
.PHONY: json
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
.PHONY: htmlhelp
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
.PHONY: qthelp
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Stencil.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Stencil.qhc"
|
||||
|
||||
.PHONY: applehelp
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
.PHONY: devhelp
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Stencil"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Stencil"
|
||||
@echo "# devhelp"
|
||||
|
||||
.PHONY: epub
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
.PHONY: epub3
|
||||
epub3:
|
||||
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
|
||||
@echo
|
||||
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
|
||||
|
||||
.PHONY: latex
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
.PHONY: latexpdf
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: latexpdfja
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: text
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
.PHONY: man
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
.PHONY: texinfo
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
.PHONY: info
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
.PHONY: gettext
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
.PHONY: changes
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
.PHONY: linkcheck
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
.PHONY: doctest
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
.PHONY: xml
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
.PHONY: pseudoxml
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
|
||||
.PHONY: dummy
|
||||
dummy:
|
||||
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
||||
@echo
|
||||
@echo "Build finished. Dummy builder generates no files."
|
||||
33
docs/_templates/sidebar_intro.html
vendored
Normal file
33
docs/_templates/sidebar_intro.html
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<h1><a href="/">Stencil</a></h1>
|
||||
|
||||
<p>
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=kylef&repo=Stencil&type=watch&count=true&size=large"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px">
|
||||
</iframe>
|
||||
</p>
|
||||
|
||||
<p>Stencil is a simple and powerful template language for Swift.</p>
|
||||
|
||||
<div class="social">
|
||||
<p>
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=kylef&type=follow&count=false"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200" height="20">
|
||||
</iframe>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://twitter.com/kylefuller" class="twitter-follow-button" data-show-count="false">Follow @kylefuller</a> <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Other Projects</h3>
|
||||
|
||||
<p>More <a href="https://fuller.li/">Kyle Fuller</a> projects:</p>
|
||||
<ul>
|
||||
<li><a href="https://github.com/kylef/Commander">Commander</a></li>
|
||||
<li><a href="https://curassow.fuller.li/">Curassow</a></li>
|
||||
<li><a href="https://github.com/kylef/Spectre">Spectre</a></li>
|
||||
<li><a href="https://github.com/kylef/heroku-buildpack-swift">Heroku Swift buildpack</a></li>
|
||||
</ul>
|
||||
150
docs/api.rst
Normal file
150
docs/api.rst
Normal file
@@ -0,0 +1,150 @@
|
||||
Template API
|
||||
============
|
||||
|
||||
This document describes Stencils Swift API, and not the Swift template language.
|
||||
|
||||
.. contents:: :depth: 2
|
||||
|
||||
Environment
|
||||
-----------
|
||||
|
||||
An environment contains shared configuration such as custom filters and tags
|
||||
along with template loaders.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let environment = Environment()
|
||||
|
||||
You can optionally provide a loader or extensions when creating an environment:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let environment = Environment(loader: ..., extensions: [...])
|
||||
|
||||
Rendering a Template
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Environment provides convinience methods to render a template either from a
|
||||
string or a template loader.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let template = "Hello {{ name }}"
|
||||
let context = ["name": "Kyle"]
|
||||
let rendered = environment.renderTemplate(string: template, context: context)
|
||||
|
||||
Rendering a template from the configured loader:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let context = ["name": "Kyle"]
|
||||
let rendered = environment.renderTemplate(name: "example.html", context: context)
|
||||
|
||||
Loading a Template
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Environment provides an API to load a template from the configured loader.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let template = try environment.loadTemplate(name: "example.html")
|
||||
|
||||
Loader
|
||||
------
|
||||
|
||||
Loaders are responsible for loading templates from a resource such as the file
|
||||
system.
|
||||
|
||||
Stencil provides a ``FileSytemLoader`` which allows you to load a template
|
||||
directly from the file system.
|
||||
|
||||
FileSystemLoader
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Loads templates from the file system. This loader can find templates in folders
|
||||
on the file system.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
FileSystemLoader(paths: ["./templates"])
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
FileSystemLoader(bundle: [Bundle.main])
|
||||
|
||||
|
||||
DictionaryLoader
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Loads templates from a dictionary.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
DictionaryLoader(templates: ["index.html": "Hello World"])
|
||||
|
||||
|
||||
Custom Loaders
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
``Loader`` is a protocol, so you can implement your own compatible loaders. You
|
||||
will need to implement a ``loadTemplate`` method to load the template,
|
||||
throwing a ``TemplateDoesNotExist`` when the template is not found.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
class ExampleMemoryLoader: Loader {
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
if name == "index.html" {
|
||||
return Template(templateString: "Hello", environment: environment)
|
||||
}
|
||||
|
||||
throw TemplateDoesNotExist(name: name, loader: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
A ``Context`` is a structure containing any templates you would like to use in
|
||||
a template. It’s somewhat like a dictionary, however you can push and pop to
|
||||
scope variables. So that means that when iterating over a for loop, you can
|
||||
push a new scope into the context to store any variables local to the scope.
|
||||
|
||||
You would normally only access the ``Context`` within a custom template tag or
|
||||
filter.
|
||||
|
||||
Subscripting
|
||||
~~~~~~~~~~~~
|
||||
|
||||
You can use subscripting to get and set values from the context.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
context["key"] = value
|
||||
let value = context["key"]
|
||||
|
||||
``push()``
|
||||
~~~~~~~~~~
|
||||
|
||||
A ``Context`` is a stack. You can push a new level onto the ``Context`` so that
|
||||
modifications can easily be poped off. This is useful for isolating mutations
|
||||
into scope of a template tag. Such as ``{% if %}`` and ``{% for %}`` tags.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
context.push(["name": "example"]) {
|
||||
// context contains name which is `example`.
|
||||
}
|
||||
|
||||
// name is popped off the context after the duration of the closure.
|
||||
|
||||
``flatten()``
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Using ``flatten()`` method you can get whole ``Context`` stack as one
|
||||
dictionary including all variables.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let dictionary = context.flatten()
|
||||
375
docs/builtins.rst
Normal file
375
docs/builtins.rst
Normal file
@@ -0,0 +1,375 @@
|
||||
Built-in template tags and filters
|
||||
==================================
|
||||
|
||||
.. _built-in-tags:
|
||||
|
||||
Built-in Tags
|
||||
-------------
|
||||
|
||||
``for``
|
||||
~~~~~~~
|
||||
|
||||
A for loop allows you to iterate over an array found by variable lookup.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li>{{ user }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
The ``for`` tag can iterate over dictionaries.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<ul>
|
||||
{% for key, value in dict %}
|
||||
<li>{{ key }}: {{ value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
It can also iterate over ranges, tuple elements, structs' and classes' stored properties (using ``Mirror``).
|
||||
|
||||
You can iterate over range literals created using ``N...M`` syntax, both in ascending and descending order:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<ul>
|
||||
{% for i in 1...array.count %}
|
||||
<li>{{ i }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
The ``for`` tag can contain optional ``where`` expression to filter out
|
||||
elements on which this expression evaluates to false.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<ul>
|
||||
{% for user in users where user.name != "Kyle" %}
|
||||
<li>{{ user }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
The ``for`` tag can take an optional ``{% empty %}`` block that will be
|
||||
displayed if the given list is empty or could not be found.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<ul>
|
||||
{% for user in users %}
|
||||
<li>{{ user }}</li>
|
||||
{% empty %}
|
||||
<li>There are no users.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
The for block sets a few variables available within the loop:
|
||||
|
||||
- ``first`` - True if this is the first time through the loop
|
||||
- ``last`` - True if this is the last time through the loop
|
||||
- ``counter`` - The current iteration of the loop (1 indexed)
|
||||
- ``counter0`` - The current iteration of the loop (0 indexed)
|
||||
- ``length`` - The total length of the loop
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% for user in users %}
|
||||
{% if forloop.first %}
|
||||
This is the first user.
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% for user in users %}
|
||||
This is user number {{ forloop.counter }} user.
|
||||
{% endfor %}
|
||||
|
||||
|
||||
``if``
|
||||
~~~~~~
|
||||
|
||||
The ``{% if %}`` tag evaluates a variable, and if that variable evaluates to
|
||||
true the contents of the block are processed. Being true is defined as:
|
||||
|
||||
* Present in the context
|
||||
* Being non-empty (dictionaries or arrays)
|
||||
* Not being a false boolean value
|
||||
* Not being a numerical value of 0 or below
|
||||
* Not being an empty string
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if admin %}
|
||||
The user is an administrator.
|
||||
{% elif user %}
|
||||
A user is logged in.
|
||||
{% else %}
|
||||
No user was found.
|
||||
{% endif %}
|
||||
|
||||
Operators
|
||||
^^^^^^^^^
|
||||
|
||||
``if`` tags may combine ``and``, ``or`` and ``not`` to test multiple variables
|
||||
or to negate a variable.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if one and two %}
|
||||
Both one and two evaluate to true.
|
||||
{% endif %}
|
||||
|
||||
{% if not one %}
|
||||
One evaluates to false
|
||||
{% endif %}
|
||||
|
||||
{% if one or two %}
|
||||
Either one or two evaluates to true.
|
||||
{% endif %}
|
||||
|
||||
{% if not one or two %}
|
||||
One does not evaluate to false or two evaluates to true.
|
||||
{% endif %}
|
||||
|
||||
You may use ``and``, ``or`` and ``not`` multiple times together. ``not`` has
|
||||
higest precedence followed by ``and``. For example:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if one or two and three %}
|
||||
|
||||
Will be treated as:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
one or (two and three)
|
||||
|
||||
``==`` operator
|
||||
"""""""""""""""
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if value == other_value %}
|
||||
value is equal to other_value
|
||||
{% endif %}
|
||||
|
||||
.. note:: The equality operator only supports numerical, string and boolean types.
|
||||
|
||||
``!=`` operator
|
||||
"""""""""""""""
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if value != other_value %}
|
||||
value is not equal to other_value
|
||||
{% endif %}
|
||||
|
||||
.. note:: The inequality operator only supports numerical, string and boolean types.
|
||||
|
||||
``<`` operator
|
||||
"""""""""""""""
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if value < other_value %}
|
||||
value is less than other_value
|
||||
{% endif %}
|
||||
|
||||
.. note:: The less than operator only supports numerical types.
|
||||
|
||||
``<=`` operator
|
||||
"""""""""""""""
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if value <= other_value %}
|
||||
value is less than or equal to other_value
|
||||
{% endif %}
|
||||
|
||||
.. note:: The less than equal operator only supports numerical types.
|
||||
|
||||
``>`` operator
|
||||
"""""""""""""""
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if value > other_value %}
|
||||
value is more than other_value
|
||||
{% endif %}
|
||||
|
||||
.. note:: The more than operator only supports numerical types.
|
||||
|
||||
``>=`` operator
|
||||
"""""""""""""""
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if value >= other_value %}
|
||||
value is more than or equal to other_value
|
||||
{% endif %}
|
||||
|
||||
.. note:: The more than equal operator only supports numerical types.
|
||||
|
||||
``ifnot``
|
||||
~~~~~~~~~
|
||||
|
||||
.. note:: ``{% ifnot %}`` is deprecated. You should use ``{% if not %}``.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% ifnot variable %}
|
||||
The variable was NOT found in the current context.
|
||||
{% else %}
|
||||
The variable was found.
|
||||
{% endif %}
|
||||
|
||||
``now``
|
||||
~~~~~~~
|
||||
|
||||
``filter``
|
||||
~~~~~~~~~~
|
||||
|
||||
Filters the contents of the block.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% filter lowercase %}
|
||||
This Text Will Be Lowercased.
|
||||
{% endfilter %}
|
||||
|
||||
You can chain multiple filters with a pipe (`|`).
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% filter lowercase|capitalize %}
|
||||
This Text Will First Be Lowercased, Then The First Character Will BE
|
||||
Capitalised.
|
||||
{% endfilter %}
|
||||
|
||||
``include``
|
||||
~~~~~~~~~~~
|
||||
|
||||
You can include another template using the `include` tag.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% include "comment.html" %}
|
||||
|
||||
By default the included file gets passed the current context. You can pass a sub context by using an optional 2nd parameter as a lookup in the current context.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% include "comment.html" comment %}
|
||||
|
||||
The `include` tag requires you to provide a loader which will be used to lookup
|
||||
the template.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let environment = Environment(bundle: [Bundle.main])
|
||||
let template = environment.loadTemplate(name: "index.html")
|
||||
|
||||
``extends``
|
||||
~~~~~~~~~~~
|
||||
|
||||
Extends the template from a parent template.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
See :ref:`template-inheritance` for more information.
|
||||
|
||||
``block``
|
||||
~~~~~~~~~
|
||||
|
||||
Defines a block that can be overridden by child templates. See
|
||||
:ref:`template-inheritance` for more information.
|
||||
|
||||
.. _built-in-filters:
|
||||
|
||||
Built-in Filters
|
||||
----------------
|
||||
|
||||
``capitalize``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The capitalize filter allows you to capitalize a string.
|
||||
For example, `stencil` to `Stencil`. Can be applied to array of strings to change each string.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ "stencil"|capitalize }}
|
||||
|
||||
``uppercase``
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The uppercase filter allows you to transform a string to uppercase.
|
||||
For example, `Stencil` to `STENCIL`. Can be applied to array of strings to change each string.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ "Stencil"|uppercase }}
|
||||
|
||||
``lowercase``
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The uppercase filter allows you to transform a string to lowercase.
|
||||
For example, `Stencil` to `stencil`. Can be applied to array of strings to change each string.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ "Stencil"|lowercase }}
|
||||
|
||||
``default``
|
||||
~~~~~~~~~~~
|
||||
|
||||
If a variable not present in the context, use given default. Otherwise, use the
|
||||
value of the variable. For example:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
Hello {{ name|default:"World" }}
|
||||
|
||||
``join``
|
||||
~~~~~~~~
|
||||
|
||||
Join an array of items.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ value|join:", " }}
|
||||
|
||||
.. note:: The value MUST be an array. Default argument value is empty string.
|
||||
|
||||
``split``
|
||||
~~~~~~~~~
|
||||
|
||||
Split string into substrings by separator.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ value|split:", " }}
|
||||
|
||||
.. note:: The value MUST be a String. Default argument value is a single-space string.
|
||||
|
||||
``indent``
|
||||
~~~~~~~~~
|
||||
|
||||
Indents lines of rendered value or block.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ value|indent:2," ",true }}
|
||||
|
||||
Filter accepts several arguments:
|
||||
|
||||
* indentation width: number of indentation characters to indent lines with. Default is ``4``.
|
||||
* indentation character: character to be used for indentation. Default is a space.
|
||||
* indent first line: whether first line of output should be indented or not. Default is ``false``.
|
||||
|
||||
341
docs/conf.py
Normal file
341
docs/conf.py
Normal file
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Stencil documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Nov 27 05:54:36 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = []
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Stencil'
|
||||
copyright = '2016, Kyle Fuller'
|
||||
author = 'Kyle Fuller'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.12.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.12.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#
|
||||
# today = ''
|
||||
#
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents.
|
||||
# "<project> v<release> documentation" by default.
|
||||
#
|
||||
# html_title = 'Stencil v0.6.0'
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#
|
||||
# html_extra_path = []
|
||||
|
||||
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||
# bottom, using the given strftime format.
|
||||
# The empty string is equivalent to '%b %d, %Y'.
|
||||
#
|
||||
# html_last_updated_fmt = None
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
html_sidebars = {
|
||||
'index': ['sidebar_intro.html', 'searchbox.html'],
|
||||
'**': ['sidebar_intro.html', 'localtoc.html', 'relations.html', 'searchbox.html'],
|
||||
}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
|
||||
#
|
||||
# html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# 'ja' uses this config value.
|
||||
# 'zh' user can custom change `jieba` dictionary path.
|
||||
#
|
||||
# html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#
|
||||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Stencildoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Stencil.tex', 'Stencil Documentation',
|
||||
'Kyle Fuller', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# latex_appendices = []
|
||||
|
||||
# It false, will not define \strong, \code, itleref, \crossref ... but only
|
||||
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
|
||||
# packages.
|
||||
#
|
||||
# latex_keep_old_macro_names = True
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'stencil', 'Stencil Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Stencil', 'Stencil Documentation',
|
||||
author, 'Stencil', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#
|
||||
# texinfo_no_detailmenu = False
|
||||
70
docs/custom-template-tags-and-filters.rst
Normal file
70
docs/custom-template-tags-and-filters.rst
Normal file
@@ -0,0 +1,70 @@
|
||||
Custom Template Tags and Filters
|
||||
================================
|
||||
|
||||
You can build your own custom filters and tags and pass them down while
|
||||
rendering your template. Any custom filters or tags must be registered with a
|
||||
extension which contains all filters and tags available to the template.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let ext = Extension()
|
||||
// Register your filters and tags with the extension
|
||||
|
||||
let environment = Environment(extensions: [ext])
|
||||
try environment.renderTemplate(name: "example.html")
|
||||
|
||||
Custom Filters
|
||||
--------------
|
||||
|
||||
Registering custom filters:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
ext.registerFilter("double") { (value: Any?) in
|
||||
if let value = value as? Int {
|
||||
return value * 2
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
Registering custom filters with arguments:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
ext.registerFilter("multiply") { (value: Any?, arguments: [Any?]) in
|
||||
let amount: Int
|
||||
|
||||
if let value = arguments.first as? Int {
|
||||
amount = value
|
||||
} else {
|
||||
throw TemplateSyntaxError("multiple tag must be called with an integer argument")
|
||||
}
|
||||
|
||||
if let value = value as? Int {
|
||||
return value * amount
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
Custom Tags
|
||||
-----------
|
||||
|
||||
You can build a custom template tag. There are a couple of APIs to allow you to
|
||||
write your own custom tags. The following is the simplest form:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
ext.registerSimpleTag("custom") { context in
|
||||
return "Hello World"
|
||||
}
|
||||
|
||||
When your tag is used via ``{% custom %}`` it will execute the registered block
|
||||
of code allowing you to modify or retrieve a value from the context. Then
|
||||
return either a string rendered in your template, or throw an error.
|
||||
|
||||
If you want to accept arguments or to capture different tokens between two sets
|
||||
of template tags. You will need to call the ``registerTag`` API which accepts a
|
||||
closure to handle the parsing. You can find examples of the ``now``, ``if`` and
|
||||
``for`` tags found inside Stencil source code.
|
||||
37
docs/getting-started.rst
Normal file
37
docs/getting-started.rst
Normal file
@@ -0,0 +1,37 @@
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
The easiest way to render a template using Stencil is to create a template and
|
||||
call render on it providing a context.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let template = Template(templateString: "Hello {{ name }}")
|
||||
try template.render(["name": "kyle"])
|
||||
|
||||
For more advanced uses, you would normally create an ``Environment`` and call
|
||||
the ``renderTemplate`` convinience method.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let environment = Environment()
|
||||
|
||||
let context = ["name": "kyle"]
|
||||
try environment.renderTemplate(string: "Hello {{ name }}", context: context)
|
||||
|
||||
Template Loaders
|
||||
----------------
|
||||
|
||||
A template loader allows you to load files from disk or elsewhere. Using a
|
||||
``FileSystemLoader`` we can easily render a template from disk.
|
||||
|
||||
For example, to render a template called ``index.html`` inside the
|
||||
``templates/`` directory we can use the following:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
let fsLoader = FileSystemLoader(paths: ["templates/"])
|
||||
let environment = Environment(loader: fsLoader)
|
||||
|
||||
let context = ["name": "kyle"]
|
||||
try environment.renderTemplate(name: "index.html", context: context)
|
||||
64
docs/index.rst
Normal file
64
docs/index.rst
Normal file
@@ -0,0 +1,64 @@
|
||||
The Stencil template language
|
||||
=============================
|
||||
|
||||
Stencil is a simple and powerful template language for Swift. It provides a
|
||||
syntax similar to Django and Mustache. If you're familiar with these, you will
|
||||
feel right at home with Stencil.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
There are {{ articles.count }} articles.
|
||||
|
||||
<ul>
|
||||
{% for article in articles %}
|
||||
<li>{{ article.title }} by {{ article.author }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
import Stencil
|
||||
|
||||
struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
|
||||
let context = [
|
||||
"articles": [
|
||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||
]
|
||||
]
|
||||
|
||||
let environment = Environment(loader: FileSystemLoader(paths: ["templates/"])
|
||||
let rendered = try environment.renderTemplate(name: context)
|
||||
|
||||
print(rendered)
|
||||
|
||||
The User Guide
|
||||
--------------
|
||||
|
||||
For Template Writers
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Resources for Stencil template authors to write Stencil templates.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
templates
|
||||
builtins
|
||||
|
||||
For Developers
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Resources to help you integrate Stencil into a Swift project.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
installation
|
||||
getting-started
|
||||
api
|
||||
custom-template-tags-and-filters
|
||||
52
docs/installation.rst
Normal file
52
docs/installation.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
Swift Package Manager
|
||||
---------------------
|
||||
|
||||
If you're using the Swift Package Manager, you can add ``Stencil`` to your
|
||||
dependencies inside ``Package.swift``.
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MyApplication",
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/Stencil.git", majorVersion: 0, minor: 8),
|
||||
]
|
||||
)
|
||||
|
||||
CocoaPods
|
||||
---------
|
||||
|
||||
If you're using CocoaPods, you can add Stencil to your ``Podfile`` and then run
|
||||
``pod install``.
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
pod 'Stencil', '~> 0.8.0'
|
||||
|
||||
Carthage
|
||||
--------
|
||||
|
||||
.. note:: Use at your own risk. We don't offer support for Carthage and instead recommend you use Swift Package Manager.
|
||||
|
||||
1) Add ``Stencil`` to your ``Cartfile``:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
github "kylef/Stencil" ~> 0.8.0
|
||||
|
||||
2) Checkout your dependencies, generate the Stencil Xcode project, and then use Carthage to build Stencil:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ carthage update
|
||||
$ (cd Carthage/Checkouts/Stencil && swift package generate-xcodeproj)
|
||||
$ carthage build
|
||||
|
||||
3) Follow the Carthage steps to add the built frameworks to your project.
|
||||
|
||||
To learn more about this approach see `Using Swift Package Manager with Carthage <https://fuller.li/posts/using-swift-package-manager-with-carthage/>`_.
|
||||
185
docs/templates.rst
Normal file
185
docs/templates.rst
Normal file
@@ -0,0 +1,185 @@
|
||||
Language overview
|
||||
==================
|
||||
|
||||
- ``{{ ... }}`` for variables to print to the template output
|
||||
- ``{% ... %}`` for tags
|
||||
- ``{# ... #}`` for comments not included in the template output
|
||||
|
||||
Variables
|
||||
---------
|
||||
|
||||
A variable can be defined in your template using the following:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ variable }}
|
||||
|
||||
Stencil will look up the variable inside the current variable context and
|
||||
evaluate it. When a variable contains a dot, it will try doing the
|
||||
following lookup:
|
||||
|
||||
- Context lookup
|
||||
- Dictionary lookup
|
||||
- Array lookup (first, last, count, index)
|
||||
- Key value coding lookup
|
||||
- Type introspection
|
||||
|
||||
For example, if `people` was an array:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
There are {{ people.count }} people. {{ people.first }} is the first
|
||||
person, followed by {{ people.1 }}.
|
||||
|
||||
You can also use the subscript operator for indirect evaluation. The expression
|
||||
between brackets will be evaluated first, before the actual lookup will happen.
|
||||
|
||||
For example, if you have the following context:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
[
|
||||
"item": [
|
||||
"name": "John"
|
||||
],
|
||||
"key": "name"
|
||||
]
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
The result of {{ item[key] }} will be the same as {{ item.name }}. It will first evaluate the result of {{ key }}, and only then evaluate the lookup expression.
|
||||
|
||||
Filters
|
||||
~~~~~~~
|
||||
|
||||
Filters allow you to transform the values of variables. For example, they look like:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ variable|uppercase }}
|
||||
|
||||
See :ref:`all builtin filters <built-in-filters>`.
|
||||
|
||||
Tags
|
||||
----
|
||||
|
||||
Tags are a mechanism to execute a piece of code, allowing you to have
|
||||
control flow within your template.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if variable %}
|
||||
{{ variable }} was found.
|
||||
{% endif %}
|
||||
|
||||
A tag can also affect the context and define variables as follows:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% for item in items %}
|
||||
{{ item }}
|
||||
{% endfor %}
|
||||
|
||||
Stencil includes of built-in tags which are listed below. You can also
|
||||
extend Stencil by providing your own tags.
|
||||
|
||||
See :ref:`all builtin tags <built-in-tags>`.
|
||||
|
||||
Comments
|
||||
--------
|
||||
|
||||
To comment out part of your template, you can use the following syntax:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{# My comment is completely hidden #}
|
||||
|
||||
.. _template-inheritance:
|
||||
|
||||
Template inheritance
|
||||
--------------------
|
||||
|
||||
Template inheritance allows the common components surrounding individual pages
|
||||
to be shared across other templates. You can define blocks which can be
|
||||
overidden in any child template.
|
||||
|
||||
Let's take a look at an example. Here is our base template (``base.html``):
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}Example{% endblock %}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside>
|
||||
{% block sidebar %}
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/notes/">Notes</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
This example declares three blocks, ``title``, ``sidebar`` and ``content``. We
|
||||
can use the ``{% extends %}`` template tag to inherit from out base template
|
||||
and then use ``{% block %}`` to override any blocks from our base template.
|
||||
|
||||
A child template might look like the following:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Notes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% for note in notes %}
|
||||
<h2>{{ note }}</h2>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
.. note:: You can use ``{{ block.super }}` inside a block to render the contents of the parent block inline.
|
||||
|
||||
Since our child template doesn't declare a sidebar block. The original sidebar
|
||||
from our base template will be used. Depending on the content of ``notes`` our
|
||||
template might be rendered like the following:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Notes</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/notes/">Notes</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
<h2>Pick up food</h2>
|
||||
<h2>Do laundry</h2>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
You can use as many levels of inheritance as needed. One common way of using
|
||||
inheritance is the following three-level approach:
|
||||
|
||||
* Create a ``base.html`` template that holds the main look-and-feel of your site.
|
||||
* Create a ``base_SECTIONNAME.html`` template for each “section” of your site.
|
||||
For example, ``base_news.html``, ``base_news.html``. These templates all
|
||||
extend ``base.html`` and include section-specific styles/design.
|
||||
* Create individual templates for each type of page, such as a news article or
|
||||
blog entry. These templates extend the appropriate section template.
|
||||
Reference in New Issue
Block a user