Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e795f052ea | ||
|
|
2c411ca494 | ||
|
|
f3d5843e78 | ||
|
|
564ccb7af7 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.conche/
|
||||
.build/
|
||||
.swiftpm/
|
||||
Packages/
|
||||
Package.resolved
|
||||
Package.pins
|
||||
*.xcodeproj
|
||||
|
||||
99
.swiftlint.yml
Normal file
99
.swiftlint.yml
Normal file
@@ -0,0 +1,99 @@
|
||||
swiftlint_version: 0.39.2
|
||||
|
||||
disabled_rules:
|
||||
# Remove this once we remove old swift support
|
||||
- implicit_return
|
||||
|
||||
opt_in_rules:
|
||||
- anyobject_protocol
|
||||
- array_init
|
||||
- attributes
|
||||
- closure_body_length
|
||||
- closure_end_indentation
|
||||
- closure_spacing
|
||||
- collection_alignment
|
||||
- contains_over_filter_count
|
||||
- contains_over_filter_is_empty
|
||||
- contains_over_first_not_nil
|
||||
- contains_over_range_nil_comparison
|
||||
- convenience_type
|
||||
- discouraged_optional_boolean
|
||||
- discouraged_optional_collection
|
||||
- duplicate_enum_cases
|
||||
- duplicate_imports
|
||||
- empty_collection_literal
|
||||
- empty_count
|
||||
- empty_string
|
||||
- fallthrough
|
||||
- fatal_error_message
|
||||
- first_where
|
||||
- flatmap_over_map_reduce
|
||||
- force_unwrapping
|
||||
- identical_operands
|
||||
- inert_defer
|
||||
- joined_default_parameter
|
||||
- last_where
|
||||
- legacy_hashing
|
||||
- legacy_random
|
||||
- literal_expression_end_indentation
|
||||
- lower_acl_than_parent
|
||||
- modifier_order
|
||||
- multiline_arguments
|
||||
- multiline_function_chains
|
||||
- multiline_literal_brackets
|
||||
- multiline_parameters
|
||||
- multiline_parameters_brackets
|
||||
- nslocalizedstring_key
|
||||
- nsobject_prefer_isequal
|
||||
- number_separator
|
||||
- object_literal
|
||||
- operator_usage_whitespace
|
||||
- overridden_super_call
|
||||
- override_in_extension
|
||||
- prefer_self_type_over_type_of_self
|
||||
- private_action
|
||||
- private_outlet
|
||||
- prohibited_super_call
|
||||
- raw_value_for_camel_cased_codable_enum
|
||||
- reduce_boolean
|
||||
- reduce_into
|
||||
- redundant_nil_coalescing
|
||||
- redundant_objc_attribute
|
||||
- sorted_first_last
|
||||
- sorted_imports
|
||||
- static_operator
|
||||
- strong_iboutlet
|
||||
- toggle_bool
|
||||
- trailing_closure
|
||||
- unavailable_function
|
||||
- unneeded_parentheses_in_closure_argument
|
||||
- unowned_variable_capture
|
||||
- unused_capture_list
|
||||
- unused_control_flow_label
|
||||
- unused_declaration
|
||||
- unused_setter_value
|
||||
- vertical_parameter_alignment_on_call
|
||||
- vertical_whitespace_closing_braces
|
||||
- vertical_whitespace_opening_braces
|
||||
- xct_specific_matcher
|
||||
- yoda_condition
|
||||
# Enable this again once we remove old swift support
|
||||
# - optional_enum_case_matching
|
||||
# - legacy_multiple
|
||||
|
||||
# Rules customization
|
||||
closure_body_length:
|
||||
warning: 25
|
||||
|
||||
line_length:
|
||||
warning: 120
|
||||
error: 200
|
||||
|
||||
nesting:
|
||||
type_level:
|
||||
warning: 2
|
||||
|
||||
# Exclude generated files
|
||||
excluded:
|
||||
- .build
|
||||
- Tests/StencilTests/XCTestManifests.swift
|
||||
22
.travis.yml
22
.travis.yml
@@ -1,22 +1,22 @@
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode8.3
|
||||
env: SWIFT_VERSION=3.1.1
|
||||
osx_image: xcode11.4
|
||||
env: SWIFT_VERSION=4.2
|
||||
- os: osx
|
||||
osx_image: xcode9
|
||||
env: SWIFT_VERSION=4.0
|
||||
- os: osx
|
||||
osx_image: xcode9.1
|
||||
env: SWIFT_VERSION=4.0
|
||||
osx_image: xcode11.4
|
||||
env: SWIFT_VERSION=5.0
|
||||
- os: linux
|
||||
env: SWIFT_VERSION=3.1.1
|
||||
env: SWIFT_VERSION=4.2
|
||||
- os: linux
|
||||
env: SWIFT_VERSION=4.0
|
||||
env: SWIFT_VERSION=5.0
|
||||
language: generic
|
||||
sudo: required
|
||||
dist: trusty
|
||||
install:
|
||||
- eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
|
||||
- if [ "$TRAVIS_OS_NAME" == "linux" ]; then eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"; fi
|
||||
- if [ "$TRAVIS_OS_NAME" == "osx" ]; then wget --output-document /tmp/SwiftLint.pkg https://github.com/realm/SwiftLint/releases/download/0.39.2/SwiftLint.pkg &&
|
||||
sudo installer -pkg /tmp/SwiftLint.pkg -target /; fi
|
||||
script:
|
||||
- swift test
|
||||
- swift test
|
||||
- if [ "$TRAVIS_OS_NAME" == "osx" ]; then swiftlint; fi
|
||||
|
||||
105
CHANGELOG.md
105
CHANGELOG.md
@@ -1,5 +1,110 @@
|
||||
# Stencil Changelog
|
||||
|
||||
## 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
|
||||
|
||||
7
Gemfile
Normal file
7
Gemfile
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "octokit"
|
||||
gem "cocoapods"
|
||||
gem "rake"
|
||||
105
Gemfile.lock
Normal file
105
Gemfile.lock
Normal file
@@ -0,0 +1,105 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.2)
|
||||
activesupport (4.2.11.3)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
algoliasearch (1.27.3)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
atomos (0.1.3)
|
||||
claide (1.0.3)
|
||||
cocoapods (1.9.3)
|
||||
activesupport (>= 4.0.2, < 5)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.9.3)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.2.2, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-stats (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.4.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (>= 2.3.0, < 3.0)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.6.6)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (~> 1.4)
|
||||
xcodeproj (>= 1.14.0, < 2.0)
|
||||
cocoapods-core (1.9.3)
|
||||
activesupport (>= 4.0.2, < 6)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
netrc (~> 0.11)
|
||||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.4)
|
||||
cocoapods-downloader (1.4.0)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.0)
|
||||
cocoapods-stats (1.1.0)
|
||||
cocoapods-trunk (1.5.0)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.1.6)
|
||||
escape (0.0.4)
|
||||
ethon (0.12.0)
|
||||
ffi (>= 1.3.0)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ffi (1.13.1)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
httpclient (2.8.3)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.3.1)
|
||||
minitest (5.14.1)
|
||||
molinillo (0.6.6)
|
||||
multipart-post (2.1.1)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
netrc (0.11.0)
|
||||
octokit (4.18.0)
|
||||
faraday (>= 0.9)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
public_suffix (4.0.5)
|
||||
rake (13.0.1)
|
||||
ruby-macho (1.4.0)
|
||||
sawyer (0.8.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (> 0.8, < 2.0)
|
||||
thread_safe (0.3.6)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (1.2.7)
|
||||
thread_safe (~> 0.1)
|
||||
xcodeproj (1.17.1)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods
|
||||
octokit
|
||||
rake
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
3
LICENSE
3
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2014, Kyle Fuller
|
||||
Copyright (c) 2018, Kyle Fuller
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
@@ -21,4 +21,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": "PathKit",
|
||||
"repositoryURL": "https://github.com/kylef/PathKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Spectre",
|
||||
"repositoryURL": "https://github.com/kylef/Spectre.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f14ff47f45642aa5703900980b014c2e9394b6e5",
|
||||
"version": "0.9.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
// swift-tools-version:3.1
|
||||
// swift-tools-version:4.2
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Stencil",
|
||||
products: [
|
||||
.library(name: "Stencil", targets: ["Stencil"])
|
||||
],
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 9),
|
||||
.Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 8),
|
||||
]
|
||||
.package(url: "https://github.com/kylef/PathKit.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Stencil", dependencies: [
|
||||
"PathKit"
|
||||
], path: "Sources"),
|
||||
.testTarget(name: "StencilTests", dependencies: [
|
||||
"Stencil",
|
||||
"Spectre"
|
||||
])
|
||||
],
|
||||
swiftLanguageVersions: [.v4_2]
|
||||
)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// swift-tools-version:3.1
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Stencil",
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/PathKit.git", majorVersion: 0, minor: 8),
|
||||
.Package(url: "https://github.com/kylef/Spectre.git", majorVersion: 0, minor: 7),
|
||||
]
|
||||
)
|
||||
23
Package@swift-5.swift
Normal file
23
Package@swift-5.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/kylef/PathKit.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0")
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Stencil", dependencies: [
|
||||
"PathKit"
|
||||
], path: "Sources"),
|
||||
.testTarget(name: "StencilTests", dependencies: [
|
||||
"Stencil",
|
||||
"Spectre"
|
||||
])
|
||||
],
|
||||
swiftLanguageVersions: [.v4_2, .v5]
|
||||
)
|
||||
@@ -68,7 +68,8 @@ Resources to help you integrate Stencil into a Swift project:
|
||||
[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)
|
||||
[Weaver](https://github.com/scribd/Weaver),
|
||||
[Genesis](https://github.com/yonaskolb/Genesis)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
10
Rakefile
Executable file
10
Rakefile
Executable file
@@ -0,0 +1,10 @@
|
||||
|
||||
PODSPEC_FILE = 'Stencil.podspec.json'
|
||||
CHANGELOG_FILE = 'CHANGELOG.md'
|
||||
|
||||
if ENV['BUNDLE_GEMFILE'].nil?
|
||||
puts "\u{274C} Please use bundle exec"
|
||||
exit 1
|
||||
end
|
||||
|
||||
task :default => 'release:new'
|
||||
@@ -4,8 +4,8 @@ public class Context {
|
||||
|
||||
public let environment: Environment
|
||||
|
||||
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
||||
if let dictionary = dictionary {
|
||||
public init(dictionary: [String: Any] = [:], environment: Environment? = nil) {
|
||||
if !dictionary.isEmpty {
|
||||
dictionaries = [dictionary]
|
||||
} else {
|
||||
dictionaries = []
|
||||
@@ -28,17 +28,16 @@ public class Context {
|
||||
|
||||
/// Set a variable in the current context, deleting the variable if it's nil
|
||||
set(value) {
|
||||
if let dictionary = dictionaries.popLast() {
|
||||
var mutable_dictionary = dictionary
|
||||
mutable_dictionary[key] = value
|
||||
dictionaries.append(mutable_dictionary)
|
||||
if var dictionary = dictionaries.popLast() {
|
||||
dictionary[key] = value
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new level into the Context
|
||||
fileprivate func push(_ dictionary: [String: Any]? = nil) {
|
||||
dictionaries.append(dictionary ?? [:])
|
||||
fileprivate func push(_ dictionary: [String: Any] = [:]) {
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
|
||||
/// Pop the last level off of the Context
|
||||
@@ -47,7 +46,7 @@ public class Context {
|
||||
}
|
||||
|
||||
/// Push a new level onto the context for the duration of the execution of the given closure
|
||||
public func push<Result>(dictionary: [String: Any]? = nil, closure: (() throws -> Result)) rethrows -> Result {
|
||||
public func push<Result>(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result {
|
||||
push(dictionary)
|
||||
defer { _ = pop() }
|
||||
return try closure()
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
public struct Environment {
|
||||
public let templateClass: Template.Type
|
||||
public var extensions: [Extension]
|
||||
|
||||
public var loader: Loader?
|
||||
|
||||
public init(loader: Loader? = nil,
|
||||
extensions: [Extension]? = nil,
|
||||
templateClass: Template.Type = Template.self) {
|
||||
|
||||
public init(
|
||||
loader: Loader? = nil,
|
||||
extensions: [Extension] = [],
|
||||
templateClass: Template.Type = Template.self
|
||||
) {
|
||||
self.templateClass = templateClass
|
||||
self.loader = loader
|
||||
self.extensions = (extensions ?? []) + [DefaultExtension()]
|
||||
self.extensions = extensions + [DefaultExtension()]
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String) throws -> Template {
|
||||
@@ -29,20 +29,19 @@ public struct Environment {
|
||||
}
|
||||
}
|
||||
|
||||
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
||||
public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String {
|
||||
let template = try loadTemplate(name: name)
|
||||
return try render(template: template, context: context)
|
||||
}
|
||||
|
||||
public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||
public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible {
|
||||
public let reason: String
|
||||
public var description: String { return reason }
|
||||
public internal(set) var token: Token?
|
||||
public internal(set) var stackTrace: [Token]
|
||||
public var templateName: String? { return token?.sourceMap.filename }
|
||||
var allTokens: [Token] {
|
||||
return stackTrace + (token.map({ [$0] }) ?? [])
|
||||
return stackTrace + (token.map { [$0] } ?? [])
|
||||
}
|
||||
|
||||
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
||||
@@ -37,11 +37,6 @@ public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||
public init(_ description: String) {
|
||||
self.init(reason: description)
|
||||
}
|
||||
|
||||
public static func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
return lhs.description == rhs.description && lhs.token == rhs.token && lhs.stackTrace == rhs.stackTrace
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Error {
|
||||
@@ -55,29 +50,32 @@ extension Error {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ErrorReporter: class {
|
||||
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 line = token.sourceMap.line
|
||||
let highlight = "\(String(Array(repeating: " ", count: line.offset)))^\(String(Array(repeating: "~", count: max(token.contents.characters.count - 1, 0))))"
|
||||
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)\(line.number):\(line.offset): error: \(templateError.reason)\n"
|
||||
+ "\(line.content)\n"
|
||||
+ "\(highlight)\n"
|
||||
return """
|
||||
\(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason)
|
||||
\(location.content)
|
||||
\(highlight)
|
||||
"""
|
||||
}
|
||||
|
||||
var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] }
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
protocol Expression: CustomStringConvertible {
|
||||
public protocol Expression: CustomStringConvertible {
|
||||
func evaluate(context: Context) throws -> Bool
|
||||
}
|
||||
|
||||
|
||||
protocol InfixOperator: Expression {
|
||||
init(lhs: Expression, rhs: Expression)
|
||||
}
|
||||
|
||||
|
||||
protocol PrefixOperator: Expression {
|
||||
init(expression: Expression)
|
||||
}
|
||||
|
||||
|
||||
final class StaticExpression: Expression, CustomStringConvertible {
|
||||
let value: Bool
|
||||
|
||||
@@ -29,7 +26,6 @@ final class StaticExpression: Expression, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class VariableExpression: Expression, CustomStringConvertible {
|
||||
let variable: Resolvable
|
||||
|
||||
@@ -48,7 +44,7 @@ final class VariableExpression: Expression, CustomStringConvertible {
|
||||
|
||||
if let result = result as? [Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if let result = result as? [String:Any] {
|
||||
} else if let result = result as? [String: Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if let result = result as? Bool {
|
||||
truthy = result
|
||||
@@ -68,7 +64,6 @@ final class VariableExpression: Expression, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||
let expression: Expression
|
||||
|
||||
@@ -118,7 +113,6 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
@@ -144,7 +138,6 @@ final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
@@ -168,7 +161,6 @@ final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
@@ -204,7 +196,6 @@ class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
@@ -215,7 +206,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) \(op) \(rhs))"
|
||||
return "(\(lhs) \(symbol) \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
@@ -233,7 +224,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
return false
|
||||
}
|
||||
|
||||
var op: String {
|
||||
var symbol: String {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -242,9 +233,8 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MoreThanExpression: NumericExpression {
|
||||
override var op: String {
|
||||
override var symbol: String {
|
||||
return ">"
|
||||
}
|
||||
|
||||
@@ -253,9 +243,8 @@ class MoreThanExpression: NumericExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MoreThanEqualExpression: NumericExpression {
|
||||
override var op: String {
|
||||
override var symbol: String {
|
||||
return ">="
|
||||
}
|
||||
|
||||
@@ -264,9 +253,8 @@ class MoreThanEqualExpression: NumericExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LessThanExpression: NumericExpression {
|
||||
override var op: String {
|
||||
override var symbol: String {
|
||||
return "<"
|
||||
}
|
||||
|
||||
@@ -275,9 +263,8 @@ class LessThanExpression: NumericExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LessThanEqualExpression: NumericExpression {
|
||||
override var op: String {
|
||||
override var symbol: String {
|
||||
return "<="
|
||||
}
|
||||
|
||||
@@ -286,7 +273,6 @@ class LessThanEqualExpression: NumericExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class InequalityExpression: EqualityExpression {
|
||||
override var description: String {
|
||||
return "(\(lhs) != \(rhs))"
|
||||
@@ -297,7 +283,7 @@ class InequalityExpression: EqualityExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func toNumber(value: Any) -> Number? {
|
||||
if let value = value as? Float {
|
||||
return Number(value)
|
||||
|
||||
@@ -14,9 +14,19 @@ open class Extension {
|
||||
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
|
||||
registerTag(name, parser: { parser, token in
|
||||
return SimpleNode(token: token, handler: handler)
|
||||
})
|
||||
registerTag(name) { _, token in
|
||||
SimpleNode(token: token, handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers boolean filter with it's negative counterpart
|
||||
// swiftlint:disable:next discouraged_optional_boolean
|
||||
public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) {
|
||||
filters[name] = .simple(filter)
|
||||
filters[negativeFilterName] = .simple {
|
||||
guard let result = try filter($0) else { return nil }
|
||||
return !result
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a template filter with the given name
|
||||
@@ -26,11 +36,15 @@ open class Extension {
|
||||
|
||||
/// 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()
|
||||
@@ -59,28 +73,27 @@ class DefaultExtension: Extension {
|
||||
registerFilter("join", filter: joinFilter)
|
||||
registerFilter("split", filter: splitFilter)
|
||||
registerFilter("indent", filter: indentFilter)
|
||||
registerFilter("filter", filter: filterFilter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protocol FilterType {
|
||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any?
|
||||
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
enum Filter: FilterType {
|
||||
case simple(((Any?) throws -> Any?))
|
||||
case arguments(((Any?, [Any?]) throws -> Any?))
|
||||
case arguments(((Any?, [Any?], Context) throws -> Any?))
|
||||
|
||||
func invoke(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? {
|
||||
switch self {
|
||||
case let .simple(filter):
|
||||
if !arguments.isEmpty {
|
||||
throw TemplateSyntaxError("cannot invoke filter with an argument")
|
||||
throw TemplateSyntaxError("Can't invoke filter with an argument")
|
||||
}
|
||||
|
||||
return try filter(value)
|
||||
case let .arguments(filter):
|
||||
return try filter(value, arguments)
|
||||
return try filter(value, arguments, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
class FilterNode : NodeType {
|
||||
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()
|
||||
let bits = token.components
|
||||
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'filter' tag takes one argument, the filter expression")
|
||||
@@ -30,8 +30,7 @@ class FilterNode : NodeType {
|
||||
let value = try renderNodes(nodes, context)
|
||||
|
||||
return try context.push(dictionary: ["filter_value": value]) {
|
||||
return try VariableNode(variable: resolvable, token: token).render(context)
|
||||
try VariableNode(variable: resolvable, token: token).render(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ func defaultFilter(value: Any?, arguments: [Any?]) -> Any? {
|
||||
|
||||
func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
guard arguments.count < 2 else {
|
||||
throw TemplateSyntaxError("'join' filter takes a single argument")
|
||||
throw TemplateSyntaxError("'join' filter takes at most one argument")
|
||||
}
|
||||
|
||||
let separator = stringify(arguments.first ?? "")
|
||||
@@ -55,7 +55,7 @@ func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
|
||||
func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
guard arguments.count < 2 else {
|
||||
throw TemplateSyntaxError("'split' filter takes a single argument")
|
||||
throw TemplateSyntaxError("'split' filter takes at most one argument")
|
||||
}
|
||||
|
||||
let separator = stringify(arguments.first ?? " ")
|
||||
@@ -72,9 +72,11 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
}
|
||||
|
||||
var indentWidth = 4
|
||||
if arguments.count > 0 {
|
||||
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])))")
|
||||
throw TemplateSyntaxError("""
|
||||
'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))
|
||||
""")
|
||||
}
|
||||
indentWidth = value
|
||||
}
|
||||
@@ -82,7 +84,9 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
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]))")
|
||||
throw TemplateSyntaxError("""
|
||||
'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))
|
||||
""")
|
||||
}
|
||||
indentationChar = value
|
||||
}
|
||||
@@ -95,19 +99,31 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
indentFirst = value
|
||||
}
|
||||
|
||||
let indentation = [String](repeating: indentationChar, count: indentWidth).joined(separator: "")
|
||||
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([firstLine]) { (result, line) in
|
||||
return result + [(line.isEmpty ? "" : "\(indentation)\(line)")]
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
class ForNode : NodeType {
|
||||
class ForNode: NodeType {
|
||||
let resolvable: Resolvable
|
||||
let loopVariables: [String]
|
||||
let nodes:[NodeType]
|
||||
let nodes: [NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
let `where`: Expression?
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let components = token.components
|
||||
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
return components.count > (index + 1) && components[index] == token
|
||||
@@ -23,7 +23,7 @@ class ForNode : NodeType {
|
||||
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.")
|
||||
}
|
||||
|
||||
let loopVariables = components[1].characters
|
||||
let loopVariables = components[1]
|
||||
.split(separator: ",")
|
||||
.map(String.init)
|
||||
.map { $0.trim(character: " ") }
|
||||
@@ -31,7 +31,7 @@ class ForNode : NodeType {
|
||||
let resolvable = try parser.compileResolvable(components[3], containedIn: token)
|
||||
|
||||
let `where` = hasToken("where", at: 4)
|
||||
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token)
|
||||
? try parser.compileExpression(components: Array(components.suffix(from: 5)), token: token)
|
||||
: nil
|
||||
|
||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||
@@ -46,10 +46,24 @@ class ForNode : NodeType {
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes: emptyNodes, where: `where`, token: token)
|
||||
return ForNode(
|
||||
resolvable: resolvable,
|
||||
loopVariables: loopVariables,
|
||||
nodes: forNodes,
|
||||
emptyNodes: emptyNodes,
|
||||
where: `where`,
|
||||
token: token
|
||||
)
|
||||
}
|
||||
|
||||
init(resolvable: Resolvable, loopVariables: [String], nodes: [NodeType], emptyNodes: [NodeType], where: Expression? = nil, token: Token? = nil) {
|
||||
init(
|
||||
resolvable: Resolvable,
|
||||
loopVariables: [String],
|
||||
nodes: [NodeType],
|
||||
emptyNodes: [NodeType],
|
||||
where: Expression? = nil,
|
||||
token: Token? = nil
|
||||
) {
|
||||
self.resolvable = resolvable
|
||||
self.loopVariables = loopVariables
|
||||
self.nodes = nodes
|
||||
@@ -58,10 +72,48 @@ class ForNode : NodeType {
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||
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
|
||||
|
||||
return try zip(0..., values)
|
||||
.map { index, item in
|
||||
let forContext: [String: Any] = [
|
||||
"first": index == 0,
|
||||
"last": index == (count - 1),
|
||||
"counter": index + 1,
|
||||
"counter0": index,
|
||||
"length": count
|
||||
]
|
||||
|
||||
return try context.push(dictionary: ["forloop": forContext]) {
|
||||
try push(value: item, context: context) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
.joined()
|
||||
}
|
||||
|
||||
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() {
|
||||
return try closure()
|
||||
return try context.push {
|
||||
try closure()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,29 +123,28 @@ class ForNode : NodeType {
|
||||
throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables")
|
||||
}
|
||||
var variablesContext = [String: Any]()
|
||||
valueMirror.children.prefix(loopVariables.count).enumerated().forEach({ (offset, element) in
|
||||
valueMirror.children.prefix(loopVariables.count).enumerated().forEach { offset, element in
|
||||
if loopVariables[offset] != "_" {
|
||||
variablesContext[loopVariables[offset]] = element.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return try context.push(dictionary: variablesContext) {
|
||||
return try closure()
|
||||
try closure()
|
||||
}
|
||||
}
|
||||
|
||||
return try context.push(dictionary: [loopVariables.first!: value]) {
|
||||
return try closure()
|
||||
return try context.push(dictionary: [loopVariables.first ?? "": value]) {
|
||||
try closure()
|
||||
}
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
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.map { ($0.key, $0.value) }
|
||||
values = dictionary.sorted { $0.key < $1.key }
|
||||
} else if let array = resolved as? [Any] {
|
||||
values = array
|
||||
} else if let range = resolved as? CountableClosedRange<Int> {
|
||||
@@ -120,36 +171,6 @@ class ForNode : NodeType {
|
||||
values = []
|
||||
}
|
||||
|
||||
if let `where` = self.where {
|
||||
values = try values.filter({ item -> Bool in
|
||||
return try push(value: item, context: context) {
|
||||
try `where`.evaluate(context: context)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if !values.isEmpty {
|
||||
let count = values.count
|
||||
|
||||
return try values.enumerated().map { index, item in
|
||||
let forContext: [String: Any] = [
|
||||
"first": index == 0,
|
||||
"last": index == (count - 1),
|
||||
"counter": index + 1,
|
||||
"counter0": index,
|
||||
"length": count
|
||||
]
|
||||
|
||||
return try context.push(dictionary: ["forloop": forContext]) {
|
||||
return try push(value: item, context: context) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}.joined(separator: "")
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
try renderNodes(emptyNodes, context)
|
||||
}
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ enum Operator {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let operators: [Operator] = [
|
||||
.infix("in", 5, InExpression.self),
|
||||
.infix("or", 6, OrExpression.self),
|
||||
@@ -23,25 +22,22 @@ let operators: [Operator] = [
|
||||
.infix(">", 10, MoreThanExpression.self),
|
||||
.infix(">=", 10, MoreThanEqualExpression.self),
|
||||
.infix("<", 10, LessThanExpression.self),
|
||||
.infix("<=", 10, LessThanEqualExpression.self),
|
||||
.infix("<=", 10, LessThanEqualExpression.self)
|
||||
]
|
||||
|
||||
|
||||
func findOperator(name: String) -> Operator? {
|
||||
for op in operators {
|
||||
if op.name == name {
|
||||
return op
|
||||
}
|
||||
for `operator` in operators where `operator`.name == name {
|
||||
return `operator`
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
enum IfToken {
|
||||
case infix(name: String, bindingPower: Int, op: InfixOperator.Type)
|
||||
case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type)
|
||||
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 {
|
||||
@@ -50,7 +46,9 @@ enum IfToken {
|
||||
return bindingPower
|
||||
case .prefix(_, let bindingPower, _):
|
||||
return bindingPower
|
||||
case .variable(_):
|
||||
case .variable:
|
||||
return 0
|
||||
case .subExpression:
|
||||
return 0
|
||||
case .end:
|
||||
return 0
|
||||
@@ -61,11 +59,13 @@ enum IfToken {
|
||||
switch self {
|
||||
case .infix(let name, _, _):
|
||||
throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side")
|
||||
case .prefix(_, let bindingPower, let op):
|
||||
case .prefix(_, let bindingPower, let operatorType):
|
||||
let expression = try parser.expression(bindingPower: bindingPower)
|
||||
return op.init(expression: expression)
|
||||
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")
|
||||
}
|
||||
@@ -73,13 +73,15 @@ enum IfToken {
|
||||
|
||||
func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression {
|
||||
switch self {
|
||||
case .infix(_, let bindingPower, let op):
|
||||
case .infix(_, let bindingPower, let operatorType):
|
||||
let right = try parser.expression(bindingPower: bindingPower)
|
||||
return op.init(lhs: left, rhs: right)
|
||||
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")
|
||||
}
|
||||
@@ -95,26 +97,78 @@ enum IfToken {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class IfExpressionParser {
|
||||
let tokens: [IfToken]
|
||||
var position: Int = 0
|
||||
|
||||
init(components: [String], tokenParser: TokenParser, token: Token) throws {
|
||||
self.tokens = try components.map { component in
|
||||
if let op = findOperator(name: component) {
|
||||
switch op {
|
||||
case .infix(let name, let bindingPower, let cls):
|
||||
return .infix(name: name, bindingPower: bindingPower, op: cls)
|
||||
case .prefix(let name, let bindingPower, let cls):
|
||||
return .prefix(name: name, bindingPower: bindingPower, op: cls)
|
||||
}
|
||||
}
|
||||
private init(tokens: [IfToken]) {
|
||||
self.tokens = tokens
|
||||
}
|
||||
|
||||
return .variable(try tokenParser.compileResolvable(component, containedIn: token))
|
||||
static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser {
|
||||
return 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
|
||||
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 IfExpressionParser.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 {
|
||||
if $0 == "(" {
|
||||
bracketsBalance += 1
|
||||
} else if $0 == ")" {
|
||||
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]
|
||||
@@ -154,13 +208,6 @@ final class IfExpressionParser {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func parseExpression(components: [String], tokenParser: TokenParser, token: Token) throws -> Expression {
|
||||
let parser = try IfExpressionParser(components: components, tokenParser: tokenParser, token: token)
|
||||
return try parser.parse()
|
||||
}
|
||||
|
||||
|
||||
/// Represents an if condition and the associated nodes when the condition
|
||||
/// evaluates
|
||||
final class IfCondition {
|
||||
@@ -174,21 +221,20 @@ final class IfCondition {
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
return try context.push {
|
||||
return try renderNodes(nodes, context)
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class IfNode : NodeType {
|
||||
class IfNode: NodeType {
|
||||
let conditions: [IfCondition]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components()
|
||||
var components = token.components
|
||||
components.removeFirst()
|
||||
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||
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)
|
||||
@@ -196,9 +242,9 @@ class IfNode : NodeType {
|
||||
|
||||
var nextToken = parser.nextToken()
|
||||
while let current = nextToken, current.contents.hasPrefix("elif") {
|
||||
var components = current.components()
|
||||
var components = current.components
|
||||
components.removeFirst()
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: current)
|
||||
let expression = try parser.compileExpression(components: components, token: current)
|
||||
|
||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||
nextToken = parser.nextToken()
|
||||
@@ -218,7 +264,7 @@ class IfNode : NodeType {
|
||||
}
|
||||
|
||||
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components()
|
||||
var components = token.components
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.")
|
||||
}
|
||||
@@ -226,7 +272,7 @@ class IfNode : NodeType {
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||
let expression = try parser.compileExpression(components: components, token: token)
|
||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
@@ -240,8 +286,8 @@ class IfNode : NodeType {
|
||||
|
||||
return IfNode(conditions: [
|
||||
IfCondition(expression: expression, nodes: trueNodes),
|
||||
IfCondition(expression: nil, nodes: falseNodes),
|
||||
], token: token)
|
||||
IfCondition(expression: nil, nodes: falseNodes)
|
||||
], token: token)
|
||||
}
|
||||
|
||||
init(conditions: [IfCondition], token: Token? = nil) {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import PathKit
|
||||
|
||||
|
||||
class IncludeNode : NodeType {
|
||||
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()
|
||||
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")
|
||||
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)
|
||||
@@ -30,9 +33,9 @@ class IncludeNode : NodeType {
|
||||
let template = try context.environment.loadTemplate(name: templateName)
|
||||
|
||||
do {
|
||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] } ?? [:]
|
||||
return try context.push(dictionary: subContext) {
|
||||
return try template.render(context)
|
||||
try template.render(context)
|
||||
}
|
||||
} catch {
|
||||
if let error = error as? TemplateSyntaxError {
|
||||
@@ -43,4 +46,3 @@ class IncludeNode : NodeType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ class BlockContext {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Collection {
|
||||
func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? {
|
||||
for element in self {
|
||||
@@ -46,14 +45,13 @@ extension Collection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExtendsNode : NodeType {
|
||||
class ExtendsNode: NodeType {
|
||||
let templateName: Variable
|
||||
let blocks: [String:BlockNode]
|
||||
let blocks: [String: BlockNode]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
let bits = token.components
|
||||
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended")
|
||||
@@ -64,12 +62,9 @@ class ExtendsNode : NodeType {
|
||||
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
|
||||
}
|
||||
|
||||
let blockNodes = parsedNodes.flatMap { $0 as? BlockNode }
|
||||
|
||||
let nodes = blockNodes.reduce([String: BlockNode]()) { (accumulator, node) -> [String: BlockNode] in
|
||||
var dict = accumulator
|
||||
dict[node.name] = node
|
||||
return dict
|
||||
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)
|
||||
@@ -102,7 +97,7 @@ class ExtendsNode : NodeType {
|
||||
// pushes base template and renders it's content
|
||||
// block_context contains all blocks from child templates
|
||||
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||
return try baseTemplate.render(context)
|
||||
try baseTemplate.render(context)
|
||||
}
|
||||
} catch {
|
||||
// if error template is already set (see catch in BlockNode)
|
||||
@@ -117,14 +112,13 @@ class ExtendsNode : NodeType {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BlockNode : NodeType {
|
||||
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()
|
||||
let bits = token.components
|
||||
|
||||
guard bits.count == 2 else {
|
||||
throw TemplateSyntaxError("'block' tag takes one argument, the block name")
|
||||
@@ -133,7 +127,7 @@ class BlockNode : NodeType {
|
||||
let blockName = bits[1]
|
||||
let nodes = try parser.parse(until(["endblock"]))
|
||||
_ = parser.nextToken()
|
||||
return BlockNode(name:blockName, nodes:nodes, token: token)
|
||||
return BlockNode(name: blockName, nodes: nodes, token: token)
|
||||
}
|
||||
|
||||
init(name: String, nodes: [NodeType], token: Token) {
|
||||
@@ -148,7 +142,7 @@ class BlockNode : NodeType {
|
||||
// render extension node
|
||||
do {
|
||||
return try context.push(dictionary: childContext) {
|
||||
return try child.render(context)
|
||||
try child.render(context)
|
||||
}
|
||||
} catch {
|
||||
throw error.withToken(child.token)
|
||||
@@ -159,12 +153,15 @@ class BlockNode : NodeType {
|
||||
}
|
||||
|
||||
// child node is a block node from child template that extends this node (has the same name)
|
||||
func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any?] {
|
||||
var childContext: [String: Any?] = [BlockContext.contextKey: blockContext]
|
||||
func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any] {
|
||||
var childContext: [String: Any] = [BlockContext.contextKey: blockContext]
|
||||
|
||||
if let blockSuperNode = child.nodes.first(where: {
|
||||
if case .variable(let variable, _)? = $0.token, variable == "block.super" { return true }
|
||||
else { return false}
|
||||
if let token = $0.token, case .variable = token.kind, token.contents == "block.super" {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
do {
|
||||
// render base node so that its content can be used as part of child node that extends it
|
||||
@@ -185,5 +182,4 @@ class BlockNode : NodeType {
|
||||
}
|
||||
return childContext
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ final class KeyPath {
|
||||
subscriptLevel = 0
|
||||
}
|
||||
|
||||
for c in variable.characters {
|
||||
switch c {
|
||||
for character in variable {
|
||||
switch character {
|
||||
case "." where subscriptLevel == 0:
|
||||
try foundSeparator()
|
||||
case "[":
|
||||
@@ -33,7 +33,7 @@ final class KeyPath {
|
||||
case "]":
|
||||
try closeBracket()
|
||||
default:
|
||||
try addCharacter(c)
|
||||
try addCharacter(character)
|
||||
}
|
||||
}
|
||||
try finish()
|
||||
@@ -90,12 +90,12 @@ final class KeyPath {
|
||||
subscriptLevel -= 1
|
||||
}
|
||||
|
||||
private func addCharacter(_ c: Character) throws {
|
||||
private func addCharacter(_ character: Character) throws {
|
||||
guard partialComponents.isEmpty || subscriptLevel > 0 else {
|
||||
throw TemplateSyntaxError("Unexpected character '\(c)' in variable '\(variable)'")
|
||||
throw TemplateSyntaxError("Unexpected character '\(character)' in variable '\(variable)'")
|
||||
}
|
||||
|
||||
current.append(c)
|
||||
current.append(character)
|
||||
}
|
||||
|
||||
private func finish() throws {
|
||||
|
||||
@@ -1,23 +1,51 @@
|
||||
import Foundation
|
||||
|
||||
typealias Line = (content: String, number: UInt, range: Range<String.Index>)
|
||||
|
||||
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 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] = [
|
||||
"{": "}",
|
||||
"%": "%",
|
||||
"#": "#"
|
||||
]
|
||||
|
||||
init(templateName: String? = nil, templateString: String) {
|
||||
self.templateName = templateName
|
||||
self.templateString = templateString
|
||||
|
||||
self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap {
|
||||
guard !$0.element.isEmpty,
|
||||
let range = templateString.range(of: $0.element) else { return nil }
|
||||
return (content: $0.element, number: UInt($0.offset + 1), range)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>) -> Token {
|
||||
func strip() -> String {
|
||||
guard string.characters.count > 4 else { return "" }
|
||||
let start = string.index(string.startIndex, offsetBy: 2)
|
||||
let end = string.index(string.endIndex, offsetBy: -2)
|
||||
let trimmed = String(string[start..<end])
|
||||
guard string.count > 4 else { return "" }
|
||||
let trimmed = String(string.dropFirst(2).dropLast(2))
|
||||
.components(separatedBy: "\n")
|
||||
.filter({ !$0.isEmpty })
|
||||
.map({ $0.trim(character: " ") })
|
||||
.filter { !$0.isEmpty }
|
||||
.map { $0.trim(character: " ") }
|
||||
.joined(separator: " ")
|
||||
return trimmed
|
||||
}
|
||||
@@ -25,8 +53,8 @@ struct Lexer {
|
||||
if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") {
|
||||
let value = strip()
|
||||
let range = templateString.range(of: value, range: range) ?? range
|
||||
let line = templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: templateName, line: line)
|
||||
let location = rangeLocation(range)
|
||||
let sourceMap = SourceMap(filename: templateName, location: location)
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return .variable(value: value, at: sourceMap)
|
||||
@@ -37,31 +65,27 @@ struct Lexer {
|
||||
}
|
||||
}
|
||||
|
||||
let line = templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: templateName, line: line)
|
||||
let location = rangeLocation(range)
|
||||
let sourceMap = SourceMap(filename: templateName, location: location)
|
||||
return .text(value: string, at: sourceMap)
|
||||
}
|
||||
|
||||
/// Returns an array of tokens from a given template string.
|
||||
/// 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)
|
||||
|
||||
let map = [
|
||||
"{{": "}}",
|
||||
"{%": "%}",
|
||||
"{#": "#}",
|
||||
]
|
||||
|
||||
while !scanner.isEmpty {
|
||||
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
|
||||
if !text.1.isEmpty {
|
||||
tokens.append(createToken(string: text.1, at: scanner.range))
|
||||
if let (char, text) = scanner.scanForTokenStart(Lexer.tokenChars) {
|
||||
if !text.isEmpty {
|
||||
tokens.append(createToken(string: text, at: scanner.range))
|
||||
}
|
||||
|
||||
let end = map[text.0]!
|
||||
let result = scanner.scan(until: end, returnUntil: true)
|
||||
guard let end = Lexer.tokenCharMap[char] else { continue }
|
||||
let result = scanner.scanForTokenEnd(end)
|
||||
tokens.append(createToken(string: result, at: scanner.range))
|
||||
} else {
|
||||
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
||||
@@ -72,81 +96,103 @@ struct Lexer {
|
||||
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.Index>
|
||||
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 = "}"
|
||||
|
||||
init(_ content: String) {
|
||||
self.originalContent = content
|
||||
self.content = content
|
||||
range = content.startIndex..<content.startIndex
|
||||
range = content.unicodeScalars.startIndex..<content.unicodeScalars.startIndex
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return content.isEmpty
|
||||
}
|
||||
|
||||
func scan(until: String, returnUntil: Bool = false) -> String {
|
||||
var index = content.startIndex
|
||||
/// 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
|
||||
|
||||
if until.isEmpty {
|
||||
return ""
|
||||
}
|
||||
|
||||
range = range.upperBound..<range.upperBound
|
||||
while index != content.endIndex {
|
||||
let substring = content.substring(from: index)
|
||||
|
||||
if substring.hasPrefix(until) {
|
||||
let result = content.substring(to: index)
|
||||
|
||||
if returnUntil {
|
||||
range = range.lowerBound..<originalContent.index(range.upperBound, offsetBy: until.characters.count)
|
||||
content = substring.substring(from: until.endIndex)
|
||||
return result + until
|
||||
}
|
||||
|
||||
content = substring
|
||||
for (index, char) in content.unicodeScalars.enumerated() {
|
||||
if foundChar && char == Scanner.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)
|
||||
}
|
||||
|
||||
index = content.index(after: index)
|
||||
range = range.lowerBound..<originalContent.index(after: range.upperBound)
|
||||
}
|
||||
|
||||
content = ""
|
||||
return ""
|
||||
}
|
||||
|
||||
func scan(until: [String]) -> (String, String)? {
|
||||
if until.isEmpty {
|
||||
return nil
|
||||
}
|
||||
/// 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.
|
||||
func scanForTokenStart(_ tokenChars: [Unicode.Scalar]) -> (Unicode.Scalar, String)? {
|
||||
var foundBrace = false
|
||||
|
||||
var index = content.startIndex
|
||||
range = range.upperBound..<range.upperBound
|
||||
while index != content.endIndex {
|
||||
let substring = content.substring(from: index)
|
||||
for string in until {
|
||||
if substring.hasPrefix(string) {
|
||||
let result = content.substring(to: index)
|
||||
content = substring
|
||||
return (string, result)
|
||||
}
|
||||
for (index, char) in content.unicodeScalars.enumerated() {
|
||||
if foundBrace && tokenChars.contains(char) {
|
||||
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 (char, result)
|
||||
} else {
|
||||
foundBrace = (char == Scanner.tokenStartDelimiter)
|
||||
}
|
||||
|
||||
index = content.index(after: index)
|
||||
range = range.lowerBound..<originalContent.index(after: range.upperBound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension String {
|
||||
func findFirstNot(character: Character) -> String.Index? {
|
||||
var index = startIndex
|
||||
@@ -179,23 +225,6 @@ extension String {
|
||||
let last = findLastNot(character: character) ?? endIndex
|
||||
return String(self[first..<last])
|
||||
}
|
||||
|
||||
public func rangeLine(_ range: Range<String.Index>) -> RangeLine {
|
||||
var lineNumber: UInt = 0
|
||||
var offset: Int = 0
|
||||
var lineContent = ""
|
||||
|
||||
for line in components(separatedBy: CharacterSet.newlines) {
|
||||
lineNumber += 1
|
||||
lineContent = line
|
||||
if let rangeOfLine = self.range(of: line), rangeOfLine.contains(range.lowerBound) {
|
||||
offset = distance(from: rangeOfLine.lowerBound, to: range.lowerBound)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (lineContent, lineNumber, offset)
|
||||
}
|
||||
}
|
||||
|
||||
public typealias RangeLine = (content: String, number: UInt, offset: Int)
|
||||
public typealias ContentLocation = (content: String, lineNumber: UInt, lineOffset: Int)
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
|
||||
public protocol Loader {
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template
|
||||
func loadTemplate(names: [String], environment: Environment) throws -> Template
|
||||
}
|
||||
|
||||
|
||||
extension Loader {
|
||||
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||
for name in names {
|
||||
@@ -24,7 +22,6 @@ extension Loader {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// A class for loading a template from disk
|
||||
public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
public let paths: [Path]
|
||||
@@ -35,7 +32,7 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
|
||||
public init(bundle: [Bundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +71,6 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class DictionaryLoader: Loader {
|
||||
public let templates: [String: String]
|
||||
|
||||
@@ -101,7 +97,6 @@ public class DictionaryLoader: Loader {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Path {
|
||||
func safeJoin(path: Path) throws -> Path {
|
||||
let newPath = self + path
|
||||
@@ -114,7 +109,6 @@ extension Path {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SuspiciousFileOperation: Error {
|
||||
let basePath: Path
|
||||
let path: Path
|
||||
|
||||
@@ -2,26 +2,27 @@ import Foundation
|
||||
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
func render(_ context:Context) throws -> String
|
||||
func render(_ context: Context) throws -> String
|
||||
|
||||
/// Reference to this node's token
|
||||
var token: Token? { get }
|
||||
}
|
||||
|
||||
|
||||
/// Render the collection of nodes in the given context
|
||||
public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String {
|
||||
return try nodes.map {
|
||||
do {
|
||||
return try $0.render(context)
|
||||
} catch {
|
||||
throw error.withToken($0.token)
|
||||
public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String {
|
||||
return try nodes
|
||||
.map {
|
||||
do {
|
||||
return try $0.render(context)
|
||||
} catch {
|
||||
throw error.withToken($0.token)
|
||||
}
|
||||
}
|
||||
}.joined(separator: "")
|
||||
.joined()
|
||||
}
|
||||
|
||||
public class SimpleNode : NodeType {
|
||||
public let handler:(Context) throws -> String
|
||||
public class SimpleNode: NodeType {
|
||||
public let handler: (Context) throws -> String
|
||||
public let token: Token?
|
||||
|
||||
public init(token: Token, handler: @escaping (Context) throws -> String) {
|
||||
@@ -34,48 +35,93 @@ public class SimpleNode : NodeType {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class TextNode : NodeType {
|
||||
public let text:String
|
||||
public class TextNode: NodeType {
|
||||
public let text: String
|
||||
public let token: Token?
|
||||
|
||||
public init(text:String) {
|
||||
public init(text: String) {
|
||||
self.text = text
|
||||
self.token = nil
|
||||
}
|
||||
|
||||
public func render(_ context:Context) throws -> String {
|
||||
public func render(_ context: Context) throws -> String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public protocol Resolvable {
|
||||
func resolve(_ context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
|
||||
public class VariableNode : NodeType {
|
||||
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 {
|
||||
return components.count > (index + 1) && components[index] == token
|
||||
}
|
||||
|
||||
let condition: Expression?
|
||||
let elseExpression: Resolvable?
|
||||
|
||||
if hasToken("if", at: 1) {
|
||||
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 = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ")
|
||||
elseExpression = try parser.compileResolvable(elseToken, containedIn: token)
|
||||
} else {
|
||||
condition = try parser.compileExpression(components: Array(components), token: token)
|
||||
elseExpression = nil
|
||||
}
|
||||
} else {
|
||||
condition = nil
|
||||
elseExpression = nil
|
||||
}
|
||||
|
||||
guard let resolvable = components.first else {
|
||||
throw TemplateSyntaxError(reason: "Missing variable name", token: token)
|
||||
}
|
||||
let filter = try parser.compileResolvable(resolvable, containedIn: token)
|
||||
return VariableNode(variable: filter, 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
|
||||
@@ -98,7 +144,6 @@ func unwrap(_ array: [Any?]) -> [Any] {
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
else { return item as Any }
|
||||
} else { return item as Any }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
#if !os(Linux)
|
||||
import Foundation
|
||||
|
||||
|
||||
class NowNode : NodeType {
|
||||
let format:Variable
|
||||
class NowNode: NodeType {
|
||||
let format: Variable
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var format: Variable?
|
||||
|
||||
let components = token.components()
|
||||
let components = token.components
|
||||
guard components.count <= 2 else {
|
||||
throw TemplateSyntaxError("'now' tags may only have one argument: the format string.")
|
||||
}
|
||||
@@ -17,10 +16,10 @@ class NowNode : NodeType {
|
||||
format = Variable(components[1])
|
||||
}
|
||||
|
||||
return NowNode(format:format, token: token)
|
||||
return NowNode(format: format, token: token)
|
||||
}
|
||||
|
||||
init(format:Variable?, token: Token? = nil) {
|
||||
init(format: Variable?, token: Token? = nil) {
|
||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
self.token = token
|
||||
}
|
||||
@@ -28,18 +27,18 @@ class NowNode : NodeType {
|
||||
func render(_ context: Context) throws -> String {
|
||||
let date = Date()
|
||||
let format = try self.format.resolve(context)
|
||||
var formatter:DateFormatter?
|
||||
|
||||
var formatter: DateFormatter
|
||||
if let format = format as? DateFormatter {
|
||||
formatter = format
|
||||
} else if let format = format as? String {
|
||||
formatter = DateFormatter()
|
||||
formatter!.dateFormat = format
|
||||
formatter.dateFormat = format
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatter!.string(from: date)
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||
return { parser, token in
|
||||
if let name = token.components().first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
return true
|
||||
}
|
||||
if let name = token.components.first {
|
||||
for tag in tags where name == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +10,6 @@ public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A class for parsing an array of tokens and converts them into a collection of Node's
|
||||
public class TokenParser {
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
@@ -30,27 +27,26 @@ public class TokenParser {
|
||||
return try parse(nil)
|
||||
}
|
||||
|
||||
public func parse(_ parse_until:((_ parser:TokenParser, _ token:Token) -> (Bool))?) throws -> [NodeType] {
|
||||
public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] {
|
||||
var nodes = [NodeType]()
|
||||
|
||||
while tokens.count > 0 {
|
||||
let token = nextToken()!
|
||||
while !tokens.isEmpty {
|
||||
guard let token = nextToken() else { break }
|
||||
|
||||
switch token {
|
||||
case .text(let text, _):
|
||||
nodes.append(TextNode(text: text))
|
||||
switch token.kind {
|
||||
case .text:
|
||||
nodes.append(TextNode(text: token.contents))
|
||||
case .variable:
|
||||
let filter = try compileResolvable(token.contents, containedIn: token)
|
||||
nodes.append(VariableNode(variable: filter, token: token))
|
||||
try nodes.append(VariableNode.parse(self, token: token))
|
||||
case .block:
|
||||
if let parse_until = parse_until , parse_until(self, token) {
|
||||
if let parseUntil = parseUntil, parseUntil(self, token) {
|
||||
prependToken(token)
|
||||
return nodes
|
||||
}
|
||||
|
||||
if let tag = token.components().first {
|
||||
if let tag = token.components.first {
|
||||
do {
|
||||
let parser = try findTag(name: tag)
|
||||
let parser = try environment.findTag(name: tag)
|
||||
let node = try parser(self, token)
|
||||
nodes.append(node)
|
||||
} catch {
|
||||
@@ -66,19 +62,36 @@ public class TokenParser {
|
||||
}
|
||||
|
||||
public func nextToken() -> Token? {
|
||||
if tokens.count > 0 {
|
||||
if !tokens.isEmpty {
|
||||
return tokens.remove(at: 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func prependToken(_ token:Token) {
|
||||
public func prependToken(_ token: Token) {
|
||||
tokens.insert(token, at: 0)
|
||||
}
|
||||
|
||||
/// Create filter expression from a string contained in provided token
|
||||
public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable {
|
||||
return try environment.compileFilter(filterToken, containedIn: token)
|
||||
}
|
||||
|
||||
/// Create boolean expression from components contained in provided token
|
||||
public func compileExpression(components: [String], token: Token) throws -> Expression {
|
||||
return 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 {
|
||||
return try environment.compileResolvable(token, containedIn: containingToken)
|
||||
}
|
||||
}
|
||||
|
||||
extension Environment {
|
||||
func findTag(name: String) throws -> Extension.TagParser {
|
||||
for ext in environment.extensions {
|
||||
for ext in extensions {
|
||||
if let filter = ext.tags[name] {
|
||||
return filter
|
||||
}
|
||||
@@ -88,7 +101,7 @@ public class TokenParser {
|
||||
}
|
||||
|
||||
func findFilter(_ name: String) throws -> FilterType {
|
||||
for ext in environment.extensions {
|
||||
for ext in extensions {
|
||||
if let filter = ext.filters[name] {
|
||||
return filter
|
||||
}
|
||||
@@ -98,36 +111,51 @@ public class TokenParser {
|
||||
if suggestedFilters.isEmpty {
|
||||
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
||||
} else {
|
||||
throw TemplateSyntaxError("Unknown filter '\(name)'. Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")).")
|
||||
throw TemplateSyntaxError("""
|
||||
Unknown filter '\(name)'. \
|
||||
Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")).
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
private func suggestedFilters(for name: String) -> [String] {
|
||||
let allFilters = environment.extensions.flatMap({ $0.filters.keys })
|
||||
let allFilters = extensions.flatMap { $0.filters.keys }
|
||||
|
||||
let filtersWithDistance = allFilters
|
||||
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
|
||||
.map { (filterName: $0, distance: $0.levenshteinDistance(name)) }
|
||||
// do not suggest filters which names are shorter than the distance
|
||||
.filter({ $0.filterName.characters.count > $0.distance })
|
||||
.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 })
|
||||
return filtersWithDistance.filter { $0.distance == minDistance }.map { $0.filterName }
|
||||
}
|
||||
|
||||
/// Create filter expression from a string
|
||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||
return 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, parser: self)
|
||||
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 rangeLine = containingToken.sourceMap.line
|
||||
rangeLine.offset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound)
|
||||
syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, line: rangeLine))
|
||||
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
|
||||
}
|
||||
@@ -135,29 +163,28 @@ public class TokenParser {
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use compileFilter(_:containedIn:)")
|
||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||
return try FilterExpression(token: token, parser: self)
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use compileResolvable(_:containedIn:)")
|
||||
/// Create resolvable (i.e. range variable or filter expression) from a string
|
||||
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||
return try RangeVariable(token, parser: self)
|
||||
return 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 {
|
||||
return try RangeVariable(token, parser: self, containedIn: containingToken)
|
||||
return 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 {
|
||||
return 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(_ i: Int) -> Character {
|
||||
return self[self.index(self.startIndex, offsetBy: i)]
|
||||
subscript(_ index: Int) -> Character {
|
||||
return self[self.index(self.startIndex, offsetBy: index)]
|
||||
}
|
||||
|
||||
func levenshteinDistance(_ target: String) -> Int {
|
||||
@@ -167,22 +194,22 @@ extension String {
|
||||
// initialize v0 (the previous row of distances)
|
||||
// this row is A[0][i]: edit distance for an empty s
|
||||
// the distance is just the number of characters to delete from t
|
||||
last = [Int](0...target.characters.count)
|
||||
current = [Int](repeating: 0, count: target.characters.count + 1)
|
||||
last = [Int](0...target.count)
|
||||
current = [Int](repeating: 0, count: target.count + 1)
|
||||
|
||||
for i in 0..<self.characters.count {
|
||||
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] = i + 1
|
||||
current[0] = selfIndex + 1
|
||||
|
||||
// use formula to fill in the rest of the row
|
||||
for j in 0..<target.characters.count {
|
||||
current[j+1] = Swift.min(
|
||||
last[j+1] + 1,
|
||||
current[j] + 1,
|
||||
last[j] + (self[i] == target[j] ? 0 : 1)
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -190,7 +217,6 @@ extension String {
|
||||
last = current
|
||||
}
|
||||
|
||||
return current[target.characters.count]
|
||||
return current[target.count]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ let NSFileNoSuchFileError = 4
|
||||
/// A class representing a template
|
||||
open class Template: ExpressibleByStringLiteral {
|
||||
let templateString: String
|
||||
internal(set) var environment: Environment
|
||||
var environment: Environment
|
||||
let tokens: [Token]
|
||||
|
||||
/// The name of the loaded Template if the Template was loaded from a Loader
|
||||
@@ -26,18 +26,18 @@ open class Template: ExpressibleByStringLiteral {
|
||||
|
||||
/// 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
|
||||
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)
|
||||
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 {
|
||||
public convenience init(URL: Foundation.URL) throws {
|
||||
try self.init(path: Path(URL.path))
|
||||
}
|
||||
|
||||
@@ -50,17 +50,17 @@ open class Template: ExpressibleByStringLiteral {
|
||||
// MARK: ExpressibleByStringLiteral
|
||||
|
||||
// Create a templaVte with a template string literal
|
||||
public convenience required init(stringLiteral value: String) {
|
||||
public required convenience init(stringLiteral value: String) {
|
||||
self.init(templateString: value)
|
||||
}
|
||||
|
||||
// Create a template with a template string literal
|
||||
public convenience required init(extendedGraphemeClusterLiteral value: StringLiteralType) {
|
||||
public required convenience init(extendedGraphemeClusterLiteral value: StringLiteralType) {
|
||||
self.init(stringLiteral: value)
|
||||
}
|
||||
|
||||
// Create a template with a template string literal
|
||||
public convenience required init(unicodeScalarLiteral value: StringLiteralType) {
|
||||
public required convenience init(unicodeScalarLiteral value: StringLiteralType) {
|
||||
self.init(stringLiteral: value)
|
||||
}
|
||||
|
||||
@@ -72,8 +72,9 @@ open class Template: ExpressibleByStringLiteral {
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
|
||||
// swiftlint:disable discouraged_optional_collection
|
||||
/// Render the given template
|
||||
open func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||
return try render(Context(dictionary: dictionary, environment: environment))
|
||||
return try render(Context(dictionary: dictionary ?? [:], environment: environment))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
extension String {
|
||||
/// Split a string by a separator leaving quoted phrases together
|
||||
func smartSplit(separator: Character = " ") -> [String] {
|
||||
@@ -10,31 +9,18 @@ extension String {
|
||||
var singleQuoteCount = 0
|
||||
var doubleQuoteCount = 0
|
||||
|
||||
let specialCharacters = ",|:"
|
||||
func appendWord(_ word: String) {
|
||||
if components.count > 0 {
|
||||
if let precedingChar = components.last?.characters.last, specialCharacters.characters.contains(precedingChar) {
|
||||
components[components.count-1] += word
|
||||
} else if specialCharacters.contains(word) {
|
||||
components[components.count-1] += word
|
||||
} else {
|
||||
components.append(word)
|
||||
}
|
||||
} else {
|
||||
components.append(word)
|
||||
for character in self {
|
||||
if character == "'" {
|
||||
singleQuoteCount += 1
|
||||
} else if character == "\"" {
|
||||
doubleQuoteCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
for character in self.characters {
|
||||
if character == "'" { singleQuoteCount += 1 }
|
||||
else if character == "\"" { doubleQuoteCount += 1 }
|
||||
|
||||
if character == separate {
|
||||
|
||||
if separate != separator {
|
||||
word.append(separate)
|
||||
} else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty {
|
||||
appendWord(word)
|
||||
appendWord(word, to: &components)
|
||||
word = ""
|
||||
}
|
||||
|
||||
@@ -48,87 +34,97 @@ extension String {
|
||||
}
|
||||
|
||||
if !word.isEmpty {
|
||||
appendWord(word)
|
||||
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) {
|
||||
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 line: RangeLine
|
||||
public let location: ContentLocation
|
||||
|
||||
init(filename: String? = nil, line: RangeLine = ("", 0, 0)) {
|
||||
init(filename: String? = nil, location: ContentLocation = ("", 0, 0)) {
|
||||
self.filename = filename
|
||||
self.line = line
|
||||
self.location = location
|
||||
}
|
||||
|
||||
static let unknown = SourceMap()
|
||||
|
||||
public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool {
|
||||
return lhs.filename == rhs.filename && lhs.line == rhs.line
|
||||
public static func == (lhs: SourceMap, rhs: SourceMap) -> Bool {
|
||||
return lhs.filename == rhs.filename && lhs.location == rhs.location
|
||||
}
|
||||
}
|
||||
|
||||
public enum Token : Equatable {
|
||||
/// A token representing a piece of text.
|
||||
case text(value: String, at: SourceMap)
|
||||
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
|
||||
}
|
||||
|
||||
/// A token representing a variable.
|
||||
case variable(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a comment.
|
||||
case comment(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a template block.
|
||||
case block(value: String, at: SourceMap)
|
||||
public let contents: String
|
||||
public let kind: Kind
|
||||
public let sourceMap: SourceMap
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public func components() -> [String] {
|
||||
switch self {
|
||||
case .block(let value, _),
|
||||
.variable(let value, _),
|
||||
.text(let value, _),
|
||||
.comment(let value, _):
|
||||
return value.smartSplit()
|
||||
}
|
||||
public private(set) lazy var components: [String] = self.contents.smartSplit()
|
||||
|
||||
init(contents: String, kind: Kind, sourceMap: SourceMap) {
|
||||
self.contents = contents
|
||||
self.kind = kind
|
||||
self.sourceMap = sourceMap
|
||||
}
|
||||
|
||||
public var contents: String {
|
||||
switch self {
|
||||
case .block(let value, _),
|
||||
.variable(let value, _),
|
||||
.text(let value, _),
|
||||
.comment(let value, _):
|
||||
return value
|
||||
}
|
||||
/// A token representing a piece of text.
|
||||
public static func text(value: String, at sourceMap: SourceMap) -> Token {
|
||||
return Token(contents: value, kind: .text, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
public var sourceMap: SourceMap {
|
||||
switch self {
|
||||
case .block(_, let sourceMap),
|
||||
.variable(_, let sourceMap),
|
||||
.text(_, let sourceMap),
|
||||
.comment(_, let sourceMap):
|
||||
return sourceMap
|
||||
}
|
||||
/// A token representing a variable.
|
||||
public static func variable(value: String, at sourceMap: SourceMap) -> Token {
|
||||
return Token(contents: value, kind: .variable, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
}
|
||||
/// A token representing a comment.
|
||||
public static func comment(value: String, at sourceMap: SourceMap) -> Token {
|
||||
return Token(contents: value, kind: .comment, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
/// A token representing a template block.
|
||||
public static func block(value: String, at sourceMap: SourceMap) -> Token {
|
||||
return Token(contents: value, kind: .block, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
public func == (lhs: Token, rhs: Token) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.text(lhsValue, lhsAt), .text(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
case let (.variable(lhsValue, lhsAt), .variable(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
case let (.block(lhsValue, lhsAt), .block(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
case let (.comment(lhsValue, lhsAt), .comment(rhsValue, rhsAt)):
|
||||
return lhsValue == rhsValue && lhsAt == rhsAt
|
||||
default:
|
||||
return false
|
||||
public static func == (lhs: Token, rhs: Token) -> Bool {
|
||||
return lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
typealias Number = Float
|
||||
|
||||
|
||||
class FilterExpression : Resolvable {
|
||||
class FilterExpression: Resolvable {
|
||||
let filters: [(FilterType, [Variable])]
|
||||
let variable: Variable
|
||||
|
||||
init(token: String, parser: TokenParser) throws {
|
||||
let bits = token.characters.split(separator: "|").map({ String($0).trim(character: " ") })
|
||||
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")
|
||||
}
|
||||
@@ -20,7 +18,7 @@ class FilterExpression : Resolvable {
|
||||
do {
|
||||
filters = try filterBits.map {
|
||||
let (name, arguments) = parseFilterComponents(token: $0)
|
||||
let filter = try parser.findFilter(name)
|
||||
let filter = try environment.findFilter(name)
|
||||
return (filter, arguments)
|
||||
}
|
||||
} catch {
|
||||
@@ -32,15 +30,15 @@ class FilterExpression : Resolvable {
|
||||
func resolve(_ context: Context) throws -> Any? {
|
||||
let result = try variable.resolve(context)
|
||||
|
||||
return try filters.reduce(result) { x, y in
|
||||
let arguments = try y.1.map { try $0.resolve(context) }
|
||||
return try y.0.invoke(value: x, arguments: arguments)
|
||||
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 struct Variable: Equatable, Resolvable {
|
||||
public let variable: String
|
||||
|
||||
/// Create a variable with a string representing the variable
|
||||
@@ -48,19 +46,11 @@ public struct Variable : Equatable, Resolvable {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
// Split the lookup string and resolve references if possible
|
||||
fileprivate func lookup(_ context: Context) throws -> [String] {
|
||||
let keyPath = KeyPath(variable, in: context)
|
||||
return try keyPath.parse()
|
||||
}
|
||||
|
||||
/// Resolve the variable in the given context
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
var current: Any? = context
|
||||
|
||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||
if variable.count > 1 && ((variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\""))) {
|
||||
// String literal
|
||||
return String(variable[variable.characters.index(after: variable.startIndex) ..< variable.characters.index(before: variable.endIndex)])
|
||||
return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)])
|
||||
}
|
||||
|
||||
// Number literal
|
||||
@@ -75,43 +65,11 @@ public struct Variable : Equatable, Resolvable {
|
||||
return bool
|
||||
}
|
||||
|
||||
var current: Any? = context
|
||||
for bit in try lookup(context) {
|
||||
current = normalize(current)
|
||||
current = resolve(bit: bit, context: current)
|
||||
|
||||
if let context = current as? Context {
|
||||
current = context[bit]
|
||||
} else if let dictionary = current as? [String: Any] {
|
||||
if bit == "count" {
|
||||
current = dictionary.count
|
||||
} else {
|
||||
current = dictionary[bit]
|
||||
}
|
||||
} else if let array = current as? [Any] {
|
||||
if let index = Int(bit) {
|
||||
if index >= 0 && index < array.count {
|
||||
current = array[index]
|
||||
} else {
|
||||
current = nil
|
||||
}
|
||||
} else if bit == "first" {
|
||||
current = array.first
|
||||
} else if bit == "last" {
|
||||
current = array.last
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
} else if let object = current as? NSObject { // NSKeyValueCoding
|
||||
#if os(Linux)
|
||||
return nil
|
||||
#else
|
||||
current = object.value(forKey: bit)
|
||||
#endif
|
||||
} else if let value = current {
|
||||
current = Mirror(reflecting: value).getValue(for: bit)
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -124,10 +82,67 @@ public struct Variable : Equatable, Resolvable {
|
||||
|
||||
return normalize(current)
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs: Variable, rhs: Variable) -> Bool {
|
||||
return lhs.variable == rhs.variable
|
||||
// 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 object = context as? NSObject { // NSKeyValueCoding
|
||||
#if os(Linux)
|
||||
return nil
|
||||
#else
|
||||
if object.responds(to: Selector(bit)) {
|
||||
return object.value(forKey: bit)
|
||||
}
|
||||
#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`.
|
||||
@@ -136,48 +151,46 @@ public func ==(lhs: Variable, rhs: Variable) -> Bool {
|
||||
/// 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
|
||||
|
||||
@available(*, deprecated, message: "Use init?(_:parser:containedIn:)")
|
||||
public init?(_ token: String, parser: TokenParser) throws {
|
||||
public init?(_ token: String, environment: Environment) throws {
|
||||
let components = token.components(separatedBy: "...")
|
||||
guard components.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.from = try parser.compileFilter(components[0])
|
||||
self.to = try parser.compileFilter(components[1])
|
||||
self.from = try environment.compileFilter(components[0])
|
||||
self.to = try environment.compileFilter(components[1])
|
||||
}
|
||||
|
||||
public init?(_ token: String, parser: TokenParser, containedIn containingToken: Token) throws {
|
||||
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 parser.compileFilter(components[0], containedIn: containingToken)
|
||||
self.to = try parser.compileFilter(components[1], containedIn: containingToken)
|
||||
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 fromResolved = try from.resolve(context)
|
||||
let toResolved = try to.resolve(context)
|
||||
let lowerResolved = try from.resolve(context)
|
||||
let upperResolved = try to.resolve(context)
|
||||
|
||||
guard let from = fromResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'from' value is not an Integer (\(fromResolved ?? "nil"))")
|
||||
guard let lower = lowerResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'from' value is not an Integer (\(lowerResolved ?? "nil"))")
|
||||
}
|
||||
|
||||
guard let to = toResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'to' value is not an Integer (\(toResolved ?? "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(from, to)...max(from, to)
|
||||
return from > to ? Array(range.reversed()) : Array(range)
|
||||
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()
|
||||
@@ -190,19 +203,19 @@ protocol Normalizable {
|
||||
func normalize() -> Any?
|
||||
}
|
||||
|
||||
extension Array : Normalizable {
|
||||
extension Array: Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
extension NSArray : Normalizable {
|
||||
extension NSArray: Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary : Normalizable {
|
||||
extension Dictionary: Normalizable {
|
||||
func normalize() -> Any? {
|
||||
var dictionary: [String: Any] = [:]
|
||||
|
||||
@@ -230,7 +243,7 @@ func parseFilterComponents(token: String) -> (String, [Variable]) {
|
||||
|
||||
extension Mirror {
|
||||
func getValue(for key: String) -> Any? {
|
||||
let result = descendant(key) ?? Int(key).flatMap({ descendant($0) })
|
||||
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)
|
||||
@@ -262,5 +275,3 @@ extension Optional: AnyOptional {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
9
Sources/_SwiftSupport.swift
Normal file
9
Sources/_SwiftSupport.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
#if !swift(>=4.2)
|
||||
extension ArraySlice where Element: Equatable {
|
||||
func firstIndex(of element: Element) -> Int? {
|
||||
return index(of: element)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Stencil",
|
||||
"version": "0.12.1",
|
||||
"version": "0.14.1",
|
||||
"summary": "Stencil is a simple and powerful template language for Swift.",
|
||||
"homepage": "https://stencil.fuller.li",
|
||||
"license": {
|
||||
@@ -13,7 +13,7 @@
|
||||
"social_media_url": "https://twitter.com/kylefuller",
|
||||
"source": {
|
||||
"git": "https://github.com/stencilproject/Stencil.git",
|
||||
"tag": "0.12.1"
|
||||
"tag": "0.14.1"
|
||||
},
|
||||
"source_files": [
|
||||
"Sources/*.swift"
|
||||
@@ -23,10 +23,15 @@
|
||||
"osx": "10.9",
|
||||
"tvos": "9.0"
|
||||
},
|
||||
"cocoapods_version": ">= 1.7.0",
|
||||
"swift_versions": [
|
||||
"4.2",
|
||||
"5.0"
|
||||
],
|
||||
"requires_arc": true,
|
||||
"dependencies": {
|
||||
"PathKit": [
|
||||
"~> 0.9.0"
|
||||
"~> 1.0.0"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import XCTest
|
||||
|
||||
import StencilTests
|
||||
|
||||
stencilTests()
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += StencilTests.__allTests()
|
||||
|
||||
XCTMain(tests)
|
||||
|
||||
3
Tests/StencilTests/.swiftlint.yml
Normal file
3
Tests/StencilTests/.swiftlint.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
disabled_rules: # rule identifiers to exclude from running
|
||||
- type_body_length
|
||||
- file_length
|
||||
@@ -1,80 +1,90 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class ContextTests: XCTestCase {
|
||||
func testContextSubscripting() {
|
||||
describe("Context Subscripting") {
|
||||
var context = Context()
|
||||
$0.before {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
func testContext() {
|
||||
describe("Context") {
|
||||
var context: Context!
|
||||
|
||||
$0.before {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
$0.it("allows you to get a value via subscripting") {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to set a value via subscripting") {
|
||||
context["name"] = "Katie"
|
||||
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("allows you to remove a value via subscripting") {
|
||||
context["name"] = nil
|
||||
|
||||
try expect(context["name"]).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("allows you to retrieve a value from a parent") {
|
||||
try context.push {
|
||||
$0.it("allows you to get a value via subscripting") {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("allows you to override a parent's value") {
|
||||
try context.push {
|
||||
$0.it("allows you to set a value via subscripting") {
|
||||
context["name"] = "Katie"
|
||||
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("allows you to pop to restore previous state") {
|
||||
context.push {
|
||||
context["name"] = "Katie"
|
||||
}
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to remove a parent's value in a level") {
|
||||
try context.push {
|
||||
$0.it("allows you to remove a value via subscripting") {
|
||||
context["name"] = nil
|
||||
|
||||
try expect(context["name"]).to.beNil()
|
||||
}
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to push a dictionary and run a closure then restoring previous state") {
|
||||
var didRun = false
|
||||
|
||||
try context.push(dictionary: ["name": "Katie"]) {
|
||||
didRun = true
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
$0.it("allows you to retrieve a value from a parent") {
|
||||
try context.push {
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
try expect(didRun).to.beTrue()
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
$0.it("allows you to override a parent's value") {
|
||||
try context.push {
|
||||
context["name"] = "Katie"
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("allows you to flatten the context contents") {
|
||||
try context.push(dictionary: ["test": "abc"]) {
|
||||
let flattened = context.flatten()
|
||||
func testContextRestoration() {
|
||||
describe("Context Restoration") {
|
||||
var context = Context()
|
||||
$0.before {
|
||||
context = Context(dictionary: ["name": "Kyle"])
|
||||
}
|
||||
|
||||
try expect(flattened.count) == 2
|
||||
try expect(flattened["name"] as? String) == "Kyle"
|
||||
try expect(flattened["test"] as? String) == "abc"
|
||||
$0.it("allows you to pop to restore previous state") {
|
||||
context.push {
|
||||
context["name"] = "Katie"
|
||||
}
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to remove a parent's value in a level") {
|
||||
try context.push {
|
||||
context["name"] = nil
|
||||
try expect(context["name"]).to.beNil()
|
||||
}
|
||||
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to push a dictionary and run a closure then restoring previous state") {
|
||||
var didRun = false
|
||||
|
||||
try context.push(dictionary: ["name": "Katie"]) {
|
||||
didRun = true
|
||||
try expect(context["name"] as? String) == "Katie"
|
||||
}
|
||||
|
||||
try expect(didRun).to.beTrue()
|
||||
try expect(context["name"] as? String) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to flatten the context contents") {
|
||||
try context.push(dictionary: ["test": "abc"]) {
|
||||
let flattened = context.flatten()
|
||||
|
||||
try expect(flattened.count) == 2
|
||||
try expect(flattened["name"] as? String) == "Kyle"
|
||||
try expect(flattened["test"] as? String) == "abc"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,329 +1,403 @@
|
||||
import Spectre
|
||||
import PathKit
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
final class EnvironmentTests: XCTestCase {
|
||||
var environment = Environment(loader: ExampleLoader())
|
||||
var template: Template = ""
|
||||
|
||||
func testEnvironment() {
|
||||
describe("Environment") {
|
||||
var environment: Environment!
|
||||
var template: Template!
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
$0.before {
|
||||
environment = Environment(loader: ExampleLoader())
|
||||
template = nil
|
||||
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)
|
||||
}
|
||||
|
||||
$0.it("can load a template from a name") {
|
||||
let template = try environment.loadTemplate(name: "example.html")
|
||||
environment = Environment(loader: ExampleLoader())
|
||||
environment.extensions += [errorExtension]
|
||||
template = ""
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
$0.it("can load a template from a names") {
|
||||
let template = try environment.loadTemplate(names: ["first.html", "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"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can render a template from a string") {
|
||||
let result = try environment.renderTemplate(string: "Hello World")
|
||||
func testRendering() {
|
||||
it("can render a template from a string") {
|
||||
let result = try self.environment.renderTemplate(string: "Hello World")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a template from a file") {
|
||||
let result = try environment.renderTemplate(name: "example.html")
|
||||
it("can render a template from a file") {
|
||||
let result = try self.environment.renderTemplate(name: "example.html")
|
||||
try expect(result) == "Hello World!"
|
||||
}
|
||||
|
||||
$0.it("allows you to provide a custom template class") {
|
||||
it("allows you to provide a custom template class") {
|
||||
let environment = Environment(loader: ExampleLoader(), templateClass: CustomTemplate.self)
|
||||
let result = try environment.renderTemplate(string: "Hello World")
|
||||
|
||||
try expect(result) == "here"
|
||||
}
|
||||
}
|
||||
|
||||
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||
guard let range = template.templateString.range(of: token) else {
|
||||
fatalError("Can't find '\(token)' in '\(template)'")
|
||||
}
|
||||
let rangeLine = template.templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: template.name, line: rangeLine)
|
||||
let token = Token.block(value: token, at: sourceMap)
|
||||
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||
func 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"
|
||||
)
|
||||
}
|
||||
|
||||
func expectError(reason: String, token: String,
|
||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||
|
||||
let error = try expect(environment.render(template: template, context: ["names": ["Bob", "Alice"], "name": "Bob"]),
|
||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||
let reporter = SimpleErrorReporter()
|
||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||
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")
|
||||
}
|
||||
|
||||
$0.context("given syntax error") {
|
||||
|
||||
$0.it("reports syntax error on invalid for tag syntax") {
|
||||
template = "Hello {% for name in %}{{ name }}, {% endfor %}!"
|
||||
try expectError(reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.", token: "for name in")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error on missing endfor") {
|
||||
template = "{% for name in names %}{{ name }}"
|
||||
try expectError(reason: "`endfor` was not found.", token: "for name in names")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error on unknown tag") {
|
||||
template = "{% for name in names %}{{ name }}{% end %}"
|
||||
try expectError(reason: "Unknown template tag 'end'", token: "end")
|
||||
}
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
$0.context("given unknown filter") {
|
||||
|
||||
$0.it("reports syntax error in for tag") {
|
||||
template = "{% for name in names|unknown %}{{ name }}{% endfor %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "names|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in for-where tag") {
|
||||
template = "{% for name in names where name|unknown %}{{ name }}{% endfor %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in if tag") {
|
||||
template = "{% if name|unknown %}{{ name }}{% endif %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in elif tag") {
|
||||
template = "{% if name %}{{ name }}{% elif name|unknown %}{% endif %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in ifnot tag") {
|
||||
template = "{% ifnot name|unknown %}{{ name }}{% endif %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in filter tag") {
|
||||
template = "{% filter unknown %}Text{% endfilter %}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "filter unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in variable tag") {
|
||||
template = "{{ name|unknown }}"
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.", token: "name|unknown")
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
$0.context("given rendering error") {
|
||||
|
||||
$0.it("reports rendering error in variable filter") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("throw") { (value: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
}
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
template = Template(templateString: "{{ name|throw }}", environment: environment)
|
||||
try expectError(reason: "filter error", token: "name|throw")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in filter tag") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("throw") { (value: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
}
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
template = Template(templateString: "{% filter throw %}Test{% endfilter %}", environment: environment)
|
||||
try expectError(reason: "filter error", token: "filter throw")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in simple tag") {
|
||||
let tagExtension = Extension()
|
||||
tagExtension.registerSimpleTag("simpletag") { context in
|
||||
throw TemplateSyntaxError("simpletag error")
|
||||
}
|
||||
environment.extensions += [tagExtension]
|
||||
|
||||
template = Template(templateString: "{% simpletag %}", environment: environment)
|
||||
try expectError(reason: "simpletag error", token: "simpletag")
|
||||
}
|
||||
|
||||
$0.it("reporsts passing argument to simple filter") {
|
||||
template = "{{ name|uppercase:5 }}"
|
||||
try expectError(reason: "cannot invoke filter with an argument", token: "name|uppercase:5")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in custom tag") {
|
||||
let tagExtension = Extension()
|
||||
tagExtension.registerTag("customtag") { parser, token in
|
||||
return ErrorNode(token: token)
|
||||
}
|
||||
environment.extensions += [tagExtension]
|
||||
|
||||
template = Template(templateString: "{% customtag %}", environment: environment)
|
||||
try expectError(reason: "Custom Error", token: "customtag")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in for body") {
|
||||
let tagExtension = Extension()
|
||||
tagExtension.registerTag("customtag") { parser, token in
|
||||
return ErrorNode(token: token)
|
||||
}
|
||||
environment.extensions += [tagExtension]
|
||||
|
||||
template = Template(templateString: "{% for name in names %}{% customtag %}{% endfor %}", environment: environment)
|
||||
try expectError(reason: "Custom Error", token: "customtag")
|
||||
}
|
||||
|
||||
$0.it("reports rendering error in block") {
|
||||
let tagExtension = Extension()
|
||||
tagExtension.registerTag("customtag") { parser, token in
|
||||
return ErrorNode(token: token)
|
||||
}
|
||||
environment.extensions += [tagExtension]
|
||||
|
||||
template = Template(templateString: "{% block some %}{% customtag %}{% endblock %}", environment: environment)
|
||||
try expectError(reason: "Custom Error", token: "customtag")
|
||||
}
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
$0.context("given included template") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
var environment = Environment(loader: loader)
|
||||
var template: Template!
|
||||
var includedTemplate: Template!
|
||||
|
||||
$0.before {
|
||||
environment = Environment(loader: loader)
|
||||
template = nil
|
||||
includedTemplate = nil
|
||||
}
|
||||
|
||||
func expectError(reason: String, token: String, includedToken: String,
|
||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||
var expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||
expectedError.stackTrace = [expectedSyntaxError(token: includedToken, template: includedTemplate, description: reason).token!]
|
||||
|
||||
let error = try expect(environment.render(template: template, context: ["target": "World"]),
|
||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||
let reporter = SimpleErrorReporter()
|
||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in included template") {
|
||||
template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment)
|
||||
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
token: "include \"invalid-include.html\"",
|
||||
includedToken: "target|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports runtime error in included template") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
})
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
template = Template(templateString: "{% include \"invalid-include.html\" %}", environment: environment)
|
||||
includedTemplate = try environment.loadTemplate(name: "invalid-include.html")
|
||||
|
||||
try expectError(reason: "filter error",
|
||||
token: "include \"invalid-include.html\"",
|
||||
includedToken: "target|unknown")
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
$0.context("given base and child templates") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
var environment: Environment!
|
||||
var childTemplate: Template!
|
||||
var baseTemplate: Template!
|
||||
|
||||
$0.before {
|
||||
environment = Environment(loader: loader)
|
||||
childTemplate = nil
|
||||
baseTemplate = nil
|
||||
}
|
||||
|
||||
func expectError(reason: String, childToken: String, baseToken: String?,
|
||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||
var expectedError = expectedSyntaxError(token: childToken, template: childTemplate, description: reason)
|
||||
if let baseToken = baseToken {
|
||||
expectedError.stackTrace = [expectedSyntaxError(token: baseToken, template: baseTemplate, description: reason).token!]
|
||||
}
|
||||
let error = try expect(environment.render(template: childTemplate, context: ["target": "World"]),
|
||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||
let reporter = SimpleErrorReporter()
|
||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in base template") {
|
||||
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
childToken: "extends \"invalid-base.html\"",
|
||||
baseToken: "target|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports runtime error in base template") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
})
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
childTemplate = try environment.loadTemplate(name: "invalid-child-super.html")
|
||||
baseTemplate = try environment.loadTemplate(name: "invalid-base.html")
|
||||
|
||||
try expectError(reason: "filter error",
|
||||
childToken: "block.super",
|
||||
baseToken: "target|unknown")
|
||||
}
|
||||
|
||||
$0.it("reports syntax error in child template") {
|
||||
childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" +
|
||||
"{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil)
|
||||
|
||||
try expectError(reason: "Unknown filter 'unknown'. Found similar filters: 'uppercase'.",
|
||||
childToken: "target|unknown",
|
||||
baseToken: nil)
|
||||
}
|
||||
|
||||
$0.it("reports runtime error in child template") {
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("unknown", filter: { (_: Any?) in
|
||||
throw TemplateSyntaxError("filter error")
|
||||
})
|
||||
environment.extensions += [filterExtension]
|
||||
|
||||
childTemplate = Template(templateString: "{% extends \"base.html\" %}\n" +
|
||||
"{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil)
|
||||
|
||||
try expectError(reason: "filter error",
|
||||
childToken: "target|unknown",
|
||||
baseToken: nil)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
final class EnvironmentIncludeTemplateTests: XCTestCase {
|
||||
var environment = Environment(loader: ExampleLoader())
|
||||
var template: Template = ""
|
||||
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 = ""
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
final class EnvironmentBaseAndChildTemplateTests: XCTestCase {
|
||||
var environment = Environment(loader: ExampleLoader())
|
||||
var childTemplate: Template = ""
|
||||
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 = ""
|
||||
}
|
||||
|
||||
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: "block.super",
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
extension Expectation {
|
||||
@discardableResult
|
||||
func toThrow<T: Error>() throws -> T {
|
||||
var thrownError: Error? = nil
|
||||
var thrownError: Error?
|
||||
|
||||
do {
|
||||
_ = try expression()
|
||||
@@ -343,7 +417,20 @@ extension Expectation {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class ExampleLoader: Loader {
|
||||
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: [])
|
||||
}
|
||||
}
|
||||
|
||||
private class ExampleLoader: Loader {
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
if name == "example.html" {
|
||||
return Template(templateString: "Hello World!", environment: environment, name: name)
|
||||
@@ -353,8 +440,8 @@ fileprivate class ExampleLoader: Loader {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CustomTemplate: Template {
|
||||
private class CustomTemplate: Template {
|
||||
// swiftlint:disable discouraged_optional_collection
|
||||
override func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||
return "here"
|
||||
}
|
||||
|
||||
@@ -1,304 +1,355 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
|
||||
func testExpressions() {
|
||||
describe("Expression") {
|
||||
let parser = TokenParser(tokens: [], environment: Environment())
|
||||
|
||||
$0.describe("VariableExpression") {
|
||||
let expression = VariableExpression(variable: Variable("value"))
|
||||
|
||||
$0.it("evaluates to true when value is not nil") {
|
||||
let context = Context(dictionary: ["value": "known"])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when value is unset") {
|
||||
let context = Context()
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true when array variable is not empty") {
|
||||
let items: [[String: Any]] = [["key":"key1","value":42],["key":"key2","value":1337]]
|
||||
let context = Context(dictionary: ["value": [items]])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when array value is empty") {
|
||||
let emptyItems = [[String: Any]]()
|
||||
let context = Context(dictionary: ["value": emptyItems])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when dictionary value is empty") {
|
||||
let emptyItems = [String:Any]()
|
||||
let context = Context(dictionary: ["value": emptyItems])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when Array<Any> value is empty") {
|
||||
let context = Context(dictionary: ["value": ([] as [Any])])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true when integer value is above 0") {
|
||||
let context = Context(dictionary: ["value": 1])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with string") {
|
||||
let context = Context(dictionary: ["value": "test"])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when empty string") {
|
||||
let context = Context(dictionary: ["value": ""])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when integer value is below 0 or below") {
|
||||
let context = Context(dictionary: ["value": 0])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
|
||||
let negativeContext = Context(dictionary: ["value": 0])
|
||||
try expect(try expression.evaluate(context: negativeContext)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true when float value is above 0") {
|
||||
let context = Context(dictionary: ["value": Float(0.5)])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when float is 0 or below") {
|
||||
let context = Context(dictionary: ["value": Float(0)])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true when double value is above 0") {
|
||||
let context = Context(dictionary: ["value": Double(0.5)])
|
||||
try expect(try expression.evaluate(context: context)).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when double is 0 or below") {
|
||||
let context = Context(dictionary: ["value": Double(0)])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when uint is 0") {
|
||||
let context = Context(dictionary: ["value": UInt(0)])
|
||||
try expect(try expression.evaluate(context: context)).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("NotExpression") {
|
||||
$0.it("returns truthy for positive expressions") {
|
||||
let expression = NotExpression(expression: StaticExpression(value: true))
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("returns falsy for negative expressions") {
|
||||
let expression = NotExpression(expression: StaticExpression(value: false))
|
||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("expression parsing") {
|
||||
$0.it("can parse a variable expression") {
|
||||
let expression = try parseExpression(components: ["value"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("can parse a not expression") {
|
||||
let expression = try parseExpression(components: ["not", "value"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
try expect(expression.evaluate(context: Context())).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["value": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.describe("and expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "and", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to false with lhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with rhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs and rhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with lhs and rhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("or expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "or", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with rhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with lhs and rhs true") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs and rhs false") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": false, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("equality expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "==", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "a"]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with non equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with nils") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: [:]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with numbers") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.0]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with non equal numbers") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1.1]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with booleans") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with falsy booleans") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": false]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with different types") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": true, "rhs": 1]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("inequality expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "!=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with inequal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "b"]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with equal lhs/rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "b", "rhs": "b"]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("more than expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", ">", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs > rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 4]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("more than equal expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", ">=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs < rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.1]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("less than expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "<", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs < rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 4.5]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5.0]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("less than equal expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "<=", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with lhs == rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.0, "rhs": 5]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with lhs > rhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 5.1, "rhs": 5.0]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("multiple expression") {
|
||||
let expression = try! parseExpression(components: ["one", "or", "two", "and", "not", "three"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true with one") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with one and three") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["one": true, "three": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to true with two") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with two and three") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with two and three") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["two": true, "three": true]))).to.beFalse()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false with nothing") {
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("in expression") {
|
||||
let expression = try! parseExpression(components: ["lhs", "in", "rhs"], tokenParser: parser, token: .text(value: "", at: .unknown))
|
||||
|
||||
$0.it("evaluates to true when rhs contains lhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [1, 2, 3]]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["a", "b", "c"]]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "abc"]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1...3]))).to.beTrue()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": 1..<3]))).to.beTrue()
|
||||
}
|
||||
|
||||
$0.it("evaluates to false when rhs does not contain lhs") {
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 1, "rhs": [2, 3, 4]]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": ["b", "c", "d"]]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": "a", "rhs": "bcd"]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 4, "rhs": 1...3]))).to.beFalse()
|
||||
try expect(expression.evaluate(context: Context(dictionary: ["lhs": 3, "rhs": 1..<3]))).to.beFalse()
|
||||
}
|
||||
}
|
||||
import XCTest
|
||||
|
||||
final class ExpressionsTests: XCTestCase {
|
||||
let parser = TokenParser(tokens: [], environment: Environment())
|
||||
|
||||
private func makeExpression(_ components: [String]) -> 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: StaticExpression(value: true))
|
||||
try expect(expression.evaluate(context: Context())).to.beFalse()
|
||||
}
|
||||
|
||||
it("returns falsy for negative expressions") {
|
||||
let expression = NotExpression(expression: StaticExpression(value: 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
|
||||
func testFilter() {
|
||||
describe("template filters") {
|
||||
final class FilterTests: XCTestCase {
|
||||
func testRegistration() {
|
||||
let context: [String: Any] = ["name": "Kyle"]
|
||||
|
||||
$0.it("allows you to register a custom filter") {
|
||||
it("allows you to register a custom filter") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
|
||||
let repeatExtension = Extension()
|
||||
@@ -18,152 +18,205 @@ func testFilter() {
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
let result = try template.render(Context(
|
||||
dictionary: context,
|
||||
environment: Environment(extensions: [repeatExtension])
|
||||
))
|
||||
try expect(result) == "Kyle Kyle"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom filter which accepts single argument") {
|
||||
let template = Template(templateString: "{{ name|repeat:'value1, \"value2\"' }}")
|
||||
|
||||
it("allows you to register boolean filters") {
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { value, arguments in
|
||||
if !arguments.isEmpty {
|
||||
return "\(value!) \(value!) with args \(arguments.first!!)"
|
||||
repeatExtension.registerFilter(name: "isPositive", negativeFilterName: "isNotPositive") { (value: Any?) in
|
||||
if let value = value as? Int {
|
||||
return value > 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "Kyle Kyle with args value1, \"value2\""
|
||||
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"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom filter which accepts several arguments") {
|
||||
let template = Template(templateString: "{{ name|repeat:'value\"1\"',\"value'2'\",'(key, value)' }}")
|
||||
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { value, arguments in
|
||||
if !arguments.isEmpty {
|
||||
return "\(value!) \(value!) with args 0: \(arguments[0]!), 1: \(arguments[1]!), 2: \(arguments[2]!)"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "Kyle Kyle with args 0: value\"1\", 1: value'2', 2: (key, value)"
|
||||
}
|
||||
|
||||
$0.it("allows you to register a custom which throws") {
|
||||
it("allows you to register a custom which throws") {
|
||||
let template = Template(templateString: "{{ name|repeat }}")
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("repeat") { (value: Any?) in
|
||||
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))
|
||||
try expect(try template.render(context))
|
||||
.toThrow(TemplateSyntaxError(reason: "No Repeat", token: template.tokens.first))
|
||||
}
|
||||
|
||||
$0.it("allows you to override a default filter") {
|
||||
let template = Template(templateString: "{{ name|join }}")
|
||||
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("join") { (value: Any?) in
|
||||
return "joined"
|
||||
}
|
||||
|
||||
let result = try template.render(Context(dictionary: context, environment: Environment(extensions: [repeatExtension])))
|
||||
try expect(result) == "joined"
|
||||
}
|
||||
|
||||
$0.it("allows whitespace in expression") {
|
||||
let template = Template(templateString: "{{ value | join : \", \" }}")
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||
try expect(result) == "One, Two"
|
||||
}
|
||||
|
||||
$0.it("throws when you pass arguments to simple filter") {
|
||||
it("throws when you pass arguments to simple filter") {
|
||||
let template = Template(templateString: "{{ name|uppercase:5 }}")
|
||||
try expect(try template.render(Context(dictionary: ["name": "kyle"]))).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
describe("string filters") {
|
||||
$0.context("given string") {
|
||||
$0.it("transforms a string to be capitalized") {
|
||||
let template = Template(templateString: "{{ name|capitalize }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
func testRegistrationOverrideDefault() throws {
|
||||
let template = Template(templateString: "{{ name|join }}")
|
||||
let context: [String: Any] = ["name": "Kyle"]
|
||||
|
||||
$0.it("transforms a string to be uppercase") {
|
||||
let template = Template(templateString: "{{ name|uppercase }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "kyle"]))
|
||||
try expect(result) == "KYLE"
|
||||
}
|
||||
|
||||
$0.it("transforms a string to be lowercase") {
|
||||
let template = Template(templateString: "{{ name|lowercase }}")
|
||||
let result = try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||
try expect(result) == "kyle"
|
||||
}
|
||||
let repeatExtension = Extension()
|
||||
repeatExtension.registerFilter("join") { (_: Any?) in
|
||||
"joined"
|
||||
}
|
||||
|
||||
$0.context("given array of strings") {
|
||||
$0.it("transforms a string to be capitalized") {
|
||||
let template = Template(templateString: "{{ names|capitalize }}")
|
||||
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||
try expect(result) == "[\"Kyle\", \"Kyle\"]"
|
||||
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 ?? "")"
|
||||
}
|
||||
|
||||
$0.it("transforms a string to be uppercase") {
|
||||
let template = Template(templateString: "{{ names|uppercase }}")
|
||||
let result = try template.render(Context(dictionary: ["names": ["kyle", "kyle"]]))
|
||||
try expect(result) == "[\"KYLE\", \"KYLE\"]"
|
||||
}
|
||||
let result = try template.render(Context(
|
||||
dictionary: context,
|
||||
environment: Environment(extensions: [repeatExtension])
|
||||
))
|
||||
try expect(result) == """
|
||||
Kyle Kyle with args value1, "value2"
|
||||
"""
|
||||
}
|
||||
|
||||
$0.it("transforms a string to be lowercase") {
|
||||
let template = Template(templateString: "{{ names|lowercase }}")
|
||||
let result = try template.render(Context(dictionary: ["names": ["Kyle", "Kyle"]]))
|
||||
try expect(result) == "[\"kyle\", \"kyle\"]"
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
describe("default filter") {
|
||||
let template = Template(templateString: "Hello {{ name|default:\"World\" }}")
|
||||
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"
|
||||
}
|
||||
|
||||
$0.it("shows the variable value") {
|
||||
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"
|
||||
}
|
||||
|
||||
$0.it("shows the default value") {
|
||||
it("shows the default value") {
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("supports multiple defaults") {
|
||||
let template = Template(templateString: "Hello {{ name|default:a,b,c,\"World\" }}")
|
||||
it("supports multiple defaults") {
|
||||
let template = Template(templateString: """
|
||||
Hello {{ name|default:a,b,c,"World" }}
|
||||
""")
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can use int as default") {
|
||||
it("can use int as default") {
|
||||
let template = Template(templateString: "{{ value|default:1 }}")
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "1"
|
||||
}
|
||||
|
||||
$0.it("can use float as default") {
|
||||
it("can use float as default") {
|
||||
let template = Template(templateString: "{{ value|default:1.5 }}")
|
||||
let result = try template.render(Context(dictionary: [:]))
|
||||
try expect(result) == "1.5"
|
||||
}
|
||||
|
||||
$0.it("checks for underlying nil value correctly") {
|
||||
let template = Template(templateString: "Hello {{ user.name|default:\"anonymous\" }}")
|
||||
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]))
|
||||
@@ -171,123 +224,230 @@ func testFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
describe("join filter") {
|
||||
let template = Template(templateString: "{{ value|join:\", \" }}")
|
||||
func testJoinFilter() {
|
||||
let template = Template(templateString: """
|
||||
{{ value|join:", " }}
|
||||
""")
|
||||
|
||||
$0.it("joins a collection of strings") {
|
||||
it("joins a collection of strings") {
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||
try expect(result) == "One, Two"
|
||||
}
|
||||
|
||||
$0.it("joins a mixed-type collection") {
|
||||
it("joins a mixed-type collection") {
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", 2, true, 10.5, "Five"]]))
|
||||
try expect(result) == "One, 2, true, 10.5, Five"
|
||||
}
|
||||
|
||||
$0.it("can join by non string") {
|
||||
let template = Template(templateString: "{{ value|join:separator }}")
|
||||
it("can join by non string") {
|
||||
let template = Template(templateString: """
|
||||
{{ value|join:separator }}
|
||||
""")
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"], "separator": true]))
|
||||
try expect(result) == "OnetrueTwo"
|
||||
}
|
||||
|
||||
$0.it("can join without arguments") {
|
||||
let template = Template(templateString: "{{ value|join }}")
|
||||
it("can join without arguments") {
|
||||
let template = Template(templateString: """
|
||||
{{ value|join }}
|
||||
""")
|
||||
let result = try template.render(Context(dictionary: ["value": ["One", "Two"]]))
|
||||
try expect(result) == "OneTwo"
|
||||
}
|
||||
}
|
||||
|
||||
describe("split filter") {
|
||||
let template = Template(templateString: "{{ value|split:\", \" }}")
|
||||
func testSplitFilter() {
|
||||
let template = Template(templateString: """
|
||||
{{ value|split:", " }}
|
||||
""")
|
||||
|
||||
$0.it("split a string into array") {
|
||||
it("split a string into array") {
|
||||
let result = try template.render(Context(dictionary: ["value": "One, Two"]))
|
||||
try expect(result) == "[\"One\", \"Two\"]"
|
||||
try expect(result) == """
|
||||
["One", "Two"]
|
||||
"""
|
||||
}
|
||||
|
||||
$0.it("can split without arguments") {
|
||||
let template = Template(templateString: "{{ value|split }}")
|
||||
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\"]"
|
||||
try expect(result) == """
|
||||
["One,", "Two"]
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("filter suggestion") {
|
||||
var template: Template!
|
||||
var filterExtension: Extension!
|
||||
|
||||
func expectedSyntaxError(token: String, template: Template, description: String) -> TemplateSyntaxError {
|
||||
guard let range = template.templateString.range(of: token) else {
|
||||
fatalError("Can't find '\(token)' in '\(template)'")
|
||||
}
|
||||
let rangeLine = template.templateString.rangeLine(range)
|
||||
let sourceMap = SourceMap(filename: template.name, line: rangeLine)
|
||||
let token = Token.block(value: token, at: sourceMap)
|
||||
return TemplateSyntaxError(reason: description, token: token, stackTrace: [])
|
||||
}
|
||||
|
||||
func expectError(reason: String, token: String,
|
||||
file: String = #file, line: Int = #line, function: String = #function) throws {
|
||||
let expectedError = expectedSyntaxError(token: token, template: template, description: reason)
|
||||
let environment = Environment(extensions: [filterExtension])
|
||||
|
||||
let error = try expect(environment.render(template: template, context: [:]),
|
||||
file: file, line: line, function: function).toThrow() as TemplateSyntaxError
|
||||
let reporter = SimpleErrorReporter()
|
||||
try expect(reporter.renderError(error), file: file, line: line, function: function) == reporter.renderError(expectedError)
|
||||
}
|
||||
|
||||
$0.it("made for unknown filter") {
|
||||
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||
|
||||
filterExtension = Extension()
|
||||
func testFilterSuggestion() {
|
||||
it("made for unknown filter") {
|
||||
let template = Template(templateString: "{{ value|unknownFilter }}")
|
||||
let filterExtension = Extension()
|
||||
filterExtension.registerFilter("knownFilter") { value, _ in value }
|
||||
|
||||
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.", token: "value|unknownFilter")
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'unknownFilter'. Found similar filters: 'knownFilter'.",
|
||||
token: "value|unknownFilter",
|
||||
template: template,
|
||||
extension: filterExtension
|
||||
)
|
||||
}
|
||||
|
||||
$0.it("made for multiple similar filters") {
|
||||
template = Template(templateString: "{{ value|lowerFirst }}")
|
||||
|
||||
filterExtension = Extension()
|
||||
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 expectError(reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.", token: "value|lowerFirst")
|
||||
try self.expectError(
|
||||
reason: "Unknown filter 'lowerFirst'. Found similar filters: 'lowerFirstWord', 'lowercase'.",
|
||||
token: "value|lowerFirst",
|
||||
template: template,
|
||||
extension: filterExtension
|
||||
)
|
||||
}
|
||||
|
||||
$0.it("not made when can't find similar filter") {
|
||||
template = Template(templateString: "{{ value|unknownFilter }}")
|
||||
try expectError(reason: "Unknown filter 'unknownFilter'. Found similar filters: 'lowerFirstWord'.", token: "value|unknownFilter")
|
||||
}
|
||||
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
|
||||
"""
|
||||
}
|
||||
|
||||
describe("indent filter") {
|
||||
$0.it("indents content") {
|
||||
let template = Template(templateString: "{{ value|indent:2 }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
||||
try expect(result) == "One\n Two"
|
||||
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
|
||||
"""
|
||||
]))
|
||||
try expect(result) == """
|
||||
One
|
||||
Two
|
||||
"""
|
||||
}
|
||||
|
||||
func testIndentNotEmptyLines() throws {
|
||||
let template = Template(templateString: """
|
||||
{{ value|indent }}
|
||||
""")
|
||||
let result = try template.render(Context(dictionary: [
|
||||
"value": """
|
||||
One
|
||||
|
||||
|
||||
Two
|
||||
|
||||
|
||||
"""
|
||||
]))
|
||||
try expect(result) == """
|
||||
One
|
||||
|
||||
|
||||
Two
|
||||
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
$0.it("can indent with arbitrary character") {
|
||||
let template = Template(templateString: "{{ value|indent:2,\"\t\" }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
||||
try expect(result) == "One\n\t\tTwo"
|
||||
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"
|
||||
}
|
||||
|
||||
$0.it("can indent first line") {
|
||||
let template = Template(templateString: "{{ value|indent:2,\" \",true }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One\nTwo"]))
|
||||
try expect(result) == " One\n Two"
|
||||
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)'")
|
||||
}
|
||||
|
||||
$0.it("does not indent empty lines") {
|
||||
let template = Template(templateString: "{{ value|indent }}")
|
||||
let result = try template.render(Context(dictionary: ["value": "One\n\n\nTwo\n\n"]))
|
||||
try expect(result) == "One\n\n\n Two\n\n"
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import XCTest
|
||||
|
||||
|
||||
func testFilterTag() {
|
||||
describe("Filter Tag") {
|
||||
$0.it("allows you to use a filter") {
|
||||
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"
|
||||
}
|
||||
|
||||
$0.it("allows you to chain filters") {
|
||||
it("allows you to chain filters") {
|
||||
let template = Template(templateString: "{% filter lowercase|capitalize %}TEST{% endfilter %}")
|
||||
let result = try template.render()
|
||||
try expect(result) == "Test"
|
||||
}
|
||||
|
||||
$0.it("errors without a filter") {
|
||||
it("errors without a filter") {
|
||||
let template = Template(templateString: "Some {% filter %}Test{% endfilter %}")
|
||||
try expect(try template.render()).toThrow()
|
||||
}
|
||||
|
||||
$0.it("can render filters with arguments") {
|
||||
it("can render filters with arguments") {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("split", filter: {
|
||||
return ($0 as! String).components(separatedBy: $1[0] as! String)
|
||||
})
|
||||
ext.registerFilter("split") {
|
||||
guard let value = $0 as? String,
|
||||
let argument = $1.first as? String else { return $0 }
|
||||
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]])
|
||||
let result = try env.renderTemplate(string: """
|
||||
{% filter split:","|join:";" %}{{ items|join:"," }}{% endfilter %}
|
||||
""", context: ["items": [1, 2]])
|
||||
try expect(result) == "1;2"
|
||||
}
|
||||
|
||||
$0.it("can render filters with quote as an argument") {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("replace", filter: {
|
||||
print($1[0] as! String)
|
||||
return ($0 as! String).replacingOccurrences(of: $1[0] as! String, with: $1[1] as! String)
|
||||
})
|
||||
let env = Environment(extensions: [ext])
|
||||
let result = try env.renderTemplate(string: "{% filter replace:'\"',\"\" %}{{ items|join:\",\" }}{% endfilter %}", context: ["items": ["\"1\"", "\"2\""]])
|
||||
try expect(result) == "1,2"
|
||||
it("can render filters with quote as an argument") {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("replace") {
|
||||
guard let value = $0 as? String,
|
||||
$1.count == 2,
|
||||
let search = $1.first as? String,
|
||||
let replacement = $1.last as? String else { return $0 }
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,286 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class ForNodeTests: XCTestCase {
|
||||
let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"anyItems": [1, 2, 3] as [Any],
|
||||
"nsItems": NSArray(array: [1, 2, 3]),
|
||||
"emptyItems": [Int](),
|
||||
"dict": [
|
||||
"one": "I",
|
||||
"two": "II"
|
||||
],
|
||||
"tuples": [(1, 2, 3), (4, 5, 6)]
|
||||
])
|
||||
|
||||
func testForNode() {
|
||||
describe("ForNode") {
|
||||
let context = Context(dictionary: [
|
||||
"items": [1, 2, 3],
|
||||
"emptyItems": [Int](),
|
||||
"dict": [
|
||||
"one": "I",
|
||||
"two": "II",
|
||||
],
|
||||
"tuples": [(1, 2, 3), (4, 5, 6)]
|
||||
])
|
||||
|
||||
$0.it("renders the given nodes for each item") {
|
||||
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(context)) == "123"
|
||||
try expect(try node.render(self.context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when no items found item") {
|
||||
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 node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes)
|
||||
try expect(try node.render(context)) == "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
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type Array<Any>") {
|
||||
let any_context = Context(dictionary: [
|
||||
"items": ([1, 2, 3] as [Any])
|
||||
])
|
||||
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
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(any_context)) == "123"
|
||||
"""
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type CountableClosedRange<Int>") {
|
||||
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: [])
|
||||
@@ -46,7 +288,7 @@ func testForNode() {
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
$0.it("renders a context variable of type CountableRange<Int>") {
|
||||
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: [])
|
||||
@@ -54,267 +296,46 @@ func testForNode() {
|
||||
try expect(try node.render(context)) == "123"
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
$0.it("renders a context variable of type NSArray") {
|
||||
let nsarray_context = Context(dictionary: [
|
||||
"items": NSArray(array: [1, 2, 3])
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(nsarray_context)) == "123"
|
||||
}
|
||||
#endif
|
||||
|
||||
$0.it("renders the given nodes while providing if the item is first in the context") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.first")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "1true2false3false"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing if the item is last in the context") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.last")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "1false2false3true"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing item counter") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "112233"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing item counter") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter0")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "102132"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while providing loop length") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.length")]
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [])
|
||||
try expect(try node.render(context)) == "132333"
|
||||
}
|
||||
|
||||
$0.it("renders the given nodes while filtering items using where expression") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item"), VariableNode(variable: "forloop.counter")]
|
||||
let `where` = try parseExpression(components: ["item", ">", "1"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
||||
let node = ForNode(resolvable: Variable("items"), loopVariables: ["item"], nodes: nodes, emptyNodes: [], where: `where`)
|
||||
try expect(try node.render(context)) == "2132"
|
||||
}
|
||||
|
||||
$0.it("renders the given empty nodes when all items filtered out with where expression") {
|
||||
let nodes: [NodeType] = [VariableNode(variable: "item")]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let `where` = try parseExpression(components: ["item", "==", "0"], tokenParser: TokenParser(tokens: [], environment: Environment()), token: .text(value: "", at: .unknown))
|
||||
let node = ForNode(resolvable: Variable("emptyItems"), loopVariables: ["item"], nodes: nodes, emptyNodes: emptyNodes, where: `where`)
|
||||
try expect(try node.render(context)) == "empty"
|
||||
}
|
||||
|
||||
$0.it("can render a filter with spaces") {
|
||||
let templateString = "{% for article in ars | default: a, b , articles %}" +
|
||||
"- {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"articles": [
|
||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||
]
|
||||
])
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "" +
|
||||
"- Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||
"- Memory Management with ARC by Kyle Fuller.\n" +
|
||||
"\n"
|
||||
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.context("given array of tuples") {
|
||||
$0.it("can iterate over all tuple values") {
|
||||
let templateString = "{% for first,second,third in tuples %}" +
|
||||
"{{ first }}, {{ second }}, {{ third }}\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "1, 2, 3\n4, 5, 6\n\n"
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can iterate with less number of variables") {
|
||||
let templateString = "{% for first,second in tuples %}" +
|
||||
"{{ first }}, {{ second }}\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "1, 2\n4, 5\n\n"
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("can use _ to skip variables") {
|
||||
let templateString = "{% for first,_,third in tuples %}" +
|
||||
"{{ first }}, {{ third }}\n" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "1, 3\n4, 6\n\n"
|
||||
try expect(result) == fixture
|
||||
}
|
||||
|
||||
$0.it("throws when number of variables is more than number of tuple values") {
|
||||
let templateString = "{% for key,value,smth in dict %}" +
|
||||
"{% endfor %}\n"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
try expect(template.render(context)).toThrow()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$0.it("can iterate over dictionary") {
|
||||
let templateString = "{% for key, value in dict %}" +
|
||||
"{{ key }}: {{ value }}," +
|
||||
"{% endfor %}"
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
||||
try expect(sortedResult) == ["one: I", "two: II"]
|
||||
}
|
||||
|
||||
$0.it("renders supports iterating over dictionary") {
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "key"),
|
||||
TextNode(text: ","),
|
||||
]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||
let result = try node.render(context)
|
||||
|
||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
||||
try expect(sortedResult) == ["one", "two"]
|
||||
}
|
||||
|
||||
$0.it("renders supports iterating over dictionary") {
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "key"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: ","),
|
||||
]
|
||||
let emptyNodes: [NodeType] = [TextNode(text: "empty")]
|
||||
let node = ForNode(resolvable: Variable("dict"), loopVariables: ["key", "value"], nodes: nodes, emptyNodes: emptyNodes, where: nil)
|
||||
|
||||
let result = try node.render(context)
|
||||
|
||||
let sortedResult = result.characters.split(separator: ",").map(String.init).sorted(by: <)
|
||||
try expect(sortedResult) == ["one=I", "two=II"]
|
||||
}
|
||||
|
||||
$0.it("handles invalid input") {
|
||||
let token = Token.block(value: "for i", at: .unknown)
|
||||
let parser = TokenParser(tokens: [token], environment: Environment())
|
||||
let error = TemplateSyntaxError(reason: "'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.", token: token)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("can iterate over struct properties") {
|
||||
struct MyStruct {
|
||||
let string: String
|
||||
let number: Int
|
||||
}
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"struct": MyStruct(string: "abc", number: 123)
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "property"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n"),
|
||||
]
|
||||
let node = ForNode(resolvable: Variable("struct"), loopVariables: ["property", "value"], nodes: nodes, emptyNodes: [])
|
||||
let result = try node.render(context)
|
||||
|
||||
try expect(result) == "string=abc\nnumber=123\n"
|
||||
}
|
||||
|
||||
$0.it("can iterate tuple items") {
|
||||
let context = Context(dictionary: [
|
||||
"tuple": (one: 1, two: "dva"),
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "label"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n"),
|
||||
]
|
||||
|
||||
let node = ForNode(resolvable: Variable("tuple"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||
let result = try node.render(context)
|
||||
|
||||
try expect(result) == "one=1\ntwo=dva\n"
|
||||
}
|
||||
|
||||
$0.it("can iterate over class properties") {
|
||||
class MyClass {
|
||||
var baseString: String
|
||||
var baseInt: Int
|
||||
init(_ string: String, _ int: Int) {
|
||||
baseString = string
|
||||
baseInt = int
|
||||
}
|
||||
}
|
||||
|
||||
class MySubclass: MyClass {
|
||||
var childString: String
|
||||
init(_ childString: String, _ string: String, _ int: Int) {
|
||||
self.childString = childString
|
||||
super.init(string, int)
|
||||
}
|
||||
}
|
||||
|
||||
let context = Context(dictionary: [
|
||||
"class": MySubclass("child", "base", 1)
|
||||
])
|
||||
|
||||
let nodes: [NodeType] = [
|
||||
VariableNode(variable: "label"),
|
||||
TextNode(text: "="),
|
||||
VariableNode(variable: "value"),
|
||||
TextNode(text: "\n"),
|
||||
]
|
||||
|
||||
let node = ForNode(resolvable: Variable("class"), loopVariables: ["label", "value"], nodes: nodes, emptyNodes: [])
|
||||
let result = try node.render(context)
|
||||
|
||||
try expect(result) == "childString=child\nbaseString=base\nbaseInt=1\n"
|
||||
}
|
||||
|
||||
$0.it("can iterate in range of variables") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private struct MyStruct {
|
||||
let string: String
|
||||
let number: Int
|
||||
}
|
||||
|
||||
fileprivate struct Article {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,287 +1,288 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
private struct SomeType {
|
||||
let value: String? = nil
|
||||
}
|
||||
|
||||
func testIfNode() {
|
||||
describe("IfNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("can parse an if block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 1
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with else block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "else", at: .unknown),
|
||||
.text(value: "false", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 2
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let falseNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with elif block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "elif something", at: .unknown),
|
||||
.text(value: "some", at: .unknown),
|
||||
.block(value: "else", at: .unknown),
|
||||
.text(value: "false", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 3
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let elifNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(elifNode?.text) == "some"
|
||||
|
||||
try expect(conditions?[2].nodes.count) == 1
|
||||
let falseNode = conditions?[2].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with elif block without else") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "elif something", at: .unknown),
|
||||
.text(value: "some", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 2
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let elifNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(elifNode?.text) == "some"
|
||||
}
|
||||
|
||||
$0.it("can parse an if with multiple elif block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "elif something1", at: .unknown),
|
||||
.text(value: "some1", at: .unknown),
|
||||
.block(value: "elif something2", at: .unknown),
|
||||
.text(value: "some2", at: .unknown),
|
||||
.block(value: "else", at: .unknown),
|
||||
.text(value: "false", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 4
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let elifNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(elifNode?.text) == "some1"
|
||||
|
||||
try expect(conditions?[2].nodes.count) == 1
|
||||
let elif2Node = conditions?[2].nodes.first as? TextNode
|
||||
try expect(elif2Node?.text) == "some2"
|
||||
|
||||
try expect(conditions?[3].nodes.count) == 1
|
||||
let falseNode = conditions?[3].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
|
||||
$0.it("can parse an if with complex expression") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value == \"test\" and not name", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.first is IfNode).beTrue()
|
||||
}
|
||||
|
||||
$0.it("can parse an ifnot block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "ifnot value", at: .unknown),
|
||||
.text(value: "false", at: .unknown),
|
||||
.block(value: "else", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IfNode
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 2
|
||||
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
|
||||
try expect(conditions?[1].nodes.count) == 1
|
||||
let falseNode = conditions?[1].nodes.first as? TextNode
|
||||
try expect(falseNode?.text) == "false"
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an if block without an endif") {
|
||||
let tokens: [Token] = [.block(value: "if value", at: .unknown)]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
$0.it("throws an error when parsing an ifnot without an endif") {
|
||||
let tokens: [Token] = [.block(value: "ifnot value", at: .unknown)]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let error = TemplateSyntaxError(reason: "`endif` was not found.", token: tokens.first)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("renders a true expression") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "1"
|
||||
}
|
||||
|
||||
$0.it("renders the first true expression") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "2"
|
||||
}
|
||||
|
||||
$0.it("renders the empty expression when other conditions are falsy") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "3"
|
||||
}
|
||||
|
||||
$0.it("renders empty when no truthy conditions") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == ""
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("supports variable filters in the if expression") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value|uppercase == \"TEST\"", at: .unknown),
|
||||
.text(value: "true", at: .unknown),
|
||||
.block(value: "endif", at: .unknown)
|
||||
]
|
||||
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
|
||||
let result = try renderNodes(nodes, Context(dictionary: ["value": "test"]))
|
||||
try expect(result) == "true"
|
||||
}
|
||||
|
||||
$0.it("evaluates nil properties as false") {
|
||||
final class IfNodeTests: XCTestCase {
|
||||
func testParseIf() {
|
||||
it("can parse an if block") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if instance.value", at: .unknown),
|
||||
.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
|
||||
|
||||
struct SomeType {
|
||||
let value: String? = nil
|
||||
}
|
||||
let result = try renderNodes(nodes, Context(dictionary: ["instance": SomeType()]))
|
||||
try expect(result) == ""
|
||||
let conditions = node?.conditions
|
||||
try expect(conditions?.count) == 1
|
||||
try expect(conditions?[0].nodes.count) == 1
|
||||
let trueNode = conditions?[0].nodes.first as? TextNode
|
||||
try expect(trueNode?.text) == "true"
|
||||
}
|
||||
|
||||
$0.it("supports closed range variables") {
|
||||
it("can parse an if with complex expression") {
|
||||
let tokens: [Token] = [
|
||||
.block(value: "if value in 1...3", at: .unknown),
|
||||
.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: "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(nodes.first is IfNode).beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
try expect(renderNodes(nodes, Context(dictionary: ["value": 3]))) == "true"
|
||||
try expect(renderNodes(nodes, Context(dictionary: ["value": 4]))) == "false"
|
||||
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: StaticExpression(value: true), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "1"
|
||||
}
|
||||
|
||||
it("renders the first true expression") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: true), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "2"
|
||||
}
|
||||
|
||||
it("renders the empty expression when other conditions are falsy") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")]),
|
||||
IfCondition(expression: nil, nodes: [TextNode(text: "3")])
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == "3"
|
||||
}
|
||||
|
||||
it("renders empty when no truthy conditions") {
|
||||
let node = IfNode(conditions: [
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "1")]),
|
||||
IfCondition(expression: StaticExpression(value: false), nodes: [TextNode(text: "2")])
|
||||
])
|
||||
|
||||
try expect(try node.render(Context())) == ""
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,72 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import PathKit
|
||||
import XCTest
|
||||
|
||||
final class IncludeTests: XCTestCase {
|
||||
let path = Path(#file as String) + ".." + "fixtures"
|
||||
lazy var loader = FileSystemLoader(paths: [path])
|
||||
lazy var environment = Environment(loader: loader)
|
||||
|
||||
func testInclude() {
|
||||
describe("Include") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let 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())
|
||||
|
||||
$0.describe("parsing") {
|
||||
$0.it("throws an error when no template is given") {
|
||||
let tokens: [Token] = [ .block(value: "include", at: .unknown) ]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let error = TemplateSyntaxError(reason: """
|
||||
'include' tag requires one argument, the template file to be included. \
|
||||
A second optional argument can be used to specify the context that will \
|
||||
be passed to the included file
|
||||
""", token: tokens.first)
|
||||
try expect(try parser.parse()).toThrow(error)
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
$0.it("can parse a valid include block") {
|
||||
let tokens: [Token] = [ .block(value: "include \"test.html\"", at: .unknown) ]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? IncludeNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.templateName) == Variable("\"test.html\"")
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("throws an error when rendering without a loader") {
|
||||
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
||||
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())
|
||||
} catch {
|
||||
try expect("\(error)") == "Template named `test.html` does not exist. No loaders found"
|
||||
}
|
||||
do {
|
||||
_ = try node.render(Context(environment: self.environment))
|
||||
} catch {
|
||||
try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("throws an error when it cannot find the included template") {
|
||||
let node = IncludeNode(templateName: Variable("\"unknown.html\""), token: .block(value: "", at: .unknown))
|
||||
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!"
|
||||
}
|
||||
|
||||
do {
|
||||
_ = try node.render(Context(environment: environment))
|
||||
} catch {
|
||||
try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue()
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("successfully renders a found included template") {
|
||||
let node = IncludeNode(templateName: Variable("\"test.html\""), token: .block(value: "", at: .unknown))
|
||||
let context = Context(dictionary: ["target": "World"], environment: environment)
|
||||
let value = try node.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
|
||||
$0.it("successfully passes context") {
|
||||
let template = Template(templateString: "{% include \"test.html\" child %}")
|
||||
let context = Context(dictionary: ["child": ["target": "World"]], environment: environment)
|
||||
let value = try template.render(context)
|
||||
try expect(value) == "Hello World!"
|
||||
}
|
||||
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!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
Tests/StencilTests/InheritanceSpec.swift
Normal file
36
Tests/StencilTests/InheritanceSpec.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
import Stencil
|
||||
import XCTest
|
||||
|
||||
final class InheritanceTests: XCTestCase {
|
||||
let path = Path(#file as String) + ".." + "fixtures"
|
||||
lazy var loader = FileSystemLoader(paths: [path])
|
||||
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
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
|
||||
|
||||
func testInheritence() {
|
||||
describe("Inheritence") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.it("can inherit from another template") {
|
||||
let template = try environment.loadTemplate(name: "child.html")
|
||||
try expect(try template.render()) == "Super_Header Child_Header\nChild_Body"
|
||||
}
|
||||
|
||||
$0.it("can inherit from another template inheriting from another template") {
|
||||
let template = try environment.loadTemplate(name: "child-child.html")
|
||||
try expect(try template.render()) == "Super_Header Child_Header Child_Child_Header\nChild_Body"
|
||||
}
|
||||
|
||||
$0.it("can inherit from a template that calls a super block") {
|
||||
let template = try environment.loadTemplate(name: "child-super.html")
|
||||
try expect(try template.render()) == "Header\nChild_Body"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +1,144 @@
|
||||
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()
|
||||
|
||||
func testLexer() {
|
||||
describe("Lexer") {
|
||||
$0.it("can tokenize text") {
|
||||
let lexer = Lexer(templateString: "Hello World")
|
||||
let tokens = lexer.tokenize()
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "Hello World", at: makeSourceMap("Hello World", for: lexer))
|
||||
}
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "Hello World", at: SourceMap(line: ("Hello World", 1, 0)))
|
||||
}
|
||||
func testComment() throws {
|
||||
let lexer = Lexer(templateString: "{# Comment #}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
$0.it("can tokenize a comment") {
|
||||
let lexer = Lexer(templateString: "{# Comment #}")
|
||||
let tokens = lexer.tokenize()
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .comment(value: "Comment", at: makeSourceMap("Comment", for: lexer))
|
||||
}
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .comment(value: "Comment", at: SourceMap(line: ("{# Comment #}", 1, 3)))
|
||||
}
|
||||
func testVariable() throws {
|
||||
let lexer = Lexer(templateString: "{{ Variable }}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
$0.it("can tokenize a variable") {
|
||||
let lexer = Lexer(templateString: "{{ Variable }}")
|
||||
let tokens = lexer.tokenize()
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
||||
}
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .variable(value: "Variable", at: SourceMap(line: ("{{ Variable }}", 1, 3)))
|
||||
}
|
||||
func testTokenWithoutSpaces() throws {
|
||||
let lexer = Lexer(templateString: "{{Variable}}")
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
$0.it("can tokenize unclosed tag by ignoring it") {
|
||||
let templateString = "{{ thing"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .variable(value: "Variable", at: makeSourceMap("Variable", for: lexer))
|
||||
}
|
||||
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "", at: SourceMap(line: ("{{ thing", 1, 0)))
|
||||
}
|
||||
func testUnclosedTag() throws {
|
||||
let templateString = "{{ thing"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
$0.it("can tokenize a mixture of content") {
|
||||
let templateString = "My name is {{ myname }}."
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
try expect(tokens.count) == 1
|
||||
try expect(tokens.first) == .text(value: "", at: makeSourceMap("{{ thing", for: lexer))
|
||||
}
|
||||
|
||||
try expect(tokens.count) == 3
|
||||
try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "My name is ")!)))
|
||||
try expect(tokens[1]) == Token.variable(value: "myname", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "myname")!)))
|
||||
try expect(tokens[2]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!)))
|
||||
}
|
||||
func testContentMixture() throws {
|
||||
let templateString = "My name is {{ myname }}."
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
$0.it("can tokenize two variables without being greedy") {
|
||||
let templateString = "{{ thing }}{{ name }}"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
try expect(tokens.count) == 3
|
||||
try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is ", for: lexer))
|
||||
try expect(tokens[1]) == Token.variable(value: "myname", at: makeSourceMap("myname", for: lexer))
|
||||
try expect(tokens[2]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||
}
|
||||
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == Token.variable(value: "thing", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "thing")!)))
|
||||
try expect(tokens[1]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name")!)))
|
||||
}
|
||||
func testVariablesWithoutBeingGreedy() throws {
|
||||
let templateString = "{{ thing }}{{ name }}"
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
$0.it("can tokenize an unclosed block") {
|
||||
let lexer = Lexer(templateString: "{%}")
|
||||
let _ = lexer.tokenize()
|
||||
}
|
||||
try expect(tokens.count) == 2
|
||||
try expect(tokens[0]) == Token.variable(value: "thing", at: makeSourceMap("thing", for: lexer))
|
||||
try expect(tokens[1]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer))
|
||||
}
|
||||
|
||||
$0.it("can tokenize an empty variable") {
|
||||
let lexer = Lexer(templateString: "{{}}")
|
||||
let _ = lexer.tokenize()
|
||||
}
|
||||
func testUnclosedBlock() throws {
|
||||
let lexer = Lexer(templateString: "{%}")
|
||||
_ = lexer.tokenize()
|
||||
}
|
||||
|
||||
$0.it("can tokenize with new lines") {
|
||||
let templateString =
|
||||
"My name is {%\n" +
|
||||
" if name\n" +
|
||||
" and\n" +
|
||||
" name\n" +
|
||||
"%}{{\n" +
|
||||
"name\n" +
|
||||
"}}{%\n" +
|
||||
"endif %}."
|
||||
func testTokenizeIncorrectSyntaxWithoutCrashing() throws {
|
||||
let lexer = Lexer(templateString: "func some() {{% if %}")
|
||||
_ = lexer.tokenize()
|
||||
}
|
||||
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
func testEmptyVariable() throws {
|
||||
let lexer = Lexer(templateString: "{{}}")
|
||||
_ = lexer.tokenize()
|
||||
}
|
||||
|
||||
let tokens = lexer.tokenize()
|
||||
func testNewlines() throws {
|
||||
let templateString = """
|
||||
My name is {%
|
||||
if name
|
||||
and
|
||||
name
|
||||
%}{{
|
||||
name
|
||||
}}{%
|
||||
endif %}.
|
||||
"""
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let tokens = lexer.tokenize()
|
||||
|
||||
try expect(tokens.count) == 5
|
||||
try expect(tokens[0]) == Token.text(value: "My name is ", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "My name is")!)))
|
||||
try expect(tokens[1]) == Token.block(value: "if name and name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "{%")!)))
|
||||
try expect(tokens[2]) == Token.variable(value: "name", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "name", options: [.backwards])!)))
|
||||
try expect(tokens[3]) == Token.block(value: "endif", at: SourceMap(line: templateString.rangeLine(templateString.range(of: "endif")!)))
|
||||
try expect(tokens[4]) == Token.text(value: ".", at: SourceMap(line: templateString.rangeLine(templateString.range(of: ".")!)))
|
||||
try expect(tokens.count) == 5
|
||||
try expect(tokens[0]) == Token.text(value: "My name is ", at: makeSourceMap("My name is", for: lexer))
|
||||
try expect(tokens[1]) == Token.block(value: "if name and name", at: makeSourceMap("{%", for: lexer))
|
||||
try expect(tokens[2]) == Token.variable(value: "name", at: makeSourceMap("name", for: lexer, options: .backwards))
|
||||
try expect(tokens[3]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
||||
try expect(tokens[4]) == Token.text(value: ".", at: makeSourceMap(".", for: lexer))
|
||||
}
|
||||
|
||||
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]) == Token.text(value: "class Some ", at: makeSourceMap("class Some ", for: lexer))
|
||||
try expect(tokens[1]) == Token.variable(value: "'{'", at: makeSourceMap("'{'", for: lexer))
|
||||
try expect(tokens[2]) == Token.block(value: "if true", at: makeSourceMap("if true", for: lexer))
|
||||
try expect(tokens[3]) == Token.variable(value: "stuff", at: makeSourceMap("stuff", for: lexer))
|
||||
try expect(tokens[4]) == Token.block(value: "endif", at: makeSourceMap("endif", for: lexer))
|
||||
}
|
||||
|
||||
func testPerformance() throws {
|
||||
let path = Path(#file as String) + ".." + "fixtures" + "huge.html"
|
||||
let content: String = try path.read()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
import PathKit
|
||||
import Spectre
|
||||
import Stencil
|
||||
import PathKit
|
||||
import XCTest
|
||||
|
||||
|
||||
func testTemplateLoader() {
|
||||
describe("FileSystemLoader") {
|
||||
let path = Path(#file) + ".." + "fixtures"
|
||||
final class TemplateLoaderTests: XCTestCase {
|
||||
func testFileSystemLoader() {
|
||||
let path = Path(#file as String) + ".." + "fixtures"
|
||||
let loader = FileSystemLoader(paths: [path])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.it("errors when a template cannot be found") {
|
||||
it("errors when a template cannot be found") {
|
||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("errors when an array of templates cannot be found") {
|
||||
it("errors when an array of templates cannot be found") {
|
||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a file") {
|
||||
it("can load a template from a file") {
|
||||
_ = try environment.loadTemplate(name: "test.html")
|
||||
}
|
||||
|
||||
$0.it("errors when loading absolute file outside of the selected path") {
|
||||
it("errors when loading absolute file outside of the selected path") {
|
||||
try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("errors when loading relative file outside of the selected path") {
|
||||
it("errors when loading relative file outside of the selected path") {
|
||||
try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow()
|
||||
}
|
||||
}
|
||||
|
||||
describe("DictionaryLoader") {
|
||||
func testDictionaryLoader() {
|
||||
let loader = DictionaryLoader(templates: [
|
||||
"index.html": "Hello World"
|
||||
])
|
||||
let environment = Environment(loader: loader)
|
||||
|
||||
$0.it("errors when a template cannot be found") {
|
||||
it("errors when a template cannot be found") {
|
||||
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("errors when an array of templates cannot be found") {
|
||||
it("errors when an array of templates cannot be found") {
|
||||
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||
}
|
||||
|
||||
$0.it("can load a template from a known templates") {
|
||||
it("can load a template from a known templates") {
|
||||
_ = try environment.loadTemplate(name: "index.html")
|
||||
}
|
||||
|
||||
$0.it("can load a known template from a collection of templates") {
|
||||
it("can load a known template from a collection of templates") {
|
||||
_ = try environment.loadTemplate(names: ["unknown.html", "index.html"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
|
||||
class ErrorNode : NodeType {
|
||||
class ErrorNode: NodeType {
|
||||
let token: Token?
|
||||
init(token: Token? = nil) {
|
||||
self.token = token
|
||||
@@ -13,53 +13,50 @@ class ErrorNode : NodeType {
|
||||
}
|
||||
}
|
||||
|
||||
final class NodeTests: XCTestCase {
|
||||
let context = Context(dictionary: [
|
||||
"name": "Kyle",
|
||||
"age": 27,
|
||||
"items": [1, 2, 3]
|
||||
])
|
||||
|
||||
func testNode() {
|
||||
describe("Node") {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("TextNode") {
|
||||
$0.it("renders the given text") {
|
||||
let node = TextNode(text: "Hello World")
|
||||
try expect(try node.render(context)) == "Hello World"
|
||||
}
|
||||
func testVariableNode() {
|
||||
it("resolves and renders the variable") {
|
||||
let node = VariableNode(variable: Variable("name"))
|
||||
try expect(try node.render(self.context)) == "Kyle"
|
||||
}
|
||||
|
||||
$0.describe("VariableNode") {
|
||||
$0.it("resolves and renders the variable") {
|
||||
let node = VariableNode(variable: Variable("name"))
|
||||
try expect(try node.render(context)) == "Kyle"
|
||||
}
|
||||
it("resolves and renders a non string variable") {
|
||||
let node = VariableNode(variable: Variable("age"))
|
||||
try expect(try node.render(self.context)) == "27"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("resolves and renders a non string variable") {
|
||||
let node = VariableNode(variable: Variable("age"))
|
||||
try expect(try node.render(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"
|
||||
}
|
||||
|
||||
$0.describe("rendering nodes") {
|
||||
$0.it("renders the nodes") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
]
|
||||
it("correctly throws a nodes failure") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text: "Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
ErrorNode()
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)) == "Hello Kyle"
|
||||
}
|
||||
|
||||
$0.it("correctly throws a nodes failure") {
|
||||
let nodes: [NodeType] = [
|
||||
TextNode(text:"Hello "),
|
||||
VariableNode(variable: "name"),
|
||||
ErrorNode(),
|
||||
]
|
||||
|
||||
try expect(try renderNodes(nodes, context)).toThrow(TemplateSyntaxError("Custom Error"))
|
||||
}
|
||||
try expect(try renderNodes(nodes, self.context)).toThrow(TemplateSyntaxError("Custom Error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,50 @@
|
||||
import Foundation
|
||||
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())
|
||||
|
||||
func testNowNode() {
|
||||
#if !os(Linux)
|
||||
describe("NowNode") {
|
||||
$0.describe("parsing") {
|
||||
$0.it("parses default format without any now arguments") {
|
||||
let tokens: [Token] = [ .block(value: "now", at: .unknown) ]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"yyyy-MM-dd 'at' HH:mm\""
|
||||
}
|
||||
|
||||
$0.it("parses now with a format") {
|
||||
let tokens: [Token] = [ .block(value: "now \"HH:mm\"", at: .unknown) ]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
let nodes = try parser.parse()
|
||||
let node = nodes.first as? NowNode
|
||||
try expect(nodes.count) == 1
|
||||
try expect(node?.format.variable) == "\"HH:mm\""
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
$0.describe("rendering") {
|
||||
$0.it("renders the date") {
|
||||
let node = NowNode(format: Variable("\"yyyy-MM-dd\""))
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
let date = formatter.string(from: NSDate() as Date)
|
||||
|
||||
try expect(try node.render(Context())) == date
|
||||
}
|
||||
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: NSDate() as Date)
|
||||
|
||||
try expect(try node.render(Context())) == date
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
|
||||
func testTokenParser() {
|
||||
describe("TokenParser") {
|
||||
$0.it("can parse a text token") {
|
||||
final class TokenParserTests: XCTestCase {
|
||||
func testTokenParser() {
|
||||
it("can parse a text token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.text(value: "Hello World", at: .unknown)
|
||||
], environment: Environment())
|
||||
@@ -16,7 +16,7 @@ func testTokenParser() {
|
||||
try expect(node?.text) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can parse a variable token") {
|
||||
it("can parse a variable token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.variable(value: "'name'", at: .unknown)
|
||||
], environment: Environment())
|
||||
@@ -28,7 +28,7 @@ func testTokenParser() {
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can parse a comment token") {
|
||||
it("can parse a comment token") {
|
||||
let parser = TokenParser(tokens: [
|
||||
.comment(value: "Secret stuff!", at: .unknown)
|
||||
], environment: Environment())
|
||||
@@ -37,25 +37,28 @@ func testTokenParser() {
|
||||
try expect(nodes.count) == 0
|
||||
}
|
||||
|
||||
$0.it("can parse a tag token") {
|
||||
it("can parse a tag token") {
|
||||
let simpleExtension = Extension()
|
||||
simpleExtension.registerSimpleTag("known") { _ in
|
||||
return ""
|
||||
""
|
||||
}
|
||||
|
||||
let parser = TokenParser(tokens: [
|
||||
.block(value: "known", at: .unknown),
|
||||
.block(value: "known", at: .unknown)
|
||||
], environment: Environment(extensions: [simpleExtension]))
|
||||
|
||||
let nodes = try parser.parse()
|
||||
try expect(nodes.count) == 1
|
||||
}
|
||||
|
||||
$0.it("errors when parsing an unknown tag") {
|
||||
it("errors when parsing an unknown tag") {
|
||||
let tokens: [Token] = [.block(value: "unknown", at: .unknown)]
|
||||
let parser = TokenParser(tokens: tokens, environment: Environment())
|
||||
|
||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError(reason: "Unknown template tag 'unknown'", token: tokens.first))
|
||||
try expect(try parser.parse()).toThrow(TemplateSyntaxError(
|
||||
reason: "Unknown template tag 'unknown'",
|
||||
token: tokens.first)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,67 @@
|
||||
import Spectre
|
||||
import Stencil
|
||||
import XCTest
|
||||
|
||||
|
||||
fileprivate struct CustomNode : NodeType {
|
||||
private struct CustomNode: NodeType {
|
||||
let token: Token?
|
||||
func render(_ context:Context) throws -> String {
|
||||
func render(_ context: Context) throws -> String {
|
||||
return "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct Article {
|
||||
private struct Article {
|
||||
let title: String
|
||||
let author: String
|
||||
}
|
||||
|
||||
|
||||
func testStencil() {
|
||||
describe("Stencil") {
|
||||
final class StencilTests: XCTestCase {
|
||||
lazy var environment: Environment = {
|
||||
let exampleExtension = Extension()
|
||||
|
||||
exampleExtension.registerSimpleTag("simpletag") { context in
|
||||
return "Hello World"
|
||||
exampleExtension.registerSimpleTag("simpletag") { _ in
|
||||
"Hello World"
|
||||
}
|
||||
|
||||
exampleExtension.registerTag("customtag") { parser, token in
|
||||
return CustomNode(token: token)
|
||||
exampleExtension.registerTag("customtag") { _, token in
|
||||
CustomNode(token: token)
|
||||
}
|
||||
return Environment(extensions: [exampleExtension])
|
||||
}()
|
||||
|
||||
let environment = Environment(extensions: [exampleExtension])
|
||||
func testStencil() {
|
||||
it("can render the README example") {
|
||||
let templateString = """
|
||||
There are {{ articles.count }} articles.
|
||||
|
||||
$0.it("can render the README example") {
|
||||
|
||||
let templateString = "There are {{ articles.count }} articles.\n" +
|
||||
"\n" +
|
||||
"{% for article in articles %}" +
|
||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
||||
"{% endfor %}\n"
|
||||
{% 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"),
|
||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller")
|
||||
]
|
||||
]
|
||||
|
||||
let template = Template(templateString: templateString)
|
||||
let result = try template.render(context)
|
||||
|
||||
let fixture = "There are 2 articles.\n" +
|
||||
"\n" +
|
||||
" - Migrating from OCUnit to XCTest by Kyle Fuller.\n" +
|
||||
" - Memory Management with ARC by Kyle Fuller.\n" +
|
||||
"\n"
|
||||
try expect(result) == """
|
||||
There are 2 articles.
|
||||
|
||||
try expect(result) == fixture
|
||||
- Migrating from OCUnit to XCTest by Kyle Fuller.
|
||||
- Memory Management with ARC by Kyle Fuller.
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
$0.it("can render a custom template tag") {
|
||||
let result = try environment.renderTemplate(string: "{% customtag %}")
|
||||
it("can render a custom template tag") {
|
||||
let result = try self.environment.renderTemplate(string: "{% customtag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can render a simple custom tag") {
|
||||
let result = try environment.renderTemplate(string: "{% simpletag %}")
|
||||
it("can render a simple custom tag") {
|
||||
let result = try self.environment.renderTemplate(string: "{% simpletag %}")
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
|
||||
func testTemplate() {
|
||||
describe("Template") {
|
||||
$0.it("can render a template from a string") {
|
||||
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"
|
||||
}
|
||||
|
||||
$0.it("can render a template from a string literal") {
|
||||
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"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
import XCTest
|
||||
|
||||
|
||||
func testToken() {
|
||||
describe("Token") {
|
||||
$0.it("can split the contents into components") {
|
||||
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()
|
||||
let components = token.components
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "world"
|
||||
}
|
||||
|
||||
$0.it("can split the contents into components with single quoted strings") {
|
||||
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()
|
||||
let components = token.components
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
try expect(components[1]) == "'kyle fuller'"
|
||||
}
|
||||
|
||||
$0.it("can split the contents into components with double quoted strings") {
|
||||
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()
|
||||
let components = token.components
|
||||
|
||||
try expect(components.count) == 2
|
||||
try expect(components[0]) == "hello"
|
||||
|
||||
@@ -1,174 +1,227 @@
|
||||
import Foundation
|
||||
import Spectre
|
||||
@testable import Stencil
|
||||
|
||||
import XCTest
|
||||
|
||||
#if os(OSX)
|
||||
@objc class Superclass: NSObject {
|
||||
@objc
|
||||
class Superclass: NSObject {
|
||||
@objc let name = "Foo"
|
||||
}
|
||||
@objc class Object : Superclass {
|
||||
@objc
|
||||
class Object: Superclass {
|
||||
@objc let title = "Hello World"
|
||||
}
|
||||
#endif
|
||||
|
||||
fileprivate struct Person {
|
||||
private struct Person {
|
||||
let name: String
|
||||
}
|
||||
|
||||
fileprivate struct Article {
|
||||
private struct Article {
|
||||
let author: Person
|
||||
}
|
||||
|
||||
fileprivate class WebSite {
|
||||
private class WebSite {
|
||||
let url: String = "blog.com"
|
||||
}
|
||||
|
||||
fileprivate class Blog: WebSite {
|
||||
private class Blog: WebSite {
|
||||
let articles: [Article] = [Article(author: Person(name: "Kyle"))]
|
||||
let featuring: Article? = Article(author: Person(name: "Jhon"))
|
||||
}
|
||||
|
||||
func testVariable() {
|
||||
describe("Variable") {
|
||||
let context = Context(dictionary: [
|
||||
final class VariableTests: XCTestCase {
|
||||
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",
|
||||
"github": "kylef"
|
||||
],
|
||||
"counter": [
|
||||
"count": "kylef",
|
||||
],
|
||||
"count": "kylef"
|
||||
],
|
||||
"article": Article(author: Person(name: "Kyle")),
|
||||
"blog": Blog(),
|
||||
"tuple": (one: 1, two: 2)
|
||||
])
|
||||
|
||||
], environment: environment)
|
||||
#if os(OSX)
|
||||
context["object"] = Object()
|
||||
#endif
|
||||
context["blog"] = Blog()
|
||||
return context
|
||||
}()
|
||||
|
||||
$0.it("can resolve a string literal with double quotes") {
|
||||
func testLiterals() {
|
||||
it("can resolve a string literal with double quotes") {
|
||||
let variable = Variable("\"name\"")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve a string literal with single quotes") {
|
||||
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(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "name"
|
||||
}
|
||||
|
||||
$0.it("can resolve an integer literal") {
|
||||
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(context) as? Int
|
||||
let result = try variable.resolve(self.context) as? Int
|
||||
try expect(result) == 5
|
||||
}
|
||||
|
||||
$0.it("can resolve an float literal") {
|
||||
it("can resolve an float literal") {
|
||||
let variable = Variable("3.14")
|
||||
let result = try variable.resolve(context) as? Number
|
||||
let result = try variable.resolve(self.context) as? Number
|
||||
try expect(result) == 3.14
|
||||
}
|
||||
|
||||
$0.it("can resolve boolean literal") {
|
||||
try expect(Variable("true").resolve(context) as? Bool) == true
|
||||
try expect(Variable("false").resolve(context) as? Bool) == false
|
||||
try expect(Variable("0").resolve(context) as? Int) == 0
|
||||
try expect(Variable("1").resolve(context) as? Int) == 1
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can resolve a string variable") {
|
||||
func testVariable() {
|
||||
it("can resolve a string variable") {
|
||||
let variable = Variable("name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from a dictionary") {
|
||||
func testDictionary() {
|
||||
it("can resolve an item from a dictionary") {
|
||||
let variable = Variable("profiles.github")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "kylef"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from an array via it's index") {
|
||||
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(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Katie"
|
||||
|
||||
let variable1 = Variable("contacts.1")
|
||||
let result1 = try variable1.resolve(context) as? String
|
||||
let result1 = try variable1.resolve(self.context) as? String
|
||||
try expect(result1) == "Carlton"
|
||||
}
|
||||
|
||||
$0.it("can resolve an item from an array via unknown index") {
|
||||
it("can resolve an item from an array via unknown index") {
|
||||
let variable = Variable("contacts.5")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result).to.beNil()
|
||||
|
||||
let variable1 = Variable("contacts.-5")
|
||||
let result1 = try variable1.resolve(context) as? String
|
||||
let result1 = try variable1.resolve(self.context) as? String
|
||||
try expect(result1).to.beNil()
|
||||
}
|
||||
|
||||
$0.it("can resolve the first item from an array") {
|
||||
it("can resolve the first item from an array") {
|
||||
let variable = Variable("contacts.first")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
|
||||
$0.it("can resolve the last item from an array") {
|
||||
it("can resolve the last item from an array") {
|
||||
let variable = Variable("contacts.last")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Carlton"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can resolve a property with reflection") {
|
||||
func testReflection() {
|
||||
it("can resolve a property with reflection") {
|
||||
let variable = Variable("article.author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can get the count of a dictionary") {
|
||||
let variable = Variable("profiles.count")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 1
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
$0.it("can resolve a value via KVO") {
|
||||
let variable = Variable("object.title")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Hello World"
|
||||
}
|
||||
|
||||
$0.it("can resolve a superclass value via KVO") {
|
||||
let variable = Variable("object.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Foo"
|
||||
}
|
||||
#endif
|
||||
|
||||
$0.it("can resolve a value via reflection") {
|
||||
it("can resolve a value via reflection") {
|
||||
let variable = Variable("blog.articles.0.author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
|
||||
$0.it("can resolve a superclass value via reflection") {
|
||||
it("can resolve a superclass value via reflection") {
|
||||
let variable = Variable("blog.url")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "blog.com"
|
||||
}
|
||||
|
||||
$0.it("can resolve optional variable property using reflection") {
|
||||
it("can resolve optional variable property using reflection") {
|
||||
let variable = Variable("blog.featuring.author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
let result = try variable.resolve(self.context) as? String
|
||||
try expect(result) == "Jhon"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("does not render Optional") {
|
||||
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])
|
||||
@@ -176,159 +229,139 @@ func testVariable() {
|
||||
try expect(VariableNode(variable: "values").render(context)) == "[1, nil, [1, nil]]"
|
||||
try expect(VariableNode(variable: "values.1").render(context)) == ""
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can subscript tuple by index") {
|
||||
let variable = Variable("tuple.0")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 1
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can subscript tuple by label") {
|
||||
let variable = Variable("tuple.two")
|
||||
let result = try variable.resolve(context) as? Int
|
||||
try expect(result) == 2
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
$0.describe("Subrscripting") {
|
||||
$0.it("can resolve a property subscript via reflection") {
|
||||
try context.push(dictionary: ["property": "name"]) {
|
||||
let variable = Variable("article.author[property]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can subscript an array with a valid index") {
|
||||
try context.push(dictionary: ["property": 0]) {
|
||||
let variable = Variable("contacts[property]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Katie"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can subscript an array with an unknown index") {
|
||||
try context.push(dictionary: ["property": 5]) {
|
||||
let variable = Variable("contacts[property]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result).to.beNil()
|
||||
}
|
||||
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)
|
||||
$0.it("can resolve a subscript via KVO") {
|
||||
try context.push(dictionary: ["property": "name"]) {
|
||||
let variable = Variable("object[property]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Foo"
|
||||
}
|
||||
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
|
||||
|
||||
$0.it("can resolve an optional subscript via reflection") {
|
||||
try context.push(dictionary: ["property": "featuring"]) {
|
||||
let variable = Variable("blog[property].author.name")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Jhon"
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can resolve multiple subscripts") {
|
||||
try context.push(dictionary: [
|
||||
"prop1": "articles",
|
||||
"prop2": 0,
|
||||
"prop3": "name"
|
||||
]) {
|
||||
let variable = Variable("blog[prop1][prop2].author[prop3]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("can resolve nested subscripts") {
|
||||
try context.push(dictionary: [
|
||||
"prop1": "prop2",
|
||||
"ref": ["prop2": "name"]
|
||||
]) {
|
||||
let variable = Variable("article.author[ref[prop1]]")
|
||||
let result = try variable.resolve(context) as? String
|
||||
try expect(result) == "Kyle"
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
$0.it("throws for invalid keypath syntax") {
|
||||
try context.push(dictionary: ["prop": "name"]) {
|
||||
let samples = [
|
||||
".",
|
||||
"..",
|
||||
".test",
|
||||
"test..test",
|
||||
"[prop]",
|
||||
"article.author[prop",
|
||||
"article.author[[prop]",
|
||||
"article.author[prop]]",
|
||||
"article.author[]",
|
||||
"article.author[[]]",
|
||||
"article.author[prop][]",
|
||||
"article.author[prop]comments",
|
||||
"article.author[.]"
|
||||
]
|
||||
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(context)).toThrow()
|
||||
}
|
||||
for lookup in samples {
|
||||
let variable = Variable(lookup)
|
||||
try expect(variable.resolve(self.context)).toThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("RangeVariable") {
|
||||
|
||||
let context: Context = {
|
||||
let ext = Extension()
|
||||
ext.registerFilter("incr", filter: { (arg: Any?) in toNumber(value: arg!)! + 1 })
|
||||
let environment = Environment(extensions: [ext])
|
||||
return Context(dictionary: [:], environment: environment)
|
||||
}()
|
||||
|
||||
func testRangeVariable() {
|
||||
func makeVariable(_ token: String) throws -> RangeVariable? {
|
||||
let token = Token.variable(value: token, at: .unknown)
|
||||
let parser = TokenParser(tokens: [token], environment: context.environment)
|
||||
return try RangeVariable(token.contents, parser: parser, containedIn: token)
|
||||
return try RangeVariable(token.contents, environment: context.environment, containedIn: token)
|
||||
}
|
||||
|
||||
$0.it("can resolve closed range as array") {
|
||||
let result = try makeVariable("1...3")?.resolve(context) as? [Int]
|
||||
it("can resolve closed range as array") {
|
||||
let result = try makeVariable("1...3")?.resolve(self.context) as? [Int]
|
||||
try expect(result) == [1, 2, 3]
|
||||
}
|
||||
|
||||
$0.it("can resolve decreasing closed range as reversed array") {
|
||||
let result = try makeVariable("3...1")?.resolve(context) as? [Int]
|
||||
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]
|
||||
}
|
||||
|
||||
$0.it("can use filter on range variables") {
|
||||
let result = try makeVariable("1|incr...3|incr")?.resolve(context) as? [Int]
|
||||
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]
|
||||
}
|
||||
|
||||
$0.it("throws when left value is not int") {
|
||||
it("throws when left value is not int") {
|
||||
let template: Template = "{% for i in k...j %}{{ i }}{% endfor %}"
|
||||
try expect(try template.render(Context(dictionary: ["j": 3, "k": "1"]))).toThrow()
|
||||
}
|
||||
|
||||
$0.it("throws when right value is not int") {
|
||||
it("throws when right value is not int") {
|
||||
let variable = try makeVariable("k...j")
|
||||
try expect(try variable?.resolve(Context(dictionary: ["j": "3", "k": 1]))).toThrow()
|
||||
}
|
||||
|
||||
$0.it("throws is left range value is missing") {
|
||||
it("throws is left range value is missing") {
|
||||
try expect(makeVariable("...1")).toThrow()
|
||||
}
|
||||
|
||||
$0.it("throws is right range value is missing") {
|
||||
it("throws is right range value is missing") {
|
||||
try expect(makeVariable("1...")).toThrow()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import XCTest
|
||||
|
||||
|
||||
public func stencilTests() {
|
||||
testContext()
|
||||
testFilter()
|
||||
testLexer()
|
||||
testToken()
|
||||
testTokenParser()
|
||||
testTemplateLoader()
|
||||
testTemplate()
|
||||
testVariable()
|
||||
testNode()
|
||||
testForNode()
|
||||
testExpressions()
|
||||
testIfNode()
|
||||
testNowNode()
|
||||
testInclude()
|
||||
testInheritence()
|
||||
testFilterTag()
|
||||
testEnvironment()
|
||||
testStencil()
|
||||
}
|
||||
|
||||
|
||||
class StencilTests: XCTestCase {
|
||||
func testRunStencilTests() {
|
||||
stencilTests()
|
||||
}
|
||||
}
|
||||
228
Tests/StencilTests/XCTestManifests.swift
Normal file
228
Tests/StencilTests/XCTestManifests.swift
Normal file
@@ -0,0 +1,228 @@
|
||||
import XCTest
|
||||
|
||||
extension ContextTests {
|
||||
static let __allTests = [
|
||||
("testContextRestoration", testContextRestoration),
|
||||
("testContextSubscripting", testContextSubscripting),
|
||||
]
|
||||
}
|
||||
|
||||
extension EnvironmentBaseAndChildTemplateTests {
|
||||
static let __allTests = [
|
||||
("testRuntimeErrorInBaseTemplate", testRuntimeErrorInBaseTemplate),
|
||||
("testRuntimeErrorInChildTemplate", testRuntimeErrorInChildTemplate),
|
||||
("testSyntaxErrorInBaseTemplate", testSyntaxErrorInBaseTemplate),
|
||||
("testSyntaxErrorInChildTemplate", testSyntaxErrorInChildTemplate),
|
||||
]
|
||||
}
|
||||
|
||||
extension EnvironmentIncludeTemplateTests {
|
||||
static let __allTests = [
|
||||
("testRuntimeError", testRuntimeError),
|
||||
("testSyntaxError", testSyntaxError),
|
||||
]
|
||||
}
|
||||
|
||||
extension EnvironmentTests {
|
||||
static let __allTests = [
|
||||
("testLoading", testLoading),
|
||||
("testRendering", testRendering),
|
||||
("testRenderingError", testRenderingError),
|
||||
("testSyntaxError", testSyntaxError),
|
||||
("testUnknownFilter", testUnknownFilter),
|
||||
]
|
||||
}
|
||||
|
||||
extension ExpressionsTests {
|
||||
static let __allTests = [
|
||||
("testAndExpression", testAndExpression),
|
||||
("testEqualityExpression", testEqualityExpression),
|
||||
("testExpressionParsing", testExpressionParsing),
|
||||
("testFalseExpressions", testFalseExpressions),
|
||||
("testFalseInExpression", testFalseInExpression),
|
||||
("testInequalityExpression", testInequalityExpression),
|
||||
("testLessThanEqualExpression", testLessThanEqualExpression),
|
||||
("testLessThanExpression", testLessThanExpression),
|
||||
("testMoreThanEqualExpression", testMoreThanEqualExpression),
|
||||
("testMoreThanExpression", testMoreThanExpression),
|
||||
("testMultipleExpressions", testMultipleExpressions),
|
||||
("testNotExpression", testNotExpression),
|
||||
("testOrExpression", testOrExpression),
|
||||
("testTrueExpressions", testTrueExpressions),
|
||||
("testTrueInExpression", testTrueInExpression),
|
||||
]
|
||||
}
|
||||
|
||||
extension FilterTagTests {
|
||||
static let __allTests = [
|
||||
("testFilterTag", testFilterTag),
|
||||
]
|
||||
}
|
||||
|
||||
extension FilterTests {
|
||||
static let __allTests = [
|
||||
("testDefaultFilter", testDefaultFilter),
|
||||
("testDynamicFilters", testDynamicFilters),
|
||||
("testFilterSuggestion", testFilterSuggestion),
|
||||
("testIndentContent", testIndentContent),
|
||||
("testIndentFirstLine", testIndentFirstLine),
|
||||
("testIndentNotEmptyLines", testIndentNotEmptyLines),
|
||||
("testIndentWithArbitraryCharacter", testIndentWithArbitraryCharacter),
|
||||
("testJoinFilter", testJoinFilter),
|
||||
("testRegistration", testRegistration),
|
||||
("testRegistrationOverrideDefault", testRegistrationOverrideDefault),
|
||||
("testRegistrationWithArguments", testRegistrationWithArguments),
|
||||
("testSplitFilter", testSplitFilter),
|
||||
("testStringFilters", testStringFilters),
|
||||
("testStringFiltersWithArrays", testStringFiltersWithArrays),
|
||||
]
|
||||
}
|
||||
|
||||
extension ForNodeTests {
|
||||
static let __allTests = [
|
||||
("testArrayOfTuples", testArrayOfTuples),
|
||||
("testForNode", testForNode),
|
||||
("testHandleInvalidInput", testHandleInvalidInput),
|
||||
("testIterateDictionary", testIterateDictionary),
|
||||
("testIterateRange", testIterateRange),
|
||||
("testIterateUsingMirroring", testIterateUsingMirroring),
|
||||
("testLoopMetadata", testLoopMetadata),
|
||||
("testWhereExpression", testWhereExpression),
|
||||
]
|
||||
}
|
||||
|
||||
extension IfNodeTests {
|
||||
static let __allTests = [
|
||||
("testEvaluatesNilAsFalse", testEvaluatesNilAsFalse),
|
||||
("testParseIf", testParseIf),
|
||||
("testParseIfnot", testParseIfnot),
|
||||
("testParseIfWithElif", testParseIfWithElif),
|
||||
("testParseIfWithElifWithoutElse", testParseIfWithElifWithoutElse),
|
||||
("testParseIfWithElse", testParseIfWithElse),
|
||||
("testParseMultipleElif", testParseMultipleElif),
|
||||
("testParsingErrors", testParsingErrors),
|
||||
("testRendering", testRendering),
|
||||
("testSupportsRangeVariables", testSupportsRangeVariables),
|
||||
("testSupportVariableFilters", testSupportVariableFilters),
|
||||
]
|
||||
}
|
||||
|
||||
extension IncludeTests {
|
||||
static let __allTests = [
|
||||
("testParsing", testParsing),
|
||||
("testRendering", testRendering),
|
||||
]
|
||||
}
|
||||
|
||||
extension InheritanceTests {
|
||||
static let __allTests = [
|
||||
("testInheritance", testInheritance),
|
||||
]
|
||||
}
|
||||
|
||||
extension LexerTests {
|
||||
static let __allTests = [
|
||||
("testComment", testComment),
|
||||
("testContentMixture", testContentMixture),
|
||||
("testEmptyVariable", testEmptyVariable),
|
||||
("testEscapeSequence", testEscapeSequence),
|
||||
("testNewlines", testNewlines),
|
||||
("testPerformance", testPerformance),
|
||||
("testText", testText),
|
||||
("testTokenizeIncorrectSyntaxWithoutCrashing", testTokenizeIncorrectSyntaxWithoutCrashing),
|
||||
("testTokenWithoutSpaces", testTokenWithoutSpaces),
|
||||
("testUnclosedBlock", testUnclosedBlock),
|
||||
("testUnclosedTag", testUnclosedTag),
|
||||
("testVariable", testVariable),
|
||||
("testVariablesWithoutBeingGreedy", testVariablesWithoutBeingGreedy),
|
||||
]
|
||||
}
|
||||
|
||||
extension NodeTests {
|
||||
static let __allTests = [
|
||||
("testRendering", testRendering),
|
||||
("testTextNode", testTextNode),
|
||||
("testVariableNode", testVariableNode),
|
||||
]
|
||||
}
|
||||
|
||||
extension NowNodeTests {
|
||||
static let __allTests = [
|
||||
("testParsing", testParsing),
|
||||
("testRendering", testRendering),
|
||||
]
|
||||
}
|
||||
|
||||
extension StencilTests {
|
||||
static let __allTests = [
|
||||
("testStencil", testStencil),
|
||||
]
|
||||
}
|
||||
|
||||
extension TemplateLoaderTests {
|
||||
static let __allTests = [
|
||||
("testDictionaryLoader", testDictionaryLoader),
|
||||
("testFileSystemLoader", testFileSystemLoader),
|
||||
]
|
||||
}
|
||||
|
||||
extension TemplateTests {
|
||||
static let __allTests = [
|
||||
("testTemplate", testTemplate),
|
||||
]
|
||||
}
|
||||
|
||||
extension TokenParserTests {
|
||||
static let __allTests = [
|
||||
("testTokenParser", testTokenParser),
|
||||
]
|
||||
}
|
||||
|
||||
extension TokenTests {
|
||||
static let __allTests = [
|
||||
("testToken", testToken),
|
||||
]
|
||||
}
|
||||
|
||||
extension VariableTests {
|
||||
static let __allTests = [
|
||||
("testArray", testArray),
|
||||
("testDictionary", testDictionary),
|
||||
("testKVO", testKVO),
|
||||
("testLiterals", testLiterals),
|
||||
("testMultipleSubscripting", testMultipleSubscripting),
|
||||
("testOptional", testOptional),
|
||||
("testRangeVariable", testRangeVariable),
|
||||
("testReflection", testReflection),
|
||||
("testSubscripting", testSubscripting),
|
||||
("testTuple", testTuple),
|
||||
("testVariable", testVariable),
|
||||
]
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
public func __allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(ContextTests.__allTests),
|
||||
testCase(EnvironmentBaseAndChildTemplateTests.__allTests),
|
||||
testCase(EnvironmentIncludeTemplateTests.__allTests),
|
||||
testCase(EnvironmentTests.__allTests),
|
||||
testCase(ExpressionsTests.__allTests),
|
||||
testCase(FilterTagTests.__allTests),
|
||||
testCase(FilterTests.__allTests),
|
||||
testCase(ForNodeTests.__allTests),
|
||||
testCase(IfNodeTests.__allTests),
|
||||
testCase(IncludeTests.__allTests),
|
||||
testCase(InheritanceTests.__allTests),
|
||||
testCase(LexerTests.__allTests),
|
||||
testCase(NodeTests.__allTests),
|
||||
testCase(NowNodeTests.__allTests),
|
||||
testCase(StencilTests.__allTests),
|
||||
testCase(TemplateLoaderTests.__allTests),
|
||||
testCase(TemplateTests.__allTests),
|
||||
testCase(TokenParserTests.__allTests),
|
||||
testCase(TokenTests.__allTests),
|
||||
testCase(VariableTests.__allTests),
|
||||
]
|
||||
}
|
||||
#endif
|
||||
1131
Tests/StencilTests/fixtures/huge.html
Normal file
1131
Tests/StencilTests/fixtures/huge.html
Normal file
File diff suppressed because it is too large
Load Diff
2
docs/_templates/sidebar_intro.html
vendored
2
docs/_templates/sidebar_intro.html
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
<p>
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=kylef&repo=Stencil&type=watch&count=true&size=large"
|
||||
src="https://ghbtns.com/github-btn.html?user=stencilproject&repo=Stencil&type=watch&count=true&size=large"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px">
|
||||
</iframe>
|
||||
</p>
|
||||
|
||||
@@ -149,6 +149,19 @@ Will be treated as:
|
||||
|
||||
one or (two and three)
|
||||
|
||||
You can use parentheses to change operator precedence. For example:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% if (one or two) and three %}
|
||||
|
||||
Will be treated as:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
(one or two) and three
|
||||
|
||||
|
||||
``==`` operator
|
||||
"""""""""""""""
|
||||
|
||||
@@ -373,3 +386,13 @@ Filter accepts several arguments:
|
||||
* indentation character: character to be used for indentation. Default is a space.
|
||||
* indent first line: whether first line of output should be indented or not. Default is ``false``.
|
||||
|
||||
``filter``
|
||||
~~~~~~~~~
|
||||
|
||||
Applies the filter with the name provided as an argument to the current expression.
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ string|filter:myfilter }}
|
||||
|
||||
This expression will resolve the `myfilter` variable, find a filter named the same as resolved value, and will apply it to the `string` variable. I.e. if `myfilter` variable resolves to string `uppercase` this expression will apply file `uppercase` to `string` variable.
|
||||
|
||||
@@ -58,9 +58,9 @@ author = 'Kyle Fuller'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.12.1'
|
||||
version = '0.14.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.12.1'
|
||||
release = '0.14.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@@ -48,6 +48,17 @@ Registering custom filters with arguments:
|
||||
return value
|
||||
}
|
||||
|
||||
Registering custom boolean filters:
|
||||
|
||||
.. code-block:: swift
|
||||
|
||||
ext.registerFilter("ordinary", negativeFilterName: "odd") { (value: Any?) in
|
||||
if let value = value as? Int {
|
||||
return myInt % 2 == 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Custom Tags
|
||||
-----------
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ feel right at home with Stencil.
|
||||
]
|
||||
|
||||
let environment = Environment(loader: FileSystemLoader(paths: ["templates/"])
|
||||
let rendered = try environment.renderTemplate(name: context)
|
||||
let rendered = try environment.renderTemplate(name: "articles.html", context: context)
|
||||
|
||||
print(rendered)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ dependencies inside ``Package.swift``.
|
||||
let package = Package(
|
||||
name: "MyApplication",
|
||||
dependencies: [
|
||||
.Package(url: "https://github.com/kylef/Stencil.git", majorVersion: 0, minor: 8),
|
||||
.Package(url: "https://github.com/stencilproject/Stencil.git", majorVersion: 0, minor: 13),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ If you're using CocoaPods, you can add Stencil to your ``Podfile`` and then run
|
||||
|
||||
.. code-block:: ruby
|
||||
|
||||
pod 'Stencil', '~> 0.8.0'
|
||||
pod 'Stencil', '~> 0.14.1'
|
||||
|
||||
Carthage
|
||||
--------
|
||||
@@ -37,7 +37,7 @@ Carthage
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
github "kylef/Stencil" ~> 0.8.0
|
||||
github "stencilproject/Stencil" ~> 0.14.1
|
||||
|
||||
2) Checkout your dependencies, generate the Stencil Xcode project, and then use Carthage to build Stencil:
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ following lookup:
|
||||
|
||||
- Context lookup
|
||||
- Dictionary lookup
|
||||
- Array lookup (first, last, count, index)
|
||||
- Array and string lookup (first, last, count, by index)
|
||||
- Key value coding lookup
|
||||
- Type introspection
|
||||
- Type introspection (via ``Mirror``)
|
||||
|
||||
For example, if `people` was an array:
|
||||
|
||||
|
||||
34
rakelib/changelog.rake
Normal file
34
rakelib/changelog.rake
Normal file
@@ -0,0 +1,34 @@
|
||||
NEW_CHANGELOG_SECTION = "## Master\n" + ['Breaking', 'Enhancements', 'Deprecations', 'Bug Fixes', 'Internal Changes'].map do |s|
|
||||
<<~MARKDOWN
|
||||
|
||||
### #{s}
|
||||
|
||||
_None_
|
||||
MARKDOWN
|
||||
end.join
|
||||
|
||||
def changelog_first_section
|
||||
content = []
|
||||
section_count = 0
|
||||
File.foreach(CHANGELOG_FILE) do |line|
|
||||
section_count += 1 if line.start_with?('## ')
|
||||
break if section_count > 1
|
||||
content.append(line) if section_count == 1
|
||||
end
|
||||
content[1..].join
|
||||
end
|
||||
|
||||
namespace :changelog do
|
||||
# rake changelog:reset
|
||||
desc "Add a new empty section at the top of the changelog and git push it"
|
||||
task :reset do
|
||||
header "Reset CHANGELOG"
|
||||
content = File.read(CHANGELOG_FILE)
|
||||
new_content = NEW_CHANGELOG_SECTION + "\n" + content
|
||||
File.write(CHANGELOG_FILE, new_content)
|
||||
|
||||
sh("git", "add", CHANGELOG_FILE)
|
||||
sh("git", "commit", "-m", "Reset CHANGELOG")
|
||||
sh("git", "push")
|
||||
end
|
||||
end
|
||||
52
rakelib/github.rake
Normal file
52
rakelib/github.rake
Normal file
@@ -0,0 +1,52 @@
|
||||
require 'octokit'
|
||||
|
||||
def repo_slug
|
||||
url_parts = `git remote get-url origin`.chomp.split(%r{/|:})
|
||||
last_two_parts = url_parts[-2..-1].join('/')
|
||||
last_two_parts.gsub(/\.git$/, '')
|
||||
end
|
||||
|
||||
def github_client
|
||||
Octokit::Client.new(:netrc => true)
|
||||
end
|
||||
|
||||
namespace :github do
|
||||
# rake github:create_release_pr[version]
|
||||
task :create_release_pr, [:version] do |_, args|
|
||||
version = args[:version]
|
||||
branch = release_branch(version)
|
||||
|
||||
title = "Release #{version}"
|
||||
body = <<~BODY
|
||||
This PR prepares the release for version #{version}.
|
||||
|
||||
Once the PR is merged into master, run `bundle exec rake release:finish` to tag and push to trunk.
|
||||
BODY
|
||||
|
||||
header "Opening PR"
|
||||
res = github_client.create_pull_request(repo_slug, "master", branch, title, body)
|
||||
info "Pull request created: #{res['html_url']}"
|
||||
end
|
||||
|
||||
# rake github:tag
|
||||
task :tag do
|
||||
tag = current_pod_version
|
||||
sh("git", "tag", tag)
|
||||
sh("git", "push", "origin", tag)
|
||||
end
|
||||
|
||||
# rake github:create_release
|
||||
task :create_release do
|
||||
tag_name = current_pod_version
|
||||
title = tag_name
|
||||
body = changelog_first_section()
|
||||
res = github_client.create_release(repo_slug, tag_name, name: title, body: body)
|
||||
info "GitHub Release created: #{res['html_url']}"
|
||||
end
|
||||
|
||||
# rake github:pull_master
|
||||
task :pull_master do
|
||||
sh("git", "switch", "master")
|
||||
sh("git", "pull")
|
||||
end
|
||||
end
|
||||
21
rakelib/pod.rake
Normal file
21
rakelib/pod.rake
Normal file
@@ -0,0 +1,21 @@
|
||||
require 'json'
|
||||
|
||||
def current_pod_version
|
||||
JSON.parse(File.read(PODSPEC_FILE))['version']
|
||||
end
|
||||
|
||||
namespace :pod do
|
||||
# rake pod:lint
|
||||
desc "Lint the podspec"
|
||||
task :lint do
|
||||
header "Linting podspec"
|
||||
sh("pod", "lib", "lint", PODSPEC_FILE)
|
||||
end
|
||||
|
||||
# rake pod:push
|
||||
desc "Push the podspec to trunk"
|
||||
task :push do
|
||||
header "Pushing podspec to trunk"
|
||||
sh("pod", "trunk", "push", PODSPEC_FILE)
|
||||
end
|
||||
end
|
||||
67
rakelib/release.rake
Normal file
67
rakelib/release.rake
Normal file
@@ -0,0 +1,67 @@
|
||||
require 'json'
|
||||
|
||||
namespace :release do
|
||||
|
||||
# rake release:new
|
||||
desc "Ask for a version number and prepare a release PR for that version"
|
||||
task :new do
|
||||
info "Current version is: #{current_pod_version}"
|
||||
print "What version do you want to release? "
|
||||
new_version = STDIN.gets.chomp
|
||||
|
||||
Rake::Task['release:start'].invoke(new_version)
|
||||
end
|
||||
|
||||
# rake release:start[version]
|
||||
desc "Start a release by creating a PR with the required changes to bump the version"
|
||||
task :start, [:version] => ['release:create_branch', 'release:update_files', 'pod:lint', 'release:push_branch', 'github:create_release_pr', 'github:pull_master']
|
||||
|
||||
# rake release:finish[version]
|
||||
desc "Finish a release after the PR has been merged, by tagging master and pushing to trunk"
|
||||
task :finish => ['github:pull_master', 'github:tag', 'pod:push', 'github:create_release', 'changelog:reset']
|
||||
|
||||
|
||||
### Helper tasks ###
|
||||
|
||||
# rake release:create_branch[version]
|
||||
task :create_branch, [:version] do |_, args|
|
||||
branch = release_branch(args[:version])
|
||||
|
||||
header "Creating release branch"
|
||||
sh("git", "checkout", "-b", branch)
|
||||
end
|
||||
|
||||
# rake release:update_files[version]
|
||||
task :update_files, [:version] do |_, args|
|
||||
version = args[:version]
|
||||
|
||||
header "Updating files for version #{version}"
|
||||
|
||||
podspec = JSON.parse(File.read(PODSPEC_FILE))
|
||||
podspec['version'] = version
|
||||
podspec['source']['tag'] = version
|
||||
File.write(PODSPEC_FILE, JSON.pretty_generate(podspec) + "\n")
|
||||
|
||||
replace(CHANGELOG_FILE, '## Master' => "\#\# #{version}")
|
||||
replace("docs/conf.py",
|
||||
/^version = .*/ => %Q(version = '#{version}'),
|
||||
/^release = .*/ => %Q(release = '#{version}')
|
||||
)
|
||||
replace("docs/installation.rst",
|
||||
/pod 'Stencil', '.*'/ => %Q(pod 'Stencil', '~> #{version}'),
|
||||
/github "stencilproject\/Stencil" ~> .*/ => %Q(github "stencilproject/Stencil" ~> #{version})
|
||||
)
|
||||
|
||||
## Commit Changes
|
||||
sh("git", "add", PODSPEC_FILE, CHANGELOG_FILE, "docs/*")
|
||||
sh("git", "commit", "-m", "Version #{version}")
|
||||
end
|
||||
|
||||
# rake release:push_branch[version]
|
||||
task :push_branch, [:version] do |_, args|
|
||||
branch = release_branch(args[:version])
|
||||
|
||||
header "Pushing #{branch} to origin"
|
||||
sh("git", "push", "-u", "origin", branch)
|
||||
end
|
||||
end
|
||||
28
rakelib/utils.rake
Normal file
28
rakelib/utils.rake
Normal file
@@ -0,0 +1,28 @@
|
||||
def colorize(string, *codes)
|
||||
if `tput colors`.chomp.to_i >= 8
|
||||
code = codes.join(';')
|
||||
puts "\e[#{code}m" + string + "\e[0m"
|
||||
else
|
||||
puts string
|
||||
end
|
||||
end
|
||||
|
||||
def header(title)
|
||||
puts colorize("==> #{title}...", 1, 32) # bold, green
|
||||
end
|
||||
|
||||
def info(string)
|
||||
puts colorize(string, 34) # blue
|
||||
end
|
||||
|
||||
def release_branch(version)
|
||||
"release/#{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
|
||||
Reference in New Issue
Block a user