114 Commits

Author SHA1 Message Date
David Jennes
973e190edf Merge pull request #307 from stencilproject/release/0.14.1
Release 0.14.1
2021-04-11 16:26:15 +02:00
David Jennes
e134aafe7f Version 0.14.1 2021-04-11 00:17:27 +02:00
David Jennes
88fd776a02 Merge pull request #306 from lkuczborski/variable-crash-fix
Fix for crashing range indexes when variable length is 1
2021-04-10 18:08:27 +02:00
Łukasz Kuczborski
8480648bd3 Fixed changelog entry 2021-04-10 05:57:40 +02:00
Łukasz Kuczborski
521a599a60 Fixed logic and tests 2021-04-09 23:37:54 +02:00
Łukasz Kuczborski
371a4737d9 Fixed missing braces 2021-04-09 23:27:00 +02:00
Łukasz Kuczborski
61919c5e8e PR fixes 2021-04-09 23:21:46 +02:00
Łukasz Kuczborski
7c635975d1 Fix for crashing range indexes when variable length is 1 2021-04-09 22:51:55 +02:00
David Jennes
fd107355c2 Merge pull request #305 from danpalmer/patch-1
Fix build warning
2021-02-07 21:09:47 +01:00
Dan Palmer
f5f85d95a9 Fix build warning
`components` is not mutated, so it can be a `let`. This fixes the build warning that otherwise shows up in build logs.
2021-01-18 17:33:13 +00:00
Olivier Halligon
22440c5369 Reset CHANGELOG 2020-08-17 20:45:21 +02:00
Olivier Halligon
94197b3adb typo in release script 2020-08-17 20:43:05 +02:00
Olivier Halligon
e93b33423b Release 0.14.0 (#300)
* Introduce script to automate release

* Rakefile levelup

* Version 0.14.0

* Fix newline at end of podspec.json

* Ensure we start and end on master branch

And that we pull latest master before tagging

* CRLF at EOF

* Remove [:version] param from `release:finish` task

It can be guessed from the current podspec version

* Fix create_release task

* Ensure we run rake via bundle exec

Co-authored-by: David Jennes <djbe@users.noreply.github.com>

Co-authored-by: David Jennes <djbe@users.noreply.github.com>
2020-08-17 20:42:00 +02:00
David Jennes
19646bcddf Update SwiftLint to 0.39.2 (#295)
* Update SwiftLint to 0.39.2

* Enable a bunch of extra rules

* Fix all warnings

* Ignore this Xcode generated folder

Co-authored-by: Olivier Halligon <olivier@halligon.net>
2020-08-12 22:52:00 +02:00
Marek Fořt
a84cd3d877 Update PathKit. (#297) 2020-08-12 21:13:45 +02:00
David Jennes
124df01d3c Merge pull request #294 from stencilproject/feature/drop-swift-4
Drop Swift 4 support
2020-05-29 02:14:43 +02:00
David Jennes
0f1286c032 Forgot to remove v4 from Package.swift 2020-05-29 01:00:48 +02:00
David Jennes
9a61aa48e3 Update podspec 2020-05-27 01:09:55 +02:00
David Jennes
520f27be65 Add changelog entry 2020-05-27 01:02:56 +02:00
David Jennes
306d97b638 Update package.swift 2020-05-27 00:58:36 +02:00
David Jennes
386e9d0234 Remove unneeded backports 2020-05-27 00:58:28 +02:00
David Jennes
0e116b6202 Remove old swift versions from travis 2020-05-27 00:55:51 +02:00
Michael Zuccarino
9c3468e300 Public Context initialization (#280)
Co-authored-by: Ilya Puchka <ilyapuchka@gmail.com>
2020-01-12 15:39:04 +00:00
Andreas Ley
a1718ae350 Fix for incorrect tokenization due to index difference of Unicode character/scalar (#286)
* Fix: `Scanner` now uses indices of the respective UnicodeScalarView

* Fix: `Scanner` now uses indices of the respective UnicodeScalarView

* Extended test for Unicode `Combining Diaeresis`

* Fixed test for combining diaeresis

* Inlined template for testing Unicode combining diaeresis

Co-authored-by: Ilya Puchka <ilyapuchka@gmail.com>
2020-01-12 15:21:02 +00:00
Yonas Kolb
5b2d5dc5e0 Merge pull request #288 from samsonjs/patch-1
Fix articles example in the documentation
2019-12-07 07:55:03 +11:00
Sami Samhuri
00fca208a2 Fix articles example in the documentation
Context was being passed as the name param to `renderTemplate`, but it should be an actual name.
2019-12-06 08:05:16 -08:00
Yonas Kolb
a229b59d3d Update CHANGELOG.md 2019-11-17 01:29:58 +11:00
Yonas Kolb
415c3eaa3d Update CHANGELOG.md 2019-11-17 01:29:15 +11:00
David Jennes
e516ca9389 Merge pull request #268 from kawoou/master
Support swift 5.0
2019-05-23 14:37:24 +02:00
kawoou
4020a9851a Test: SIL issues, #file as String 2019-04-15 13:35:33 +09:00
kawoou
3c973689a4 Fix swift SIL exception 2019-04-05 14:26:02 +09:00
Jungwon An
06ea016fd7 Merge branch 'master' into master 2019-04-05 13:25:31 +09:00
kawoou
c2f18790e3 Update travis CI. 2019-03-20 15:35:15 +09:00
Cruz
6addc46681 Update README.md (#270)
Simple change adding dot

as is
Sourcery, SwiftGen, Kitura, Weaver Genesis

to be
Sourcery, SwiftGen, Kitura, Weaver, Genesis
2019-02-17 14:58:36 +01:00
kawoou
782ffdd4c7 Code review by @djbe 2019-01-30 15:29:04 +09:00
kawoou
ebb7ece511 Update podspec swift version 2019-01-29 19:55:42 +09:00
kawoou
305dc31abd Support swift 5.0 2019-01-29 15:17:26 +09:00
David Jennes
3394929008 Merge pull request #263 from stencilproject/empty-var-syntax-error
Syntax error on empty variable tag
2019-01-13 00:50:36 +01:00
Ilya Puchka
693565ddda syntax error on empty variable tag 2019-01-12 22:28:09 +00:00
Yonas Kolb
0f18d43d9e Merge pull request #261 from tomj/master
Stencil is used by Genesis
2018-12-12 16:59:07 +11:00
tomj
ee4203a269 Stencil is used by Genesis 2018-12-12 13:54:50 +08:00
David Jennes
5220c3791e Merge pull request #249 from stencilproject/feature/swiftlint
SwiftLint integration
2018-11-11 17:43:29 +01:00
David Jennes
9243bba2b7 Fix typo in "InheritenceSpec" 2018-11-11 17:34:47 +01:00
David Jennes
deec93fbe1 Changelog entry 2018-11-11 17:34:47 +01:00
David Jennes
8510193d09 Run swiftlint on CI 2018-11-11 17:34:47 +01:00
David Jennes
2d82dcb003 Fix issues in Tests
t
2018-11-11 17:34:47 +01:00
David Jennes
3f4622f54f Fix issues in Sources
Sources

sources
2018-11-11 17:34:47 +01:00
David Jennes
799490198f Rules
rules

rules
2018-11-11 15:52:43 +01:00
David Jennes
6f3ca60e2b Merge pull request #203 from stencilproject/dynamic-filter
Added filter to apply dynamic filters
2018-10-02 00:50:04 +02:00
Ilya Puchka
08fc21d177 Merge branch 'master' into dynamic-filter 2018-10-01 22:45:21 +01:00
Ilya Puchka
019d0cca76 updated docs 2018-10-01 22:16:43 +01:00
Ilya Puchka
da6a0ccaca added some doc comments 2018-10-01 22:12:21 +01:00
Ilya Puchka
dbb5e14e9f solve merge conflict issues 2018-10-01 21:59:03 +01:00
Ilya Puchka
0269052d6a Merge branch 'master' into dynamic-filter
# Conflicts:
#	CHANGELOG.md
#	Sources/ForTag.swift
#	Sources/IfTag.swift
#	Sources/Parser.swift
#	Sources/Variable.swift
#	Tests/StencilTests/ExpressionSpec.swift
#	Tests/StencilTests/FilterSpec.swift
#	Tests/StencilTests/ForNodeSpec.swift
#	Tests/StencilTests/VariableSpec.swift
2018-10-01 21:21:56 +01:00
David Jennes
4faf8f5ee6 Merge pull request #258 from Andrew-Lees11/Swift4.0
Feat: Add support for Swift 4.0
2018-10-01 16:15:22 +02:00
andy
4154cd31ff Changed to if swift package generate-xcodeproj(>=4.1) 2018-10-01 15:11:03 +01:00
andy
fd79045053 removed whitespace changes 2018-10-01 14:56:09 +01:00
andy
9bd86d9fd5 Moved swift4.0 support into single file 2018-10-01 14:54:10 +01:00
andy
66a9bc563a Feat: Add support for Swift 4.0 2018-10-01 11:58:20 +01:00
Ilya Puchka
01afae9b79 Fix parsing token components with parenthesis without spaces (#254)
* fix parsing token components with brackets without spaces

* handle more edge cases

* do not use force unwrap

* use first/last instead of hasPrefix/hasSuffix

* update CHANGELOG
2018-09-30 21:57:19 +01:00
Ilya Puchka
d9f6a82f97 Convert Token from enum to struct (#256)
* convert Token from enum to struct

* private setter for components

* updated CHANGELOG
2018-09-30 21:48:44 +01:00
David Jennes
9a6ba94d7d Reset changelog 2018-09-26 13:10:56 +02:00
David Jennes
0e9a78d658 Revert change (sorry!) 2018-09-26 03:26:27 +02:00
David Jennes
8eae79dbff Version 0.13.1 2018-09-26 03:22:43 +02:00
David Jennes
8cceac921a Avoid swift installation on macOS image 2018-09-26 03:20:08 +02:00
David Jennes
7417332fa2 Merge pull request #252 from stencilproject/fix/lexer-range
Fix lexer range calculation for tokens
2018-09-26 03:18:50 +02:00
David Jennes
524c0acce6 Changelog entry 2018-09-26 03:10:53 +02:00
David Jennes
2e67755118 Fix a bug where tokens without spaces were parsed incorrectly 2018-09-26 03:06:49 +02:00
David Jennes
c7dbba41a5 Fix cocoapods min. version 2018-09-26 00:52:18 +02:00
David Jennes
69af469d0d Merge pull request #251 from stencilproject/release/0.13.0
Release 0.13.0
2018-09-26 00:46:09 +02:00
David Jennes
42e415a9bf Version 0.13.0 2018-09-26 00:38:40 +02:00
David Jennes
2760843236 Update some old refs 2018-09-26 00:38:40 +02:00
David Jennes
535a8061d9 Match old Changelog section names
t
2018-09-26 00:38:40 +02:00
David Jennes
88bec575a5 Compile with Swift 4.2 if possible
t

t

t
2018-09-26 00:38:40 +02:00
David Jennes
6f9bb3e931 Merge pull request #226 from Liquidsoul/faster-scanner
Optimise Scanner performance
2018-09-26 00:38:05 +02:00
David Jennes
cb4e514846 Code documentation 2018-09-26 00:33:15 +02:00
David Jennes
fff93f18dd Add performance test (no reporting yet) 2018-09-26 00:33:15 +02:00
David Jennes
652dcd246d Add lexer test for escape sequence 2018-09-26 00:33:15 +02:00
Liquidsoul
e77bd22e83 Add changelog entry 2018-09-26 00:33:15 +02:00
David Jennes
4f84627caa Add test for crashing 2018-09-26 00:33:15 +02:00
ethorpe
07a6b2aea5 Rewrites scanner for better performance. This is primarily an improvement under Ubuntu
Cleanup readability a little bit
Rewrite original scan function so it's available. Syntax improvements

Fix deprecation warnings in Lexer

Cleanup some syntax issues

lexer

t

t
2018-09-26 00:33:15 +02:00
Ilya Puchka
fce3dc5e48 Added method to register boolean filters (#160)
* added method to register boolean filters

* parametrised negative filter name

* Update Extension.swift

* Update CHANGELOG.md

* renamed registerBooleanFilter to registerFilter

* updated docs
2018-09-25 23:29:21 +01:00
Ilya Puchka
f7bda226e8 Update to Spectre 0.9.0 (#247)
* update to Spectre 0.9.0

* fix variable spec tests

* fix flatMap warning

* updated CHANGELOG
2018-09-23 03:46:27 +03:00
Ilya Puchka
d238c25eef Allow using collection accessors on strings (#245)
* allow using collection accessors on strings

* refactored resolving collection accessors

* refactored to fileprivate function

* Update Variable.swift

* Update templates.rst
2018-09-22 16:41:45 +03:00
Ilya Puchka
df2e193891 Allow conditions in variable node (#243)
* use condition in variable node

* added support for else expression

* addressing code review comments
2018-09-22 14:09:25 +03:00
Ilya Puchka
2c3962a3de Added support for brackets in boolean expressions (#165)
* added support for brackets in boolean expressions

* more descriptive error messages

* use array slices

* added test for nested expressions

* removed brackets validation step

* address code review comments

* added doc comment

* simplify expression spec

* fixed docs
2018-09-21 22:07:28 +03:00
David Jennes
7ed95aec91 Merge pull request #242 from stencilproject/feature/deterministic-for-loop
Deterministic `for` loops for dictionaries
2018-09-21 12:09:12 +02:00
David Jennes
064b2f706c Changelog entry 2018-09-21 00:19:08 +02:00
David Jennes
fce4e85a63 Ensure the "for" iteration over a dictionary is consistent 2018-09-21 00:17:42 +02:00
David Jennes
275e583e4a Merge pull request #239 from stencilproject/feature/swift4.2
Use Swift 4 features
2018-09-21 00:00:13 +02:00
David Jennes
9c408d488e Test on Xcode 10 and Linux Swift 4.2 2018-09-20 04:17:42 +02:00
David Jennes
f9f6d95f25 Changelog entry 2018-09-20 04:17:42 +02:00
David Jennes
0d4dee29b2 Use multiline strings
multi

t

t
2018-09-20 04:17:42 +02:00
David Jennes
1704cd2ddf Use auto equatable 2018-09-20 02:20:21 +02:00
David Jennes
831cdf5f36 Merge pull request #234 from stencilproject/check-kvo
Check for property via selector before using value(forKey:)
2018-09-11 23:59:54 +02:00
Ilya Puchka
8210fa57f1 Update CHANGELOG.md 2018-09-11 18:14:51 +01:00
Ilya Puchka
0074ee1d4a check for property via selector before using value(forKey:) 2018-09-11 18:12:27 +01:00
Yonas Kolb
d71fe2a2ee Merge pull request #228 from stencilproject/swift_4
Swift 4.1
2018-09-10 21:22:38 +10:00
Yonas Kolb
93ccc56540 update lexer to swift 4 2018-09-10 21:19:25 +10:00
David Jennes
247a35fd2c Add CocoaPods version 2018-09-10 20:59:02 +10:00
Yonas Kolb
8e9692c696 add swift version to podspec 2018-09-10 20:59:02 +10:00
Yonas Kolb
8bda4d5bbb add changelog entry 2018-09-10 20:59:02 +10:00
Yonas Kolb
e6b12c09d3 update travis builds to Swift 4.1 2018-09-10 20:59:02 +10:00
Yonas Kolb
420c0eacd7 update code to Swift 4.1 2018-09-10 20:59:02 +10:00
Yonas Kolb
adb443229d add xcodeproject to gitignore 2018-09-10 20:58:24 +10:00
Yonas Kolb
1098921dc8 remove Swift 3 package 2018-09-10 20:58:24 +10:00
Yonas Kolb
9de8190988 upgrade Package to swift 4 2018-09-10 20:58:24 +10:00
Ilya Puchka
acda1b0caf process template lines when Lexer is created not when parsing each token. (#230) 2018-09-10 11:39:19 +01:00
Sébastien Duperron
00e71c1b4d Fix typo in VariableSpec describing subscripting (#229) 2018-09-08 14:05:05 +01:00
David Jennes
1b85b816fd Reset changelog 2018-08-30 13:58:30 +02:00
Ilya Puchka
e795f052ea updated docs 2018-08-04 20:19:50 +01:00
Ilya Puchka
2c411ca494 Merge branch 'master' into dynamic-filter 2018-08-04 20:05:59 +01:00
Ilya Puchka
f3d5843e78 updated CHANGELOG 2018-08-04 19:49:57 +01:00
Ilya Puchka
564ccb7af7 added filter to apply dynamic filter 2018-05-13 12:46:51 +01:00
71 changed files with 5061 additions and 2481 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.conche/
.build/
.swiftpm/
Packages/
Package.resolved
Package.pins
*.xcodeproj

99
.swiftlint.yml Normal file
View 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

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
source "https://rubygems.org"
gem "octokit"
gem "cocoapods"
gem "rake"

105
Gemfile.lock Normal file
View 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

View File

@@ -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
View 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
}

View File

@@ -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]
)

View File

@@ -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
View 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]
)

View File

@@ -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
View 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'

View File

@@ -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()

View File

@@ -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)
}
}

View File

@@ -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")
}
}

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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) {

View File

@@ -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 {
}
}
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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

View File

@@ -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 }
}
}

View File

@@ -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

View File

@@ -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]
}
}

View File

@@ -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))
}
}

View File

@@ -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
}
}

View File

@@ -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 {
}
}
}

View 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

View File

@@ -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"
]
}
}

View File

@@ -1,3 +1,8 @@
import XCTest
import StencilTests
stencilTests()
var tests = [XCTestCaseEntry]()
tests += StencilTests.__allTests()
XCTMain(tests)

View File

@@ -0,0 +1,3 @@
disabled_rules: # rule identifiers to exclude from running
- type_body_length
- file_length

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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"
}
}
}

View File

@@ -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)
}
}

View File

@@ -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"
}
}

View File

@@ -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!"
}
}
}

View 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
"""
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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))
}
}

View File

@@ -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"])
}
}

View File

@@ -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"))
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
)
}
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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.

View File

@@ -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.

View File

@@ -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
-----------

View File

@@ -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)

View File

@@ -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:

View File

@@ -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
View 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
View 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
View 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
View 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
View 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