Compare commits
486 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02f61d55a4 | ||
|
|
c25b7a52e7 | ||
|
|
25d1507159 | ||
|
|
6811c71bd6 | ||
|
|
17af3bace1 | ||
|
|
1aeeced65d | ||
|
|
ea58733eb6 | ||
|
|
003341d94c | ||
|
|
930db33028 | ||
|
|
6b6d6c2730 | ||
|
|
973609e141 | ||
|
|
644687b885 | ||
|
|
4f222ac85d | ||
|
|
3a98d1ef7d | ||
|
|
95a24b950f | ||
|
|
a3df900bd2 | ||
|
|
59b0c176c7 | ||
|
|
bc5051ffe3 | ||
|
|
9444ee5c86 | ||
|
|
8989f8a189 | ||
|
|
6bafcffd2b | ||
|
|
c690f3e613 | ||
|
|
2ddc039129 | ||
|
|
078c7a84e6 | ||
|
|
5f0c01809d | ||
|
|
07d36651bf | ||
|
|
1072e919a3 | ||
|
|
0d8fdbc3aa | ||
|
|
5828770138 | ||
|
|
71879ecdc9 | ||
|
|
6481534f6c | ||
|
|
0fa830c5cb | ||
|
|
479fdad30b | ||
|
|
6649b7e716 | ||
|
|
242bea54c3 | ||
|
|
14f4c2a131 | ||
|
|
f12d6ed7f3 | ||
|
|
dd7ea1e097 | ||
|
|
91df84b1a5 | ||
|
|
a7448b74cf | ||
|
|
248d664d4a | ||
|
|
41e0c9c9e0 | ||
|
|
67f94aa9f0 | ||
|
|
8c379296ca | ||
|
|
4d3f911f5d | ||
|
|
b95b18ff60 | ||
|
|
27a543d748 | ||
|
|
ef97973e85 | ||
|
|
d4dc631752 | ||
|
|
20b41782a1 | ||
|
|
888797b27e | ||
|
|
f32c772b99 | ||
|
|
e6ee27f64e | ||
|
|
256388ddc8 | ||
|
|
099b8414d2 | ||
|
|
7247d0a83d | ||
|
|
203510175f | ||
|
|
8e890db688 | ||
|
|
701221c0fb | ||
|
|
a6d0428036 | ||
|
|
ee8b4bc4bc | ||
|
|
99cc1cac4a | ||
|
|
779820ed99 | ||
|
|
828a9b6fc4 | ||
|
|
1b72ef27a4 | ||
|
|
bf6c7ce456 | ||
|
|
0bbb8005bb | ||
|
|
d9a48fbda6 | ||
|
|
d18e27d6e4 | ||
|
|
ec031f9c7f | ||
|
|
7dbccf9686 | ||
|
|
c444fb959d | ||
|
|
c7e1c890f8 | ||
|
|
ccd9402682 | ||
|
|
9f0b9388d2 | ||
|
|
38f5faec78 | ||
|
|
a724419474 | ||
|
|
12b3a2e9bd | ||
|
|
47a44889ae | ||
|
|
01740c61d3 | ||
|
|
c729a7d58f | ||
|
|
973e190edf | ||
|
|
e134aafe7f | ||
|
|
88fd776a02 | ||
|
|
8480648bd3 | ||
|
|
521a599a60 | ||
|
|
371a4737d9 | ||
|
|
61919c5e8e | ||
|
|
7c635975d1 | ||
|
|
fd107355c2 | ||
|
|
f5f85d95a9 | ||
|
|
22440c5369 | ||
|
|
94197b3adb | ||
|
|
e93b33423b | ||
|
|
19646bcddf | ||
|
|
a84cd3d877 | ||
|
|
124df01d3c | ||
|
|
0f1286c032 | ||
|
|
9a61aa48e3 | ||
|
|
520f27be65 | ||
|
|
306d97b638 | ||
|
|
386e9d0234 | ||
|
|
0e116b6202 | ||
|
|
9c3468e300 | ||
|
|
a1718ae350 | ||
|
|
5b2d5dc5e0 | ||
|
|
00fca208a2 | ||
|
|
a229b59d3d | ||
|
|
415c3eaa3d | ||
|
|
e516ca9389 | ||
|
|
4020a9851a | ||
|
|
3c973689a4 | ||
|
|
06ea016fd7 | ||
|
|
c2f18790e3 | ||
|
|
6addc46681 | ||
|
|
782ffdd4c7 | ||
|
|
ebb7ece511 | ||
|
|
305dc31abd | ||
|
|
3394929008 | ||
|
|
693565ddda | ||
|
|
0f18d43d9e | ||
|
|
ee4203a269 | ||
|
|
5220c3791e | ||
|
|
9243bba2b7 | ||
|
|
deec93fbe1 | ||
|
|
8510193d09 | ||
|
|
2d82dcb003 | ||
|
|
3f4622f54f | ||
|
|
799490198f | ||
|
|
6f3ca60e2b | ||
|
|
08fc21d177 | ||
|
|
019d0cca76 | ||
|
|
da6a0ccaca | ||
|
|
dbb5e14e9f | ||
|
|
0269052d6a | ||
|
|
4faf8f5ee6 | ||
|
|
4154cd31ff | ||
|
|
fd79045053 | ||
|
|
9bd86d9fd5 | ||
|
|
66a9bc563a | ||
|
|
01afae9b79 | ||
|
|
d9f6a82f97 | ||
|
|
9a6ba94d7d | ||
|
|
0e9a78d658 | ||
|
|
8eae79dbff | ||
|
|
8cceac921a | ||
|
|
7417332fa2 | ||
|
|
524c0acce6 | ||
|
|
2e67755118 | ||
|
|
c7dbba41a5 | ||
|
|
69af469d0d | ||
|
|
42e415a9bf | ||
|
|
2760843236 | ||
|
|
535a8061d9 | ||
|
|
88bec575a5 | ||
|
|
6f9bb3e931 | ||
|
|
cb4e514846 | ||
|
|
fff93f18dd | ||
|
|
652dcd246d | ||
|
|
e77bd22e83 | ||
|
|
4f84627caa | ||
|
|
07a6b2aea5 | ||
|
|
fce3dc5e48 | ||
|
|
f7bda226e8 | ||
|
|
d238c25eef | ||
|
|
df2e193891 | ||
|
|
2c3962a3de | ||
|
|
7ed95aec91 | ||
|
|
064b2f706c | ||
|
|
fce4e85a63 | ||
|
|
275e583e4a | ||
|
|
9c408d488e | ||
|
|
f9f6d95f25 | ||
|
|
0d4dee29b2 | ||
|
|
1704cd2ddf | ||
|
|
831cdf5f36 | ||
|
|
8210fa57f1 | ||
|
|
0074ee1d4a | ||
|
|
d71fe2a2ee | ||
|
|
93ccc56540 | ||
|
|
247a35fd2c | ||
|
|
8e9692c696 | ||
|
|
8bda4d5bbb | ||
|
|
e6b12c09d3 | ||
|
|
420c0eacd7 | ||
|
|
adb443229d | ||
|
|
1098921dc8 | ||
|
|
9de8190988 | ||
|
|
acda1b0caf | ||
|
|
00e71c1b4d | ||
|
|
1b85b816fd | ||
|
|
b476e50f89 | ||
|
|
2ed5763fe4 | ||
|
|
fff3d21e37 | ||
|
|
99be5f0459 | ||
|
|
2eeb7babd3 | ||
|
|
fc404b25d8 | ||
|
|
42972a1c10 | ||
|
|
6a4959cea0 | ||
|
|
ffe8f9dab0 | ||
|
|
96a004eb34 | ||
|
|
92ebfe59b1 | ||
|
|
71ad162268 | ||
|
|
b9702afbd4 | ||
|
|
4f1a5b3e3d | ||
|
|
3a4cd8aa27 | ||
|
|
e795f052ea | ||
|
|
2c411ca494 | ||
|
|
f3d5843e78 | ||
|
|
4f14b4b044 | ||
|
|
b66abc3112 | ||
|
|
5bbd994581 | ||
|
|
3995ff9acf | ||
|
|
2e18892f4c | ||
|
|
564ccb7af7 | ||
|
|
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 |
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
26
.github/workflows/danger.yml
vendored
Normal file
26
.github/workflows/danger.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Danger
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Danger Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
-
|
||||
name: Run Danger
|
||||
run: bundle exec danger --verbose --dangerfile=rakelib/Dangerfile
|
||||
env:
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.danger_github_api_token }}
|
||||
24
.github/workflows/lint-cocoapods.yml
vendored
Normal file
24
.github/workflows/lint-cocoapods.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Lint Cocoapods
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Pod Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
-
|
||||
name: Lint podspec
|
||||
run: bundle exec rake pod:lint
|
||||
23
.github/workflows/release-check-versions.yml
vendored
Normal file
23
.github/workflows/release-check-versions.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Check Versions
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'release/**'
|
||||
|
||||
jobs:
|
||||
check_versions:
|
||||
name: Check Versions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
-
|
||||
name: Check versions
|
||||
run: bundle exec rake release:check_versions
|
||||
27
.github/workflows/swiftlint.yml
vendored
Normal file
27
.github/workflows/swiftlint.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: SwiftLint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: SwiftLint
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
-
|
||||
name: Lint source code
|
||||
run: bundle exec rake lint:code
|
||||
-
|
||||
name: Lint tests source code
|
||||
run: bundle exec rake lint:tests
|
||||
26
.github/workflows/tag-publish.yml
vendored
Normal file
26
.github/workflows/tag-publish.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Publish on Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
github:
|
||||
name: GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
-
|
||||
name: Create release on GitHub
|
||||
run: bundle exec rake release:github
|
||||
env:
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.danger_github_api_token }}
|
||||
67
.github/workflows/test-spm.yml
vendored
Normal file
67
.github/workflows/test-spm.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Test SPM
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Test SPM Linux
|
||||
runs-on: ubuntu-latest
|
||||
container: swiftgen/swift:5.6
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
# Note: we can't use `ruby/setup-ruby` on custom docker images, so we
|
||||
# have to do our own caching
|
||||
name: Cache gems
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gems-
|
||||
-
|
||||
name: Cache SPM
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .build
|
||||
key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-spm-
|
||||
-
|
||||
name: Bundle install
|
||||
run: |
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
-
|
||||
name: Run tests
|
||||
run: bundle exec rake spm:test
|
||||
|
||||
macos:
|
||||
name: Test SPM macOS
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
-
|
||||
name: Cache SPM
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .build
|
||||
key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-spm-
|
||||
-
|
||||
name: Run tests
|
||||
run: bundle exec rake spm:test
|
||||
59
.gitignore
vendored
59
.gitignore
vendored
@@ -1,6 +1,13 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData/
|
||||
Fixtures/stub-env/**/*.swiftmodule
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
@@ -9,18 +16,60 @@ build/
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
xcuserdata/
|
||||
|
||||
## Other
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.xcuserstate
|
||||
*.xcscmblueprint
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
Packages/
|
||||
.build/
|
||||
.swiftpm/
|
||||
|
||||
# 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
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
Carthage/Checkouts
|
||||
Carthage/Build
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
|
||||
# Other stuff
|
||||
.apitoken
|
||||
.DS_Store
|
||||
.idea/
|
||||
bin/
|
||||
Frameworks/
|
||||
Rome/
|
||||
|
||||
1
.ruby-version
Normal file
1
.ruby-version
Normal file
@@ -0,0 +1 @@
|
||||
3.0.4
|
||||
116
.swiftlint.yml
Normal file
116
.swiftlint.yml
Normal file
@@ -0,0 +1,116 @@
|
||||
swiftlint_version: 0.61.0
|
||||
|
||||
opt_in_rules:
|
||||
- accessibility_label_for_image
|
||||
- anonymous_argument_in_multiline_closure
|
||||
- anyobject_protocol
|
||||
- array_init
|
||||
- attributes
|
||||
- balanced_xctest_lifecycle
|
||||
- closure_body_length
|
||||
- closure_end_indentation
|
||||
- closure_spacing
|
||||
- collection_alignment
|
||||
- comment_spacing
|
||||
- conditional_returns_on_newline
|
||||
- contains_over_filter_count
|
||||
- contains_over_filter_is_empty
|
||||
- contains_over_first_not_nil
|
||||
- contains_over_range_nil_comparison
|
||||
- convenience_type
|
||||
- discarded_notification_center_observer
|
||||
- discouraged_assert
|
||||
- discouraged_none_name
|
||||
- discouraged_optional_boolean
|
||||
- discouraged_optional_collection
|
||||
- empty_collection_literal
|
||||
- empty_count
|
||||
- empty_string
|
||||
- empty_xctest_method
|
||||
- enum_case_associated_values_count
|
||||
- fallthrough
|
||||
- fatal_error_message
|
||||
- file_header
|
||||
- first_where
|
||||
- flatmap_over_map_reduce
|
||||
- force_unwrapping
|
||||
- ibinspectable_in_extension
|
||||
- identical_operands
|
||||
- implicit_return
|
||||
- implicitly_unwrapped_optional
|
||||
- inclusive_language
|
||||
- indentation_width
|
||||
- joined_default_parameter
|
||||
- last_where
|
||||
- legacy_multiple
|
||||
- legacy_objc_type
|
||||
- legacy_random
|
||||
- literal_expression_end_indentation
|
||||
- lower_acl_than_parent
|
||||
- missing_docs
|
||||
- modifier_order
|
||||
- multiline_arguments
|
||||
- multiline_arguments_brackets
|
||||
- multiline_function_chains
|
||||
- multiline_literal_brackets
|
||||
- multiline_parameters
|
||||
- multiline_parameters_brackets
|
||||
- nslocalizedstring_key
|
||||
- nslocalizedstring_require_bundle
|
||||
- number_separator
|
||||
- operator_usage_whitespace
|
||||
- optional_enum_case_matching
|
||||
- overridden_super_call
|
||||
- override_in_extension
|
||||
- prefer_self_in_static_references
|
||||
- prefer_self_type_over_type_of_self
|
||||
- prefer_zero_over_explicit_init
|
||||
- prefixed_toplevel_constant
|
||||
- private_action
|
||||
- private_outlet
|
||||
- private_subject
|
||||
- prohibited_super_call
|
||||
- raw_value_for_camel_cased_codable_enum
|
||||
- reduce_into
|
||||
- redundant_nil_coalescing
|
||||
- redundant_type_annotation
|
||||
- required_enum_case
|
||||
- return_value_from_void_function
|
||||
- single_test_class
|
||||
- sorted_first_last
|
||||
- sorted_imports
|
||||
- static_operator
|
||||
- strong_iboutlet
|
||||
- switch_case_on_newline
|
||||
- test_case_accessibility
|
||||
- toggle_bool
|
||||
- trailing_closure
|
||||
- unavailable_function
|
||||
- unneeded_parentheses_in_closure_argument
|
||||
- unowned_variable_capture
|
||||
- unused_closure_parameter
|
||||
- vertical_parameter_alignment_on_call
|
||||
- vertical_whitespace_closing_braces
|
||||
- vertical_whitespace_opening_braces
|
||||
- void_function_in_ternary
|
||||
- weak_delegate
|
||||
- xct_specific_matcher
|
||||
- yoda_condition
|
||||
|
||||
# Rules customization
|
||||
closure_body_length:
|
||||
warning: 25
|
||||
|
||||
conditional_returns_on_newline:
|
||||
if_only: true
|
||||
|
||||
indentation_width:
|
||||
indentation_width: 2
|
||||
|
||||
line_length:
|
||||
warning: 120
|
||||
error: 200
|
||||
|
||||
nesting:
|
||||
type_level:
|
||||
warning: 2
|
||||
180
ARCHITECTURE.md
180
ARCHITECTURE.md
@@ -1,180 +0,0 @@
|
||||
Stencil Architecture
|
||||
====================
|
||||
|
||||
This document outlines the architecture of Stencil and how it works internally.
|
||||
|
||||
Stencil uses a three-step process for rendering templates. The first step is tokenising the template into an array of Token’s. Afterwards, the array of token’s are transformed into a collection of Node’s. Once we have a collection of Node’s (objects conforming to the `Node` protocol), we then call `render(context)` on each Node instructing it to render itself inside the given context.
|
||||
|
||||
## Token
|
||||
|
||||
Token is an enum which has four members. These represent a piece of text, a variable, a comment or a template block. They are parsed using the `TokenParser` which takes the template as a string as input and returns an array of Token’s.
|
||||
|
||||
### Values
|
||||
|
||||
#### Text
|
||||
|
||||
A text token represents a string which will be rendered in the template. For example, a text token with the string `Hello World` will be rendered as such in the output.
|
||||
|
||||
#### Variable
|
||||
|
||||
A variable token represents a variable inside a context. It will be evaluated and rendered in the output. It is created from the template using `{{ string }}`.
|
||||
|
||||
#### Comment
|
||||
|
||||
The comment token represents a comment inside the source. It is created using `{# This is a comment #}`.
|
||||
|
||||
#### Block
|
||||
|
||||
A block represents a template tag. It is created using `{% this is a template block %}` inside a template. The template tag in this case would be called `this`. See “Block Token” below for more information.
|
||||
|
||||
### Parsing
|
||||
|
||||
A template is parsed using the TokenParser into an array of Token’s. For example:
|
||||
|
||||
```html+django
|
||||
Hello {{ name }}
|
||||
```
|
||||
|
||||
Would be parsed into two tokens. A token representing the string, `Hello ` and a token representing the variable called `name`. So, in Swift it would be represented as follows:
|
||||
|
||||
```swift
|
||||
let tokens = [
|
||||
Token.Text("Hello "),
|
||||
Token.Variable("name"),
|
||||
]
|
||||
```
|
||||
|
||||
## Node
|
||||
|
||||
Node is a protocol with a single method, to render it inside a context. When rendering a node, it is converted into the output string, or an error if there is a failure. Token’s are converted to Node’s using the `TokenParser` class.
|
||||
|
||||
For some Token’s, there is a direct mapping from a Token to a Node. However block node’s do not have a 1:1 mapping.
|
||||
|
||||
### Token Parsing
|
||||
|
||||
#### Text Token
|
||||
|
||||
A text token is converted directly to a `TextNode` which simply returns the text when rendered.
|
||||
|
||||
#### Variable Token
|
||||
|
||||
Variable Token’s are transformed directly to a `VariableNode`, which will evaluate a variable in the given template when rendered.
|
||||
|
||||
#### Comment Token
|
||||
|
||||
A comment token is simply omitted, a comment token will be dropped when it is converted to a Node.
|
||||
|
||||
#### Block Token
|
||||
|
||||
Block token’s are slightly different from the other tokens, there is no direct mapping. A block token is made up of a string representing the token. For example `now` or `for article in articles`. The `TokenParser` will pull out the first word inside the string representation and use that to look-up a parser for the block. So, in this example, the template tag names will be `now` or `for`.
|
||||
|
||||
The template tag’s are registered with a block of code which deals with the parsing for the given tag. This allows the parser to parse a set of tokens ahead of the block tone. This is useful for control flow, such as the `for` template tag will want to parse any following tokens up until the `endblock` block token.
|
||||
|
||||
For example:
|
||||
|
||||
```html+django
|
||||
{% for article in articles %}
|
||||
An Article
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
Or as a set of tokens:
|
||||
|
||||
```swift
|
||||
let tokens = [
|
||||
Token.Block("for article in articles"),
|
||||
Token.Text(" An Article")
|
||||
Token.Block("endfor")
|
||||
]
|
||||
```
|
||||
|
||||
Will result in a single Node (a `ForNode`) which contains the sub-node containing the text. The `ForNode` class has a property called `forNodes` which contains the text node representing the text token (` An Article`).
|
||||
|
||||
When the `ForNode` is rendered in a context, it will look up the variable `articles` and if it’s an array it will loop over it. Inserting the variable `article` into the context while rendered the `forNodes` for each article.
|
||||
|
||||
### Custom Node’s
|
||||
|
||||
There are two ways to register custom template tags. A simple way which allows you to map 1:1 a block token to a Node. You can also register a more advanced template tag which has it’s own block of code for handling parsing if you want to parse up until another token such as if you are trying to provide flow-control.
|
||||
|
||||
The tags are registered onto the `TokenParser` which you can access from your `Template`.
|
||||
|
||||
#### Simple Tags
|
||||
|
||||
A simple tag is registered with a string for the tag name and a block of code which is evaluated when the block is rendered in a given context.
|
||||
|
||||
Here’s an example. Registering a template tag called `custom` which just renders `Hello World` in the rendered template:
|
||||
|
||||
```swift
|
||||
parser.registerSimpleTag("custom") { context in
|
||||
return .Success("Hello World")
|
||||
}
|
||||
```
|
||||
|
||||
You would use it as such in a template:
|
||||
|
||||
```html+django
|
||||
{% custom %}
|
||||
```
|
||||
|
||||
#### Advanced Tags
|
||||
|
||||
If you need more control or functionality than the simple tag’s above, you can use the node based API where you can provide a block of code to deal with parsing. There are a few examples of this in use over at `Node.swift` inside Stencil. There is an implementation of `if` and `for` template tags.
|
||||
|
||||
You would register a template tag using the `registerTag` API inside a `TokenParser` which accepts a name for the tag and a block of code to handle parsing. The block of code is invoked with the parser and the current token as an argument. This allows you to use the API on `TokenParser` to parse node’s further in the token array.
|
||||
|
||||
As an example, we’re going to create a template tag called `debug` which will optionally render nodes from `debug` up until `enddebug`. When rendering the `DebugNode`, it will only render the nodes inside if a variable called `debug` is set to `true` inside the template Context.
|
||||
|
||||
```html+django
|
||||
{% debug %}
|
||||
Debugging is enabled!
|
||||
{% enddebug %}
|
||||
```
|
||||
|
||||
This will be represented by a `DebugNode` which will have a property containing all of the Node’s inside the `debug`/`enddebug` block. In the above example, this will just be a TextNode containing ` Debugging is enabled!`.
|
||||
|
||||
When the `DebugNode` is rendered, it will determine if debug is enabled by introspecting the context and if it is enabled. We will render the nodes, otherwise just return an empty string to hide the debug output.
|
||||
|
||||
So, our `DebugNode` would look like as following:
|
||||
|
||||
```swift
|
||||
class DebugNode : Node {
|
||||
let nodes:[Node]
|
||||
|
||||
init(nodes:[Node]) {
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
func render(context: Context) -> Result {
|
||||
// Is there a debug variable inside the context?
|
||||
if let debug = context["debug"] as? Bool {
|
||||
// Is debug set to true?
|
||||
if debug {
|
||||
// Let's render the given nodes in the context since debug is enabled.
|
||||
return renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
|
||||
// Debug is turned off, so let's not render anything
|
||||
return .Success("")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We will need to write a parser to parse up until the `enddebug` template block and create a `DebugNode` with the nodes in-between. If there was another error form another Node inside, then we will return that error.
|
||||
|
||||
```swift
|
||||
parser.registerTag("debug") { (parser, token) -> TokenParser.Result in
|
||||
// Use the parser to parse every token up until the `enddebug` block.
|
||||
switch parser.parse(until(["enddebug"]))
|
||||
case .Success(let nodes):
|
||||
nodes
|
||||
case .Error(let error):
|
||||
// There was an error, this is most-likely due to another template block returning an error.
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
501
CHANGELOG.md
Normal file
501
CHANGELOG.md
Normal file
@@ -0,0 +1,501 @@
|
||||
## 0.15.2
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Prefer `DynamicMemberLookup` over KVC.
|
||||
[##342](https://github.com/stencilproject/Stencil/pull/342)
|
||||
[@art-divin](https://github.com/art-divin)
|
||||
|
||||
- Allow tokens to be escaped by a backslash, i.e. `\{{ something }}` would render to `{{ something }}`.
|
||||
|
||||
## 0.15.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix bug in `LazyValueWrapper`, causing it to never resolve.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#328](https://github.com/stencilproject/Stencil/pull/328)
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Breaking
|
||||
|
||||
- Drop support for Swift < 5. For Swift 4.2 support, you should use Stencil 0.14.2.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#323](https://github.com/stencilproject/Stencil/pull/323)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Added support for trimming whitespace around blocks with Jinja2 whitespace control symbols. eg `{%- if value +%}`.
|
||||
[Miguel Bejar](https://github.com/bejar37)
|
||||
[Yonas Kolb](https://github.com/yonaskolb)
|
||||
[#92](https://github.com/stencilproject/Stencil/pull/92)
|
||||
[#287](https://github.com/stencilproject/Stencil/pull/287)
|
||||
- Added support for adding default whitespace trimming behaviour to an environment.
|
||||
[Yonas Kolb](https://github.com/yonaskolb)
|
||||
[#287](https://github.com/stencilproject/Stencil/pull/287)
|
||||
- Blocks now can be used repeatedly in the template. When block is rendered for the first time its content will be cached and it can be rendered again later using `{{ block.block_name }}`.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#158](https://github.com/stencilproject/Stencil/issues/158)
|
||||
[#182](https://github.com/stencilproject/Stencil/pull/182)
|
||||
- Added `break` and `continue` tags to break or continue current loop.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#175](https://github.com/stencilproject/Stencil/pull/175)
|
||||
- You can now access outer loop's scope by labeling it: `{% outer: for ... %}... {% for ... %} {{ outer.counter }} {% endfor %}{% endfor %}`.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#175](https://github.com/stencilproject/Stencil/pull/175)
|
||||
- Boolean expressions can now be rendered, i.e `{{ name == "John" }}` will render `true` or `false` depending on the evaluation result.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#164](https://github.com/stencilproject/Stencil/pull/164)
|
||||
[#325](https://github.com/stencilproject/Stencil/pull/325)
|
||||
- Enable dynamic member lookup using a new `DynamicMemberLookup` marker protocol. Conform your own types to this protocol to support dynamic member from with contexts.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#219](https://github.com/stencilproject/Stencil/issues/219)
|
||||
[#246](https://github.com/stencilproject/Stencil/pull/246)
|
||||
- Allow providing lazily evaluated context data, using the `LazyValueWrapper` structure.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#324](https://github.com/stencilproject/Stencil/pull/324)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed using `{{ block.super }}` inside nodes other than `block`.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#266](https://github.com/stencilproject/Stencil/issues/266)
|
||||
[#267](https://github.com/stencilproject/Stencil/pull/267)
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Updated internal maintenance scripts, and switched to GitHub actions.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#321](https://github.com/stencilproject/Stencil/pull/321)
|
||||
- Made the `tokens` property on a `Template` public.
|
||||
[Stefanomondino](https://github.com/stefanomondino)
|
||||
[#292](https://github.com/stencilproject/Stencil/pull/292)
|
||||
- Made the `Template.render(_:)` method (that accepts a `Context`) public.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#322](https://github.com/stencilproject/Stencil/pull/322)
|
||||
|
||||
## 0.14.2
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Update Spectre (0.10) and PathKit to support Xcode 13.
|
||||
[Astromonkee](https://github.com/astromonkee)
|
||||
[#314](https://github.com/stencilproject/Stencil/pull/314)
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix for crashing range indexes when variable length is 1.
|
||||
[Łukasz Kuczborski](https://github.com/lkuczborski)
|
||||
[#306](https://github.com/stencilproject/Stencil/pull/306)
|
||||
|
||||
|
||||
## 0.14.0
|
||||
|
||||
### Breaking
|
||||
|
||||
- Drop support for Swift < 4.2. For Swift 4 support, you should use Stencil 0.13.1.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#294](https://github.com/stencilproject/Stencil/pull/294)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Added support for dynamic filter using `filter` filter. With that you can define a variable with a name of filter
|
||||
, i.e. `myfilter = "uppercase"` and then use it to invoke this filter with `{{ string|filter:myfilter }}`.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#203](https://github.com/stencilproject/Stencil/pull/203)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed using parenthesis in boolean expressions, they now can be used without spaces around them.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#254](https://github.com/stencilproject/Stencil/pull/254)
|
||||
- Throw syntax error on empty variable tags (`{{ }}`) instead `fatalError`.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#263](https://github.com/stencilproject/Stencil/pull/263)
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- `Token` type converted to struct to allow computing token components only once.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#256](https://github.com/stencilproject/Stencil/pull/256)
|
||||
- Added SwiftLint to the project.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#249](https://github.com/stencilproject/Stencil/pull/249)
|
||||
- Updated to Swift 5.
|
||||
[Jungwon An](https://github.com/kawoou)
|
||||
[#268](https://github.com/stencilproject/Stencil/pull/268)
|
||||
|
||||
|
||||
## 0.13.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed a bug in Stencil 0.13 where tags without spaces were incorrectly parsed.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#252](https://github.com/stencilproject/Stencil/pull/252)
|
||||
|
||||
|
||||
## 0.13.0
|
||||
|
||||
### Breaking
|
||||
|
||||
- Now requires Swift 4.1 or newer.
|
||||
[Yonas Kolb](https://github.com/yonaskolb)
|
||||
[#228](https://github.com/stencilproject/Stencil/pull/228)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- You can now use parentheses in boolean expressions to change operator precedence.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#165](https://github.com/stencilproject/Stencil/pull/165)
|
||||
- Added method to add boolean filters with their negative counterparts.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#160](https://github.com/stencilproject/Stencil/pull/160)
|
||||
- Now you can conditionally render variables with `{{ variable if condition }}`, which is a shorthand for `{% if condition %}{{ variable }}{% endif %}`. You can also use `else` like `{{ variable1 if condition else variable2 }}`, which is a shorthand for `{% if condition %}{{ variable1 }}{% else %}{{ variable2 }}{% endif %}`
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#243](https://github.com/stencilproject/Stencil/pull/243)
|
||||
- Now you can access string characters by index or get string length the same was as if it was an array, i.e. `{{ 'string'.first }}`, `{{ 'string'.last }}`, `{{ 'string'.1 }}`, `{{ 'string'.count }}`.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#245](https://github.com/stencilproject/Stencil/pull/245)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed the performance issues introduced in Stencil 0.12 with the error log improvements.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#230](https://github.com/stencilproject/Stencil/pull/230)
|
||||
- Now accessing undefined keys in NSObject does not cause runtime crash and instead renders empty string.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#234](https://github.com/stencilproject/Stencil/pull/234)
|
||||
- `for` tag: When iterating over a dictionary the keys will now always be sorted (in an ascending order) to ensure consistent output generation.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#240](https://github.com/stencilproject/Stencil/pull/240)
|
||||
|
||||
### Internal Changes
|
||||
|
||||
- Updated the codebase to use Swift 4 features.
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#239](https://github.com/stencilproject/Stencil/pull/239)
|
||||
- Update to Spectre 0.9.0.
|
||||
[Ilya Puchka](https://github.com/ilyapuchka)
|
||||
[#247](https://github.com/stencilproject/Stencil/pull/247)
|
||||
- Optimise Scanner performance.
|
||||
[Eric Thorpe](https://github.com/trametheka)
|
||||
[Sébastien Duperron](https://github.com/Liquidsoul)
|
||||
[David Jennes](https://github.com/djbe)
|
||||
[#226](https://github.com/stencilproject/Stencil/pull/226)
|
||||
|
||||
|
||||
## 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)
|
||||
[#173](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/stencilproject/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/stencilproject/Stencil/issues/72)
|
||||
|
||||
- Templates can now extend templates that extend other templates.
|
||||
[#60](https://github.com/stencilproject/Stencil/issues/60)
|
||||
|
||||
- If comparisons will now treat 0 and below numbers as negative.
|
||||
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Adds support for Swift 3.0.
|
||||
21
Gemfile
Normal file
21
Gemfile
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
# The bare minimum for building, e.g. in Homebrew
|
||||
group :build do
|
||||
gem 'base64', '~> 0.3'
|
||||
gem 'rake', '~> 13.0'
|
||||
gem 'xcpretty', '~> 0.3'
|
||||
end
|
||||
|
||||
# In addition to :build, for contributing
|
||||
group :development do
|
||||
gem 'danger', '~> 8.4'
|
||||
gem 'rubocop', '~> 1.22'
|
||||
end
|
||||
|
||||
# For releasing to GitHub
|
||||
group :release do
|
||||
gem 'octokit', '~> 4.7'
|
||||
end
|
||||
132
Gemfile.lock
Normal file
132
Gemfile.lock
Normal file
@@ -0,0 +1,132 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.3)
|
||||
base64 (0.3.0)
|
||||
claide (1.1.0)
|
||||
claide-plugins (0.9.2)
|
||||
cork
|
||||
nap
|
||||
open4 (~> 1.3)
|
||||
colored2 (3.1.2)
|
||||
cork (0.3.0)
|
||||
colored2 (~> 3.1)
|
||||
danger (8.6.1)
|
||||
claide (~> 1.0)
|
||||
claide-plugins (>= 0.9.2)
|
||||
colored2 (~> 3.1)
|
||||
cork (~> 0.1)
|
||||
faraday (>= 0.9.0, < 2.0)
|
||||
faraday-http-cache (~> 2.0)
|
||||
git (~> 1.7)
|
||||
kramdown (~> 2.3)
|
||||
kramdown-parser-gfm (~> 1.0)
|
||||
no_proxy_fix
|
||||
octokit (~> 4.7)
|
||||
terminal-table (>= 1, < 4)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-http-cache (2.5.1)
|
||||
faraday (>= 0.8)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.1)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
git (1.19.1)
|
||||
addressable (~> 2.8)
|
||||
rchardet (~> 1.8)
|
||||
json (2.15.0)
|
||||
kramdown (2.5.1)
|
||||
rexml (>= 3.3.9)
|
||||
kramdown-parser-gfm (1.1.0)
|
||||
kramdown (~> 2.0)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
multipart-post (2.4.1)
|
||||
nap (1.1.0)
|
||||
no_proxy_fix (0.1.2)
|
||||
octokit (4.25.1)
|
||||
faraday (>= 1, < 3)
|
||||
sawyer (~> 0.9)
|
||||
open4 (1.3.4)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.9.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
prism (1.5.1)
|
||||
public_suffix (4.0.7)
|
||||
racc (1.8.1)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.0)
|
||||
rchardet (1.10.0)
|
||||
regexp_parser (2.11.3)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
rubocop (1.81.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.47.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (>= 0.17.3, < 3)
|
||||
terminal-table (1.6.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
xcpretty (0.4.1)
|
||||
rouge (~> 3.28.0)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux-gnu
|
||||
aarch64-linux-musl
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86-linux-gnu
|
||||
x86-linux-musl
|
||||
x86_64-darwin
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
base64 (~> 0.3)
|
||||
danger (~> 8.4)
|
||||
octokit (~> 4.7)
|
||||
rake (~> 13.0)
|
||||
rubocop (~> 1.22)
|
||||
xcpretty (~> 0.3)
|
||||
|
||||
BUNDLED WITH
|
||||
2.7.2
|
||||
4
LICENSE
4
LICENSE
@@ -1,4 +1,5 @@
|
||||
Copyright (c) 2014, Kyle Fuller
|
||||
Copyright (c) 2022, Kyle Fuller as the original author
|
||||
Copyright (c) 2025, Astzweig GmbH & Co. KG
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
@@ -21,4 +22,3 @@ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
25
Package.resolved
Normal file
25
Package.resolved
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Spectre",
|
||||
"repositoryURL": "https://github.com/kylef/Spectre.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "26cc5e9ae0947092c7139ef7ba612e34646086c7",
|
||||
"version": "0.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swiftpm-pathkit",
|
||||
"repositoryURL": "https://github.com/astzweig/swiftpm-pathkit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "5d164c34f35fa7241d77357e965acde80efcb6d9",
|
||||
"version": "1.5.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
23
Package.swift
Normal file
23
Package.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
// swift-tools-version:5.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Stencil",
|
||||
products: [
|
||||
.library(name: "Stencil", targets: ["Stencil"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/astzweig/swiftpm-pathkit.git", from: "1.5.0"),
|
||||
.package(url: "https://github.com/kylef/Spectre.git", from: "0.10.1")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Stencil", dependencies: [
|
||||
"PathKit"
|
||||
]),
|
||||
.testTarget(name: "StencilTests", dependencies: [
|
||||
"Stencil",
|
||||
"Spectre"
|
||||
])
|
||||
],
|
||||
swiftLanguageVersions: [.v5]
|
||||
)
|
||||
10
Podfile
10
Podfile
@@ -1,10 +0,0 @@
|
||||
use_frameworks!
|
||||
|
||||
target 'Stencil' do
|
||||
podspec
|
||||
end
|
||||
|
||||
target 'StencilTests' do
|
||||
podspec
|
||||
end
|
||||
|
||||
10
Podfile.lock
10
Podfile.lock
@@ -1,10 +0,0 @@
|
||||
PODS:
|
||||
- PathKit (0.3.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- PathKit (~> 0.3.0)
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
PathKit: 0f4d1becb7002e3cd62a1583ce8de964d57485ce
|
||||
|
||||
COCOAPODS: 0.37.2
|
||||
182
README.md
182
README.md
@@ -1,47 +1,40 @@
|
||||
Stencil
|
||||
=======
|
||||
|
||||
[](https://circleci.com/gh/kylef/Stencil)
|
||||
# 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
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
let context = [
|
||||
"articles": [
|
||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||
]
|
||||
]
|
||||
|
||||
Installation with CocoaPods is recommended.
|
||||
let environment = Environment(loader: FileSystemLoader(paths: ["templates/"]))
|
||||
let rendered = try environment.renderTemplate(name: "article_list.html", context: context)
|
||||
|
||||
```ruby
|
||||
pod 'Stencil'
|
||||
print(rendered)
|
||||
```
|
||||
|
||||
## Philosophy
|
||||
@@ -54,134 +47,29 @@ 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, allowing you to have
|
||||
control flow within your template.
|
||||
|
||||
```html+django
|
||||
{% if variable %}
|
||||
{{ variable }} was found.
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
A tag can also affect 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 by providing 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 %}
|
||||
```
|
||||
|
||||
#### include
|
||||
|
||||
You can include another template using the `include` tag.
|
||||
|
||||
```html+django
|
||||
{% include "comment.html" %}
|
||||
```
|
||||
|
||||
The `include` tag requires a TemplateLoader to be found inside your context with the paths, or bundles used to lookup the template.
|
||||
|
||||
```swift
|
||||
let context = Context(dictionary: [
|
||||
"loader": TemplateLoader(bundle:[NSBundle.mainBundle()])
|
||||
])
|
||||
```
|
||||
|
||||
#### 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 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 `Node.swift`.
|
||||
|
||||
The architecture of Stencil along with how to build advanced plugins can be found in the [architecture](ARCHITECTURE.md) document.
|
||||
|
||||
### 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),
|
||||
[Genesis](https://github.com/yonaskolb/Genesis)
|
||||
|
||||
## License
|
||||
|
||||
Stencil is licensed under the BSD license. See [LICENSE](LICENSE) for more
|
||||
info.
|
||||
|
||||
|
||||
50
Rakefile
Executable file
50
Rakefile
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/rake
|
||||
|
||||
unless defined?(Bundler)
|
||||
puts 'Please use bundle exec to run the rake command'
|
||||
exit 1
|
||||
end
|
||||
|
||||
require 'English'
|
||||
|
||||
## [ Constants ] ##############################################################
|
||||
|
||||
POD_NAME = 'Stencil'
|
||||
MIN_XCODE_VERSION = 13.0
|
||||
BUILD_DIR = File.absolute_path('./.build')
|
||||
|
||||
## [ Build Tasks ] ############################################################
|
||||
|
||||
namespace :files do
|
||||
desc 'Update all files containing a version'
|
||||
task :update, [:version] do |_, args|
|
||||
version = args[:version]
|
||||
|
||||
Utils.print_header "Updating files for version #{version}"
|
||||
|
||||
podspec = Utils.podspec(POD_NAME)
|
||||
podspec['version'] = version
|
||||
podspec['source']['tag'] = version
|
||||
File.write("#{POD_NAME}.podspec.json", JSON.pretty_generate(podspec) + "\n")
|
||||
|
||||
replace('CHANGELOG.md', '## Master' => "\#\# #{version}")
|
||||
replace("docs/conf.py",
|
||||
/^version = .*/ => %Q(version = '#{version}'),
|
||||
/^release = .*/ => %Q(release = '#{version}')
|
||||
)
|
||||
docs_package = Utils.first_match_in_file('docs/installation.rst', /\.package\(url: .+ from: "(.+)"/, 1)
|
||||
replace("docs/installation.rst",
|
||||
/\.package\(url: .+, from: "(.+)"/ => %Q(.package\(url: "https://github.com/swiftstencil/swiftpm-stencil.git", from: "#{version}")
|
||||
)
|
||||
end
|
||||
|
||||
def replace(file, replacements)
|
||||
content = File.read(file)
|
||||
replacements.each do |match, replacement|
|
||||
content.gsub!(match, replacement)
|
||||
end
|
||||
File.write(file, content)
|
||||
end
|
||||
end
|
||||
|
||||
task :default => 'release:new'
|
||||
106
Sources/Stencil/Context.swift
Normal file
106
Sources/Stencil/Context.swift
Normal file
@@ -0,0 +1,106 @@
|
||||
/// A container for template variables.
|
||||
public class Context {
|
||||
var dictionaries: [[String: Any?]]
|
||||
|
||||
/// The context's environment, such as registered extensions, classes, …
|
||||
public let environment: Environment
|
||||
|
||||
init(dictionaries: [[String: Any?]], environment: Environment) {
|
||||
self.dictionaries = dictionaries
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
/// Create a context from a dictionary (and an env.)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dictionary: The context's data
|
||||
/// - environment: Environment such as extensions, …
|
||||
public convenience init(dictionary: [String: Any] = [:], environment: Environment? = nil) {
|
||||
self.init(
|
||||
dictionaries: dictionary.isEmpty ? [] : [dictionary],
|
||||
environment: environment ?? Environment()
|
||||
)
|
||||
}
|
||||
|
||||
/// Access variables in this context by name
|
||||
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 var dictionary = dictionaries.popLast() {
|
||||
dictionary[key] = value
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new level into the Context
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dictionary: The new level data
|
||||
fileprivate func push(_ dictionary: [String: Any] = [:]) {
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
|
||||
/// Pop the last level off of the Context
|
||||
///
|
||||
/// - returns: The popped level
|
||||
// swiftlint:disable:next discouraged_optional_collection
|
||||
fileprivate func pop() -> [String: Any?]? {
|
||||
dictionaries.popLast()
|
||||
}
|
||||
|
||||
/// Push a new level onto the context for the duration of the execution of the given closure
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dictionary: The new level data
|
||||
/// - closure: The closure to execute
|
||||
/// - returns: Return value of the closure
|
||||
public func push<Result>(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result {
|
||||
push(dictionary)
|
||||
defer { _ = pop() }
|
||||
return try closure()
|
||||
}
|
||||
|
||||
/// Flatten all levels of context data into 1, merging duplicate variables
|
||||
///
|
||||
/// - returns: All collected variables
|
||||
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
|
||||
}
|
||||
|
||||
/// Cache result of block by its name in the context top-level, so that it can be later rendered
|
||||
/// via `{{ block.name }}`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the stored block
|
||||
/// - content: The block's rendered content
|
||||
public func cacheBlock(_ name: String, content: String) {
|
||||
if var block = dictionaries.first?["block"] as? [String: String] {
|
||||
block[name] = content
|
||||
dictionaries[0]["block"] = block
|
||||
} else {
|
||||
dictionaries.insert(["block": [name: content]], at: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Sources/Stencil/DynamicMemberLookup.swift
Normal file
18
Sources/Stencil/DynamicMemberLookup.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
/// Marker protocol so we can know which types support `@dynamicMemberLookup`. Add this to your own types that support
|
||||
/// lookup by String.
|
||||
public protocol DynamicMemberLookup {
|
||||
/// Get a value for a given `String` key
|
||||
subscript(dynamicMember member: String) -> Any? { get }
|
||||
}
|
||||
|
||||
public extension DynamicMemberLookup where Self: RawRepresentable {
|
||||
/// Get a value for a given `String` key
|
||||
subscript(dynamicMember member: String) -> Any? {
|
||||
switch member {
|
||||
case "rawValue":
|
||||
return rawValue
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
84
Sources/Stencil/Environment.swift
Normal file
84
Sources/Stencil/Environment.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
/// Container for environment data, such as registered extensions
|
||||
public struct Environment {
|
||||
/// The class for loading new templates
|
||||
public let templateClass: Template.Type
|
||||
/// List of registered extensions
|
||||
public var extensions: [Extension]
|
||||
/// How to handle whitespace
|
||||
public var trimBehaviour: TrimBehaviour
|
||||
/// Mechanism for loading new files
|
||||
public var loader: Loader?
|
||||
|
||||
/// Basic initializer
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - loader: Mechanism for loading new files
|
||||
/// - extensions: List of extension containers
|
||||
/// - templateClass: Class for newly loaded templates
|
||||
/// - trimBehaviour: How to handle whitespace
|
||||
public init(
|
||||
loader: Loader? = nil,
|
||||
extensions: [Extension] = [],
|
||||
templateClass: Template.Type = Template.self,
|
||||
trimBehaviour: TrimBehaviour = .nothing
|
||||
) {
|
||||
self.templateClass = templateClass
|
||||
self.loader = loader
|
||||
self.extensions = extensions + [DefaultExtension()]
|
||||
self.trimBehaviour = trimBehaviour
|
||||
}
|
||||
|
||||
/// Load a template with the given name
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: Name of the template
|
||||
/// - returns: Loaded template instance
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a template with the given names
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - names: Names of the template
|
||||
/// - returns: Loaded template instance
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a template with the given name, providing some data
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: Name of the template
|
||||
/// - context: Data for rendering
|
||||
/// - returns: Rendered output
|
||||
public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String {
|
||||
let template = try loadTemplate(name: name)
|
||||
return try render(template: template, context: context)
|
||||
}
|
||||
|
||||
/// Render the given template string, providing some data
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - string: Template string
|
||||
/// - context: Data for rendering
|
||||
/// - returns: Rendered output
|
||||
public func renderTemplate(string: String, context: [String: Any] = [:]) 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)
|
||||
}
|
||||
}
|
||||
81
Sources/Stencil/Errors.swift
Normal file
81
Sources/Stencil/Errors.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
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 { reason }
|
||||
public internal(set) var token: Token?
|
||||
public internal(set) var stackTrace: [Token]
|
||||
public var templateName: String? { token?.sourceMap.filename }
|
||||
var allTokens: [Token] {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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: AnyObject {
|
||||
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 location = token.sourceMap.location
|
||||
let highlight = """
|
||||
\(String(Array(repeating: " ", count: location.lineOffset)))\
|
||||
^\(String(Array(repeating: "~", count: max(token.contents.count - 1, 0))))
|
||||
"""
|
||||
|
||||
return """
|
||||
\(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason)
|
||||
\(location.content)
|
||||
\(highlight)
|
||||
"""
|
||||
}
|
||||
|
||||
var descriptions = templateError.stackTrace.reduce(into: []) { $0.append(describe(token: $1)) }
|
||||
let description = templateError.token.map(describe(token:)) ?? templateError.reason
|
||||
descriptions.append(description)
|
||||
return descriptions.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
327
Sources/Stencil/Expression.swift
Normal file
327
Sources/Stencil/Expression.swift
Normal file
@@ -0,0 +1,327 @@
|
||||
public protocol Expression: CustomStringConvertible, Resolvable {
|
||||
func evaluate(context: Context) throws -> Bool
|
||||
}
|
||||
|
||||
extension Expression {
|
||||
func resolve(_ context: Context) throws -> Any? {
|
||||
try "\(evaluate(context: context))"
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
value
|
||||
}
|
||||
|
||||
var description: String {
|
||||
"\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
final class VariableExpression: Expression, CustomStringConvertible {
|
||||
let variable: Resolvable
|
||||
|
||||
init(variable: Resolvable) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
var description: String {
|
||||
"(variable: \(variable))"
|
||||
}
|
||||
|
||||
func resolve(_ context: Context) throws -> Any? {
|
||||
try variable.resolve(context)
|
||||
}
|
||||
|
||||
/// Resolves a variable in the given context as boolean
|
||||
func evaluate(context: Context) 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
|
||||
}
|
||||
}
|
||||
|
||||
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||
let expression: Expression
|
||||
|
||||
init(expression: Expression) {
|
||||
self.expression = expression
|
||||
}
|
||||
|
||||
var description: String {
|
||||
"not \(expression)"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
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 {
|
||||
"(\(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 {
|
||||
"(\(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 {
|
||||
"(\(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 {
|
||||
"(\(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 {
|
||||
"(\(lhs) \(symbol) \(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 symbol: String {
|
||||
""
|
||||
}
|
||||
|
||||
func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
class MoreThanExpression: NumericExpression {
|
||||
override var symbol: String {
|
||||
">"
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
lhs > rhs
|
||||
}
|
||||
}
|
||||
|
||||
class MoreThanEqualExpression: NumericExpression {
|
||||
override var symbol: String {
|
||||
">="
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
lhs >= rhs
|
||||
}
|
||||
}
|
||||
|
||||
class LessThanExpression: NumericExpression {
|
||||
override var symbol: String {
|
||||
"<"
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
lhs < rhs
|
||||
}
|
||||
}
|
||||
|
||||
class LessThanEqualExpression: NumericExpression {
|
||||
override var symbol: String {
|
||||
"<="
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
lhs <= rhs
|
||||
}
|
||||
}
|
||||
|
||||
class InequalityExpression: EqualityExpression {
|
||||
override var description: String {
|
||||
"(\(lhs) != \(rhs))"
|
||||
}
|
||||
|
||||
override func evaluate(context: Context) throws -> Bool {
|
||||
try !super.evaluate(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
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
|
||||
}
|
||||
103
Sources/Stencil/Extension.swift
Normal file
103
Sources/Stencil/Extension.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
/// Container for registered tags and filters
|
||||
open class Extension {
|
||||
typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
var tags = [String: TagParser]()
|
||||
var filters = [String: Filter]()
|
||||
|
||||
/// Simple initializer
|
||||
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) { _, token in
|
||||
SimpleNode(token: token, handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers boolean filter with it's negative counterpart
|
||||
public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) {
|
||||
// swiftlint:disable:previous discouraged_optional_boolean
|
||||
filters[name] = .simple(filter)
|
||||
filters[negativeFilterName] = .simple { value in
|
||||
guard let result = try filter(value) else { return nil }
|
||||
return !result
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 { value, args, _ in try filter(value, args) }
|
||||
}
|
||||
|
||||
/// Registers a template filter with the given name
|
||||
public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?], Context) throws -> Any?) {
|
||||
filters[name] = .arguments(filter)
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultExtension: Extension {
|
||||
override init() {
|
||||
super.init()
|
||||
registerDefaultTags()
|
||||
registerDefaultFilters()
|
||||
}
|
||||
|
||||
fileprivate func registerDefaultTags() {
|
||||
registerTag("for", parser: ForNode.parse)
|
||||
registerTag("break", parser: LoopTerminationNode.parse)
|
||||
registerTag("continue", parser: LoopTerminationNode.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)
|
||||
registerFilter("filter", filter: filterFilter)
|
||||
}
|
||||
}
|
||||
|
||||
protocol FilterType {
|
||||
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
enum Filter: FilterType {
|
||||
case simple(((Any?) throws -> Any?))
|
||||
case arguments(((Any?, [Any?], Context) throws -> Any?))
|
||||
|
||||
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
|
||||
switch self {
|
||||
case let .simple(filter):
|
||||
if !arguments.isEmpty {
|
||||
throw TemplateSyntaxError("Can't invoke filter with an argument")
|
||||
}
|
||||
return try filter(value)
|
||||
case let .arguments(filter):
|
||||
return try filter(value, arguments, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Sources/Stencil/FilterTag.swift
Normal file
36
Sources/Stencil/FilterTag.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
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]) {
|
||||
try VariableNode(variable: resolvable, token: token).render(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
133
Sources/Stencil/Filters.swift
Normal file
133
Sources/Stencil/Filters.swift
Normal file
@@ -0,0 +1,133 @@
|
||||
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 at most one 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 at most one 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.isEmpty {
|
||||
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()
|
||||
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(into: [firstLine]) { result, line in
|
||||
result.append(line.isEmpty ? "" : "\(indentation)\(line)")
|
||||
}
|
||||
return result.joined(separator: "\n")
|
||||
}
|
||||
|
||||
func filterFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
|
||||
guard let value = value else { return nil }
|
||||
guard arguments.count == 1 else {
|
||||
throw TemplateSyntaxError("'filter' filter takes one argument")
|
||||
}
|
||||
|
||||
let attribute = stringify(arguments[0])
|
||||
|
||||
let expr = try context.environment.compileFilter("$0|\(attribute)")
|
||||
return try context.push(dictionary: ["$0": value]) {
|
||||
try expr.resolve(context)
|
||||
}
|
||||
}
|
||||
274
Sources/Stencil/ForTag.swift
Normal file
274
Sources/Stencil/ForTag.swift
Normal file
@@ -0,0 +1,274 @@
|
||||
import Foundation
|
||||
|
||||
class ForNode: NodeType {
|
||||
let resolvable: Resolvable
|
||||
let loopVariables: [String]
|
||||
let nodes: [NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
let `where`: Expression?
|
||||
let label: String?
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components
|
||||
|
||||
var label: String?
|
||||
if components.first?.hasSuffix(":") == true {
|
||||
label = String(components.removeFirst().dropLast())
|
||||
}
|
||||
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
components.count > (index + 1) && components[index] == token
|
||||
}
|
||||
|
||||
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
|
||||
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]
|
||||
.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 parser.compileExpression(components: Array(components.suffix(from: 5)), 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`,
|
||||
label: label,
|
||||
token: token
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
resolvable: Resolvable,
|
||||
loopVariables: [String],
|
||||
nodes: [NodeType],
|
||||
emptyNodes: [NodeType],
|
||||
where: Expression? = nil,
|
||||
label: String? = nil,
|
||||
token: Token? = nil
|
||||
) {
|
||||
self.resolvable = resolvable
|
||||
self.loopVariables = loopVariables
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
self.where = `where`
|
||||
self.label = label
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
var values = try resolve(context)
|
||||
|
||||
if let `where` = self.where {
|
||||
values = try values.filter { item -> Bool in
|
||||
try push(value: item, context: context) {
|
||||
try `where`.evaluate(context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !values.isEmpty {
|
||||
let count = values.count
|
||||
var result = ""
|
||||
|
||||
// collect parent loop contexts
|
||||
let parentLoopContexts = (context["forloop"] as? [String: Any])?
|
||||
.filter { ($1 as? [String: Any])?["label"] != nil } ?? [:]
|
||||
|
||||
for (index, item) in zip(0..., values) {
|
||||
var forContext: [String: Any] = [
|
||||
"first": index == 0,
|
||||
"last": index == (count - 1),
|
||||
"counter": index + 1,
|
||||
"counter0": index,
|
||||
"length": count
|
||||
]
|
||||
if let label = label {
|
||||
forContext["label"] = label
|
||||
forContext[label] = forContext
|
||||
}
|
||||
forContext.merge(parentLoopContexts) { lhs, _ in lhs }
|
||||
|
||||
var shouldBreak = false
|
||||
result += try context.push(dictionary: ["forloop": forContext]) {
|
||||
defer {
|
||||
// if outer loop should be continued we should break from current loop
|
||||
if let shouldContinueLabel = context[LoopTerminationNode.continueContextKey] as? String {
|
||||
shouldBreak = shouldContinueLabel != label || label == nil
|
||||
} else {
|
||||
shouldBreak = context[LoopTerminationNode.breakContextKey] != nil
|
||||
}
|
||||
}
|
||||
return try push(value: item, context: context) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
|
||||
if shouldBreak {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} else {
|
||||
return try context.push {
|
||||
try renderNodes(emptyNodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||
if loopVariables.isEmpty {
|
||||
return try context.push {
|
||||
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) {
|
||||
try closure()
|
||||
}
|
||||
}
|
||||
|
||||
return try context.push(dictionary: [loopVariables.first ?? "": value]) {
|
||||
try closure()
|
||||
}
|
||||
}
|
||||
|
||||
private func resolve(_ context: Context) throws -> [Any] {
|
||||
let resolved = try resolvable.resolve(context)
|
||||
|
||||
var values: [Any]
|
||||
if let dictionary = resolved as? [String: Any], !dictionary.isEmpty {
|
||||
values = dictionary.sorted { $0.key < $1.key }
|
||||
} 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 = []
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
struct LoopTerminationNode: NodeType {
|
||||
static let breakContextKey = "_internal_forloop_break"
|
||||
static let continueContextKey = "_internal_forloop_continue"
|
||||
|
||||
let name: String
|
||||
let label: String?
|
||||
let token: Token?
|
||||
|
||||
var contextKey: String {
|
||||
"_internal_forloop_\(name)"
|
||||
}
|
||||
|
||||
private init(name: String, label: String? = nil, token: Token? = nil) {
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.token = token
|
||||
}
|
||||
|
||||
static func parse(_ parser: TokenParser, token: Token) throws -> Self {
|
||||
let components = token.components
|
||||
|
||||
guard components.count <= 2 else {
|
||||
throw TemplateSyntaxError("'\(token.contents)' can accept only one parameter")
|
||||
}
|
||||
guard parser.hasOpenedForTag() else {
|
||||
throw TemplateSyntaxError("'\(token.contents)' can be used only inside loop body")
|
||||
}
|
||||
|
||||
return Self(name: components[0], label: components.count == 2 ? components[1] : nil, token: token)
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
let offset = zip(0..., context.dictionaries).reversed().first { _, dictionary in
|
||||
guard let forContext = dictionary["forloop"] as? [String: Any],
|
||||
dictionary["forloop"] != nil else { return false }
|
||||
|
||||
if let label = label {
|
||||
return label == forContext["label"] as? String
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}?.0
|
||||
|
||||
if let offset = offset {
|
||||
context.dictionaries[offset][contextKey] = label ?? true
|
||||
} else if let label = label {
|
||||
throw TemplateSyntaxError("No loop labeled '\(label)' is currently running")
|
||||
} else {
|
||||
throw TemplateSyntaxError("No loop is currently running")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private extension TokenParser {
|
||||
func hasOpenedForTag() -> Bool {
|
||||
var openForCount = 0
|
||||
for parsedToken in parsedTokens.reversed() where parsedToken.kind == .block {
|
||||
if parsedToken.components.first == "endfor" { openForCount -= 1 }
|
||||
if parsedToken.components.first == "for" { openForCount += 1 }
|
||||
}
|
||||
return openForCount > 0
|
||||
}
|
||||
}
|
||||
314
Sources/Stencil/IfTag.swift
Normal file
314
Sources/Stencil/IfTag.swift
Normal file
@@ -0,0 +1,314 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
static let all: [Self] = [
|
||||
.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 `operator` in Operator.all where `operator`.name == name {
|
||||
return `operator`
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
indirect enum IfToken {
|
||||
case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type)
|
||||
case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type)
|
||||
case variable(Resolvable)
|
||||
case subExpression(Expression)
|
||||
case end
|
||||
|
||||
var bindingPower: Int {
|
||||
switch self {
|
||||
case .infix(_, let bindingPower, _):
|
||||
return bindingPower
|
||||
case .prefix(_, let bindingPower, _):
|
||||
return bindingPower
|
||||
case .variable:
|
||||
return 0
|
||||
case .subExpression:
|
||||
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 operatorType):
|
||||
let expression = try parser.expression(bindingPower: bindingPower)
|
||||
return operatorType.init(expression: expression)
|
||||
case .variable(let variable):
|
||||
return VariableExpression(variable: variable)
|
||||
case .subExpression(let expression):
|
||||
return expression
|
||||
case .end:
|
||||
throw TemplateSyntaxError("'if' expression error: end")
|
||||
}
|
||||
}
|
||||
|
||||
func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression {
|
||||
switch self {
|
||||
case .infix(_, let bindingPower, let operatorType):
|
||||
let right = try parser.expression(bindingPower: bindingPower)
|
||||
return operatorType.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 .subExpression:
|
||||
throw TemplateSyntaxError("'if' expression error: sub expression 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
|
||||
|
||||
private init(tokens: [IfToken]) {
|
||||
self.tokens = tokens
|
||||
}
|
||||
|
||||
static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser {
|
||||
try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token)
|
||||
}
|
||||
|
||||
private init(components: ArraySlice<String>, environment: Environment, token: Token) throws {
|
||||
var parsedComponents = Set<Int>()
|
||||
var bracketsBalance = 0
|
||||
// swiftlint:disable:next closure_body_length
|
||||
self.tokens = try zip(components.indices, components).compactMap { index, component in
|
||||
guard !parsedComponents.contains(index) else { return nil }
|
||||
|
||||
if component == "(" {
|
||||
bracketsBalance += 1
|
||||
let (expression, parsedCount) = try Self.subExpression(
|
||||
from: components.suffix(from: index + 1),
|
||||
environment: environment,
|
||||
token: token
|
||||
)
|
||||
parsedComponents.formUnion(Set(index...(index + parsedCount)))
|
||||
return .subExpression(expression)
|
||||
} else if component == ")" {
|
||||
bracketsBalance -= 1
|
||||
if bracketsBalance < 0 {
|
||||
throw TemplateSyntaxError("'if' expression error: missing opening bracket")
|
||||
}
|
||||
parsedComponents.insert(index)
|
||||
return nil
|
||||
} else {
|
||||
parsedComponents.insert(index)
|
||||
if let `operator` = findOperator(name: component) {
|
||||
switch `operator` {
|
||||
case .infix(let name, let bindingPower, let operatorType):
|
||||
return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||
case .prefix(let name, let bindingPower, let operatorType):
|
||||
return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||
}
|
||||
}
|
||||
return .variable(try environment.compileResolvable(component, containedIn: token))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func subExpression(
|
||||
from components: ArraySlice<String>,
|
||||
environment: Environment,
|
||||
token: Token
|
||||
) throws -> (Expression, Int) {
|
||||
var bracketsBalance = 1
|
||||
let subComponents = components.prefix { component in
|
||||
if component == "(" {
|
||||
bracketsBalance += 1
|
||||
} else if component == ")" {
|
||||
bracketsBalance -= 1
|
||||
}
|
||||
return bracketsBalance != 0
|
||||
}
|
||||
if bracketsBalance > 0 {
|
||||
throw TemplateSyntaxError("'if' expression error: missing closing bracket")
|
||||
}
|
||||
|
||||
let expressionParser = try IfExpressionParser(components: subComponents, environment: environment, token: token)
|
||||
let expression = try expressionParser.parse()
|
||||
return (expression, subComponents.count)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
try context.push {
|
||||
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 parser.compileExpression(components: components, 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 parser.compileExpression(components: components, 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 parser.compileExpression(components: components, 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 ""
|
||||
}
|
||||
}
|
||||
48
Sources/Stencil/Include.swift
Normal file
48
Sources/Stencil/Include.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
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) {
|
||||
try template.render(context)
|
||||
}
|
||||
} catch {
|
||||
if let error = error as? TemplateSyntaxError {
|
||||
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
Sources/Stencil/Inheritance.swift
Normal file
158
Sources/Stencil/Inheritance.swift
Normal file
@@ -0,0 +1,158 @@
|
||||
class BlockContext {
|
||||
class var contextKey: String { "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 where 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 Self }) == nil else {
|
||||
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
|
||||
}
|
||||
|
||||
let blockNodes = parsedNodes.compactMap { $0 as? BlockNode }
|
||||
let nodes = blockNodes.reduce(into: [String: BlockNode]()) { accumulator, node in
|
||||
accumulator[node.name] = node
|
||||
}
|
||||
|
||||
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]) {
|
||||
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: [String: Any] = [
|
||||
BlockContext.contextKey: blockContext,
|
||||
"block": ["super": try self.render(context)]
|
||||
]
|
||||
|
||||
// render extension node
|
||||
do {
|
||||
return try context.push(dictionary: childContext) {
|
||||
try child.render(context)
|
||||
}
|
||||
} catch {
|
||||
throw error.withToken(child.token)
|
||||
}
|
||||
}
|
||||
|
||||
let result = try renderNodes(nodes, context)
|
||||
context.cacheBlock(name, content: result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
112
Sources/Stencil/KeyPath.swift
Normal file
112
Sources/Stencil/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 character in variable {
|
||||
switch character {
|
||||
case "." where subscriptLevel == 0:
|
||||
try foundSeparator()
|
||||
case "[":
|
||||
try openBracket()
|
||||
case "]":
|
||||
try closeBracket()
|
||||
default:
|
||||
try addCharacter(character)
|
||||
}
|
||||
}
|
||||
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(_ character: Character) throws {
|
||||
guard partialComponents.isEmpty || subscriptLevel > 0 else {
|
||||
throw TemplateSyntaxError("Unexpected character '\(character)' in variable '\(variable)'")
|
||||
}
|
||||
|
||||
current.append(character)
|
||||
}
|
||||
|
||||
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)'")
|
||||
}
|
||||
}
|
||||
}
|
||||
57
Sources/Stencil/LazyValueWrapper.swift
Normal file
57
Sources/Stencil/LazyValueWrapper.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
/// Used to lazily set context data. Useful for example if you have some data that requires heavy calculations, and may
|
||||
/// not be used in every render possiblity.
|
||||
public final class LazyValueWrapper {
|
||||
private let closure: (Context) throws -> Any
|
||||
private let context: Context?
|
||||
private var cachedValue: Any?
|
||||
|
||||
/// Create a wrapper that'll use a **reference** to the current context.
|
||||
/// This means when the closure is evaluated, it'll use the **active** context at that moment.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - closure: The closure to lazily evaluate
|
||||
public init(closure: @escaping (Context) throws -> Any) {
|
||||
self.context = nil
|
||||
self.closure = closure
|
||||
}
|
||||
|
||||
/// Create a wrapper that'll create a **copy** of the current context.
|
||||
/// This means when the closure is evaluated, it'll use the context **as it was** when this wrapper was created.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - context: The context to use during evaluation
|
||||
/// - closure: The closure to lazily evaluate
|
||||
/// - Note: This will use more memory than the other `init` as it needs to keep a copy of the full context around.
|
||||
public init(copying context: Context, closure: @escaping (Context) throws -> Any) {
|
||||
self.context = Context(dictionaries: context.dictionaries, environment: context.environment)
|
||||
self.closure = closure
|
||||
}
|
||||
|
||||
/// Shortcut for creating a lazy wrapper when you don't need access to the Stencil context.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - closure: The closure to lazily evaluate
|
||||
public init(_ closure: @autoclosure @escaping () throws -> Any) {
|
||||
self.context = nil
|
||||
self.closure = { _ in try closure() }
|
||||
}
|
||||
}
|
||||
|
||||
extension LazyValueWrapper {
|
||||
func value(context: Context) throws -> Any {
|
||||
if let value = cachedValue {
|
||||
return value
|
||||
} else {
|
||||
let value = try closure(self.context ?? context)
|
||||
cachedValue = value
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension LazyValueWrapper: Resolvable {
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
let value = try self.value(context: context)
|
||||
return try (value as? Resolvable)?.resolve(context) ?? value
|
||||
}
|
||||
}
|
||||
265
Sources/Stencil/Lexer.swift
Normal file
265
Sources/Stencil/Lexer.swift
Normal file
@@ -0,0 +1,265 @@
|
||||
import Foundation
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
typealias Line = (content: String, number: UInt, range: Range<String.Index>)
|
||||
/// Location in some content (text)
|
||||
public typealias ContentLocation = (content: String, lineNumber: UInt, lineOffset: Int)
|
||||
// swiftlint:enable large_tuple
|
||||
|
||||
struct Lexer {
|
||||
let templateName: String?
|
||||
let templateString: String
|
||||
let lines: [Line]
|
||||
|
||||
/// The potential token start characters. In a template these appear after a
|
||||
/// `{` character, for example `{{`, `{%`, `{#`, ...
|
||||
private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"]
|
||||
|
||||
/// The minimum length of a tag
|
||||
private static let tagLength = 2
|
||||
|
||||
/// The token end characters, corresponding to their token start characters.
|
||||
/// For example, a variable token starts with `{{` and ends with `}}`
|
||||
private static let tokenCharMap: [Unicode.Scalar: Unicode.Scalar] = [
|
||||
"{": "}",
|
||||
"%": "%",
|
||||
"#": "#"
|
||||
]
|
||||
|
||||
/// Characters controlling whitespace trimming behaviour
|
||||
private static let behaviourMap: [Character: WhitespaceBehaviour.Behaviour] = [
|
||||
"+": .keep,
|
||||
"-": .trim
|
||||
]
|
||||
|
||||
init(templateName: String? = nil, templateString: String) {
|
||||
self.templateName = templateName
|
||||
self.templateString = templateString
|
||||
|
||||
self.lines = zip(1..., templateString.components(separatedBy: .newlines)).compactMap { index, line in
|
||||
guard !line.isEmpty,
|
||||
let range = templateString.range(of: line) else { return nil }
|
||||
return (content: line, number: UInt(index), range)
|
||||
}
|
||||
}
|
||||
|
||||
private func behaviour(string: String, tagLength: Int) -> WhitespaceBehaviour {
|
||||
let leftIndex = string.index(string.startIndex, offsetBy: tagLength, limitedBy: string.endIndex)
|
||||
let rightIndex = string.index(string.endIndex, offsetBy: -(tagLength + 1), limitedBy: string.startIndex)
|
||||
|
||||
return WhitespaceBehaviour(
|
||||
leading: Self.behaviourMap[leftIndex.map { string[$0] } ?? " "] ?? .unspecified,
|
||||
trailing: Self.behaviourMap[rightIndex.map { string[$0] } ?? " "] ?? .unspecified
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a token that will be passed on to the parser, with the given
|
||||
/// content and a range. The content will be tested to see if it's a
|
||||
/// `variable`, a `block` or a `comment`, otherwise it'll default to a simple
|
||||
/// `text` token.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - string: The content string of the token
|
||||
/// - range: The range within the template content, used for smart
|
||||
/// error reporting
|
||||
func createToken(string: String, at range: Range<String.Index>, _ isInEscapeMode: Bool = false) -> Token {
|
||||
func strip(length: (Int, Int) = (Self.tagLength, Self.tagLength)) -> String {
|
||||
guard string.count > (length.0 + length.1) else { return "" }
|
||||
let trimmed = String(string.dropFirst(length.0).dropLast(length.1))
|
||||
.components(separatedBy: "\n")
|
||||
.filter { !$0.isEmpty }
|
||||
.map { $0.trim(character: " ") }
|
||||
.joined(separator: " ")
|
||||
return trimmed
|
||||
}
|
||||
|
||||
if !isInEscapeMode && (string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#")) {
|
||||
let behaviour = string.hasPrefix("{%") ? behaviour(string: string, tagLength: Self.tagLength) : .unspecified
|
||||
let stripLengths = (
|
||||
Self.tagLength + (behaviour.leading != .unspecified ? 1 : 0),
|
||||
Self.tagLength + (behaviour.trailing != .unspecified ? 1 : 0)
|
||||
)
|
||||
|
||||
let value = strip(length: stripLengths)
|
||||
let range = templateString.range(of: value, range: range) ?? range
|
||||
let location = rangeLocation(range)
|
||||
let sourceMap = SourceMap(filename: templateName, location: location)
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return .variable(value: value, at: sourceMap)
|
||||
} else if string.hasPrefix("{%") {
|
||||
return .block(value: strip(length: stripLengths), at: sourceMap, whitespace: behaviour)
|
||||
} else if string.hasPrefix("{#") {
|
||||
return .comment(value: value, at: sourceMap)
|
||||
}
|
||||
}
|
||||
|
||||
let location = rangeLocation(range)
|
||||
let sourceMap = SourceMap(filename: templateName, location: location)
|
||||
return .text(value: string, at: sourceMap)
|
||||
}
|
||||
|
||||
/// Transforms the template into a list of tokens, that will eventually be
|
||||
/// passed on to the parser.
|
||||
///
|
||||
/// - Returns: The list of tokens (see `createToken(string: at:)`).
|
||||
func tokenize() -> [Token] {
|
||||
var tokens: [Token] = []
|
||||
|
||||
let scanner = Scanner(templateString)
|
||||
while !scanner.isEmpty {
|
||||
if let (char, text, isInEscapeMode) = scanner.scanForTokenStart(Self.tokenChars) {
|
||||
if !text.isEmpty {
|
||||
tokens.append(createToken(string: text, at: scanner.range))
|
||||
}
|
||||
|
||||
guard let end = Self.tokenCharMap[char] else { continue }
|
||||
let result = scanner.scanForTokenEnd(end)
|
||||
tokens.append(createToken(string: result, at: scanner.range, isInEscapeMode))
|
||||
} else {
|
||||
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
||||
scanner.content = ""
|
||||
}
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
/// Finds the line matching the given range (for a token)
|
||||
///
|
||||
/// - Parameter range: The range to search for.
|
||||
/// - Returns: The content for that line, the line number and offset within
|
||||
/// the line.
|
||||
func rangeLocation(_ range: Range<String.Index>) -> ContentLocation {
|
||||
guard let line = self.lines.first(where: { $0.range.contains(range.lowerBound) }) else {
|
||||
return ("", 0, 0)
|
||||
}
|
||||
let offset = templateString.distance(from: line.range.lowerBound, to: range.lowerBound)
|
||||
return (line.content, line.number, offset)
|
||||
}
|
||||
}
|
||||
|
||||
class Scanner {
|
||||
let originalContent: String
|
||||
var content: String
|
||||
var range: Range<String.UnicodeScalarView.Index>
|
||||
|
||||
/// The start delimiter for a token.
|
||||
private static let tokenStartDelimiter: Unicode.Scalar = "{"
|
||||
/// And the corresponding end delimiter for a token.
|
||||
private static let tokenEndDelimiter: Unicode.Scalar = "}"
|
||||
private static let tokenDelimiterEscape: Unicode.Scalar = "\\"
|
||||
|
||||
init(_ content: String) {
|
||||
self.originalContent = content
|
||||
self.content = content
|
||||
range = content.unicodeScalars.startIndex..<content.unicodeScalars.startIndex
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
content.isEmpty
|
||||
}
|
||||
|
||||
/// Scans for the end of a token, with a specific ending character. If we're
|
||||
/// searching for the end of a block token `%}`, this method receives a `%`.
|
||||
/// The scanner will search for that `%` followed by a `}`.
|
||||
///
|
||||
/// Note: if the end of a token is found, the `content` and `range`
|
||||
/// properties are updated to reflect this. `content` will be set to what
|
||||
/// remains of the template after the token. `range` will be set to the range
|
||||
/// of the token within the template.
|
||||
///
|
||||
/// - Parameter tokenChar: The token end character to search for.
|
||||
/// - Returns: The content of a token, or "" if no token end was found.
|
||||
func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String {
|
||||
var foundChar = false
|
||||
|
||||
for (index, char) in zip(0..., content.unicodeScalars) {
|
||||
if foundChar && char == Self.tokenEndDelimiter {
|
||||
let result = String(content.unicodeScalars.prefix(index + 1))
|
||||
content = String(content.unicodeScalars.dropFirst(index + 1))
|
||||
range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index + 1)
|
||||
return result
|
||||
} else {
|
||||
foundChar = (char == tokenChar)
|
||||
}
|
||||
}
|
||||
|
||||
content = ""
|
||||
return ""
|
||||
}
|
||||
|
||||
/// Scans for the start of a token, with a list of potential starting
|
||||
/// characters. To scan for the start of variables (`{{`), blocks (`{%`) and
|
||||
/// comments (`{#`), this method receives the characters `{`, `%` and `#`.
|
||||
/// The scanner will search for a `{`, followed by one of the search
|
||||
/// characters. It will give the found character, and the content that came
|
||||
/// before the token.
|
||||
///
|
||||
/// Note: if the start of a token is found, the `content` and `range`
|
||||
/// properties are updated to reflect this. `content` will be set to what
|
||||
/// remains of the template starting with the token. `range` will be set to
|
||||
/// the start of the token within the template.
|
||||
///
|
||||
/// - Parameter tokenChars: List of token start characters to search for.
|
||||
/// - Returns: The found token start character, together with the content
|
||||
/// before the token, or nil of no token start was found.
|
||||
// swiftlint:disable:next large_tuple
|
||||
func scanForTokenStart(_ tokenChars: [Unicode.Scalar]) -> (Unicode.Scalar, String, Bool)? {
|
||||
var foundBrace = false
|
||||
var isInEscapeMode = false
|
||||
var lastChar: Unicode.Scalar = " "
|
||||
|
||||
range = range.upperBound..<range.upperBound
|
||||
for (index, char) in zip(0..., content.unicodeScalars) {
|
||||
if foundBrace && tokenChars.contains(char) {
|
||||
let prefixOffset = isInEscapeMode ? 1 : 0
|
||||
let prefix = String(content.unicodeScalars.prefix(index - 1 - prefixOffset))
|
||||
content = String(content.unicodeScalars.dropFirst(index - 1))
|
||||
range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index - 1)
|
||||
|
||||
return (char, prefix, isInEscapeMode)
|
||||
} else {
|
||||
foundBrace = (char == Self.tokenStartDelimiter)
|
||||
isInEscapeMode = (lastChar == Self.tokenDelimiterEscape)
|
||||
lastChar = char
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
128
Sources/Stencil/Loader.swift
Normal file
128
Sources/Stencil/Loader.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
/// Type used for loading a template
|
||||
public protocol Loader {
|
||||
/// Load a template with the given name
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template
|
||||
/// Load a template with the given list of names
|
||||
func loadTemplate(names: [String], environment: Environment) throws -> Template
|
||||
}
|
||||
|
||||
extension Loader {
|
||||
/// Default implementation, tries to load the first template that exists from the list of given names
|
||||
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.compactMap { bundle in
|
||||
Path(bundle.path)
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
"FileSystemLoader(\(paths))"
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
for path in paths {
|
||||
let templatePath = try path.safeJoin(path: name)
|
||||
|
||||
if !templatePath.exists {
|
||||
continue
|
||||
}
|
||||
|
||||
let content: String = try String(contentsOf: templatePath)
|
||||
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: templateName)
|
||||
|
||||
if templatePath.exists {
|
||||
let content: String = try String(contentsOf: templatePath)
|
||||
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: String) throws -> Path {
|
||||
let newPath = self / path
|
||||
|
||||
if !newPath.string.hasPrefix(self.string) {
|
||||
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 {
|
||||
"Path `\(path)` is located outside of base path `\(basePath)`"
|
||||
}
|
||||
}
|
||||
185
Sources/Stencil/Node.swift
Normal file
185
Sources/Stencil/Node.swift
Normal file
@@ -0,0 +1,185 @@
|
||||
import Foundation
|
||||
|
||||
/// Represents a parsed node
|
||||
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 {
|
||||
var result = ""
|
||||
|
||||
for node in nodes {
|
||||
do {
|
||||
result += try node.render(context)
|
||||
} catch {
|
||||
throw error.withToken(node.token)
|
||||
}
|
||||
|
||||
let shouldBreak = context[LoopTerminationNode.breakContextKey] != nil
|
||||
let shouldContinue = context[LoopTerminationNode.continueContextKey] != nil
|
||||
|
||||
if shouldBreak || shouldContinue {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Simple node, used for triggering a closure during rendering
|
||||
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 {
|
||||
try handler(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a block of text, renders the text
|
||||
public class TextNode: NodeType {
|
||||
public let text: String
|
||||
public let token: Token?
|
||||
public let trimBehaviour: TrimBehaviour
|
||||
|
||||
public init(text: String, trimBehaviour: TrimBehaviour = .nothing) {
|
||||
self.text = text
|
||||
self.token = nil
|
||||
self.trimBehaviour = trimBehaviour
|
||||
}
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
var string = self.text
|
||||
if trimBehaviour.leading != .nothing, !string.isEmpty {
|
||||
let range = NSRange(..<string.endIndex, in: string)
|
||||
string = TrimBehaviour.leadingRegex(trim: trimBehaviour.leading)
|
||||
.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
|
||||
}
|
||||
if trimBehaviour.trailing != .nothing, !string.isEmpty {
|
||||
let range = NSRange(..<string.endIndex, in: string)
|
||||
string = TrimBehaviour.trailingRegex(trim: trimBehaviour.trailing)
|
||||
.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: "")
|
||||
}
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
/// Representing something that can be resolved in a context
|
||||
public protocol Resolvable {
|
||||
/// Try to resolve this with the given context
|
||||
func resolve(_ context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
/// Represents a variable, renders the variable, may have conditional expressions.
|
||||
public class VariableNode: NodeType {
|
||||
public let variable: Resolvable
|
||||
public var token: Token?
|
||||
let condition: Expression?
|
||||
let elseExpression: Resolvable?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let components = token.components
|
||||
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
components.count > (index + 1) && components[index] == token
|
||||
}
|
||||
func compileResolvable(_ components: [String], containedIn token: Token) throws -> Resolvable {
|
||||
try (try? parser.compileExpression(components: components, token: token)) ??
|
||||
parser.compileFilter(components.joined(separator: " "), containedIn: token)
|
||||
}
|
||||
|
||||
let variable: Resolvable
|
||||
let condition: Expression?
|
||||
let elseExpression: Resolvable?
|
||||
|
||||
if hasToken("if", at: 1) {
|
||||
variable = try compileResolvable([components[0]], containedIn: token)
|
||||
|
||||
let components = components.suffix(from: 2)
|
||||
if let elseIndex = components.firstIndex(of: "else") {
|
||||
condition = try parser.compileExpression(components: Array(components.prefix(upTo: elseIndex)), token: token)
|
||||
let elseToken = Array(components.suffix(from: elseIndex.advanced(by: 1)))
|
||||
elseExpression = try compileResolvable(elseToken, containedIn: token)
|
||||
} else {
|
||||
condition = try parser.compileExpression(components: Array(components), token: token)
|
||||
elseExpression = nil
|
||||
}
|
||||
} else if !components.isEmpty {
|
||||
variable = try compileResolvable(components, containedIn: token)
|
||||
condition = nil
|
||||
elseExpression = nil
|
||||
} else {
|
||||
throw TemplateSyntaxError(reason: "Missing variable name", token: token)
|
||||
}
|
||||
|
||||
return VariableNode(variable: variable, token: token, condition: condition, elseExpression: elseExpression)
|
||||
}
|
||||
|
||||
public init(variable: Resolvable, token: Token? = nil) {
|
||||
self.variable = variable
|
||||
self.token = token
|
||||
self.condition = nil
|
||||
self.elseExpression = nil
|
||||
}
|
||||
|
||||
init(variable: Resolvable, token: Token? = nil, condition: Expression?, elseExpression: Resolvable?) {
|
||||
self.variable = variable
|
||||
self.token = token
|
||||
self.condition = condition
|
||||
self.elseExpression = elseExpression
|
||||
}
|
||||
|
||||
public init(variable: String, token: Token? = nil) {
|
||||
self.variable = Variable(variable)
|
||||
self.token = token
|
||||
self.condition = nil
|
||||
self.elseExpression = nil
|
||||
}
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
if let condition = self.condition, try condition.evaluate(context: context) == false {
|
||||
return try elseExpression?.resolve(context).map(stringify) ?? ""
|
||||
}
|
||||
|
||||
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] {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Sources/Stencil/NowTag.swift
Normal file
44
Sources/Stencil/NowTag.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
#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
|
||||
272
Sources/Stencil/Parser.swift
Normal file
272
Sources/Stencil/Parser.swift
Normal file
@@ -0,0 +1,272 @@
|
||||
/// Creates a checker that will stop parsing if it encounters a list of tags.
|
||||
/// Useful for example for scanning until a given "end"-node.
|
||||
public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||
{ _, token in
|
||||
if let name = token.components.first {
|
||||
for tag in tags where 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 {
|
||||
/// Parser for finding a kind of node
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
fileprivate var tokens: [Token]
|
||||
fileprivate(set) var parsedTokens: [Token] = []
|
||||
fileprivate let environment: Environment
|
||||
fileprivate var previousWhiteSpace: WhitespaceBehaviour.Behaviour?
|
||||
|
||||
/// Simple initializer
|
||||
public init(tokens: [Token], environment: Environment) {
|
||||
self.tokens = tokens
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
/// Parse the given tokens into nodes
|
||||
public func parse() throws -> [NodeType] {
|
||||
try parse(nil)
|
||||
}
|
||||
|
||||
/// Parse nodes until a specific "something" is detected, determined by the provided closure.
|
||||
/// Combine this with the `until(:)` function above to scan nodes until a given token.
|
||||
public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] {
|
||||
var nodes = [NodeType]()
|
||||
|
||||
while !tokens.isEmpty {
|
||||
guard let token = nextToken() else { break }
|
||||
|
||||
switch token.kind {
|
||||
case .text:
|
||||
nodes.append(TextNode(text: token.contents, trimBehaviour: trimBehaviour))
|
||||
case .variable:
|
||||
previousWhiteSpace = nil
|
||||
try nodes.append(VariableNode.parse(self, token: token))
|
||||
case .block:
|
||||
previousWhiteSpace = token.whitespace?.trailing
|
||||
if let parseUntil = parseUntil, parseUntil(self, token) {
|
||||
prependToken(token)
|
||||
return nodes
|
||||
}
|
||||
|
||||
if var tag = token.components.first {
|
||||
do {
|
||||
// special case for labeled tags (such as for loops)
|
||||
if tag.hasSuffix(":") && token.components.count >= 2 {
|
||||
tag = token.components[1]
|
||||
}
|
||||
|
||||
let parser = try environment.findTag(name: tag)
|
||||
let node = try parser(self, token)
|
||||
nodes.append(node)
|
||||
} catch {
|
||||
throw error.withToken(token)
|
||||
}
|
||||
}
|
||||
case .comment:
|
||||
previousWhiteSpace = nil
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
/// Pop the next token (returning it)
|
||||
public func nextToken() -> Token? {
|
||||
if !tokens.isEmpty {
|
||||
let nextToken = tokens.remove(at: 0)
|
||||
parsedTokens.append(nextToken)
|
||||
return nextToken
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func peekWhitespace() -> WhitespaceBehaviour.Behaviour? {
|
||||
tokens.first?.whitespace?.leading
|
||||
}
|
||||
|
||||
/// Insert a token
|
||||
public func prependToken(_ token: Token) {
|
||||
tokens.insert(token, at: 0)
|
||||
if parsedTokens.last == token {
|
||||
parsedTokens.removeLast()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create filter expression from a string contained in provided token
|
||||
public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable {
|
||||
try environment.compileFilter(filterToken, containedIn: token)
|
||||
}
|
||||
|
||||
/// Create boolean expression from components contained in provided token
|
||||
public func compileExpression(components: [String], token: Token) throws -> Expression {
|
||||
try environment.compileExpression(components: components, containedIn: token)
|
||||
}
|
||||
|
||||
/// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token
|
||||
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
try environment.compileResolvable(token, containedIn: containingToken)
|
||||
}
|
||||
|
||||
private var trimBehaviour: TrimBehaviour {
|
||||
var behaviour: TrimBehaviour = .nothing
|
||||
|
||||
if let leading = previousWhiteSpace {
|
||||
if leading == .unspecified {
|
||||
behaviour.leading = environment.trimBehaviour.trailing
|
||||
} else {
|
||||
behaviour.leading = leading == .trim ? .whitespaceAndNewLines : .nothing
|
||||
}
|
||||
}
|
||||
if let trailing = peekWhitespace() {
|
||||
if trailing == .unspecified {
|
||||
behaviour.trailing = environment.trimBehaviour.leading
|
||||
} else {
|
||||
behaviour.trailing = trailing == .trim ? .whitespaceAndNewLines : .nothing
|
||||
}
|
||||
}
|
||||
|
||||
return behaviour
|
||||
}
|
||||
}
|
||||
|
||||
extension Environment {
|
||||
func findTag(name: String) throws -> Extension.TagParser {
|
||||
for ext in extensions {
|
||||
if let filter = ext.tags[name] {
|
||||
return filter
|
||||
}
|
||||
}
|
||||
|
||||
throw TemplateSyntaxError("Unknown template tag '\(name)'")
|
||||
}
|
||||
|
||||
func findFilter(_ name: String) throws -> FilterType {
|
||||
for ext in 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 = 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.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 }
|
||||
}
|
||||
|
||||
/// Create filter expression from a string
|
||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||
try FilterExpression(token: token, environment: self)
|
||||
}
|
||||
|
||||
/// Create filter expression from a string contained in provided token
|
||||
public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
do {
|
||||
return try FilterExpression(token: filterToken, environment: 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 location = containingToken.sourceMap.location
|
||||
location.lineOffset += containingToken.contents.distance(
|
||||
from: containingToken.contents.startIndex,
|
||||
to: filterTokenRange.lowerBound
|
||||
)
|
||||
syntaxError.token = .variable(
|
||||
value: filterToken,
|
||||
at: SourceMap(filename: containingToken.sourceMap.filename, location: location)
|
||||
)
|
||||
} else {
|
||||
syntaxError.token = containingToken
|
||||
}
|
||||
throw syntaxError
|
||||
}
|
||||
}
|
||||
|
||||
/// Create resolvable (i.e. range variable or filter expression) from a string
|
||||
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||
try RangeVariable(token, environment: self)
|
||||
?? compileFilter(token)
|
||||
}
|
||||
|
||||
/// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token
|
||||
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
try RangeVariable(token, environment: self, containedIn: containingToken)
|
||||
?? compileFilter(token, containedIn: containingToken)
|
||||
}
|
||||
|
||||
/// Create boolean expression from components contained in provided token
|
||||
public func compileExpression(components: [String], containedIn token: Token) throws -> Expression {
|
||||
try IfExpressionParser.parser(components: components, environment: self, token: token).parse()
|
||||
}
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||
extension String {
|
||||
subscript(_ index: Int) -> Character {
|
||||
self[self.index(self.startIndex, offsetBy: index)]
|
||||
}
|
||||
|
||||
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.count)
|
||||
current = [Int](repeating: 0, count: target.count + 1)
|
||||
|
||||
for selfIndex in 0..<self.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] = selfIndex + 1
|
||||
|
||||
// use formula to fill in the rest of the row
|
||||
for targetIndex in 0..<target.count {
|
||||
current[targetIndex + 1] = Swift.min(
|
||||
last[targetIndex + 1] + 1,
|
||||
current[targetIndex] + 1,
|
||||
last[targetIndex] + (self[selfIndex] == target[targetIndex] ? 0 : 1)
|
||||
)
|
||||
}
|
||||
|
||||
// copy v1 (current row) to v0 (previous row) for next iteration
|
||||
last = current
|
||||
}
|
||||
|
||||
return current[target.count]
|
||||
}
|
||||
}
|
||||
87
Sources/Stencil/Template.swift
Normal file
87
Sources/Stencil/Template.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
#if os(Linux)
|
||||
// swiftlint:disable:next prefixed_toplevel_constant
|
||||
let NSFileNoSuchFileError = 4
|
||||
#endif
|
||||
|
||||
/// A class representing a template
|
||||
open class Template: ExpressibleByStringLiteral {
|
||||
let templateString: String
|
||||
var environment: Environment
|
||||
|
||||
/// The list of parsed (lexed) tokens
|
||||
public 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 {
|
||||
guard let path = Path(url: URL) else {
|
||||
throw TemplateDoesNotExist(templateNames: [URL.lastPathComponent])
|
||||
}
|
||||
try self.init(path: 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 {
|
||||
let value = try String(contentsOf: path)
|
||||
self.init(templateString: value, environment: environment, name: name)
|
||||
}
|
||||
|
||||
// MARK: ExpressibleByStringLiteral
|
||||
|
||||
// Create a templaVte with a template string literal
|
||||
public required convenience init(stringLiteral value: String) {
|
||||
self.init(templateString: value)
|
||||
}
|
||||
|
||||
// Create a template with a template string literal
|
||||
public required convenience init(extendedGraphemeClusterLiteral value: StringLiteralType) {
|
||||
self.init(stringLiteral: value)
|
||||
}
|
||||
|
||||
// Create a template with a template string literal
|
||||
public required convenience init(unicodeScalarLiteral value: StringLiteralType) {
|
||||
self.init(stringLiteral: value)
|
||||
}
|
||||
|
||||
/// Render the given template with a context
|
||||
public 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
|
||||
// swiftlint:disable:next discouraged_optional_collection
|
||||
open func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||
try render(Context(dictionary: dictionary ?? [:], environment: environment))
|
||||
}
|
||||
}
|
||||
154
Sources/Stencil/Tokenizer.swift
Normal file
154
Sources/Stencil/Tokenizer.swift
Normal file
@@ -0,0 +1,154 @@
|
||||
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
|
||||
|
||||
for character in self {
|
||||
if character == "'" {
|
||||
singleQuoteCount += 1
|
||||
} else if character == "\"" {
|
||||
doubleQuoteCount += 1
|
||||
}
|
||||
|
||||
if character == separate {
|
||||
if separate != separator {
|
||||
word.append(separate)
|
||||
} else if (singleQuoteCount.isMultiple(of: 2) || doubleQuoteCount.isMultiple(of: 2)) && !word.isEmpty {
|
||||
appendWord(word, to: &components)
|
||||
word = ""
|
||||
}
|
||||
|
||||
separate = separator
|
||||
} else {
|
||||
if separate == separator && (character == "'" || character == "\"") {
|
||||
separate = character
|
||||
}
|
||||
word.append(character)
|
||||
}
|
||||
}
|
||||
|
||||
if !word.isEmpty {
|
||||
appendWord(word, to: &components)
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
private func appendWord(_ word: String, to components: inout [String]) {
|
||||
let specialCharacters = ",|:"
|
||||
|
||||
if !components.isEmpty {
|
||||
if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) {
|
||||
// special case for labeled for-loops
|
||||
if components.count == 1 && word == "for" {
|
||||
components.append(word)
|
||||
} else {
|
||||
components[components.count - 1] += word
|
||||
}
|
||||
} else if specialCharacters.contains(word) {
|
||||
components[components.count - 1] += word
|
||||
} else if word != "(" && word.first == "(" || word != ")" && word.first == ")" {
|
||||
components.append(String(word.prefix(1)))
|
||||
appendWord(String(word.dropFirst()), to: &components)
|
||||
} else if word != "(" && word.last == "(" || word != ")" && word.last == ")" {
|
||||
appendWord(String(word.dropLast()), to: &components)
|
||||
components.append(String(word.suffix(1)))
|
||||
} else {
|
||||
components.append(word)
|
||||
}
|
||||
} else {
|
||||
components.append(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct SourceMap: Equatable {
|
||||
public let filename: String?
|
||||
public let location: ContentLocation
|
||||
|
||||
init(filename: String? = nil, location: ContentLocation = ("", 0, 0)) {
|
||||
self.filename = filename
|
||||
self.location = location
|
||||
}
|
||||
|
||||
static let unknown = Self()
|
||||
|
||||
public static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.filename == rhs.filename && lhs.location == rhs.location
|
||||
}
|
||||
}
|
||||
|
||||
public struct WhitespaceBehaviour: Equatable {
|
||||
public enum Behaviour {
|
||||
case unspecified
|
||||
case trim
|
||||
case keep
|
||||
}
|
||||
|
||||
let leading: Behaviour
|
||||
let trailing: Behaviour
|
||||
|
||||
public static let unspecified = Self(leading: .unspecified, trailing: .unspecified)
|
||||
}
|
||||
|
||||
public class Token: Equatable {
|
||||
public enum Kind: Equatable {
|
||||
/// A token representing a piece of text.
|
||||
case text
|
||||
/// A token representing a variable.
|
||||
case variable
|
||||
/// A token representing a comment.
|
||||
case comment
|
||||
/// A token representing a template block.
|
||||
case block
|
||||
}
|
||||
|
||||
public let contents: String
|
||||
public let kind: Kind
|
||||
public let sourceMap: SourceMap
|
||||
public var whitespace: WhitespaceBehaviour?
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public private(set) lazy var components: [String] = self.contents.smartSplit()
|
||||
|
||||
init(contents: String, kind: Kind, sourceMap: SourceMap, whitespace: WhitespaceBehaviour? = nil) {
|
||||
self.contents = contents
|
||||
self.kind = kind
|
||||
self.sourceMap = sourceMap
|
||||
self.whitespace = whitespace
|
||||
}
|
||||
|
||||
/// A token representing a piece of text.
|
||||
public static func text(value: String, at sourceMap: SourceMap) -> Token {
|
||||
Token(contents: value, kind: .text, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
/// A token representing a variable.
|
||||
public static func variable(value: String, at sourceMap: SourceMap) -> Token {
|
||||
Token(contents: value, kind: .variable, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
/// A token representing a comment.
|
||||
public static func comment(value: String, at sourceMap: SourceMap) -> Token {
|
||||
Token(contents: value, kind: .comment, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
/// A token representing a template block.
|
||||
public static func block(
|
||||
value: String,
|
||||
at sourceMap: SourceMap,
|
||||
whitespace: WhitespaceBehaviour = .unspecified
|
||||
) -> Token {
|
||||
Token(contents: value, kind: .block, sourceMap: sourceMap, whitespace: whitespace)
|
||||
}
|
||||
|
||||
public static func == (lhs: Token, rhs: Token) -> Bool {
|
||||
lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap
|
||||
}
|
||||
}
|
||||
75
Sources/Stencil/TrimBehaviour.swift
Normal file
75
Sources/Stencil/TrimBehaviour.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
import Foundation
|
||||
|
||||
public struct TrimBehaviour: Equatable {
|
||||
var leading: Trim
|
||||
var trailing: Trim
|
||||
|
||||
public enum Trim {
|
||||
/// nothing
|
||||
case nothing
|
||||
|
||||
/// tabs and spaces
|
||||
case whitespace
|
||||
|
||||
/// tabs and spaces and a single new line
|
||||
case whitespaceAndOneNewLine
|
||||
|
||||
/// all tabs spaces and newlines
|
||||
case whitespaceAndNewLines
|
||||
}
|
||||
|
||||
public init(leading: Trim, trailing: Trim) {
|
||||
self.leading = leading
|
||||
self.trailing = trailing
|
||||
}
|
||||
|
||||
/// doesn't touch newlines
|
||||
public static let nothing = Self(leading: .nothing, trailing: .nothing)
|
||||
|
||||
/// removes whitespace before a block and whitespace and a single newline after a block
|
||||
public static let smart = Self(leading: .whitespace, trailing: .whitespaceAndOneNewLine)
|
||||
|
||||
/// removes all whitespace and newlines before and after a block
|
||||
public static let all = Self(leading: .whitespaceAndNewLines, trailing: .whitespaceAndNewLines)
|
||||
|
||||
static func leadingRegex(trim: Trim) -> NSRegularExpression {
|
||||
switch trim {
|
||||
case .nothing:
|
||||
fatalError("No RegularExpression for none")
|
||||
case .whitespace:
|
||||
return Self.leadingWhitespace
|
||||
case .whitespaceAndOneNewLine:
|
||||
return Self.leadingWhitespaceAndOneNewLine
|
||||
case .whitespaceAndNewLines:
|
||||
return Self.leadingWhitespaceAndNewlines
|
||||
}
|
||||
}
|
||||
|
||||
static func trailingRegex(trim: Trim) -> NSRegularExpression {
|
||||
switch trim {
|
||||
case .nothing:
|
||||
fatalError("No RegularExpression for none")
|
||||
case .whitespace:
|
||||
return Self.trailingWhitespace
|
||||
case .whitespaceAndOneNewLine:
|
||||
return Self.trailingWhitespaceAndOneNewLine
|
||||
case .whitespaceAndNewLines:
|
||||
return Self.trailingWhitespaceAndNewLines
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next force_try
|
||||
private static let leadingWhitespaceAndNewlines = try! NSRegularExpression(pattern: "^\\s+")
|
||||
// swiftlint:disable:next force_try
|
||||
private static let trailingWhitespaceAndNewLines = try! NSRegularExpression(pattern: "\\s+$")
|
||||
|
||||
// swiftlint:disable:next force_try
|
||||
private static let leadingWhitespaceAndOneNewLine = try! NSRegularExpression(pattern: "^[ \t]*\n")
|
||||
// swiftlint:disable:next force_try
|
||||
private static let trailingWhitespaceAndOneNewLine = try! NSRegularExpression(pattern: "\n[ \t]*$")
|
||||
|
||||
// swiftlint:disable:next force_try
|
||||
private static let leadingWhitespace = try! NSRegularExpression(pattern: "^[ \t]*")
|
||||
// swiftlint:disable:next force_try
|
||||
private static let trailingWhitespace = try! NSRegularExpression(pattern: "[ \t]*$")
|
||||
}
|
||||
285
Sources/Stencil/Variable.swift
Normal file
285
Sources/Stencil/Variable.swift
Normal file
@@ -0,0 +1,285 @@
|
||||
import Foundation
|
||||
|
||||
typealias Number = Float
|
||||
|
||||
class FilterExpression: Resolvable {
|
||||
let filters: [(FilterType, [Variable])]
|
||||
let variable: Variable
|
||||
|
||||
init(token: String, environment: Environment) throws {
|
||||
let bits = token.smartSplit(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 { bit in
|
||||
let (name, arguments) = parseFilterComponents(token: bit)
|
||||
let filter = try environment.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) { value, filter in
|
||||
let arguments = try filter.1.map { try $0.resolve(context) }
|
||||
return try filter.0.invoke(value: value, arguments: arguments, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Resolve the variable in the given context
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
if variable.count > 1 &&
|
||||
((variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\""))) {
|
||||
// String literal
|
||||
return String(variable[variable.index(after: variable.startIndex) ..< variable.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
|
||||
}
|
||||
|
||||
var current: Any? = context
|
||||
for bit in try lookup(context) {
|
||||
current = resolve(bit: bit, context: current)
|
||||
|
||||
if current == nil {
|
||||
return nil
|
||||
} else if let lazyCurrent = current as? LazyValueWrapper {
|
||||
current = try lazyCurrent.value(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Split the lookup string and resolve references if possible
|
||||
private func lookup(_ context: Context) throws -> [String] {
|
||||
let keyPath = KeyPath(variable, in: context)
|
||||
return try keyPath.parse()
|
||||
}
|
||||
|
||||
// Try to resolve a partial keypath for the given context
|
||||
private func resolve(bit: String, context: Any?) -> Any? {
|
||||
let context = normalize(context)
|
||||
|
||||
if let context = context as? Context {
|
||||
return context[bit]
|
||||
} else if let dictionary = context as? [String: Any] {
|
||||
return resolve(bit: bit, dictionary: dictionary)
|
||||
} else if let array = context as? [Any] {
|
||||
return resolve(bit: bit, collection: array)
|
||||
} else if let string = context as? String {
|
||||
return resolve(bit: bit, collection: string)
|
||||
} else if let value = context as? DynamicMemberLookup {
|
||||
return value[dynamicMember: bit]
|
||||
} else if let object = context as? NSObject { // NSKeyValueCoding
|
||||
#if canImport(ObjectiveC)
|
||||
if object.responds(to: Selector(bit)) {
|
||||
return object.value(forKey: bit)
|
||||
}
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
} else if let value = context {
|
||||
return Mirror(reflecting: value).getValue(for: bit)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to resolve a partial keypath for the given dictionary
|
||||
private func resolve(bit: String, dictionary: [String: Any]) -> Any? {
|
||||
if bit == "count" {
|
||||
return dictionary.count
|
||||
} else {
|
||||
return dictionary[bit]
|
||||
}
|
||||
}
|
||||
|
||||
// Try to resolve a partial keypath for the given collection
|
||||
private func resolve<T: Collection>(bit: String, collection: T) -> Any? {
|
||||
if let index = Int(bit) {
|
||||
if index >= 0 && index < collection.count {
|
||||
return collection[collection.index(collection.startIndex, offsetBy: index)]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else if bit == "first" {
|
||||
return collection.first
|
||||
} else if bit == "last" {
|
||||
return collection[collection.index(collection.endIndex, offsetBy: -1)]
|
||||
} else if bit == "count" {
|
||||
return collection.count
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
// swiftlint:disable:next identifier_name
|
||||
public let to: Resolvable
|
||||
|
||||
public init?(_ token: String, environment: Environment) throws {
|
||||
let components = token.components(separatedBy: "...")
|
||||
guard components.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.from = try environment.compileFilter(components[0])
|
||||
self.to = try environment.compileFilter(components[1])
|
||||
}
|
||||
|
||||
public init?(_ token: String, environment: Environment, containedIn containingToken: Token) throws {
|
||||
let components = token.components(separatedBy: "...")
|
||||
guard components.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.from = try environment.compileFilter(components[0], containedIn: containingToken)
|
||||
self.to = try environment.compileFilter(components[1], containedIn: containingToken)
|
||||
}
|
||||
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
let lowerResolved = try from.resolve(context)
|
||||
let upperResolved = try to.resolve(context)
|
||||
|
||||
guard let lower = lowerResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'from' value is not an Integer (\(lowerResolved ?? "nil"))")
|
||||
}
|
||||
|
||||
guard let upper = upperResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'to' value is not an Integer (\(upperResolved ?? "nil") )")
|
||||
}
|
||||
|
||||
let range = min(lower, upper)...max(lower, upper)
|
||||
return lower > upper ? 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? {
|
||||
map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next legacy_objc_type
|
||||
extension NSArray: Normalizable {
|
||||
func normalize() -> Any? {
|
||||
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,16 +0,0 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = 'Stencil'
|
||||
spec.version = '0.2.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/*.swift', 'Stencil/TemplateLoader/*.swift'
|
||||
spec.ios.deployment_target = '8.0'
|
||||
spec.osx.deployment_target = '10.9'
|
||||
spec.requires_arc = true
|
||||
spec.dependency 'PathKit', '~> 0.3.0'
|
||||
end
|
||||
|
||||
34
Stencil.podspec.json
Normal file
34
Stencil.podspec.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "Stencil",
|
||||
"version": "0.15.2",
|
||||
"summary": "Stencil is a simple and powerful template language for Swift.",
|
||||
"license": {
|
||||
"type": "BSD",
|
||||
"file": "LICENSE"
|
||||
},
|
||||
"authors": {
|
||||
"Thomas Bernstein": "developer@astzweig.kg"
|
||||
},
|
||||
"social_media_url": "https://twitter.com/trbernstein",
|
||||
"source": {
|
||||
"git": "https://github.com/swiftstencil/swiftpm-stencil.git",
|
||||
"tag": "0.15.2"
|
||||
},
|
||||
"source_files": [
|
||||
"Sources/Stencil/*.swift"
|
||||
],
|
||||
"platforms": {
|
||||
"ios": "8.0",
|
||||
"osx": "10.9",
|
||||
"tvos": "9.0"
|
||||
},
|
||||
"swift_versions": [
|
||||
"5.0"
|
||||
],
|
||||
"requires_arc": true,
|
||||
"dependencies": {
|
||||
"PathKit": [
|
||||
"~> 1.5.0"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,646 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1474245D3CE34A8BC76F8D20 /* Pods_StencilTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 40E4E61A4F4EA12FE3FA6E39 /* Pods_StencilTests.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
27A848E41B42240E004ACA13 /* Inheritence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A848E31B42240E004ACA13 /* Inheritence.swift */; };
|
||||
27A848E91B42242C004ACA13 /* base.html in Resources */ = {isa = PBXBuildFile; fileRef = 27A848E71B42242C004ACA13 /* base.html */; };
|
||||
27A848EA1B42242C004ACA13 /* child.html in Resources */ = {isa = PBXBuildFile; fileRef = 27A848E81B42242C004ACA13 /* child.html */; };
|
||||
27A848EC1B42247D004ACA13 /* InheritenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A848EB1B42247D004ACA13 /* InheritenceTests.swift */; };
|
||||
27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */; };
|
||||
27CE0AE01A50BF05004A105B /* TemplateLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */; };
|
||||
27CE0AFA1A50C963004A105B /* test.html in Resources */ = {isa = PBXBuildFile; fileRef = 27CE0AF91A50C963004A105B /* test.html */; };
|
||||
27CE0B011A50CBD1004A105B /* Include.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B001A50CBD1004A105B /* Include.swift */; };
|
||||
27CE0B041A50CBEA004A105B /* IncludeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B031A50CBEA004A105B /* IncludeTests.swift */; };
|
||||
32C6976B95F3D42995990054 /* Pods_Stencil.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A574F1600267822AA34CB00 /* Pods_Stencil.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
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 */
|
||||
27CE0AF51A50C292004A105B /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 77FAAE4919F91E480029DC5E /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 77FAAE5119F91E480029DC5E;
|
||||
remoteInfo = Stencil;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
216AE96E764D5BD92D11049B /* Pods-Stencil.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stencil.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Stencil/Pods-Stencil.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
27A848E31B42240E004ACA13 /* Inheritence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Inheritence.swift; sourceTree = "<group>"; };
|
||||
27A848E71B42242C004ACA13 /* base.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = base.html; sourceTree = "<group>"; };
|
||||
27A848E81B42242C004ACA13 /* child.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = child.html; sourceTree = "<group>"; };
|
||||
27A848EB1B42247D004ACA13 /* InheritenceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InheritenceTests.swift; sourceTree = "<group>"; };
|
||||
27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoader.swift; sourceTree = "<group>"; };
|
||||
27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoaderTests.swift; sourceTree = "<group>"; };
|
||||
27CE0AF91A50C963004A105B /* test.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = test.html; sourceTree = "<group>"; };
|
||||
27CE0B001A50CBD1004A105B /* Include.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Include.swift; sourceTree = "<group>"; };
|
||||
27CE0B031A50CBEA004A105B /* IncludeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncludeTests.swift; sourceTree = "<group>"; };
|
||||
40E4E61A4F4EA12FE3FA6E39 /* Pods_StencilTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_StencilTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
604897C26D7C87809187940C /* Pods-StencilTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StencilTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-StencilTests/Pods-StencilTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
7A574F1600267822AA34CB00 /* Pods_Stencil.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Stencil.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A4451BEDB5F71116FF3216CC /* Pods-Stencil.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stencil.release.xcconfig"; path = "Pods/Target Support Files/Pods-Stencil/Pods-Stencil.release.xcconfig"; sourceTree = "<group>"; };
|
||||
AB71617F99ED5A669D83419F /* Pods-StencilTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StencilTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-StencilTests/Pods-StencilTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
77FAAE4E19F91E480029DC5E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
32C6976B95F3D42995990054 /* Pods_Stencil.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
77FAAE5A19F91E480029DC5E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE5E19F91E480029DC5E /* Stencil.framework in Frameworks */,
|
||||
1474245D3CE34A8BC76F8D20 /* Pods_StencilTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27CE0AFF1A50CBBF004A105B /* TemplateLoader */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
27CE0B001A50CBD1004A105B /* Include.swift */,
|
||||
27A848E31B42240E004ACA13 /* Inheritence.swift */,
|
||||
);
|
||||
path = TemplateLoader;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
27CE0B021A50CBEA004A105B /* TemplateLoader */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
27CE0B031A50CBEA004A105B /* IncludeTests.swift */,
|
||||
27A848EB1B42247D004ACA13 /* InheritenceTests.swift */,
|
||||
);
|
||||
path = TemplateLoader;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE4819F91E480029DC5E = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE5419F91E480029DC5E /* Stencil */,
|
||||
77FAAE6119F91E480029DC5E /* StencilTests */,
|
||||
77FAAE5319F91E480029DC5E /* Products */,
|
||||
F0837D60120FB15CC00B9EBD /* Pods */,
|
||||
BE2E2DF8F488C4669126E920 /* Frameworks */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
};
|
||||
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 */,
|
||||
27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */,
|
||||
7725B3CE19F94214002CF74B /* Tokenizer.swift */,
|
||||
7725B3CC19F92B61002CF74B /* Variable.swift */,
|
||||
27CE0AFF1A50CBBF004A105B /* TemplateLoader */,
|
||||
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 */,
|
||||
27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */,
|
||||
27CE0B021A50CBEA004A105B /* TemplateLoader */,
|
||||
27CE0AF91A50C963004A105B /* test.html */,
|
||||
27A848E71B42242C004ACA13 /* base.html */,
|
||||
27A848E81B42242C004ACA13 /* child.html */,
|
||||
77FAAE6219F91E480029DC5E /* Supporting Files */,
|
||||
);
|
||||
path = StencilTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
77FAAE6219F91E480029DC5E /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
77FAAE6319F91E480029DC5E /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BE2E2DF8F488C4669126E920 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A574F1600267822AA34CB00 /* Pods_Stencil.framework */,
|
||||
40E4E61A4F4EA12FE3FA6E39 /* Pods_StencilTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F0837D60120FB15CC00B9EBD /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
216AE96E764D5BD92D11049B /* Pods-Stencil.debug.xcconfig */,
|
||||
A4451BEDB5F71116FF3216CC /* Pods-Stencil.release.xcconfig */,
|
||||
AB71617F99ED5A669D83419F /* Pods-StencilTests.debug.xcconfig */,
|
||||
604897C26D7C87809187940C /* Pods-StencilTests.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
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 = (
|
||||
2F5F764C3F2D9B507C32F902 /* Check Pods Manifest.lock */,
|
||||
77FAAE4D19F91E480029DC5E /* Sources */,
|
||||
77FAAE4E19F91E480029DC5E /* Frameworks */,
|
||||
77FAAE4F19F91E480029DC5E /* Headers */,
|
||||
77FAAE5019F91E480029DC5E /* Resources */,
|
||||
99E82E6639BC7C1A0E1E28DC /* Copy Pods 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 = (
|
||||
2D3D90E839235D400843A900 /* Check Pods Manifest.lock */,
|
||||
77FAAE5919F91E480029DC5E /* Sources */,
|
||||
77FAAE5A19F91E480029DC5E /* Frameworks */,
|
||||
77FAAE5B19F91E480029DC5E /* Resources */,
|
||||
A3AAD60B3C344DD6993AE800 /* Embed Pods Frameworks */,
|
||||
BD3457ACA3F28092E2EBD7C4 /* Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
27CE0AF61A50C292004A105B /* 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 = 0630;
|
||||
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 = (
|
||||
27CE0AFA1A50C963004A105B /* test.html in Resources */,
|
||||
27A848EA1B42242C004ACA13 /* child.html in Resources */,
|
||||
27A848E91B42242C004ACA13 /* base.html in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
2D3D90E839235D400843A900 /* Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
2F5F764C3F2D9B507C32F902 /* Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
99E82E6639BC7C1A0E1E28DC /* Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Copy Pods Resources";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Stencil/Pods-Stencil-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
A3AAD60B3C344DD6993AE800 /* Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-StencilTests/Pods-StencilTests-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
BD3457ACA3F28092E2EBD7C4 /* Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Copy Pods Resources";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-StencilTests/Pods-StencilTests-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
77FAAE4D19F91E480029DC5E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
27CE0B011A50CBD1004A105B /* Include.swift in Sources */,
|
||||
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 */,
|
||||
27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */,
|
||||
27A848E41B42240E004ACA13 /* Inheritence.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
77FAAE5919F91E480029DC5E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
77FAAE6519F91E480029DC5E /* StencilTests.swift in Sources */,
|
||||
7725B3D319F9437F002CF74B /* NodeTests.swift in Sources */,
|
||||
27A848EC1B42247D004ACA13 /* InheritenceTests.swift in Sources */,
|
||||
27CE0AE01A50BF05004A105B /* TemplateLoaderTests.swift in Sources */,
|
||||
7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */,
|
||||
77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */,
|
||||
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */,
|
||||
27CE0B041A50CBEA004A105B /* IncludeTests.swift in Sources */,
|
||||
77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */,
|
||||
77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
27CE0AF61A50C292004A105B /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 77FAAE5119F91E480029DC5E /* Stencil */;
|
||||
targetProxy = 27CE0AF51A50C292004A105B /* 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.9;
|
||||
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.9;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
77FAAE6919F91E480029DC5E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 216AE96E764D5BD92D11049B /* Pods-Stencil.debug.xcconfig */;
|
||||
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;
|
||||
baseConfigurationReference = A4451BEDB5F71116FF3216CC /* Pods-Stencil.release.xcconfig */;
|
||||
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;
|
||||
baseConfigurationReference = AB71617F99ED5A669D83419F /* Pods-StencilTests.debug.xcconfig */;
|
||||
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;
|
||||
baseConfigurationReference = 604897C26D7C87809187940C /* Pods-StencilTests.release.xcconfig */;
|
||||
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,110 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0630"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "77FAAE5119F91E480029DC5E"
|
||||
BuildableName = "Stencil.framework"
|
||||
BlueprintName = "Stencil"
|
||||
ReferencedContainer = "container:Stencil.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "77FAAE5C19F91E480029DC5E"
|
||||
BuildableName = "StencilTests.xctest"
|
||||
BlueprintName = "StencilTests"
|
||||
ReferencedContainer = "container:Stencil.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "77FAAE5C19F91E480029DC5E"
|
||||
BuildableName = "StencilTests.xctest"
|
||||
BlueprintName = "StencilTests"
|
||||
ReferencedContainer = "container:Stencil.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "77FAAE5119F91E480029DC5E"
|
||||
BuildableName = "Stencil.framework"
|
||||
BlueprintName = "Stencil"
|
||||
ReferencedContainer = "container:Stencil.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "77FAAE5119F91E480029DC5E"
|
||||
BuildableName = "Stencil.framework"
|
||||
BlueprintName = "Stencil"
|
||||
ReferencedContainer = "container:Stencil.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Release"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "77FAAE5119F91E480029DC5E"
|
||||
BuildableName = "Stencil.framework"
|
||||
BlueprintName = "Stencil"
|
||||
ReferencedContainer = "container:Stencil.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
10
Stencil.xcworkspace/contents.xcworkspacedata
generated
10
Stencil.xcworkspace/contents.xcworkspacedata
generated
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Stencil.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,52 +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? {
|
||||
/// Retrieves a variable's value, starting at the current context and going upwards
|
||||
get {
|
||||
for dictionary in reverse(dictionaries) {
|
||||
if let value:AnyObject = dictionary[key] {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Set a variable in the current context, deleting the variable if it's nil
|
||||
set(value) {
|
||||
if dictionaries.count > 0 {
|
||||
var dictionary = dictionaries.removeLast()
|
||||
dictionary[key] = value
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func push() {
|
||||
push(Dictionary<String, AnyObject>())
|
||||
}
|
||||
|
||||
public func push(dictionary:Dictionary<String, AnyObject>) {
|
||||
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,57 +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)
|
||||
}
|
||||
|
||||
/// Returns an array of tokens from a given template 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, count(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 < count(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 {
|
||||
/// Return the node rendered as a string, or returns a failure
|
||||
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()
|
||||
|
||||
if count(components) == 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: 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: 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: 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: 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: 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: 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,111 +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
|
||||
}
|
||||
|
||||
/// 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) -> 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", parser: ForNode.parse)
|
||||
registerTag("if", parser: IfNode.parse)
|
||||
registerTag("ifnot", parser: IfNode.parse_ifnot)
|
||||
registerTag("now", parser: NowNode.parse)
|
||||
registerTag("include", parser: IncludeNode.parse)
|
||||
registerTag("extends", parser: ExtendsNode.parse)
|
||||
registerTag("block", parser: BlockNode.parse)
|
||||
}
|
||||
|
||||
/// Registers a new template tag
|
||||
public func registerTag(name:String, parser:TagParser) {
|
||||
tags[name] = parser
|
||||
}
|
||||
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
public func registerSimpleTag(name:String, handler:((Context) -> (Stencil.Result))) {
|
||||
registerTag(name, parser: { (parser, token) -> TokenParser.Result in
|
||||
return .Success(node:SimpleNode(handler: handler))
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the given tokens into nodes
|
||||
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 <Foundation/Foundation.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,73 +0,0 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
/// A class representing a template
|
||||
public class Template {
|
||||
public let parser:TokenParser
|
||||
|
||||
/// Create a template with the given name inside the main bundle
|
||||
public convenience init?(named:String) {
|
||||
self.init(named:named, inBundle:nil)
|
||||
}
|
||||
|
||||
/// Create a template with the given name inside the given bundle
|
||||
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!)
|
||||
}
|
||||
|
||||
/// Create a template with a file found at the given 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 as String)
|
||||
} else {
|
||||
self.init(templateString:"")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a template with a file found at the given path
|
||||
public convenience init?(path:Path) {
|
||||
var error:NSError?
|
||||
|
||||
if let string:String = path.read() {
|
||||
self.init(templateString:string)
|
||||
} else {
|
||||
self.init(templateString:"")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a template with a template string
|
||||
public init(templateString:String) {
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
parser = TokenParser(tokens: tokens)
|
||||
}
|
||||
|
||||
/// Render the given template in a context
|
||||
public func render(context:Context) -> Result {
|
||||
switch parser.parse() {
|
||||
case .Success(let nodes):
|
||||
return renderNodes(nodes, context)
|
||||
|
||||
case .Error(let error):
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the given template without a context
|
||||
public func render() -> Result {
|
||||
let context = Context()
|
||||
return render(context)
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
//
|
||||
// TemplateLoader.swift
|
||||
// Stencil
|
||||
//
|
||||
// Created by Kyle Fuller on 28/12/2014.
|
||||
// Copyright (c) 2014 Cocode. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
// A class for loading a template from disk
|
||||
public class TemplateLoader {
|
||||
public let paths:[Path]
|
||||
|
||||
public init(paths:[Path]) {
|
||||
self.paths = paths
|
||||
}
|
||||
|
||||
public init(bundle:[NSBundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
public func loadTemplate(templateName:String) -> Template? {
|
||||
return loadTemplate([templateName])
|
||||
}
|
||||
|
||||
public func loadTemplate(templateNames:[String]) -> Template? {
|
||||
for path in paths {
|
||||
for templateName in templateNames {
|
||||
let templatePath = path + Path(templateName)
|
||||
|
||||
if templatePath.exists() {
|
||||
if let template = Template(path: templatePath) {
|
||||
return template
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
extension String : Error {
|
||||
public var description:String {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public class IncludeNode : Node {
|
||||
public let templateName:String
|
||||
|
||||
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
let bits = token.contents.componentsSeparatedByString("\"")
|
||||
|
||||
if bits.count != 3 {
|
||||
return .Error(error:NodeError(token: token, message: "Tag takes one argument, the template file to be included"))
|
||||
}
|
||||
|
||||
return .Success(node:IncludeNode(templateName: bits[1]))
|
||||
}
|
||||
|
||||
public init(templateName:String) {
|
||||
self.templateName = templateName
|
||||
}
|
||||
|
||||
public func render(context: Context) -> Result {
|
||||
if let loader = context["loader"] as? TemplateLoader {
|
||||
if let template = loader.loadTemplate(templateName) {
|
||||
return template.render(context)
|
||||
}
|
||||
|
||||
let paths:String = join(", ", loader.paths.map { path in
|
||||
return path.description
|
||||
})
|
||||
let error = "Template '\(templateName)' not found in \(paths)"
|
||||
return .Error(error)
|
||||
}
|
||||
|
||||
let error = "Template loader not in context"
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
class BlockContext {
|
||||
class var contextKey:String { return "block_context" }
|
||||
|
||||
var blocks:[String:BlockNode]
|
||||
|
||||
init(blocks:[String:BlockNode]) {
|
||||
self.blocks = blocks
|
||||
}
|
||||
|
||||
func pop(blockName:String) -> BlockNode? {
|
||||
return blocks.removeValueForKey(blockName)
|
||||
}
|
||||
}
|
||||
|
||||
func any<Element>(elements:[Element], closure:(Element -> Bool)) -> Element? {
|
||||
for element in elements {
|
||||
if closure(element) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
class ExtendsNode : Node {
|
||||
let templateName:String
|
||||
let blocks:[String:BlockNode]
|
||||
|
||||
class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
let bits = token.contents.componentsSeparatedByString("\"")
|
||||
|
||||
if bits.count != 3 {
|
||||
return .Error(error:NodeError(token: token, message: "Tag takes one argument, the template file to be extended"))
|
||||
}
|
||||
|
||||
switch parser.parse() {
|
||||
case .Success(let nodes):
|
||||
if (any(nodes) { ($0 as? ExtendsNode) != nil }) != nil {
|
||||
return .Error(error:"'extends' cannot appear more than once in the same template")
|
||||
}
|
||||
|
||||
let blockNodes = filter(nodes) { node in node is BlockNode }
|
||||
|
||||
let nodes = reduce(blockNodes, [String:BlockNode](), { (accumulator, node:Node) -> [String:BlockNode] in
|
||||
let node = (node as! BlockNode)
|
||||
var dict = accumulator
|
||||
dict[node.name] = node
|
||||
return dict
|
||||
})
|
||||
|
||||
return .Success(node:ExtendsNode(templateName: bits[1], blocks: nodes))
|
||||
case .Error(let error):
|
||||
return .Error(error:error)
|
||||
}
|
||||
}
|
||||
|
||||
init(templateName:String, blocks:[String:BlockNode]) {
|
||||
self.templateName = templateName
|
||||
self.blocks = blocks
|
||||
}
|
||||
|
||||
func render(context: Context) -> Result {
|
||||
if let loader = context["loader"] as? TemplateLoader {
|
||||
if let template = loader.loadTemplate(templateName) {
|
||||
let blockContext = BlockContext(blocks: blocks)
|
||||
context.push([BlockContext.contextKey: blockContext])
|
||||
let result = template.render(context)
|
||||
context.pop()
|
||||
return result
|
||||
}
|
||||
|
||||
let paths:String = join(", ", loader.paths.map { path in
|
||||
return path.description
|
||||
})
|
||||
let error = "Template '\(templateName)' not found in \(paths)"
|
||||
return .Error(error)
|
||||
}
|
||||
|
||||
let error = "Template loader not in context"
|
||||
return .Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
class BlockNode : Node {
|
||||
let name:String
|
||||
let nodes:[Node]
|
||||
|
||||
class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
|
||||
let bits = token.components()
|
||||
|
||||
if bits.count != 2 {
|
||||
return .Error(error:NodeError(token: token, message: "Tag takes one argument, the template file to be included"))
|
||||
}
|
||||
|
||||
let blockName = bits[1]
|
||||
var nodes = [Node]()
|
||||
|
||||
switch parser.parse(until(["endblock"])) {
|
||||
case .Success(let blockNodes):
|
||||
nodes = blockNodes
|
||||
case .Error(let error):
|
||||
return .Error(error: error)
|
||||
}
|
||||
|
||||
return .Success(node:BlockNode(name:blockName, nodes:nodes))
|
||||
}
|
||||
|
||||
init(name:String, nodes:[Node]) {
|
||||
self.name = name
|
||||
self.nodes = nodes
|
||||
}
|
||||
|
||||
func render(context: Context) -> Result {
|
||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext {
|
||||
if let node = blockContext.pop(name) {
|
||||
return node.render(context)
|
||||
}
|
||||
}
|
||||
|
||||
return renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public enum Token : Equatable {
|
||||
/// A token representing a piece of text.
|
||||
case Text(value:String)
|
||||
|
||||
/// A token representing a variable.
|
||||
case Variable(value:String)
|
||||
|
||||
/// A token representing a comment.
|
||||
case Comment(value:String)
|
||||
|
||||
/// A token representing a template block.
|
||||
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,53 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
/// A structure used to represent a template variable, and to resolve it in a given context.
|
||||
public struct Variable : Equatable {
|
||||
public let variable:String
|
||||
|
||||
/// Create a variable with a string representing the variable
|
||||
public init(_ variable:String) {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
private func lookup() -> [String] {
|
||||
return variable.componentsSeparatedByString(".")
|
||||
}
|
||||
|
||||
/// Resolve the variable in the given context
|
||||
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 = count(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 Foundation
|
||||
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 Foundation
|
||||
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 Foundation
|
||||
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 Foundation
|
||||
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 Foundation
|
||||
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,75 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
class IncludeTests: NodeTests {
|
||||
|
||||
var loader:TemplateLoader!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let path = (Path(__FILE__) + Path("../..")).absolute()
|
||||
loader = TemplateLoader(paths: [path])
|
||||
}
|
||||
|
||||
// MARK: Parsing
|
||||
|
||||
func testParseMissingTemplate() {
|
||||
let tokens = [ Token.Block(value: "include") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
assertFailure(parser.parse(), "include: Tag takes one argument, the template file to be included")
|
||||
}
|
||||
|
||||
func testParse() {
|
||||
let tokens = [ Token.Block(value: "include \"test.html\"") ]
|
||||
let parser = TokenParser(tokens: tokens)
|
||||
|
||||
assertSuccess(parser.parse()) { nodes in
|
||||
let node = nodes.first as! IncludeNode
|
||||
XCTAssertEqual(nodes.count, 1)
|
||||
XCTAssertEqual(node.templateName, "test.html")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Render
|
||||
|
||||
func testRenderWithoutLoader() {
|
||||
let node = IncludeNode(templateName: "test.html")
|
||||
let result = node.render(Context())
|
||||
|
||||
switch result {
|
||||
case .Success(let string):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
case .Error(let error):
|
||||
XCTAssertEqual("\(error)", "Template loader not in context")
|
||||
}
|
||||
}
|
||||
|
||||
func testRenderWithoutTemplateNamed() {
|
||||
let node = IncludeNode(templateName: "unknown.html")
|
||||
let result = node.render(Context(dictionary:["loader":loader]))
|
||||
|
||||
switch result {
|
||||
case .Success(let string):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
case .Error(let error):
|
||||
XCTAssertTrue("\(error)".hasPrefix("Template 'unknown.html' not found"))
|
||||
}
|
||||
}
|
||||
|
||||
func testRender() {
|
||||
let node = IncludeNode(templateName: "test.html")
|
||||
let result = node.render(Context(dictionary:["loader":loader, "target": "World"]))
|
||||
|
||||
switch result {
|
||||
case .Success(let string):
|
||||
XCTAssertEqual(string, "Hello World!")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
class InheritenceTests: NodeTests {
|
||||
var loader:TemplateLoader!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let path = (Path(__FILE__) + Path("../..")).absolute()
|
||||
loader = TemplateLoader(paths: [path])
|
||||
}
|
||||
|
||||
func testInheritence() {
|
||||
context = Context(dictionary: ["loader": loader])
|
||||
let template = loader.loadTemplate("child.html")!
|
||||
let result = template.render(context)
|
||||
|
||||
switch result {
|
||||
case .Success(let rendered):
|
||||
XCTAssertEqual(rendered, "Header\nChild")
|
||||
case .Error(let error):
|
||||
XCTAssert(false, "Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//class BlockNodeTests: NodeTests {
|
||||
// func testBlockNodeWithoutChildren() {
|
||||
// let context = Context()
|
||||
// let block = BlockNode(name:"header", nodes:[TextNode(text: "contents")])
|
||||
// let result = block.render(context)
|
||||
//
|
||||
// assertSuccess(result) { rendered in
|
||||
// XCTAssertEqual(rendered, "contents")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func testBlockNodeWithChild() {
|
||||
// let context = Context()
|
||||
// let node = BlockNode(name:"header", nodes:[TextNode(text: "contents")])
|
||||
// let childBlock = BlockNode(name: "header", nodes: [TextNode(text: "child contents")])
|
||||
// let result = node.render(context)
|
||||
//
|
||||
// assertSuccess(result) { rendered in
|
||||
// XCTAssertEqual(rendered, "child contents")
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,24 +0,0 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
class TemplateLoaderTests: XCTestCase {
|
||||
|
||||
func testLoadingUnknownTemplate() {
|
||||
let loader = TemplateLoader(paths:[])
|
||||
XCTAssertNil(loader.loadTemplate("unknown.html"))
|
||||
}
|
||||
|
||||
func testLoadingUnknownTemplates() {
|
||||
let loader = TemplateLoader(paths:[])
|
||||
XCTAssertNil(loader.loadTemplate(["unknown.html", "unknown2.html"]))
|
||||
}
|
||||
|
||||
func testLoadingTemplate() {
|
||||
let path = (Path(__FILE__) + Path("..")).absolute()
|
||||
let loader = TemplateLoader(paths: [path])
|
||||
XCTAssertTrue(loader.loadTemplate("test.html") != nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import Foundation
|
||||
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 Foundation
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body %}Child{% endblock %}
|
||||
5
Tests/StencilTests/.swiftlint.yml
Normal file
5
Tests/StencilTests/.swiftlint.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
parent_config: ../../.swiftlint.yml
|
||||
|
||||
disabled_rules: # rule identifiers to exclude from running
|
||||
- type_body_length
|
||||
- file_length
|
||||
160
Tests/StencilTests/ContextSpec.swift
Normal file
160
Tests/StencilTests/ContextSpec.swift
Normal file
@@ -0,0 +1,160 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class ContextTests: XCTestCase {
|
||||
func testContextSubscripting() {
|
||||
describe("Context Subscripting") { test in
|
||||
var context = Context()
|
||||
test.before {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
test.it("allows you to get a value via subscripting") {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
test.it("allows you to set a value via subscripting") {
|
||||
context["name"] = "Katie"
|
||||
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
test.it("allows you to remove a value via subscripting") {
|
||||
context["name"] = nil
|
||||
|
||||
try expect(context["name"]).to.beNil()
|
||||
}
|
||||
|
||||
test.it("allows you to retrieve a value from a parent") {
|
||||
try context.push {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
test.it("allows you to override a parent's value") {
|
||||
try context.push {
|
||||
context["name"] = "Katie"
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testContextRestoration() {
|
||||
describe("Context Restoration") { test in
|
||||
var context = Context()
|
||||
test.before {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
test.it("allows you to pop to restore previous state") {
|
||||
context.push {
|
||||
context["name"] = "Katie"
|
||||
}
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
test.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"
|
||||
}
|
||||
|
||||
test.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"
|
||||
}
|
||||
|
||||
test.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testContextLazyEvaluation() {
|
||||
let ticker = Ticker()
|
||||
var context = Context()
|
||||
var wrapper = LazyValueWrapper("")
|
||||
|
||||
describe("Lazy evaluation") { test in
|
||||
test.before {
|
||||
ticker.count = 0
|
||||
wrapper = LazyValueWrapper(ticker.tick())
|
||||
context = Context(dictionary: ["name": wrapper])
|
||||
}
|
||||
|
||||
test.it("Evaluates lazy data") {
|
||||
let template = Template(templateString: "{{ name }}")
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "Kyle"
|
||||
try expect(ticker.count) == 1
|
||||
}
|
||||
|
||||
test.it("Evaluates lazy only once") {
|
||||
let template = Template(templateString: "{{ name }}{{ name }}")
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "KyleKyle"
|
||||
try expect(ticker.count) == 1
|
||||
}
|
||||
|
||||
test.it("Does not evaluate lazy data when not used") {
|
||||
let template = Template(templateString: "{{ 'Katie' }}")
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "Katie"
|
||||
try expect(ticker.count) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testContextLazyAccessTypes() {
|
||||
it("Supports evaluation via context reference") {
|
||||
let context = Context(dictionary: ["name": "Kyle"])
|
||||
context["alias"] = LazyValueWrapper { $0["name"] ?? "" }
|
||||
let template = Template(templateString: "{{ alias }}")
|
||||
|
||||
try context.push(dictionary: ["name": "Katie"]) {
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
}
|
||||
|
||||
it("Supports evaluation via context copy") {
|
||||
let context = Context(dictionary: ["name": "Kyle"])
|
||||
context["alias"] = LazyValueWrapper(copying: context) { $0["name"] ?? "" }
|
||||
let template = Template(templateString: "{{ alias }}")
|
||||
|
||||
try context.push(dictionary: ["name": "Katie"]) {
|
||||
let result = try template.render(context)
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private final class Ticker {
|
||||
var count: Int = 0
|
||||
func tick() -> String {
|
||||
count += 1
|
||||
return "Kyle"
|
||||
}
|
||||
}
|
||||
125
Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift
Normal file
125
Tests/StencilTests/EnvironmentBaseAndChildTemplateSpec.swift
Normal file
@@ -0,0 +1,125 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class EnvironmentBaseAndChildTemplateTests: XCTestCase {
|
||||
private var environment = Environment(loader: ExampleLoader())
|
||||
private var childTemplate: Template = ""
|
||||
private var baseTemplate: Template = ""
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let path = Path(#file as String)! / ".." / "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
environment = Environment(loader: loader)
|
||||
childTemplate = ""
|
||||
baseTemplate = ""
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSyntaxErrorInBaseTemplate() throws {
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
func testRuntimeErrorInBaseTemplate() throws {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown") { (_: 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: "extends \"invalid-base.html\"",
|
||||
baseToken: "target|unknown"
|
||||
)
|
||||
}
|
||||
|
||||
func testSyntaxErrorInChildTemplate() throws {
|
||||
childTemplate = Template(
|
||||
templateString: """
|
||||
{% extends "base.html" %}
|
||||
{% 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
|
||||
)
|
||||
}
|
||||
|
||||
func testRuntimeErrorInChildTemplate() throws {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown") { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
}
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
childTemplate = Template(
|
||||
templateString: """
|
||||
{% extends "base.html" %}
|
||||
{% block body %}Child {{ target|unknown }}{% endblock %}
|
||||
""",
|
||||
environment: environment,
|
||||
name: nil
|
||||
)
|
||||
|
||||
try expectError(
|
||||
reason: "filter error",
|
||||
childToken: "target|unknown",
|
||||
baseToken: nil
|
||||
)
|
||||
}
|
||||
|
||||
private 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
|
||||
].compactMap { $0 }
|
||||
}
|
||||
let error = try expect(
|
||||
self.environment.render(template: self.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)
|
||||
}
|
||||
}
|
||||
88
Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift
Normal file
88
Tests/StencilTests/EnvironmentIncludeTemplateSpec.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class EnvironmentIncludeTemplateTests: XCTestCase {
|
||||
private var environment = Environment(loader: ExampleLoader())
|
||||
private var template: Template = ""
|
||||
private var includedTemplate: Template = ""
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let path = Path(#file as String)! / ".." / "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
environment = Environment(loader: loader)
|
||||
template = ""
|
||||
includedTemplate = ""
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testSyntaxError() throws {
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
func testRuntimeError() throws {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown") { (_: 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"
|
||||
)
|
||||
}
|
||||
|
||||
private 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
|
||||
].compactMap { $0 }
|
||||
|
||||
let error = try expect(
|
||||
self.environment.render(template: self.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)
|
||||
}
|
||||
}
|
||||
220
Tests/StencilTests/EnvironmentSpec.swift
Normal file
220
Tests/StencilTests/EnvironmentSpec.swift
Normal file
@@ -0,0 +1,220 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class EnvironmentTests: XCTestCase {
|
||||
private var environment = Environment(loader: ExampleLoader())
|
||||
private var template: Template = ""
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
let errorExtension = Extension()
|
||||
errorExtension.registerFilter("throw") { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
}
|
||||
errorExtension.registerSimpleTag("simpletag") { _ in
|
||||
throw TemplateSyntaxError("simpletag error")
|
||||
}
|
||||
errorExtension.registerTag("customtag") { _, token in
|
||||
ErrorNode(token: token)
|
||||
}
|
||||
|
||||
environment = Environment(loader: ExampleLoader())
|
||||
environment.extensions += [errorExtension]
|
||||
template = ""
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testLoading() {
|
||||
it("can load a template from a name") {
|
||||
let template = try self.environment.loadTemplate(name: "example.html")
|
||||
try expect(template.name) == "example.html"
|
||||
}
|
||||
|
||||
it("can load a template from a names") {
|
||||
let template = try self.environment.loadTemplate(names: ["first.html", "example.html"])
|
||||
try expect(template.name) == "example.html"
|
||||
}
|
||||
}
|
||||
|
||||
func testRendering() {
|
||||
it("can render a template from a string") {
|
||||
let result = try self.environment.renderTemplate(string: "Hello World")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
it("can render a template from a file") {
|
||||
let result = try self.environment.renderTemplate(name: "example.html")
|
||||
try expect(result) == "Hello World!"
|
||||
}
|
||||
|
||||
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 testSyntaxError() {
|
||||
it("reports syntax error on invalid for tag syntax") {
|
||||
self.template = "Hello {% for name in %}{{ name }}, {% endfor %}!"
|
||||
try self.expectError(
|
||||
reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.",
|
||||
token: "for name in"
|
||||
)
|
||||
}
|
||||
|
||||
it("reports syntax error on missing endfor") {
|
||||
self.template = "{% for name in names %}{{ name }}"
|
||||
try self.expectError(reason: "`endfor` was not found.", token: "for name in names")
|
||||
}
|
||||
|
||||
it("reports syntax error on unknown tag") {
|
||||
self.template = "{% for name in names %}{{ name }}{% end %}"
|
||||
try self.expectError(reason: "Unknown template tag 'end'", token: "end")
|
||||
}
|
||||
}
|
||||
|
||||
func testUnknownFilter() {
|
||||
it("reports syntax error in for tag") {
|
||||
self.template = "{% for name in names|unknown %}{{ name }}{% endfor %}"
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
token: "names|unknown"
|
||||
)
|
||||
}
|
||||
|
||||
it("reports syntax error in for-where tag") {
|
||||
self.template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}"
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
token: "name|unknown"
|
||||
)
|
||||
}
|
||||
|
||||
it("reports syntax error in if tag") {
|
||||
self.template = "{% if name|unknown %}{{ name }}{% endif %}"
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
token: "name|unknown"
|
||||
)
|
||||
}
|
||||
|
||||
it("reports syntax error in elif tag") {
|
||||
self.template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}"
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
token: "name|unknown"
|
||||
)
|
||||
}
|
||||
|
||||
it("reports syntax error in ifnot tag") {
|
||||
self.template = "{% ifnot name|unknown %}{{ name }}{% endif %}"
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
token: "name|unknown"
|
||||
)
|
||||
}
|
||||
|
||||
it("reports syntax error in filter tag") {
|
||||
self.template = "{% filter unknown %}Text{% endfilter %}"
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
token: "filter unknown"
|
||||
)
|
||||
}
|
||||
|
||||
it("reports syntax error in variable tag") {
|
||||
self.template = "{{ name|unknown }}"
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
token: "name|unknown"
|
||||
)
|
||||
}
|
||||
|
||||
it("reports error in variable tag") {
|
||||
self.template = "{{ }}"
|
||||
try self.expectError(reason: "Missing variable name", token: " ")
|
||||
}
|
||||
}
|
||||
|
||||
func testRenderingError() {
|
||||
it("reports rendering error in variable filter") {
|
||||
self.template = Template(templateString: "{{ name|throw }}", environment: self.environment)
|
||||
try self.expectError(reason: "filter error", token: "name|throw")
|
||||
}
|
||||
|
||||
it("reports rendering error in filter tag") {
|
||||
self.template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: self.environment)
|
||||
try self.expectError(reason: "filter error", token: "filter throw")
|
||||
}
|
||||
|
||||
it("reports rendering error in simple tag") {
|
||||
self.template = Template(templateString: "{% simpletag %}", environment: self.environment)
|
||||
try self.expectError(reason: "simpletag error", token: "simpletag")
|
||||
}
|
||||
|
||||
it("reports passing argument to simple filter") {
|
||||
self.template = "{{ name|uppercase:5 }}"
|
||||
try self.expectError(reason: "Can't invoke filter with an argument", token: "name|uppercase:5")
|
||||
}
|
||||
|
||||
it("reports rendering error in custom tag") {
|
||||
self.template = Template(templateString: "{% customtag %}", environment: self.environment)
|
||||
try self.expectError(reason: "Custom Error", token: "customtag")
|
||||
}
|
||||
|
||||
it("reports rendering error in for body") {
|
||||
self.template = Template(templateString: """
|
||||
{% for name in names %}{% customtag %}{% endfor %}
|
||||
""", environment: self.environment)
|
||||
try self.expectError(reason: "Custom Error", token: "customtag")
|
||||
}
|
||||
|
||||
it("reports rendering error in block") {
|
||||
self.template = Template(
|
||||
templateString: "{% block some %}{% customtag %}{% endblock %}",
|
||||
environment: self.environment
|
||||
)
|
||||
try self.expectError(reason: "Custom Error", token: "customtag")
|
||||
}
|
||||
}
|
||||
|
||||
private 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(
|
||||
self.environment.render(template: self.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)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private class CustomTemplate: Template {
|
||||
// swiftlint:disable discouraged_optional_collection
|
||||
override func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||
"here"
|
||||
}
|
||||
}
|
||||
355
Tests/StencilTests/ExpressionSpec.swift
Normal file
355
Tests/StencilTests/ExpressionSpec.swift
Normal file
@@ -0,0 +1,355 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class ExpressionsTests: XCTestCase {
|
||||
private let parser = TokenParser(tokens: [], environment: Environment())
|
||||
|
||||
private func makeExpression(_ components: [String]) -> Stencil.Expression {
|
||||
do {
|
||||
let parser = try IfExpressionParser.parser(
|
||||
components: components,
|
||||
environment: Environment(),
|
||||
token: .text(value: "", at: .unknown)
|
||||
)
|
||||
return try parser.parse()
|
||||
} catch {
|
||||
fatalError(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func testTrueExpressions() {
|
||||
let expression = VariableExpression(variable: Variable("value"))
|
||||
|
||||
it("evaluates to true when value is not nil") {
|
||||
let context = Context(dictionary: ["value": "known"])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to true when array variable is not empty") {
|
||||
let items: [[String: Any]] = [["key": "key1", "value": 42], ["key": "key2", "value": 1_337]]
|
||||
let context = Context(dictionary: ["value": [items]])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
it("evaluates to true with string") {
|
||||
let context = Context(dictionary: ["value": "test"])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
func testFalseExpressions() {
|
||||
let expression = VariableExpression(variable: Variable("value"))
|
||||
|
||||
it("evaluates to false when value is unset") {
|
||||
let context = Context()
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
it("evaluates to false when empty string") {
|
||||
let context = Context(dictionary: ["value": ""])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
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": -1])
|
||||
try expect(try expression.evaluate(context: negativeContext)).to.beFalse()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
it("evaluates to false when uint is 0") {
|
||||
let context = Context(dictionary: ["value": UInt(0)])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testNotExpression() {
|
||||
it("returns truthy for positive expressions") {
|
||||
let expression = NotExpression(expression: VariableExpression(variable: Variable("true")))
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
}
|
||||
|
||||
it("returns falsy for negative expressions") {
|
||||
let expression = NotExpression(expression: VariableExpression(variable: Variable("false")))
|
||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
func testExpressionParsing() {
|
||||
it("can parse a variable expression") {
|
||||
let expression = self.makeExpression(["value"])
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("can parse a not expression") {
|
||||
let expression = self.makeExpression(["not", "value"])
|
||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testAndExpression() {
|
||||
let expression = makeExpression(["lhs", "and", "rhs"])
|
||||
|
||||
it("evaluates to false with lhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
it("evaluates to false with rhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
|
||||
it("evaluates to false with lhs and rhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
|
||||
it("evaluates to true with lhs and rhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
func testOrExpression() {
|
||||
let expression = makeExpression(["lhs", "or", "rhs"])
|
||||
|
||||
it("evaluates to true with lhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to true with rhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to true with lhs and rhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with lhs and rhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testEqualityExpression() {
|
||||
let expression = makeExpression(["lhs", "==", "rhs"])
|
||||
|
||||
it("evaluates to true with equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with non equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse()
|
||||
}
|
||||
|
||||
it("evaluates to true with nils") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to true with numbers") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with non equal numbers") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse()
|
||||
}
|
||||
|
||||
it("evaluates to true with booleans") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with falsy booleans") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
|
||||
it("evaluates to false with different types") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testInequalityExpression() {
|
||||
let expression = makeExpression(["lhs", "!=", "rhs"])
|
||||
|
||||
it("evaluates to true with inequal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testMoreThanExpression() {
|
||||
let expression = makeExpression(["lhs", ">", "rhs"])
|
||||
|
||||
it("evaluates to true with lhs > rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testMoreThanEqualExpression() {
|
||||
let expression = makeExpression(["lhs", ">=", "rhs"])
|
||||
|
||||
it("evaluates to true with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with lhs < rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testLessThanExpression() {
|
||||
let expression = makeExpression(["lhs", "<", "rhs"])
|
||||
|
||||
it("evaluates to true with lhs < rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testLessThanEqualExpression() {
|
||||
let expression = makeExpression(["lhs", "<=", "rhs"])
|
||||
|
||||
it("evaluates to true with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with lhs > rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testMultipleExpressions() {
|
||||
let expression = makeExpression(["one", "or", "two", "and", "not", "three"])
|
||||
|
||||
it("evaluates to true with one") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to true with one and three") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to true with two") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
it("evaluates to false with two and three") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
it("evaluates to false with two and three") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
it("evaluates to false with nothing") {
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
func testTrueInExpression() throws {
|
||||
let expression = makeExpression(["lhs", "in", "rhs"])
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func testFalseInExpression() throws {
|
||||
let expression = makeExpression(["lhs", "in", "rhs"])
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
457
Tests/StencilTests/FilterSpec.swift
Normal file
457
Tests/StencilTests/FilterSpec.swift
Normal file
@@ -0,0 +1,457 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class FilterTests: XCTestCase {
|
||||
func testRegistration() {
|
||||
let context: [String: Any] = ["name": "Kyle"]
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
it("allows you to register boolean filters") {
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter(name: "isPositive", negativeFilterName: "isNotPositive") { (value: Any?) in
|
||||
if let value = value as? Int {
|
||||
return value > 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try Template(templateString: "{{ value|isPositive }}")
|
||||
.render(Context(dictionary: ["value": 1], environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "true"
|
||||
|
||||
let negativeResult = try Template(templateString: "{{ value|isNotPositive }}")
|
||||
.render(Context(dictionary: ["value": -1], environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(negativeResult) == "true"
|
||||
}
|
||||
|
||||
it("allows you to register a custom which throws") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { (_: 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))
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
func testRegistrationOverrideDefault() throws {
|
||||
let template = Template(templateString: "{{ name|join }}")
|
||||
let context: [String: Any] = ["name": "Kyle"]
|
||||
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("join") { (_: Any?) in
|
||||
"joined"
|
||||
}
|
||||
|
||||
let result = try template.render(Context(
|
||||
dictionary: context,
|
||||
environment: Environment(extensions: [repeatExtension])
|
||||
))
|
||||
try expect(result) == "joined"
|
||||
}
|
||||
|
||||
func testRegistrationWithArguments() {
|
||||
let context: [String: Any] = ["name": "Kyle"]
|
||||
|
||||
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
|
||||
guard let value = value,
|
||||
let argument = arguments.first else { return nil }
|
||||
|
||||
return "\(value) \(value) with args \(argument ?? "")"
|
||||
}
|
||||
|
||||
let result = try template.render(Context(
|
||||
dictionary: context,
|
||||
environment: Environment(extensions: [repeatExtension])
|
||||
))
|
||||
try expect(result) == """
|
||||
Kyle Kyle with args value1, "value2"
|
||||
"""
|
||||
}
|
||||
|
||||
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
|
||||
guard let value = value else { return nil }
|
||||
let args = arguments.compactMap { $0 }
|
||||
return "\(value) \(value) with args 0: \(args[0]), 1: \(args[1]), 2: \(args[2])"
|
||||
}
|
||||
|
||||
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)
|
||||
"""
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
func testStringFilters() {
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
func testStringFiltersWithArrays() {
|
||||
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"]
|
||||
"""
|
||||
}
|
||||
|
||||
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"]
|
||||
"""
|
||||
}
|
||||
|
||||
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"]
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultFilter() {
|
||||
let template = Template(templateString: """
|
||||
Hello {{ name|default:"World" }}
|
||||
""")
|
||||
|
||||
it("shows the variable value") {
|
||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||
try expect(result) == "Hello Kyle"
|
||||
}
|
||||
|
||||
it("shows the default value") {
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
it("can use int as default") {
|
||||
let template = Template(templateString: "{{ value|default:1 }}")
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "1"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
func testJoinFilter() {
|
||||
let template = Template(templateString: """
|
||||
{{ value|join:", " }}
|
||||
""")
|
||||
|
||||
it("joins a collection of strings") {
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||
try expect(result) == "One, Two"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
func testSplitFilter() {
|
||||
let template = Template(templateString: """
|
||||
{{ value|split:", " }}
|
||||
""")
|
||||
|
||||
it("split a string into array") {
|
||||
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
||||
try expect(result) == """
|
||||
["One", "Two"]
|
||||
"""
|
||||
}
|
||||
|
||||
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"]
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
func testFilterSuggestion() {
|
||||
it("made for unknown filter") {
|
||||
let template = Template(templateString: "{{ value|unknownFilter }}")
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
||||
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.",
|
||||
token: "value|unknownFilter",
|
||||
template: template,
|
||||
extension: filterExtension
|
||||
)
|
||||
}
|
||||
|
||||
it("made for multiple similar filters") {
|
||||
let template = Template(templateString: "{{ value|lowerFirst }}")
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
||||
filterExtension.registerFilter("lowerFirstLetter") { value, _ in value }
|
||||
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.",
|
||||
token: "value|lowerFirst",
|
||||
template: template,
|
||||
extension: filterExtension
|
||||
)
|
||||
}
|
||||
|
||||
it("not made when can't find similar filter") {
|
||||
let template = Template(templateString: "{{ value|unknownFilter }}")
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("lowerFirstWord") { value, _ in value }
|
||||
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.",
|
||||
token: "value|unknownFilter",
|
||||
template: template,
|
||||
extension: filterExtension
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testIndentContent() throws {
|
||||
let template = Template(templateString: """
|
||||
{{ value|indent:2 }}
|
||||
""")
|
||||
let result = try template.render(Context(dictionary: [
|
||||
"value": """
|
||||
One
|
||||
Two
|
||||
"""
|
||||
]))
|
||||
try expect(result) == """
|
||||
One
|
||||
Two
|
||||
"""
|
||||
}
|
||||
|
||||
func testIndentWithArbitraryCharacter() throws {
|
||||
let template = Template(templateString: """
|
||||
{{ value|indent:2,"\t" }}
|
||||
""")
|
||||
let result = try template.render(Context(dictionary: [
|
||||
"value": """
|
||||
One
|
||||
Two
|
||||
"""
|
||||
]))
|
||||
try expect(result) == """
|
||||
One
|
||||
\t\tTwo
|
||||
"""
|
||||
}
|
||||
|
||||
func testIndentFirstLine() throws {
|
||||
let template = Template(templateString: """
|
||||
{{ value|indent:2," ",true }}
|
||||
""")
|
||||
let result = try template.render(Context(dictionary: [
|
||||
"value": """
|
||||
One
|
||||
Two
|
||||
"""
|
||||
]))
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
One
|
||||
Two
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
|
||||
func testIndentNotEmptyLines() throws {
|
||||
let template = Template(templateString: """
|
||||
{{ value|indent }}
|
||||
""")
|
||||
let result = try template.render(Context(dictionary: [
|
||||
"value": """
|
||||
One
|
||||
|
||||
|
||||
Two
|
||||
|
||||
|
||||
"""
|
||||
]))
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
One
|
||||
|
||||
|
||||
Two
|
||||
|
||||
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
|
||||
func testDynamicFilters() throws {
|
||||
it("can apply dynamic filter") {
|
||||
let template = Template(templateString: "{{ name|filter:somefilter }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "Jhon", "somefilter": "uppercase"]))
|
||||
try expect(result) == "JHON"
|
||||
}
|
||||
|
||||
it("can apply dynamic filter on array") {
|
||||
let template = Template(templateString: "{{ values|filter:joinfilter }}")
|
||||
let result = try template.render(Context(dictionary: ["values": [1, 2, 3], "joinfilter": "join:\", \""]))
|
||||
try expect(result) == "1, 2, 3"
|
||||
}
|
||||
|
||||
it("throws on unknown dynamic filter") {
|
||||
let template = Template(templateString: "{{ values|filter:unknown }}")
|
||||
let context = Context(dictionary: ["values": [1, 2, 3], "unknown": "absurd"])
|
||||
try expect(try template.render(context)).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
private func expectError(
|
||||
reason: String,
|
||||
token: String,
|
||||
template: Template,
|
||||
extension: Extension,
|
||||
file: String = #file,
|
||||
line: Int = #line,
|
||||
function: String = #function
|
||||
) throws {
|
||||
guard let range = template.templateString.range(of: token) else {
|
||||
fatalError("Can't find '\(token)' in '\(template)'")
|
||||
}
|
||||
|
||||
let environment = Environment(extensions: [`extension`])
|
||||
let expectedError: Error = {
|
||||
let lexer = Lexer(templateString: template.templateString)
|
||||
let location = lexer.rangeLocation(range)
|
||||
let sourceMap = SourceMap(filename: template.name, location: location)
|
||||
let token = Token.block(value: token, at: sourceMap)
|
||||
return TemplateSyntaxError(reason: reason, token: token, stackTrace: [])
|
||||
}()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
54
Tests/StencilTests/FilterTagSpec.swift
Normal file
54
Tests/StencilTests/FilterTagSpec.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import XCTest
|
||||
|
||||
final class FilterTagTests: XCTestCase {
|
||||
func testFilterTag() {
|
||||
it("allows you to use a filter") {
|
||||
let template = Template(templateString: "{% filter uppercase %}Test{% endfilter %}")
|
||||
let result = try template.render()
|
||||
try expect(result) == "TEST"
|
||||
}
|
||||
|
||||
it("allows you to chain filters") {
|
||||
let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}")
|
||||
let result = try template.render()
|
||||
try expect(result) == "Test"
|
||||
}
|
||||
|
||||
it("errors without a filter") {
|
||||
let template = Template(templateString: "Some {% filter %}Test{% endfilter %}")
|
||||
try expect(try template.render()).toThrow()
|
||||
}
|
||||
|
||||
it("can render filters with arguments") {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("split") { value, args in
|
||||
guard let value = value as? String,
|
||||
let argument = args.first as? String else { return value }
|
||||
return value.components(separatedBy: argument)
|
||||
}
|
||||
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"
|
||||
}
|
||||
|
||||
it("can render filters with quote as an argument") {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("replace") { value, args in
|
||||
guard let value = value as? String,
|
||||
args.count == 2,
|
||||
let search = args.first as? String,
|
||||
let replacement = args.last as? String else { return value }
|
||||
return value.replacingOccurrences(of: search, with: replacement)
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
591
Tests/StencilTests/ForNodeSpec.swift
Normal file
591
Tests/StencilTests/ForNodeSpec.swift
Normal file
@@ -0,0 +1,591 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class ForNodeTests: XCTestCase {
|
||||
private let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"anyItems": [1, 2, 3] as [Any],
|
||||
// swiftlint:disable:next legacy_objc_type
|
||||
"nsItems": NSArray(array: [1, 2, 3]),
|
||||
"emptyItems": [Int](),
|
||||
"dict": [
|
||||
"one": "I",
|
||||
"two": "II"
|
||||
],
|
||||
"tuples": [(1, 2, 3), (4, 5, 6)]
|
||||
])
|
||||
|
||||
func testForNode() {
|
||||
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(self.context)) == "123"
|
||||
}
|
||||
|
||||
it("renders the given empty nodes when no items found item") {
|
||||
let node = ForNode(
|
||||
resolvable: Variable("emptyItems"),
|
||||
loopVariables: ["item"],
|
||||
nodes: [VariableNode(variable: "item")],
|
||||
emptyNodes: [TextNode(text: "empty")]
|
||||
)
|
||||
try expect(try node.render(self.context)) == "empty"
|
||||
}
|
||||
|
||||
it("renders a context variable of type Array<Any>") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("anyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(self.context)) == "123"
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
it("renders a context variable of type NSArray") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("nsItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(self.context)) == "123"
|
||||
}
|
||||
#endif
|
||||
|
||||
it("can render a filter with spaces") {
|
||||
let template = Template(templateString: """
|
||||
{% for article in ars | default: a, b , articles %}\
|
||||
- {{ article.title }} by {{ article.author }}.
|
||||
{% endfor %}
|
||||
""")
|
||||
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 result = try template.render(context)
|
||||
|
||||
try expect(result) == """
|
||||
- Migrating from OCUnit to XCTest by Kyle Fuller.
|
||||
- Memory Management with ARC by Kyle Fuller.
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
func testLoopMetadata() {
|
||||
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(self.context)) == "1true2false3false"
|
||||
}
|
||||
|
||||
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(self.context)) == "1false2false3true"
|
||||
}
|
||||
|
||||
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(self.context)) == "112233"
|
||||
}
|
||||
|
||||
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(self.context)) == "102132"
|
||||
}
|
||||
|
||||
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(self.context)) == "132333"
|
||||
}
|
||||
}
|
||||
|
||||
func testWhereExpression() {
|
||||
it("renders the given nodes while filtering items using where expression") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||
let parser = TokenParser(tokens: [], environment: Environment())
|
||||
let `where` = try parser.compileExpression(components: ["item", ">", "1"], token: .text(value: "", at: .unknown))
|
||||
let node = ForNode(
|
||||
resolvable: Variable("items"),
|
||||
loopVariables: ["item"],
|
||||
nodes: nodes,
|
||||
emptyNodes: [],
|
||||
where: `where`
|
||||
)
|
||||
try expect(try node.render(self.context)) == "2132"
|
||||
}
|
||||
|
||||
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 parser = TokenParser(tokens: [], environment: Environment())
|
||||
let `where` = try parser.compileExpression(components: ["item", "==", "0"], token: .text(value: "", at: .unknown))
|
||||
let node = ForNode(
|
||||
resolvable: Variable("emptyItems"),
|
||||
loopVariables: ["item"],
|
||||
nodes: nodes,
|
||||
emptyNodes: emptyNodes,
|
||||
where: `where`
|
||||
)
|
||||
try expect(try node.render(self.context)) == "empty"
|
||||
}
|
||||
}
|
||||
|
||||
func testArrayOfTuples() {
|
||||
it("can iterate over all tuple values") {
|
||||
let template = Template(templateString: """
|
||||
{% for first,second,third in tuples %}\
|
||||
{{ first }}, {{ second }}, {{ third }}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
1, 2, 3
|
||||
4, 5, 6
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("can iterate with less number of variables") {
|
||||
let template = Template(templateString: """
|
||||
{% for first,second in tuples %}\
|
||||
{{ first }}, {{ second }}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
1, 2
|
||||
4, 5
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("can use _ to skip variables") {
|
||||
let template = Template(templateString: """
|
||||
{% for first,_,third in tuples %}\
|
||||
{{ first }}, {{ third }}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
1, 3
|
||||
4, 6
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("throws when number of variables is more than number of tuple values") {
|
||||
let template = Template(templateString: """
|
||||
{% for key,value,smth in dict %}{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
func testIterateDictionary() {
|
||||
it("can iterate over dictionary") {
|
||||
let template = Template(templateString: """
|
||||
{% for key, value in dict %}\
|
||||
{{ key }}: {{ value }},\
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
one: I,two: II,
|
||||
"""
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
try expect(node.render(self.context)) == """
|
||||
one,two,
|
||||
"""
|
||||
}
|
||||
|
||||
it("renders supports iterating over dictionary with values") {
|
||||
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
|
||||
)
|
||||
|
||||
try expect(node.render(self.context)) == """
|
||||
one=I,two=II,
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
func testIterateUsingMirroring() {
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "label"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n")
|
||||
]
|
||||
let node = ForNode(
|
||||
resolvable: Variable("item"),
|
||||
loopVariables: ["label", "value"],
|
||||
nodes: nodes,
|
||||
emptyNodes: []
|
||||
)
|
||||
|
||||
it("can iterate over struct properties") {
|
||||
let context = Context(dictionary: [
|
||||
"item": MyStruct(string: "abc", number: 123)
|
||||
])
|
||||
try expect(node.render(context)) == """
|
||||
string=abc
|
||||
number=123
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("can iterate tuple items") {
|
||||
let context = Context(dictionary: [
|
||||
"item": (one: 1, two: "dva")
|
||||
])
|
||||
try expect(node.render(context)) == """
|
||||
one=1
|
||||
two=dva
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("can iterate over class properties") {
|
||||
let context = Context(dictionary: [
|
||||
"item": MySubclass("child", "base", 1)
|
||||
])
|
||||
try expect(node.render(context)) == """
|
||||
childString=child
|
||||
baseString=base
|
||||
baseInt=1
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
func testIterateRange() {
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
func testHandleInvalidInput() throws {
|
||||
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)
|
||||
}
|
||||
|
||||
func testBreak() {
|
||||
it("can break from loop") {
|
||||
let template = Template(templateString: """
|
||||
{% for item in items %}\
|
||||
{{ item }}{% break %}\
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
1
|
||||
"""
|
||||
}
|
||||
|
||||
it("can break from inner node") {
|
||||
let template = Template(templateString: """
|
||||
{% for item in items %}\
|
||||
{{ item }}\
|
||||
{% if forloop.first %}<{% break %}>{% endif %}!\
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
1<
|
||||
"""
|
||||
}
|
||||
|
||||
it("does not allow break outside loop") {
|
||||
let template = Template(templateString: "{% for item in items %}{% endfor %}{% break %}")
|
||||
let error = self.expectedSyntaxError(
|
||||
token: "break",
|
||||
template: template,
|
||||
description: "'break' can be used only inside loop body"
|
||||
)
|
||||
try expect(template.render(self.context)).toThrow(error)
|
||||
}
|
||||
}
|
||||
|
||||
func testBreakNested() {
|
||||
it("breaks outer loop") {
|
||||
let template = Template(templateString: """
|
||||
{% for item in items %}\
|
||||
outer: {{ item }}
|
||||
{% for item in items %}\
|
||||
inner: {{ item }}
|
||||
{% endfor %}\
|
||||
{% break %}\
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
outer: 1
|
||||
inner: 1
|
||||
inner: 2
|
||||
inner: 3
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("breaks inner loop") {
|
||||
let template = Template(templateString: """
|
||||
{% for item in items %}\
|
||||
outer: {{ item }}
|
||||
{% for item in items %}\
|
||||
inner: {{ item }}
|
||||
{% break %}\
|
||||
{% endfor %}\
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
outer: 1
|
||||
inner: 1
|
||||
outer: 2
|
||||
inner: 1
|
||||
outer: 3
|
||||
inner: 1
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
func testBreakLabeled() {
|
||||
it("breaks labeled loop") {
|
||||
let template = Template(templateString: """
|
||||
{% outer: for item in items %}\
|
||||
outer: {{ item }}
|
||||
{% for item in items %}\
|
||||
{% break outer %}\
|
||||
inner: {{ item }}
|
||||
{% endfor %}\
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
outer: 1
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("throws when breaking with unknown label") {
|
||||
let template = Template(templateString: """
|
||||
{% outer: for item in items %}
|
||||
{% break inner %}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
func testContinue() {
|
||||
it("can continue loop") {
|
||||
let template = Template(templateString: """
|
||||
{% for item in items %}\
|
||||
{{ item }}{% continue %}!\
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == "123"
|
||||
}
|
||||
|
||||
it("can continue from inner node") {
|
||||
let template = Template(templateString: """
|
||||
{% for item in items %}\
|
||||
{% if forloop.last %}<{% continue %}>{% endif %}!\
|
||||
{{ item }}\
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == "!1!2<"
|
||||
}
|
||||
|
||||
it("does not allow continue outside loop") {
|
||||
let template = Template(templateString: "{% for item in items %}{% endfor %}{% continue %}")
|
||||
let error = self.expectedSyntaxError(
|
||||
token: "continue",
|
||||
template: template,
|
||||
description: "'continue' can be used only inside loop body"
|
||||
)
|
||||
try expect(template.render(self.context)).toThrow(error)
|
||||
}
|
||||
}
|
||||
|
||||
func testContinueNested() {
|
||||
it("breaks outer loop") {
|
||||
let template = Template(templateString: """
|
||||
{% for item in items %}\
|
||||
{% for item in items %}\
|
||||
inner: {{ item }}\
|
||||
{% endfor %}
|
||||
{% continue %}
|
||||
outer: {{ item }}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
inner: 1inner: 2inner: 3
|
||||
inner: 1inner: 2inner: 3
|
||||
inner: 1inner: 2inner: 3
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("breaks inner loop") {
|
||||
let template = Template(templateString: """
|
||||
{% for item in items %}\
|
||||
{% for item in items %}\
|
||||
{% continue %}\
|
||||
inner: {{ item }}
|
||||
{% endfor %}\
|
||||
outer: {{ item }}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
outer: 1
|
||||
outer: 2
|
||||
outer: 3
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
func testContinueLabeled() {
|
||||
it("continues labeled loop") {
|
||||
let template = Template(templateString: """
|
||||
{% outer: for item in items %}\
|
||||
{% for item in items %}\
|
||||
inner: {{ item }}
|
||||
{% continue outer %}\
|
||||
{% endfor %}\
|
||||
outer: {{ item }}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
inner: 1
|
||||
inner: 1
|
||||
inner: 1
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("throws when continuing with unknown label") {
|
||||
let template = Template(templateString: """
|
||||
{% outer: for item in items %}
|
||||
{% continue inner %}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
func testAccessLabeled() {
|
||||
it("can access labeled outer loop context from inner loop") {
|
||||
let template = Template(templateString: """
|
||||
{% outer: for item in 1...2 %}\
|
||||
{% for item in items %}\
|
||||
{{ forloop.counter }}-{{ forloop.outer.counter }},\
|
||||
{% endfor %}---\
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
1-1,2-1,3-1,---1-2,2-2,3-2,---
|
||||
"""
|
||||
}
|
||||
|
||||
it("can access labeled outer loop from double inner loop") {
|
||||
let template = Template(templateString: """
|
||||
{% outer: for item in 1...2 %}{% for item in 1...2 %}\
|
||||
{% for item in items %}\
|
||||
{{ forloop.counter }}-{{ forloop.outer.counter }},\
|
||||
{% endfor %}---{% endfor %}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
1-1,2-1,3-1,---1-1,2-1,3-1,---
|
||||
1-2,2-2,3-2,---1-2,2-2,3-2,---
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("can access two labeled outer loop contexts from inner loop") {
|
||||
let template = Template(templateString: """
|
||||
{% outer1: for item in 1...2 %}{% outer2: for item in 1...2 %}\
|
||||
{% for item in items %}\
|
||||
{{ forloop.counter }}-{{ forloop.outer2.counter }}-{{ forloop.outer1.counter }},\
|
||||
{% endfor %}---{% endfor %}
|
||||
{% endfor %}
|
||||
""")
|
||||
try expect(template.render(self.context)) == """
|
||||
1-1-1,2-1-1,3-1-1,---1-2-1,2-2-1,3-2-1,---
|
||||
1-1-2,2-1-2,3-1-2,---1-2-2,2-2-2,3-2-2,---
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private struct MyStruct {
|
||||
let string: String
|
||||
let number: Int
|
||||
}
|
||||
|
||||
private struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
|
||||
private class MyClass {
|
||||
var baseString: String
|
||||
var baseInt: Int
|
||||
init(_ string: String, _ int: Int) {
|
||||
baseString = string
|
||||
baseInt = int
|
||||
}
|
||||
}
|
||||
|
||||
private class MySubclass: MyClass {
|
||||
var childString: String
|
||||
init(_ childString: String, _ string: String, _ int: Int) {
|
||||
self.childString = childString
|
||||
super.init(string, int)
|
||||
}
|
||||
}
|
||||
63
Tests/StencilTests/Helpers.swift
Normal file
63
Tests/StencilTests/Helpers.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
extension Expectation {
|
||||
@discardableResult
|
||||
func toThrow<E: Error>() throws -> E {
|
||||
var thrownError: Error?
|
||||
|
||||
do {
|
||||
_ = try expression()
|
||||
} catch {
|
||||
thrownError = error
|
||||
}
|
||||
|
||||
if let thrownError = thrownError {
|
||||
if let thrownError = thrownError as? E {
|
||||
return thrownError
|
||||
} else {
|
||||
throw failure("\(thrownError) is not \(T.self)")
|
||||
}
|
||||
} else {
|
||||
throw failure("expression did not throw an error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension XCTestCase {
|
||||
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 lexer = Lexer(templateString: template.templateString)
|
||||
let location = lexer.rangeLocation(range)
|
||||
let sourceMap = SourceMap(filename: template.name, location: location)
|
||||
let token = Token.block(value: token, at: sourceMap)
|
||||
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Types
|
||||
|
||||
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 ErrorNode: NodeType {
|
||||
let token: Token?
|
||||
init(token: Token? = nil) {
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
throw TemplateSyntaxError("Custom Error")
|
||||
}
|
||||
}
|
||||
290
Tests/StencilTests/IfNodeSpec.swift
Normal file
290
Tests/StencilTests/IfNodeSpec.swift
Normal file
@@ -0,0 +1,290 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class IfNodeTests: XCTestCase {
|
||||
func testParseIf() {
|
||||
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"
|
||||
}
|
||||
|
||||
it("can parse an if with complex expression") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: """
|
||||
if value == \"test\" and (not name or not (name and surname) or( some )and other )
|
||||
""", 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()
|
||||
}
|
||||
}
|
||||
|
||||
func testParseIfWithElse() throws {
|
||||
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"
|
||||
}
|
||||
|
||||
func testParseIfWithElif() throws {
|
||||
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"
|
||||
}
|
||||
|
||||
func testParseIfWithElifWithoutElse() throws {
|
||||
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"
|
||||
}
|
||||
|
||||
func testParseMultipleElif() throws {
|
||||
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"
|
||||
}
|
||||
|
||||
func testParseIfnot() throws {
|
||||
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"
|
||||
}
|
||||
|
||||
func testParsingErrors() {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func testRendering() {
|
||||
it("renders a true expression") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "1"
|
||||
}
|
||||
|
||||
it("renders the first true expression") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: VariableExpression(variable: Variable("true")), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "2"
|
||||
}
|
||||
|
||||
it("renders the empty expression when other conditions are falsy") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "3"
|
||||
}
|
||||
|
||||
it("renders empty when no truthy conditions") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: VariableExpression(variable: Variable("false")), nodes: [TextNode(text: "2")])
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == ""
|
||||
}
|
||||
}
|
||||
|
||||
func testSupportVariableFilters() throws {
|
||||
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"
|
||||
}
|
||||
|
||||
func testEvaluatesNilAsFalse() throws {
|
||||
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()
|
||||
|
||||
let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()]))
|
||||
try expect(result) == ""
|
||||
}
|
||||
|
||||
func testSupportsRangeVariables() throws {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private struct SomeType {
|
||||
let value: String? = nil
|
||||
}
|
||||
72
Tests/StencilTests/IncludeSpec.swift
Normal file
72
Tests/StencilTests/IncludeSpec.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class IncludeTests: XCTestCase {
|
||||
private let path = Path(#file as String)! / ".." / "fixtures"
|
||||
private lazy var loader = FileSystemLoader(paths: [path])
|
||||
private lazy var environment = Environment(loader: loader)
|
||||
|
||||
func testParsing() {
|
||||
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)
|
||||
}
|
||||
|
||||
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\"")
|
||||
}
|
||||
}
|
||||
|
||||
func testRendering() {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
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: self.environment))
|
||||
} catch {
|
||||
try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
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: self.environment)
|
||||
let value = try node.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
|
||||
it("successfully passes context") {
|
||||
let template = Template(templateString: """
|
||||
{% include "test.html" child %}
|
||||
""")
|
||||
let context = Context(dictionary: ["child": ["target": "World"]], environment: self.environment)
|
||||
let value = try template.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Tests/StencilTests/InheritanceSpec.swift
Normal file
73
Tests/StencilTests/InheritanceSpec.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
import Stencil
|
||||
import XCTest
|
||||
|
||||
final class InheritanceTests: XCTestCase {
|
||||
private let path = Path(#file as String)! / ".." / "fixtures"
|
||||
private lazy var loader = FileSystemLoader(paths: [path])
|
||||
private lazy var environment = Environment(loader: loader)
|
||||
|
||||
func testInheritance() {
|
||||
it("can inherit from another template") {
|
||||
let template = try self.environment.loadTemplate(name: "child.html")
|
||||
try expect(try template.render()) == """
|
||||
Super_Header Child_Header
|
||||
Child_Body
|
||||
"""
|
||||
}
|
||||
|
||||
it("can inherit from another template inheriting from another template") {
|
||||
let template = try self.environment.loadTemplate(name: "child-child.html")
|
||||
try expect(try template.render()) == """
|
||||
Super_Header Child_Header Child_Child_Header
|
||||
Child_Body
|
||||
"""
|
||||
}
|
||||
|
||||
it("can inherit from a template that calls a super block") {
|
||||
let template = try self.environment.loadTemplate(name: "child-super.html")
|
||||
try expect(try template.render()) == """
|
||||
Header
|
||||
Child_Body
|
||||
"""
|
||||
}
|
||||
|
||||
it("can render block.super in if tag") {
|
||||
let template = try self.environment.loadTemplate(name: "if-block-child.html")
|
||||
|
||||
try expect(try template.render(["sort": "new"])) == """
|
||||
Title - Nieuwste spellen
|
||||
|
||||
"""
|
||||
|
||||
try expect(try template.render(["sort": "upcoming"])) == """
|
||||
Title - Binnenkort op de agenda
|
||||
|
||||
"""
|
||||
|
||||
try expect(try template.render(["sort": "near-me"])) == """
|
||||
Title - In mijn buurt
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
func testInheritanceCache() {
|
||||
it("can call block twice") {
|
||||
let template: Template = "{% block repeat %}Block{% endblock %}{{ block.repeat }}"
|
||||
try expect(try template.render()) == "BlockBlock"
|
||||
}
|
||||
|
||||
it("renders child content when calling block twice in base template") {
|
||||
let template = try self.environment.loadTemplate(name: "child-repeat.html")
|
||||
try expect(try template.render()) == """
|
||||
Super_Header Child_Header
|
||||
Child_Body
|
||||
Repeat
|
||||
Super_Header Child_Header
|
||||
Child_Body
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
177
Tests/StencilTests/LexerSpec.swift
Normal file
177
Tests/StencilTests/LexerSpec.swift
Normal file
@@ -0,0 +1,177 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class LexerTests: XCTestCase {
|
||||
func testText() throws {
|
||||
let lexer = Lexer(templateString: "Hello World")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "Hello World", at: makeSourceMap("Hello World", for: lexer))
|
||||
}
|
||||
|
||||
func testComment() throws {
|
||||
let lexer = Lexer(templateString: "{# Comment #}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .comment(value: "Comment", at: makeSourceMap("Comment", for: lexer))
|
||||
}
|
||||
|
||||
func testEscapedVariableToken() throws {
|
||||
let lexer = Lexer(templateString: "\\{{ Variable }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "{{ Variable }}", at: makeSourceMap("{{ Variable }}", for: lexer))
|
||||
}
|
||||
|
||||
func testEscapedBehaviourToken() throws {
|
||||
let lexer = Lexer(templateString: "\\{% Variable %}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "{% Variable %}", at: makeSourceMap("{% Variable %}", for: lexer))
|
||||
}
|
||||
|
||||
func testVariable() throws {
|
||||
let lexer = Lexer(templateString: "{{ Variable }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
||||
}
|
||||
|
||||
func testTokenWithoutSpaces() throws {
|
||||
let lexer = Lexer(templateString: "{{Variable}}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
||||
}
|
||||
|
||||
func testUnclosedTag() throws {
|
||||
let templateString = "{{ thing"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "", at: makeSourceMap("{{ thing", for: lexer))
|
||||
}
|
||||
|
||||
func testContentMixture() throws {
|
||||
let templateString = "My name is {{ myname }}."
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 3
|
||||
try expect(tokens[0]) == .text(value: "My name is ", at: makeSourceMap("My name is ", for: lexer))
|
||||
try expect(tokens[1]) == .variable(value: "myname", at: makeSourceMap("myname", for: lexer))
|
||||
try expect(tokens[2]) == .text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||
}
|
||||
|
||||
func testVariablesWithoutBeingGreedy() throws {
|
||||
let templateString = "{{ thing }}{{ name }}"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == .variable(value: "thing", at: makeSourceMap("thing", for: lexer))
|
||||
try expect(tokens[1]) == .variable(value: "name", at: makeSourceMap("name", for: lexer))
|
||||
}
|
||||
|
||||
func testUnclosedBlock() throws {
|
||||
let lexer = Lexer(templateString: "{%}")
|
||||
_ = lexer.tokenize()
|
||||
}
|
||||
|
||||
func testTokenizeIncorrectSyntaxWithoutCrashing() throws {
|
||||
let lexer = Lexer(templateString: "func some() {{% if %}")
|
||||
_ = lexer.tokenize()
|
||||
}
|
||||
|
||||
func testEmptyVariable() throws {
|
||||
let lexer = Lexer(templateString: "{{}}")
|
||||
_ = lexer.tokenize()
|
||||
}
|
||||
|
||||
func testNewlines() throws {
|
||||
// swiftlint:disable indentation_width
|
||||
let templateString = """
|
||||
My name is {%
|
||||
if name
|
||||
and
|
||||
name
|
||||
%}{{
|
||||
name
|
||||
}}{%
|
||||
endif %}.
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 5
|
||||
try expect(tokens[0]) == .text(value: "My name is ", at: makeSourceMap("My name is", for: lexer))
|
||||
try expect(tokens[1]) == .block(value: "if name and name", at: makeSourceMap("{%", for: lexer))
|
||||
try expect(tokens[2]) == .variable(value: "name", at: makeSourceMap("name", for: lexer, options: .backwards))
|
||||
try expect(tokens[3]) == .block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
||||
try expect(tokens[4]) == .text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||
}
|
||||
|
||||
func testTrimSymbols() throws {
|
||||
let fBlock = "if hello"
|
||||
let sBlock = "ta da"
|
||||
let lexer = Lexer(templateString: "{%+ \(fBlock) -%}{% \(sBlock) -%}")
|
||||
let tokens = lexer.tokenize()
|
||||
let behaviours = (
|
||||
WhitespaceBehaviour(leading: .keep, trailing: .trim),
|
||||
WhitespaceBehaviour(leading: .unspecified, trailing: .trim)
|
||||
)
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == .block(value: fBlock, at: makeSourceMap(fBlock, for: lexer), whitespace: behaviours.0)
|
||||
try expect(tokens[1]) == .block(value: sBlock, at: makeSourceMap(sBlock, for: lexer), whitespace: behaviours.1)
|
||||
}
|
||||
|
||||
func testEscapeSequence() throws {
|
||||
let templateString = "class Some {{ '{' }}{% if true %}{{ stuff }}{% endif %}"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 5
|
||||
try expect(tokens[0]) == .text(value: "class Some ", at: makeSourceMap("class Some ", for: lexer))
|
||||
try expect(tokens[1]) == .variable(value: "'{'", at: makeSourceMap("'{'", for: lexer))
|
||||
try expect(tokens[2]) == .block(value: "if true", at: makeSourceMap("if true", for: lexer))
|
||||
try expect(tokens[3]) == .variable(value: "stuff", at: makeSourceMap("stuff", for: lexer))
|
||||
try expect(tokens[4]) == .block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
||||
}
|
||||
|
||||
func testPerformance() throws {
|
||||
let path = Path(#file as String)! / ".." / "fixtures" / "huge.html"
|
||||
let content: String = try NSString(contentsOfFile: path.string, encoding: String.Encoding.utf8.rawValue).substring(from: 0) as String
|
||||
|
||||
measure {
|
||||
let lexer = Lexer(templateString: content)
|
||||
_ = lexer.tokenize()
|
||||
}
|
||||
}
|
||||
|
||||
func testCombiningDiaeresis() throws {
|
||||
// the symbol "ü" in the `templateString` is unusually encoded as 0x75 0xCC 0x88 (LATIN SMALL LETTER U + COMBINING
|
||||
// DIAERESIS) instead of 0xC3 0xBC (LATIN SMALL LETTER U WITH DIAERESIS)
|
||||
let templateString = "ü\n{% if test %}ü{% endif %}\n{% if ü %}ü{% endif %}\n"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 9
|
||||
assert(tokens[1].contents == "if test")
|
||||
}
|
||||
|
||||
private func makeSourceMap(_ token: String, for lexer: Lexer, options: String.CompareOptions = []) -> SourceMap {
|
||||
guard let range = lexer.templateString.range(of: token, options: options) else { fatalError("Token not found") }
|
||||
return SourceMap(location: lexer.rangeLocation(range))
|
||||
}
|
||||
}
|
||||
55
Tests/StencilTests/LoaderSpec.swift
Normal file
55
Tests/StencilTests/LoaderSpec.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
import Stencil
|
||||
import XCTest
|
||||
|
||||
final class TemplateLoaderTests: XCTestCase {
|
||||
func testFileSystemLoader() {
|
||||
let path = Path(#file as String)! / ".." / "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
it("errors when a template cannot be found") {
|
||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||
}
|
||||
|
||||
it("errors when an array of templates cannot be found") {
|
||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||
}
|
||||
|
||||
it("can load a template from a file") {
|
||||
_ = try environment.loadTemplate(name: "test.html")
|
||||
}
|
||||
|
||||
it("errors when loading absolute file outside of the selected path") {
|
||||
try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow()
|
||||
}
|
||||
|
||||
it("errors when loading relative file outside of the selected path") {
|
||||
try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
func testDictionaryLoader() {
|
||||
let loader = DictionaryLoader(templates: [
|
||||
"index.html": "Hello World"
|
||||
])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
it("errors when a template cannot be found") {
|
||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||
}
|
||||
|
||||
it("errors when an array of templates cannot be found") {
|
||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||
}
|
||||
|
||||
it("can load a template from a known templates") {
|
||||
_ = try environment.loadTemplate(name: "index.html")
|
||||
}
|
||||
|
||||
it("can load a known template from a collection of templates") {
|
||||
_ = try environment.loadTemplate(names: ["unknown.html", "index.html"])
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Tests/StencilTests/NodeSpec.swift
Normal file
111
Tests/StencilTests/NodeSpec.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class NodeTests: XCTestCase {
|
||||
private let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"age": 27,
|
||||
"items": [1, 2, 3]
|
||||
])
|
||||
|
||||
func testTextNode() {
|
||||
it("renders the given text") {
|
||||
let node = TextNode(text: "Hello World")
|
||||
try expect(try node.render(self.context)) == "Hello World"
|
||||
}
|
||||
it("Trims leading whitespace") {
|
||||
let text = " \n Some text "
|
||||
let trimBehaviour = TrimBehaviour(leading: .whitespace, trailing: .nothing)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == "\n Some text "
|
||||
}
|
||||
it("Trims leading whitespace and one newline") {
|
||||
let text = "\n\n Some text "
|
||||
let trimBehaviour = TrimBehaviour(leading: .whitespaceAndOneNewLine, trailing: .nothing)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == "\n Some text "
|
||||
}
|
||||
it("Trims leading whitespace and one newline") {
|
||||
let text = "\n\n Some text "
|
||||
let trimBehaviour = TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .nothing)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == "Some text "
|
||||
}
|
||||
it("Trims trailing whitespace") {
|
||||
let text = " Some text \n"
|
||||
let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespace)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == " Some text\n"
|
||||
}
|
||||
it("Trims trailing whitespace and one newline") {
|
||||
let text = " Some text \n \n "
|
||||
let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespaceAndOneNewLine)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == " Some text \n "
|
||||
}
|
||||
it("Trims trailing whitespace and newlines") {
|
||||
let text = " Some text \n \n "
|
||||
let trimBehaviour = TrimBehaviour(leading: .nothing, trailing: .whitespaceAndNewLines)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == " Some text"
|
||||
}
|
||||
it("Trims all whitespace") {
|
||||
let text = " \n \nSome text \n "
|
||||
let trimBehaviour = TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .whitespaceAndNewLines)
|
||||
let node = TextNode(text: text, trimBehaviour: trimBehaviour)
|
||||
try expect(try node.render(self.context)) == "Some text"
|
||||
}
|
||||
}
|
||||
|
||||
func testVariableNode() {
|
||||
it("resolves and renders the variable") {
|
||||
let node = VariableNode(variable: Variable("name"))
|
||||
try expect(try node.render(self.context)) == "Kyle"
|
||||
}
|
||||
|
||||
it("resolves and renders a non string variable") {
|
||||
let node = VariableNode(variable: Variable("age"))
|
||||
try expect(try node.render(self.context)) == "27"
|
||||
}
|
||||
}
|
||||
|
||||
func testRendering() {
|
||||
it("renders the nodes") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text: "Hello "),
|
||||
VariableNode(variable: "name")
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, self.context)) == "Hello Kyle"
|
||||
}
|
||||
|
||||
it("correctly throws a nodes failure") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text: "Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
ErrorNode()
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, self.context)).toThrow(TemplateSyntaxError("Custom Error"))
|
||||
}
|
||||
}
|
||||
|
||||
func testRenderingBooleans() {
|
||||
it("can render true & false") {
|
||||
try expect(Template(templateString: "{{ true }}").render()) == "true"
|
||||
try expect(Template(templateString: "{{ false }}").render()) == "false"
|
||||
}
|
||||
|
||||
it("can resolve variable") {
|
||||
let template = Template(templateString: "{{ value == \"known\" }}")
|
||||
try expect(template.render(["value": "known"])) == "true"
|
||||
try expect(template.render(["value": "unknown"])) == "false"
|
||||
}
|
||||
|
||||
it("can render a boolean expression") {
|
||||
try expect(Template(templateString: "{{ 1 > 0 }}").render()) == "true"
|
||||
try expect(Template(templateString: "{{ 1 == 2 }}").render()) == "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
Tests/StencilTests/NowNodeSpec.swift
Normal file
50
Tests/StencilTests/NowNodeSpec.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class NowNodeTests: XCTestCase {
|
||||
func testParsing() {
|
||||
it("parses default format without any now arguments") {
|
||||
#if os(Linux)
|
||||
throw skip()
|
||||
#else
|
||||
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\""
|
||||
#endif
|
||||
}
|
||||
|
||||
it("parses now with a format") {
|
||||
#if os(Linux)
|
||||
throw skip()
|
||||
#else
|
||||
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\""
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func testRendering() {
|
||||
it("renders the date") {
|
||||
#if os(Linux)
|
||||
throw skip()
|
||||
#else
|
||||
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let date = formatter.string(from: Date())
|
||||
|
||||
try expect(try node.render(Context())) == date
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
79
Tests/StencilTests/ParserSpec.swift
Normal file
79
Tests/StencilTests/ParserSpec.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class TokenParserTests: XCTestCase {
|
||||
func testTextToken() throws {
|
||||
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"
|
||||
}
|
||||
|
||||
func testVariableToken() throws {
|
||||
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"
|
||||
}
|
||||
|
||||
func testCommentToken() throws {
|
||||
let parser = TokenParser(tokens: [
|
||||
.comment(value: "Secret stuff!", at: .unknown)
|
||||
], environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 0
|
||||
}
|
||||
|
||||
func testTagToken() throws {
|
||||
let simpleExtension = Extension()
|
||||
simpleExtension.registerSimpleTag("known") { _ in
|
||||
""
|
||||
}
|
||||
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "known", at: .unknown)
|
||||
], environment: Environment(extensions: [simpleExtension]))
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 1
|
||||
}
|
||||
|
||||
func testErrorUnknownTag() throws {
|
||||
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
|
||||
))
|
||||
}
|
||||
|
||||
func testTransformWhitespaceBehaviourToTrimBehaviour() throws {
|
||||
let simpleExtension = Extension()
|
||||
simpleExtension.registerSimpleTag("known") { _ in "" }
|
||||
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "known", at: .unknown, whitespace: WhitespaceBehaviour(leading: .unspecified, trailing: .trim)),
|
||||
.text(value: " \nSome text ", at: .unknown),
|
||||
.block(value: "known", at: .unknown, whitespace: WhitespaceBehaviour(leading: .keep, trailing: .trim))
|
||||
], environment: Environment(extensions: [simpleExtension]))
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 3
|
||||
let textNode = nodes[1] as? TextNode
|
||||
try expect(textNode?.text) == " \nSome text "
|
||||
try expect(textNode?.trimBehaviour) == TrimBehaviour(leading: .whitespaceAndNewLines, trailing: .nothing)
|
||||
}
|
||||
}
|
||||
70
Tests/StencilTests/StencilSpec.swift
Normal file
70
Tests/StencilTests/StencilSpec.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import XCTest
|
||||
|
||||
final class StencilTests: XCTestCase {
|
||||
private lazy var environment: Environment = {
|
||||
let exampleExtension = Extension()
|
||||
exampleExtension.registerSimpleTag("simpletag") { _ in
|
||||
"Hello World"
|
||||
}
|
||||
exampleExtension.registerTag("customtag") { _, token in
|
||||
CustomNode(token: token)
|
||||
}
|
||||
return Environment(extensions: [exampleExtension])
|
||||
}()
|
||||
|
||||
func testStencil() {
|
||||
it("can render the README example") {
|
||||
let templateString = """
|
||||
There are {{ articles.count }} articles.
|
||||
|
||||
{% for article in articles %}\
|
||||
- {{ article.title }} by {{ article.author }}.
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
try expect(result) == """
|
||||
There are 2 articles.
|
||||
|
||||
- Migrating from OCUnit to XCTest by Kyle Fuller.
|
||||
- Memory Management with ARC by Kyle Fuller.
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
it("can render a custom template tag") {
|
||||
let result = try self.environment.renderTemplate(string: "{% customtag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
it("can render a simple custom tag") {
|
||||
let result = try self.environment.renderTemplate(string: "{% simpletag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private struct CustomNode: NodeType {
|
||||
let token: Token?
|
||||
func render(_ context: Context) throws -> String {
|
||||
"Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
private struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
25
Tests/StencilTests/TemplateSpec.swift
Normal file
25
Tests/StencilTests/TemplateSpec.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class TemplateTests: XCTestCase {
|
||||
func testTemplate() {
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
it("can render a template with escaped token") {
|
||||
let template: Template = "Hello \\{{ name }}"
|
||||
let result = try template.render([ "name": "Kyle" ])
|
||||
try expect(result) == "Hello {{ name }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Tests/StencilTests/TokenSpec.swift
Normal file
34
Tests/StencilTests/TokenSpec.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class TokenTests: XCTestCase {
|
||||
func testToken() {
|
||||
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"
|
||||
}
|
||||
|
||||
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'"
|
||||
}
|
||||
|
||||
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\""
|
||||
}
|
||||
}
|
||||
}
|
||||
137
Tests/StencilTests/TrimBehaviourSpec.swift
Normal file
137
Tests/StencilTests/TrimBehaviourSpec.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import XCTest
|
||||
|
||||
final class TrimBehaviourTests: XCTestCase {
|
||||
func testSmartTrimCanRemoveNewlines() throws {
|
||||
let templateString = """
|
||||
{% for item in items %}
|
||||
- {{item}}
|
||||
{% endfor %}
|
||||
text
|
||||
"""
|
||||
|
||||
let context = ["items": ["item 1", "item 2"]]
|
||||
let template = Template(templateString: templateString, environment: .init(trimBehaviour: .smart))
|
||||
let result = try template.render(context)
|
||||
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
- item 1
|
||||
- item 2
|
||||
text
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
|
||||
func testSmartTrimOnlyRemoveSingleNewlines() throws {
|
||||
let templateString = """
|
||||
{% for item in items %}
|
||||
|
||||
- {{item}}
|
||||
{% endfor %}
|
||||
text
|
||||
"""
|
||||
|
||||
let context = ["items": ["item 1", "item 2"]]
|
||||
let template = Template(templateString: templateString, environment: .init(trimBehaviour: .smart))
|
||||
let result = try template.render(context)
|
||||
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
|
||||
- item 1
|
||||
|
||||
- item 2
|
||||
text
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
|
||||
func testSmartTrimCanRemoveNewlinesWhileKeepingWhitespace() throws {
|
||||
// swiftlint:disable indentation_width
|
||||
let templateString = """
|
||||
Items:
|
||||
{% for item in items %}
|
||||
- {{item}}
|
||||
{% endfor %}
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
|
||||
let context = ["items": ["item 1", "item 2"]]
|
||||
let template = Template(templateString: templateString, environment: .init(trimBehaviour: .smart))
|
||||
let result = try template.render(context)
|
||||
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
Items:
|
||||
- item 1
|
||||
- item 2
|
||||
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
|
||||
func testTrimSymbols() {
|
||||
it("Respects whitespace control symbols in for tags") {
|
||||
// swiftlint:disable indentation_width
|
||||
let template: Template = """
|
||||
{% for num in numbers -%}
|
||||
{{num}}
|
||||
{%- endfor %}
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
let result = try template.render([ "numbers": Array(1...9) ])
|
||||
try expect(result) == "123456789"
|
||||
}
|
||||
it("Respects whitespace control symbols in if tags") {
|
||||
let template: Template = """
|
||||
{% if value -%}
|
||||
{{text}}
|
||||
{%- endif %}
|
||||
"""
|
||||
let result = try template.render([ "text": "hello", "value": true ])
|
||||
try expect(result) == "hello"
|
||||
}
|
||||
}
|
||||
|
||||
func testTrimSymbolsOverridingEnvironment() {
|
||||
let environment = Environment(trimBehaviour: .all)
|
||||
|
||||
it("respects whitespace control symbols in if tags") {
|
||||
// swiftlint:disable indentation_width
|
||||
let templateString = """
|
||||
{% if value +%}
|
||||
{{text}}
|
||||
{%+ endif %}
|
||||
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
let template = Template(templateString: templateString, environment: environment)
|
||||
let result = try template.render([ "text": "hello", "value": true ])
|
||||
try expect(result) == "\n hello\n"
|
||||
}
|
||||
|
||||
it("can customize blocks on same line as text") {
|
||||
// swiftlint:disable indentation_width
|
||||
let templateString = """
|
||||
Items:{% for item in items +%}
|
||||
- {{item}}
|
||||
{%- endfor %}
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
|
||||
let context = ["items": ["item 1", "item 2"]]
|
||||
let template = Template(templateString: templateString, environment: environment)
|
||||
let result = try template.render(context)
|
||||
|
||||
// swiftlint:disable indentation_width
|
||||
try expect(result) == """
|
||||
Items:
|
||||
- item 1
|
||||
- item 2
|
||||
"""
|
||||
// swiftlint:enable indentation_width
|
||||
}
|
||||
}
|
||||
}
|
||||
398
Tests/StencilTests/VariableSpec.swift
Normal file
398
Tests/StencilTests/VariableSpec.swift
Normal file
@@ -0,0 +1,398 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class VariableTests: XCTestCase {
|
||||
private let context: Context = {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("incr") { arg in
|
||||
(arg.flatMap { toNumber(value: $0) } ?? 0) + 1
|
||||
}
|
||||
let environment = Environment(extensions: [ext])
|
||||
|
||||
var context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"contacts": ["Katie", "Carlton"],
|
||||
"profiles": [
|
||||
"github": "kylef"
|
||||
],
|
||||
"counter": [
|
||||
"count": "kylef"
|
||||
],
|
||||
"article": Article(author: Person(name: "Kyle")),
|
||||
"blog": Blog(),
|
||||
"tuple": (one: 1, two: 2),
|
||||
"dynamic": [
|
||||
"enum": DynamicEnum.someValue,
|
||||
"struct": DynamicStruct()
|
||||
]
|
||||
], environment: environment)
|
||||
#if os(OSX)
|
||||
context["object"] = Object()
|
||||
#endif
|
||||
return context
|
||||
}()
|
||||
|
||||
func testLiterals() {
|
||||
it("can resolve a string literal with double quotes") {
|
||||
let variable = Variable("\"name\"")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
it("can resolve a string literal with one double quote") {
|
||||
let variable = Variable("\"")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result).to.beNil()
|
||||
}
|
||||
|
||||
it("can resolve a string literal with single quotes") {
|
||||
let variable = Variable("'name'")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
it("can resolve a string literal with one single quote") {
|
||||
let variable = Variable("'")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result).to.beNil()
|
||||
}
|
||||
|
||||
it("can resolve an integer literal") {
|
||||
let variable = Variable("5")
|
||||
let result = try variable.resolve(self.context) as? Int
|
||||
try expect(result) == 5
|
||||
}
|
||||
|
||||
it("can resolve an float literal") {
|
||||
let variable = Variable("3.14")
|
||||
let result = try variable.resolve(self.context) as? Number
|
||||
try expect(result) == 3.14
|
||||
}
|
||||
|
||||
it("can resolve boolean literal") {
|
||||
try expect(Variable("true").resolve(self.context) as? Bool) == true
|
||||
try expect(Variable("false").resolve(self.context) as? Bool) == false
|
||||
try expect(Variable("0").resolve(self.context) as? Int) == 0
|
||||
try expect(Variable("1").resolve(self.context) as? Int) == 1
|
||||
}
|
||||
}
|
||||
|
||||
func testVariable() {
|
||||
it("can resolve a string variable") {
|
||||
let variable = Variable("name")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
func testDictionary() {
|
||||
it("can resolve an item from a dictionary") {
|
||||
let variable = Variable("profiles.github")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "kylef"
|
||||
}
|
||||
|
||||
it("can get the count of a dictionary") {
|
||||
let variable = Variable("profiles.count")
|
||||
let result = try variable.resolve(self.context) as? Int
|
||||
try expect(result) == 1
|
||||
}
|
||||
}
|
||||
|
||||
func testArray() {
|
||||
it("can resolve an item from an array via it's index") {
|
||||
let variable = Variable("contacts.0")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Katie"
|
||||
|
||||
let variable1 = Variable("contacts.1")
|
||||
let result1 = try variable1.resolve(self.context) as? String
|
||||
try expect(result1) == "Carlton"
|
||||
}
|
||||
|
||||
it("can resolve an item from an array via unknown index") {
|
||||
let variable = Variable("contacts.5")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result).to.beNil()
|
||||
|
||||
let variable1 = Variable("contacts.-5")
|
||||
let result1 = try variable1.resolve(self.context) as? String
|
||||
try expect(result1).to.beNil()
|
||||
}
|
||||
|
||||
it("can resolve the first item from an array") {
|
||||
let variable = Variable("contacts.first")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
|
||||
it("can resolve the last item from an array") {
|
||||
let variable = Variable("contacts.last")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Carlton"
|
||||
}
|
||||
}
|
||||
|
||||
func testDynamicMemberLookup() {
|
||||
it("can resolve dynamic member lookup") {
|
||||
let variable = Variable("dynamic.struct.test")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "this is a dynamic response"
|
||||
}
|
||||
|
||||
it("can resolve dynamic enum rawValue") {
|
||||
let variable = Variable("dynamic.enum.rawValue")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "this is raw value"
|
||||
}
|
||||
}
|
||||
|
||||
func testReflection() {
|
||||
it("can resolve a property with reflection") {
|
||||
let variable = Variable("article.author.name")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
it("can resolve a value via reflection") {
|
||||
let variable = Variable("blog.articles.0.author.name")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
it("can resolve a superclass value via reflection") {
|
||||
let variable = Variable("blog.url")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "blog.com"
|
||||
}
|
||||
|
||||
it("can resolve optional variable property using reflection") {
|
||||
let variable = Variable("blog.featuring.author.name")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Jhon"
|
||||
}
|
||||
}
|
||||
|
||||
func testKVO() {
|
||||
#if os(OSX)
|
||||
it("can resolve a value via KVO") {
|
||||
let variable = Variable("object.title")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
it("can resolve a superclass value via KVO") {
|
||||
let variable = Variable("object.name")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Foo"
|
||||
}
|
||||
|
||||
it("does not crash on KVO") {
|
||||
let variable = Variable("object.fullname")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result).to.beNil()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func testTuple() {
|
||||
it("can resolve tuple by index") {
|
||||
let variable = Variable("tuple.0")
|
||||
let result = try variable.resolve(self.context) as? Int
|
||||
try expect(result) == 1
|
||||
}
|
||||
|
||||
it("can resolve tuple by label") {
|
||||
let variable = Variable("tuple.two")
|
||||
let result = try variable.resolve(self.context) as? Int
|
||||
try expect(result) == 2
|
||||
}
|
||||
}
|
||||
|
||||
func testOptional() {
|
||||
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)) == ""
|
||||
}
|
||||
}
|
||||
|
||||
func testSubscripting() {
|
||||
it("can resolve a property subscript via reflection") {
|
||||
try self.context.push(dictionary: ["property": "name"]) {
|
||||
let variable = Variable("article.author[property]")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
it("can subscript an array with a valid index") {
|
||||
try self.context.push(dictionary: ["property": 0]) {
|
||||
let variable = Variable("contacts[property]")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
}
|
||||
|
||||
it("can subscript an array with an unknown index") {
|
||||
try self.context.push(dictionary: ["property": 5]) {
|
||||
let variable = Variable("contacts[property]")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result).to.beNil()
|
||||
}
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
it("can resolve a subscript via KVO") {
|
||||
try self.context.push(dictionary: ["property": "name"]) {
|
||||
let variable = Variable("object[property]")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Foo"
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
it("can resolve an optional subscript via reflection") {
|
||||
try self.context.push(dictionary: ["property": "featuring"]) {
|
||||
let variable = Variable("blog[property].author.name")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Jhon"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testMultipleSubscripting() {
|
||||
it("can resolve multiple subscripts") {
|
||||
try self.context.push(dictionary: [
|
||||
"prop1": "articles",
|
||||
"prop2": 0,
|
||||
"prop3": "name"
|
||||
]) {
|
||||
let variable = Variable("blog[prop1][prop2].author[prop3]")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
it("can resolve nested subscripts") {
|
||||
try self.context.push(dictionary: [
|
||||
"prop1": "prop2",
|
||||
"ref": ["prop2": "name"]
|
||||
]) {
|
||||
let variable = Variable("article.author[ref[prop1]]")
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
it("throws for invalid keypath syntax") {
|
||||
try self.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(self.context)).toThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRangeVariable() {
|
||||
func makeVariable(_ token: String) throws -> RangeVariable? {
|
||||
let token = Token.variable(value: token, at: .unknown)
|
||||
return try RangeVariable(token.contents, environment: context.environment, containedIn: token)
|
||||
}
|
||||
|
||||
it("can resolve closed range as array") {
|
||||
let result = try makeVariable("1...3")?.resolve(self.context) as? [Int]
|
||||
try expect(result) == [1, 2, 3]
|
||||
}
|
||||
|
||||
it("can resolve decreasing closed range as reversed array") {
|
||||
let result = try makeVariable("3...1")?.resolve(self.context) as? [Int]
|
||||
try expect(result) == [3, 2, 1]
|
||||
}
|
||||
|
||||
it("can use filter on range variables") {
|
||||
let result = try makeVariable("1|incr...3|incr")?.resolve(self.context) as? [Int]
|
||||
try expect(result) == [2, 3, 4]
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
it("throws is left range value is missing") {
|
||||
try expect(makeVariable("...1")).toThrow()
|
||||
}
|
||||
|
||||
it("throws is right range value is missing") {
|
||||
try expect(makeVariable("1...")).toThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
#if os(OSX)
|
||||
@objc
|
||||
class Superclass: NSObject {
|
||||
@objc let name = "Foo"
|
||||
}
|
||||
@objc
|
||||
class Object: Superclass {
|
||||
@objc let title = "Hello World"
|
||||
}
|
||||
#endif
|
||||
|
||||
private struct Person {
|
||||
let name: String
|
||||
}
|
||||
|
||||
private struct Article {
|
||||
let author: Person
|
||||
}
|
||||
|
||||
private class WebSite {
|
||||
let url: String = "blog.com"
|
||||
}
|
||||
|
||||
private class Blog: WebSite {
|
||||
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
|
||||
let featuring: Article? = Article(author: Person(name: "Jhon"))
|
||||
}
|
||||
|
||||
@dynamicMemberLookup
|
||||
private struct DynamicStruct: DynamicMemberLookup {
|
||||
subscript(dynamicMember member: String) -> Any? {
|
||||
member == "test" ? "this is a dynamic response" : nil
|
||||
}
|
||||
}
|
||||
|
||||
private enum DynamicEnum: String, DynamicMemberLookup {
|
||||
case someValue = "this is raw value"
|
||||
}
|
||||
5
Tests/StencilTests/fixtures/base-repeat.html
Normal file
5
Tests/StencilTests/fixtures/base-repeat.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% block header %}Header{% endblock %}
|
||||
{% block body %}Body{% endblock %}
|
||||
Repeat
|
||||
{{ block.header }}
|
||||
{{ block.body }}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user