Repository: tevelee/Eval
Branch: master
Commit: df80bd880d56
Files: 98
Total size: 374.9 KB
Directory structure:
gitextract_vfh2ubbu/
├── .codecov.yml
├── .github/
│ └── .config.yml
├── .gitignore
├── .jazzy.yml
├── .swift-version
├── .swiftlint.yml
├── .travis.yml
├── .version
├── CONTRIBUTING.md
├── Dangerfile
├── Documentation/
│ ├── Example projects.md
│ ├── Interpreter engine details.md
│ ├── Strongly-typed evaluator.md
│ ├── Template evaluator.md
│ └── Tips & Tricks.md
├── Eval.playground/
│ ├── Contents.swift
│ ├── Sources/
│ │ ├── Helpers.swift
│ │ └── TypesAndFunctions.swift
│ ├── contents.xcplayground
│ ├── playground.xcworkspace/
│ │ └── contents.xcworkspacedata
│ └── xcshareddata/
│ └── xcschemes/
│ └── Playground.xcscheme
├── Eval.podspec
├── Eval.xcodeproj/
│ ├── EvalTests_Info.plist
│ ├── Eval_Info.plist
│ ├── project.pbxproj
│ └── xcshareddata/
│ └── xcschemes/
│ ├── Eval-Package.xcscheme
│ └── xcschememanagement.plist
├── Eval.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ └── IDEWorkspaceChecks.plist
├── Examples/
│ ├── .swiftlint.yml
│ ├── AttributedStringExample/
│ │ ├── .gitignore
│ │ ├── Package.swift
│ │ ├── README.md
│ │ ├── Sources/
│ │ │ └── AttributedStringExample/
│ │ │ └── TemplateExample.swift
│ │ └── Tests/
│ │ ├── .swiftlint.yml
│ │ ├── AttributedStringExampleTests/
│ │ │ └── AttributedStringExampleTests.swift
│ │ └── LinuxMain.swift
│ ├── ColorParserExample/
│ │ ├── .gitignore
│ │ ├── Package.swift
│ │ ├── README.md
│ │ ├── Sources/
│ │ │ └── ColorParserExample/
│ │ │ └── ColorParserExample.swift
│ │ └── Tests/
│ │ ├── .swiftlint.yml
│ │ ├── ColorParserExampleTests/
│ │ │ └── ColorParserExampleTests.swift
│ │ └── LinuxMain.swift
│ └── TemplateExample/
│ ├── .gitignore
│ ├── Package.swift
│ ├── README.md
│ ├── Sources/
│ │ └── TemplateExample/
│ │ └── TemplateExample.swift
│ └── Tests/
│ ├── .swiftlint.yml
│ ├── LinuxMain.swift
│ └── TemplateExampleTests/
│ ├── TemplateExampleComponentTests.swift
│ ├── TemplateExampleTests.swift
│ ├── import.txt
│ └── template.txt
├── Gemfile
├── LICENSE.txt
├── Package.swift
├── README.md
├── Scripts/
│ ├── .gitignore
│ ├── .swiftlint.yml
│ ├── Package.swift
│ ├── Sources/
│ │ └── Automation/
│ │ ├── Error.swift
│ │ ├── Eval.swift
│ │ ├── Shell.swift
│ │ ├── Travis.swift
│ │ └── main.swift
│ ├── ci.sh
│ └── git_auth.sh
├── Sources/
│ └── Eval/
│ ├── Common.swift
│ ├── Elements.swift
│ ├── TemplateInterpreter.swift
│ ├── TypedInterpreter.swift
│ └── Utilities/
│ ├── MatchResult.swift
│ ├── Matcher.swift
│ ├── Pattern.swift
│ └── Utils.swift
├── Tests/
│ ├── .swiftlint.yml
│ ├── EvalTests/
│ │ ├── IntegrationTests/
│ │ │ ├── InterpreterTests.swift
│ │ │ ├── PerformanceTest.swift
│ │ │ ├── Suffix.swift
│ │ │ └── TemplateTests.swift
│ │ ├── UnitTests/
│ │ │ ├── DataTypeTests.swift
│ │ │ ├── FunctionTests.swift
│ │ │ ├── InterpreterContextTests.swift
│ │ │ ├── KeywordTests.swift
│ │ │ ├── LiteralTests.swift
│ │ │ ├── MatchResultTests.swift
│ │ │ ├── MatchStatementTests.swift
│ │ │ ├── MatcherTests.swift
│ │ │ ├── PatternTests.swift
│ │ │ ├── TemplateInterpreterTests.swift
│ │ │ ├── TypedInterpreterTests.swift
│ │ │ ├── UtilTests.swift
│ │ │ ├── VariableProcessor.swift
│ │ │ └── VariableTests.swift
│ │ └── Utils.swift
│ └── LinuxMain.swift
└── github_rsa.enc
================================================
FILE CONTENTS
================================================
================================================
FILE: .codecov.yml
================================================
coverage:
ignore:
- Applications/Xcode.app/.*
- build/.*
- .build/.*
- Documentation/.*
- Example/Pods/.*
- Scripts/.*
- vendor/.*
precision: 2
round: down
range: "80...100"
status:
patch: yes
project: yes
changes: no
================================================
FILE: .github/.config.yml
================================================
updateDocsComment: >
Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would update some of our documentation based on your changes.
requestInfoReplyComment: >
We would appreciate it if you could provide us with more info about this issue/pr!
requestInfoLabelToAdd: request-more-info
newPRWelcomeComment: >
Thanks so much for opening your first PR here!
firstPRMergeComment: >
Congrats on merging your first pull request here! :tada: How awesome!
newIssueWelcomeComment: >
Thanks for opening this issue, a maintainer will get back to you shortly!
sentimentBotToxicityThreshold: .7
sentimentBotReplyComment: >
Please be sure to review the code of conduct and be respectful of other users
lockThreads:
toxicityThreshold: .7
numComments: 2
setTimeInHours: 72
replyComment: >
This thread is being locked due to exceeding the toxicity minimums
================================================
FILE: .gitignore
================================================
.DS_Store
/.build
build
/Packages
xcuserdata
.xcuserstate
Pods
Package.resolved
docs
gh-pages
Documentation/Output
================================================
FILE: .jazzy.yml
================================================
author: Laszlo Teveli
author_url: https://tevelee.github.io
readme: README.md
documentation: Documentation/*.md
abstract: Documentation/Sections/*.md
module: Eval
xcodebuild_arguments: [-scheme,Eval-Package]
output: Documentation/Output
theme: apple
github_url: https://github.com/tevelee/Eval
github_file_prefix: https://github.com/tevelee/Eval/tree/master
root_url: https://tevelee.github.io/Eval
clean: true
min_acl: internal
framework_root: Sources
source_directory: .
================================================
FILE: .swift-version
================================================
5.0
================================================
FILE: .swiftlint.yml
================================================
reporter: "xcode"
opt_in_rules:
- array_init
- attributes
- block_based_kvo
- class_delegate_protocol
- closing_brace
- closure_end_indentation
- closure_parameter_position
- closure_spacing
- colon
- comma
- compiler_protocol_init
# - conditional_returns_on_newline
- contains_over_first_not_nil
- control_statement
- custom_rules
- cyclomatic_complexity
- discarded_notification_center_observer
- discouraged_direct_init
- discouraged_object_literal
- dynamic_inline
- empty_count
- empty_enum_arguments
- empty_parameters
- empty_parentheses_with_trailing_closure
# - explicit_acl
- explicit_enum_raw_value
- explicit_init
- explicit_top_level_acl
# - explicit_type_interface
- extension_access_modifier
- fallthrough
- fatal_error_message
- file_header
- file_length
- first_where
- for_where
- force_cast
- force_try
- force_unwrapping
- function_body_length
- function_parameter_count
- generic_type_name
- identifier_name
- implicit_getter
- implicit_return
- implicitly_unwrapped_optional
- is_disjoint
- joined_default_parameter
- large_tuple
- leading_whitespace
- legacy_cggeometry_functions
- legacy_constant
- legacy_constructor
- legacy_nsgeometry_functions
- let_var_whitespace
- line_length
- literal_expression_end_indentation
- mark
- multiline_arguments
- multiline_parameters
- multiple_closures_with_trailing_closure
- nesting
- nimble_operator
# - no_extension_access_modifier
# - no_grouping_extension
- notification_center_detachment
- number_separator
- object_literal
- opening_brace
- operator_usage_whitespace
- operator_whitespace
- overridden_super_call
- override_in_extension
- pattern_matching_keywords
- prefixed_toplevel_constant
- private_action
- private_outlet
- private_over_fileprivate
- private_unit_test
- prohibited_super_call
- protocol_property_accessors_order
- quick_discouraged_call
- quick_discouraged_focused_test
- quick_discouraged_pending_test
- redundant_discardable_let
- redundant_nil_coalescing
- redundant_optional_initialization
- redundant_string_enum_value
- redundant_void_return
- required_enum_case
- return_arrow_whitespace
- shorthand_operator
- single_test_class
- sorted_first_last
- sorted_imports
- statement_position
- strict_fileprivate
- superfluous_disable_command
- switch_case_alignment
- switch_case_on_newline
- syntactic_sugar
- todo
- trailing_closure
- trailing_comma
- trailing_newline
- trailing_semicolon
- trailing_whitespace
- type_body_length
- type_name
- unneeded_break_in_switch
- unneeded_parentheses_in_closure_argument
- unused_closure_parameter
- unused_enumerated
- unused_optional_binding
- valid_ibinspectable
- vertical_parameter_alignment
- vertical_parameter_alignment_on_call
- vertical_whitespace
- void_return
- weak_delegate
- xctfail_message
- yoda_condition
included:
- Sources
- Tests
- Examples/AttributedStringExample/Sources
- Examples/AttributedStringExample/Tests
- Examples/ColorParserExample/Sources
- Examples/ColorParserExample/Tests
- Examples/TemplateExample/Sources
- Examples/TemplateExample/Tests
- Scripts/Sources
force_cast: error
force_try: error
line_length: 320
type_body_length:
- 300
- 400
file_length:
warning: 500
error: 1000
type_name:
min_length: 4
max_length: 25
identifier_name:
min_length: 3
max_length: 35
file_header:
severity: error
required_string: |
/*
* Copyright (c) 2018 Laszlo Teveli.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
================================================
FILE: .travis.yml
================================================
osx_image: xcode11.3
language: swift
sudo: true
env:
global:
- EXPANDED_CODE_SIGN_IDENTITY="-"
- EXPANDED_CODE_SIGN_IDENTITY_NAME="-"
- EXPANDED_PROVISIONING_PROFILE="-"
before_script:
- sh Scripts/git_auth.sh
script:
- travis_retry Scripts/ci.sh
- sleep 3
================================================
FILE: .version
================================================
1.5.0
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Eval
## 👍🎉 First off, thanks for taking the time to contribute! 🎉👍
**You are more than welcomed to do so!**
The following is a set of guidelines and best practices for contributing to Eval. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a Pull Request.
## Code of Conduct
Nothing serious, all I ask is to be respectful and as helpful as you would expect others commnicating with you.
## I don't want to read this whole thing, I just have a question!
The easiest channel is Twitter. You can reach out to me `@tevelee`, or feel free to write a mail at `tevelee [at] gmail [dot] com`.
If you have bigger concerns, feel free to write a GitHub issue.
## What should I know before I get started?
First of all, please read the [documentation pages](Documentation), and feel free to check out the examples.
There are a few things you need to be aware of when contributing:
* The repository is only opened to a very selected set of contributors. If you need any code modifications, you need to fork and create a Pull Request.
* There is a CI up and running
* I use SwiftLint with build-in rules to keep the code as nice as possible
* There is Danger configured with some rules, auto-checking contributions before I do
* I intend to keep the code test coverage as high as possible. Please be mindful about this when contributing
# How Can I Contribute?
## Reporting Bugs or Suggesting Enhancements
Feel free to use the same channels as I described in the README:
The easiest channel is Twitter. You can reach out to me `@tevelee`, or feel free to write a mail at `tevelee [at] gmail [dot] com`.
If you have bigger concerns, feel free to write a GitHub issue.
## Pull Requests
### Git Commit Messages
Please use git rebase keep your number commits as low as possible.
### Documentation Styleguide
I use a standard style of markdown pages in the [README.md](README.md) and in the [Documentation folder](Documentation).
I also document the code, most importantly the public interfaces. I intend to keep the line documentation coverage of the publicly available methods and classes a 100%.
### Issue and Pull Request Labels
No rules you need to be aware of
================================================
FILE: Dangerfile
================================================
# Sometimes it's a README fix, or something like that - which isn't relevant for
# including in a project's CHANGELOG for example
not_declared_trivial = !(github.pr_title.include? "#trivial")
has_app_changes = !git.modified_files.grep(/Sources/).empty?
# ENSURE THAT LABELS HAVE BEEN USED ON THE PR
fail "Please add labels to this PR" if github.pr_labels.empty?
# Mainly to encourage writing up some reasoning about the PR, rather than just leaving a title
if github.pr_body.length < 5
fail "Please provide a summary in the Pull Request description"
end
# Pay extra attention if external contributors modify certain files
if git.modified_files.include?("LICENSE.txt")
fail "External contributor has edited the LICENSE.txt"
end
if git.modified_files.include?("Gemfile") or git.modified_files.include?("Gemfile.lock")
warn "External contributor has edited the Gemfile and/or Gemfile.lock"
end
if git.modified_files.include?("Eval.podspec") or git.modified_files.include?("Package.swift")
warn "External contributor has edited the Eval.podspec and/or Package.swift"
end
# Make it more obvious that a PR is a work in progress and shouldn't be merged yet
warn("PR is classed as Work in Progress") if github.pr_title.include? "WIP"
# Warn when there is a big PR
warn("Big PR, try to keep changes smaller if you can") if git.lines_of_code > 500
# Changelog entries are required for changes to library files.
no_changelog_entry = !git.modified_files.include?("Changelog.md")
if has_app_changes && no_changelog_entry && not_declared_trivial
#warn("Any changes to library code should be reflected in the Changelog. Please consider adding a note there")
end
# Added (or removed) library files need to be added (or removed) from the Carthage Xcode project to avoid breaking things for our Carthage users.
added_swift_library_files = !(git.added_files.grep(/Sources.*\.swift/).empty?)
deleted_swift_library_files = !(git.deleted_files.grep(/Sources.*\.swift/).empty?)
modified_carthage_xcode_project = !(git.modified_files.grep(/Eval\.xcodeproj/).empty?)
if (added_swift_library_files || deleted_swift_library_files) && !modified_carthage_xcode_project
warn("Added or removed library files require the Carthage Xcode project to be updated")
end
missing_doc_changes = git.modified_files.grep(/Documentation/).empty?
doc_changes_recommended = git.insertions > 15
if has_app_changes && missing_doc_changes && doc_changes_recommended && not_declared_trivial
warn("Consider adding supporting documentation to this change. Documentation can be found in the `Documentation` directory.")
end
# Warn when library files has been updated but not tests.
tests_updated = !git.modified_files.grep(/Tests/).empty?
if has_app_changes && !tests_updated
warn("The library files were changed, but the tests remained unmodified. Consider updating or adding to the tests to match the library changes.")
end
# Give inline build results (compile and link time warnings and errors)
xcode_summary.report 'build/tests/summary.json' if File.file?('build/tests/summary.json')
xcode_summary.report 'build/example/summary.json' if File.file?('build/example/summary.json')
# Run SwiftLint
swiftlint.lint_files
#swiftlint.lint_files inline_mode: true
================================================
FILE: Documentation/Example projects.md
================================================
# Example projects
I included a few use-cases, which bring significant improvements on how things are processed before - at least in my previous projects.
### [Template language](https://github.com/tevelee/Eval/blob/master/Examples/TemplateExample/Tests/TemplateExampleTests/TemplateExampleTests.swift)
I was able to create a full-blown template language, completely, using this framework and nothing else. It's almost like a competitor of the one I mentioned ([Twig](https://github.com/twigphp/Twig)). This is the most advanced example of them all!
I created a standard library with all the possible operators you can imagine. With helpers, each operator is a small, one-liner addition. Added the important data types, such as arrays, strings, numbers, booleans, dates, etc., and a few functions, to be more awesome. [Take a look for inspiration!](https://github.com/tevelee/Eval/blob/master/Examples/TemplateExample/Sources/TemplateExample/TemplateExample.swift)
Together, it makes an excellent addition to my model-object generation project, and **REALLY useful for server-side Swift development as well**!
### [Attributed string parser](https://github.com/tevelee/Eval/blob/master/Examples/AttributedStringExample/Tests/AttributedStringExampleTests/AttributedStringExampleTests.swift)
I created another small example, parsing attribtuted strings from simple expressions using XML style tags, such as bold, italic, underlined, colored, etc.
With just a few operators, this solution can deliver attributed strings from basic APIs, which otherwise would be hard to manage.
My connected project is an iOS application, using the Spotify [HUB framework](https://github.com/spotify/HubFramework), in which I can now provide rich strings with my view-models and parse them from the JSON string results.
### [Color parser](https://github.com/tevelee/Eval/blob/master/Examples/ColorParserExample/Tests/ColorParserExampleTests/ColorParserExampleTests.swift)
A color parser is also used by the BFF (Backend For Frontend, not 👭) project I mentioned before. It can parse Swift Color objects from many different styles of strings, such as `#ffddee`, or `red`, or `rgba(1,0.5,0.4,1)`. I included this basic example in the repository as well.
================================================
FILE: Documentation/Interpreter engine details.md
================================================
# Technical details
## Interpreter engine
TBD
## Template engine
TBD
================================================
FILE: Documentation/Strongly-typed evaluator.md
================================================
# Strongly typed evaluator
This kind of evaluator interprets its input as one function. It searches for the one with the highest precedence and works its way down from that.
Ocassionally, more functions are present in the same expression. In this case, it goes recursively, all the way down to the most basic elements: variables or literals, which are trivial to evaluate.
## Creation
The way to create typed interpreters is the following:
```swift
let interpreter = TypedInterpreter(dataTypes: [number, string, boolean, array, date],
functions: [concat, add, multiply, substract, divide],
context: Context(variables: ["example": 1]))
```
First, you'll need the data types you are going to work with. These are a smaller subsets of the build in Swift data types, you can map them to existing types (Swift types of the ones of your own)
The second parameter are the functions you can apply on the above listed data types. All the functions - regardless of the grouping of the data types - should be listed here.
Typically, in case of numbers, these are numeric operators. In case of string, these can be concatenation, getters, slicing, etc.
Or, these can be complex things, such as parentheses, data factories, or high level functional operations, such as filter, map or reduce.
And lastly, an optional context object, if you have any global variables.
Variables in the context can be expression specific, or global that apply to every evaluation session.
## Data types
Data types map the outside world to the inside of the expression. They map existing types to inner data types.
They don't restrict any behaviour, so these types can either be built-in Swift types, such as String, Array, Date; or they can be your custom classes, srtucts, or enums.
Let's see it in action:
```swift
let number = DataType(type: Double.self, literals: [numberLiteral, piConstant]) { String(describing: $0) }
```
If has a type, that is the existing type of the sorrounding program. The literals, which can tell the framework whether a given string can be converted to a given type.
Typical example is the `String` literal, which encloses something between quotes: `'like this'`. The can also be constants, for example `pi` for numbers of `true` for booleans.
The last parameter is a `print` closure. It tells the framework how to render the given type when needed. Typically used while debugging, or when templates use the `print` statement.
In summary: literals provide the input, print provides the output of a mapped type.
### Literals
Let's check out some literals a bit more deeply.
The block used for literals have two parameters: the input string, and an interpreter.
Most of the times, only the input is enough to recognise things, like numbers:
```swift
Literal { value, _ in Double(value) }
```
Arrays, on the other hand, should process their content (Comma `,` separated values between brackets `[` `]`). For this, the second parameter, the interpreter can be used.
#### Constants
Literals are the perfect place to recognise constants, such as:
```swift
Literal("pi", convertsTo: Double.pi)
```
or
```swift
[Literal("false", convertsTo: false)
```
Of course, there are multiple ways to represent them (for example, as a single keyword function pattern), but this seems like a place where they can be most closely connected to their type.
The `convertsTo` parameter of `Literal`s are `autoclosure` parameters, which means, that they are going to be processed lazily.
```swift
Literal("now", convertsTo: Date())
```
The `now` string is going to be expressed as the current timestamp at the time of the evaluation, not the time of the initialisation.
## Functions
Similarly to templates, typed interpreters use the same building blocks to build up their patterns: `Keyword`s and `Variable`s.
### Keywords
`Keyword`s are the most basic elements of a pattern; they represent simple, static `String`s. You can chain them, for example `Keyword("<") + Keyword("br/") + Keyword(>}")`, or simply merge them `Keyword("
")`. Logically, these two are the same, but the former accepts any number of whitespaces between the tags, while the latter allows none, as it is a strict match.
Most of the time though, you are going to need to handle placeholders, varying number of elements. That's where `Variable`s come into place.
### Variables
Let's check out the following, really straightforward pattern:
```swift
Function(Keyword("(") + Variable("body") + Keyword(")")) { variables, _, _ in
return variables["body"]
}
```
Something between two enclosing parentheses `(`, `)`. The middle tag is a `Variable`, which means that its value is going to be passed through in the block, using its name. Let's imagine the following input: `(5)`. Here, the `variables` dictionary is going to have `5` under the key `body`.
#### Generics
Since its value is going to be processed, there is a generic sign as well, signalling that this current `Variable` accepts `Any` kind of data, no transfer is needed. Let's imagine if we wrote `Variable` instead. In this case, `5` would not match to the pattern, it would be intact. But, for example, `('Hello')` would do.
Let check out a `+` operator. This could equally mean addition for numeric types
```swift
Function(Variable("lhs") + Keyword("+") + Variable("rhs")) { arguments,_,_ in
guard let lhs = arguments["lhs"] as? Double, let rhs = arguments["rhs"] as? Double else { return nil }
return lhs + rhs
}
```
or concatenation for strings
```swift
Function(Variable("lhs") + Keyword("+") + Variable("rhs")) { arguments,_,_ in
guard let lhs = arguments["lhs"] as? String, let rhs = arguments["rhs"] as? String else { return nil }
return lhs + rhs
}
```
Since the interpreter is strongly typed, always the appropriate one is going to be selected by the framework.
#### Evaluation
Variables also have optional properties, such as `interpreted`, `shortest`, or `acceptsNilValue`. They might also have a `map` block, which by default is `nil`.
* `interpreted` tells the framework, that its value should be evaluated. This is true, by default. But, the option exists to modify this to false. In that case, `(2 + 3)` would not generate the number `5` under the `body` key, but `2 + 3` as a `String`.
* `shortest` signals the "strength" of the matching operation. By default it's false, we need the most possible characters. The only scenario where this could get tricky is if the last element of a pattern is a `Variable`. In that case, the preferred setting is `false`, so we need the largest possible match!
Let's find out why! A general addition operator (which looks like this `Variable("lhs") + Keyword("+") + Variable("rhs")`) would recognise the pattern `12 + 34`, but it also matches to `12 + 3`. What's what shortest means, the shortest match, in this case, is `12 + 3`, which - semantically - is an incorrect match.
But don't worry, the framework already knows about this, so it sets the right value for your variables, even in the last place!
* `acceptsNilValue` informs the framework if `nil` should be accepted by the pattern. For example, `1 + '5'` with the previous example (`Double + Double`) would not match. But, if the `acceptsNilValue` is defined, then the block would trigger, with `{'lhs': 1, 'rhs': nil}`, so you can decide by your own logic what to do in this case.
* Finally, the `map` block can be used to further transform the value of your `Variable` before calling the block on the `Pattern`. Since map is a trailing closure, it's quite easy to add. For example, `Variable("example") { Double($0) }` would recognise only `Int` values, but would transform them to `Double` instances when providing them in the `variables` dictionary. This map can also return `nil` values but depends on your logic if you want to accept them or not. Side note: the previous map generates a `Variable` kind of variable instance.
### Specialised elements
#### Open & Close Keyword
Parentheses are quite common in expressions. They are often embedded in each other. Embedding is a nasty problem of interpreters, as `(a * (b + c))` would logically be evaluated with `(b + c)` first, and the rest afterwards.
But, an algorithm, by default, would interpret things linearly, disregarding the semantics: `(a * (b + c))` would be the match for the first if statement, with a totally invalid `a * (b + c` data, until the first match.
This, of course, needs to be solved, but it's not that easy as it first looks! Some edge cases would not work unless we somehow try to connect them together. For this reason, I added two special elements: `OpenKeyword` and `CloseKeyword`. These work exactly the same way as normal `Keyword`s do, but add a bit more semantics to the framework: these two should be connected together, and therefore embedding them should not be a problem as they come in pairs.
The previous parentheses statement should - correctly - look like this:
```swift
Function(OpenVariable("lhs") + Keyword("+") + CloseVariable("rhs")) { arguments,_,_ in
guard let lhs = arguments["lhs"] as? String, let rhs = arguments["rhs"] as? String else { return nil }
return lhs + rhs
}
```
By using the `OpenKeyword` and `CloseKeyword` types, these become connected, so embedding parentheses in an expression shouldn't be a problem.
After this match is defined, they can be embedded in each other as deeply as needed.
#### Multiple Patterns in one Function
This is a rarely used pattern, but `Function`s consists of an array of `Pattern` elements.
Usually, one `Function` does only one operation. Unless this is true, grouping multiple `Pattern`s into one `Function` allows semantical grouping of opeartors.
For example a Boolean negation can be expressed in multiple ways: `not(true)` or `!true`. In this case, semantically both expressions do the same thing, therefore it might be a good practice to use one `Function` with two `Pattern`s for this.
## Context
You can also pass contextual values, which - for now - equal to variables.
```swift
expression.evaluate("1 + var", context: Context(variables: ["var": 2]))
```
The reason that the variables are encapsulated in a context is that context is a class, while variables are mutable `var` struct properties on that object. With this construction the context reference can be passed around to multiple interpreter instances, but keeps the copy-on-write (🐮) behaviour of the modification.
Context defined during the initialisation apply to every evaluation performed with the given interpreter, while the ones passed to the `evaluate` method only apply to that specific expression instance.
If some patterns modify the context, they have the option to modify the general context (for long term settings, such as `value++`), or the local one (for example, the interation variable of a `for` loop).
### Order of statements define precedence
The earlier a pattern is represent in the array of `functions`, the higher precedence it gets.
Practically, if there is an addition function and a multiplication one, the multiplication should be defined earlier (as it has higher precedence), because both are going to match the following expression:
`1 * 2 + 3`, but if addition goes first, then the evaluation would process `1 * 2` on `lhs` and `3` on `rhs`, which - of course - is incorrect.
Typically, parentheses and higher precedence operators should go earlier in the array.
================================================
FILE: Documentation/Template evaluator.md
================================================
# Template evaluator
The logic of the interpreter is fairly easy: it goes over the input character by character and replaces any patterns it can find.
## Creation
The way to create template interpreters is the following:
```swift
let template = StringTemplateInterpreter(statements: [ifStatement, printStatement],
interpreter: interpreter,
context: Context(variables: ["example": 1]))
```
First, you'll need the statements that you aim to recognise.
Then, you'll need a typed interpreter, so that you can evaluate strongly typed expressions.
And lastly, an optional context object, if you have any global variables.
Variables in the context can be expression specific, or global that apply to every evaluation session.
The template interpreter and the given typed interpreter don't share the same context. Apart from the containment dependency, they don't have any logical connection. The reason for this is that templates need a special context feeding the template content, but typed interpreters might work with totally different data types. It is totally up to the developer how they want their context to be managed. Since the context is a class, its reference can be passed around, so it's quite straightforward to have them share the same context object - if needed.
## Statement examples
### Keywords
`Keyword`s are the most basic elements of a pattern; they represent simple, static `String`s. You can chain them, for example `Keyword("{%") + Keyword("if") + Keyword("%}")`, or simply merge them `Keyword("{% if %}")`. Logically, these two are the same, but the former accepts any number of whitespaces between the tags, while the latter allows only one, as it is a strict match.
Most of the time though, you are going to need to handle placeholders, varying number of elements. That's where `Variable`s come into place.
### Variables
Let's check out the following, really straightforward pattern:
```swift
Pattern(Keyword("{{") + Variable("body") + Keyword("}}")) { variables, interpreter, _ in
guard let body = variables["body"] else { return nil }
return interpreter.typedInterpreter.print(body)
}
```
Something between two enclosing parentheses `{{`, `}}`. The middle tag is a `Variable`, which means that its value is going to be passed through in the block, using its name. Let's imagine the following input: `The winner is: {{ 5 }}`. Here, the `variables` dictionary is going to have `5` under the key `body`.
#### Generics
Since its value is going to be processed, there is a generic sign as well, signalling that this current `Variable` accepts `Any` kind of data, no transfer is needed. Let's imagine if we wrote `Variable` instead. In this case, `5` would not match to the template, it would be intact. But, for example, `{{ 'Hello' }}` would do.
#### Evaluation
Variables also have optional properties, such as `interpreted`, `shortest`, or `acceptsNilValue`. They might also have a `map` block, which by default is `nil`.
* `interpreted` tells the framework, that its value should be evaluated. This is true, by default. But, the option exists to modify this to false. In that case, `{{ 2 + 3 }}` would not generate the number `5` under the `body` key, but `2 + 3` as a `String`.
* `shortest` signals the "strength" of the matching operation. By default it's false, we need the most possible characters. The only scenario where this could get tricky is if the last element of a pattern is a `Variable`. In that case, the preferred setting is `false`, so we need the largest possible match!
Let's find out why! A general addition operator (which looks like this `Variable("lhs") + Keyword("+") + Variable("rhs")`) would recognise the pattern `12 + 34`, but it also matches to `12 + 3`. What's what shortest means, the shortest match, in this case, is `12 + 3`, which - semantically - is an incorrect match.
But don't worry, the framework already knows about this, so it sets the right value for your variables, even in the last place!
* `acceptsNilValue` informs the framework if `nil` should be accepted by the pattern. For example, `1 + '5'` with the previous example (`Double + Double`) would not match. But, if the `acceptsNilValue` is defined, then the block would trigger, with `{'lhs': 1, 'rhs': nil}`, so you can decide by your own logic what to do in this case.
* Finally, the `map` block can be used to further transform the value of your `Variable` before calling the block on the `Pattern`. Since map is a trailing closure, it's quite easy to add. For example, `Variable("example") { Double($0) }` would recognise only `Int` values, but would transform them to `Double` instances when providing them in the `variables` dictionary. This map can also return `nil` values but depends on your logic if you want to accept them or not. Side note: the previous map generates a `Variable` kind of variable instance.
### Specialised elements
#### Template Variable
By default, `Variable` instances use typed interpreters to evaluate their value. Sometimes though, they should be processed with the template interpreter. A good example is the `if` statement:
```swift
Pattern(Keyword("{%") + Keyword("if") + Variable("condition") + Keyword("%}") + TemplateVariable("body") + Keyword("{% endif %}")) { variables, interpreter, _ in
guard let condition = variables["condition"] as? Bool, let body = variables["body"] as? String else { return nil }
if condition {
return body
}
return nil
}
```
This statement has two semantically different kinds of variable, but they both are just placeholders. The first (`condition`) is an interpreted variable, which at the end returns a `Boolean` value.
The second one is a bit different; it should not be evaluated the same way as `condition`. We need to further evaluate the enclosed template, that's why this variable
1. Should not be interpreted
2. Should be evaluated using the template interpreter, not the typed interpreter
That's why there's a subclass called `TemplateVariable`, which forces these two options when initialised. It DOES evaluate its content but uses the template interpreter to do so.
A quick example: `Header ... {% if x > 0 %}Number of results: {{ x }} {% endif %} ... Footer`
Here, `x > 0` is a `Boolean` expression, but the body between the `if`, and `endif` tags is a template, such as the whole expression.
#### Open & Close Keyword
`if` statements are quite common in templates. They are often chained and embedded in each other. Embedding is a nasty problem of interpreters, as `{% if %}a{% if %}b{% endif %}c{% endif %}` would logically be evaluated with `{% if %}b{% endif %}` first, and the rest afterwards.
But, an algorithm, by default, would interpret things linearly, disregarding the semantics: `{% if %}a{% if %}b{% endif %}` would be the match for the first if statement, with a totally invalid `a{% if %}b` data.
This, of course, needs to be solved, but it's not that easy as it first looks! Some edge cases would not work unless we somehow try to connect them together. For this reason, I added two special elements: `OpenKeyword` and `CloseKeyword`. These work exactly the same way as normal `Keyword`s do, but add a bit more semantics to the framework: these two should be connected together, and therefore embedding them should not be a problem as they come in pairs.
The previous `if` statement, now with an `else` block should - correctly - look like this:
```swift
Pattern(OpenKeyword("{% "if") + Variable("condition") + Keyword("%}") + TemplateVariable("body") + Keyword("{% else %}") + TemplateVariable("else") + CloseKeyword("{% endif %}")) { variables, interpreter, _ in
guard let condition = variables["condition"] as? Bool, let body = variables["body"] as? String else { return nil }
if condition {
return body
} else {
return variables["else"] as? String
}
}
```
By using the `OpenKeyword` and `CloseKeyword` types, these become connected, so embedding `if` statements in a template shouldn't be a problem.
Similarly, this works for the `print` statement from an earlier example:
```swift
Pattern(OpenKeyword("{{") + Variable("body") + CloseKeyword("}}")) { variables, interpreter, _ in
guard let body = variables["body"] else { return nil }
return interpreter.typedInterpreter.print(body)
}
```
## Evaluation
The evaluation of the templates happens with the `evaluate` function on the interpreter:
```swift
template.evaluate("{{ 1 + 2 }}")
```
The result of the evaluation - in case of templates - is always a `String`. In the result you shouldn't see any template elements, because they were recognised, processed, and replaced during the evaluation by the interpreter.
### Context
You can also pass contextual values, which - for now - equal to variables.
```swift
template.evaluate("{{ 1 + var }}", context: Context(variables: ["var": 2]))
```
The reason that the variables are encapsulated in a context is that context is a class, while variables are mutable `var` struct properties on that object. With this construction the context reference can be passed around to multiple interpreter instances, but keeps the copy-on-write (🐮) behaviour of the modification.
Context defined during the initialisation apply to every evaluation performed with the given interpreter, while the ones passed to the `evaluate` method only apply to that specific expression instance.
If some patterns modify the context, they have the option to modify the general context (for long term settings), or the local one (for example, the interation variable of a `for` loop).
### Order of statements define precedence
The earlier a pattern is represent in the array of `statements`, the higher precedence it gets.
Practically, if there is an `if` statement and an `if-else` one, the `if-else` should be defined earlier, because both are going to match the following expression:
`{% if x < 0 %}A{% else %}B{% endif %}`, but if `if` goes first, then the output - and the `body` of the `if` statement - is going to be processed as `A{% else %}B`.
Typically, parentheses and richer type of expressions should go earlier in the array.
================================================
FILE: Documentation/Tips & Tricks.md
================================================
# Tips & Tricks
The following sections provide handy Tips and Tricks to help you effectively build up your own interpreter using custom operators and data types.
## Get inspired by checking out the examples
There are quite a few operators and data types available in the [TemplateLanguage Example](https://github.com/tevelee/Eval/blob/master/Examples/TemplateExample/Sources/TemplateExample/TemplateExample.swift#L114-L150) project, under the StandardLibrary class
Also, there are quite a few expressions available [in some of the unit tests](https://github.com/tevelee/Eval/blob/master/Tests/EvalTests/IntegrationTests/InterpreterTests.swift#L47-L86) as well.
## Use helper functions to define operators
It's a lot readable to define operators in a one-liner expression, rather than using long patterns:
```swift
infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs }
```
```swift
suffixOperator("is odd") { (value: Double) in Int(value) % 2 == 1 }
```
```swift
prefixOperator("!") { (value: Bool) in !value }
```
You can find a few helpers [in the examples](https://github.com/tevelee/Eval/tree/master/Examples/TemplateExample/Sources/TemplateExample/TemplateExample.swift#L331-L412). Feel free to use them!
## Be mindful about precedence
#### Template expressions
The earlier a pattern is represent in the array of `statements`, the higher precedence it gets.
Practically, if there is an `if` statement and an `if-else` one, the `if-else` should be defined earlier, because both are going to match the following expression:
`{% if x < 0 %}A{% else %}B{% endif %}`, but if `if` goes first, then the output - and the `body` of the `if` statement - is going to be processed as `A{% else %}B`.
Typically, parentheses and richer type of expressions should go earlier in the array.
#### Typed expressions
The earlier a pattern is represent in the array of `functions`, the higher precedence it gets.
Practically, if there is an addition function and a multiplication one, the multiplication should be defined earlier (as it has higher precedence), because both are going to match the following expression:
`1 * 2 + 3`, but if addition goes first, then the evaluation would process `1 * 2` on `lhs` and `3` on `rhs`, which - of course - is incorrect.
Typically, parentheses and higher precedence operators should go earlier in the array.
## Use Any for generics: `Variable`
If you are not sure about the allowed input type of your expressions, or you just want to defer that decision until your match is ran and your hit the block in the pattern, feel free to use `Variable("name")` in your patterns.
It makes life a lot easier, than definig functions for each type.
## Use map on `Variable`s for pre-filtering
Before processing Variable values, there is an option to pre-filter or modify them before it hits the match block.
Examples include data type conversion and other types of validation.
## Use `OpenKeyword` and `CloseKeyword` for embedding parentheses
Embedding is a common issue with interpreters and compilers. In order to provide some extra semantics to the engine, please use the `OpenKeyword("[")` and `OpenKeyword("]")` options, when defining `Keyword`s that come in pairs.
## Share context between `StringTemplateInterpreter` and `TypedInterpreter`
If you use template interpreters, they need a typed interpreter to hold. Both interpreters have `context` variables, so if you are not being careful enough, it can cause headaches.
Since `Context` is a class, its reference can be passed around and used in multiple places.
The reason that the variables are encapsulated in a context is that context is a class, while variables are mutable `var` struct properties on that object. With this construction the context reference can be passed around to multiple interpreter instances, but keeps the copy-on-write (🐮) behaviour of the modification.
Context defined during the initialisation apply to every evaluation performed with the given interpreter, while the ones passed to the `evaluate` method only apply to that specific expression instance.
## Define constants in `Literal`s
The frameworks allows multiple ways to express static strings and convert them.
I believe the best place to put constants are in the `Literal`s of `DataType`s.
Use the `Literal("YES", convertsTo: true)` `Literal` initialiser for easy definition.
The `convertsTo` parameter of `Literal`s are `autoclosure` parameters, which means, that they are going to be processed lazily.
```swift
Literal("now", convertsTo: Date())
```
The `now` string is going to be expressed as the current timestamp at the time of the evaluation, not the time of the initialisation.
## Map any function signatures from Swift, dynamically
The framework is really lightweight and not really restrictive in regards of how to parse your expressions. Free your mind, and do stuff dynamically.
```swift
Function(Variable("lhs") + Keyword(".") + Variable("rhs", interpreted: false)) { (arguments,_,_) -> Double? in
if let lhs = arguments["lhs"] as? NSObjectProtocol,
let rhs = arguments["rhs"] as? String,
let result = lhs.perform(Selector(rhs)) {
return Double(Int(bitPattern: result.toOpaque()))
}
return nil
}
])
```
Perform any method call of any type and maybe process their output as well. It's not the safest way to go with it, but this is just an example.
This opens up the way of running almost any arbitrary code on Apple platforms, from any backend. But, this does it in a very controlled way, as you must define a set of data types and functions that apply, unless you call them dynamically at runtime.
## Experiment with your expressions!
It's quite easy to add new operators, functions, and data types. I suggest not to think about them too long, just dare to experpiment with them, what's possible and what is not.
You can always add new types or functions if you need extra functionality. The options are practically endless!
## Debugging tips
#### If an expression haven't been matched
* It's common, that some validation caught the value
* Print your expressions or put breakpoints into the affected match blocks or variable map blocks
#### If you see weird output
* Play with the order of the newly added opeartions.
* Incorrect precedence can turn expressions upside down
The framework is still in an early stage, so debugging helpers will follow in upcoming releases. Please stay tuned!
## Validate your expressions before putting them out in production code
Not every expression work out of the box as you might expect. Operators and functions depend on each other, especially in terms of precedence. If one pattern was recognised before the other one, your code might not run as you expected.
Pro Tip: Write unit tests to validate expressions. Feel free to use `as!` operator to force-cast the result expressions in tests, but only in tests. It's not a problem is tests crash, you can fix it right away, but it's not okay in production.
================================================
FILE: Eval.playground/Contents.swift
================================================
//: Playground - noun: a place where people can play
import Foundation
import Eval
let context = InterpreterContext()
let interpreter = TypedInterpreter(dataTypes: [numberDataType, stringDataType, arrayDataType, booleanDataType, dateDataType],
functions: [parentheses, multipication, addition, lessThan],
context: context)
let template = TemplateInterpreter(statements: [ifStatement, printStatement],
interpreter: interpreter,
context: context)
interpreter.evaluate("2 + 3 * 4")
template.evaluate("{% if 10 < 21 %}Hello{% endif %} {{ name }}!", context: InterpreterContext(variables: ["name": "Eval"]))
================================================
FILE: Eval.playground/Sources/Helpers.swift
================================================
import Foundation
import Eval
public func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function {
return Function([Variable("lhs", shortest: true), Keyword(symbol), Variable("rhs", shortest: false)]) { arguments,_,_ in
guard let lhs = arguments["lhs"] as? A, let rhs = arguments["rhs"] as? B else { return nil }
return body(lhs, rhs)
}
}
public func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function {
return Function([Keyword(symbol), Variable("value", shortest: false)]) { arguments,_,_ in
guard let value = arguments["value"] as? A else { return nil }
return body(value)
}
}
public func suffixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function {
return Function([Variable("value", shortest: true), Keyword(symbol)]) { arguments,_,_ in
guard let value = arguments["value"] as? A else { return nil }
return body(value)
}
}
public func function(_ name: String, body: @escaping ([Any]) -> T?) -> Function {
return Function([Keyword(name), Keyword("("), Variable("arguments", shortest: true, interpreted: false), Keyword(")")]) { variables, interpreter, _ in
guard let arguments = variables["arguments"] as? String else { return nil }
let interpretedArguments = arguments.split(separator: ",").flatMap { interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) }
return body(interpretedArguments)
}
}
public func functionWithNamedParameters(_ name: String, body: @escaping ([String: Any]) -> T?) -> Function {
return Function([Keyword(name), Keyword("("), Variable("arguments", shortest: true, interpreted: false), Keyword(")")]) { variables, interpreter, _ in
guard let arguments = variables["arguments"] as? String else { return nil }
var interpretedArguments: [String: Any] = [:]
for argument in arguments.split(separator: ",") {
let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=")
if let key = parts.first, let value = parts.last {
interpretedArguments[String(key)] = interpreter.evaluate(String(value))
}
}
return body(interpretedArguments)
}
}
public func objectFunction(_ name: String, body: @escaping (O) -> T?) -> Function {
return Function([Variable("lhs", shortest: true), Keyword("."), Variable("rhs", shortest: false, interpreted: false) { value,_ in
guard let value = value as? String, value == name else { return nil }
return value
}]) { variables, interpreter, _ in
guard let object = variables["lhs"] as? O, variables["rhs"] != nil else { return nil }
return body(object)
}
}
public func objectFunctionWithParameters(_ name: String, body: @escaping (O, [Any]) -> T?) -> Function {
return Function([Variable("lhs", shortest: true), Keyword("."), Variable("rhs", interpreted: false) { value,_ in
guard let value = value as? String, value == name else { return nil }
return value
}, Keyword("("), Variable("arguments", interpreted: false), Keyword(")")]) { variables, interpreter, _ in
guard let object = variables["lhs"] as? O, variables["rhs"] != nil, let arguments = variables["arguments"] as? String else { return nil }
let interpretedArguments = arguments.split(separator: ",").flatMap { interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) }
return body(object, interpretedArguments)
}
}
public func objectFunctionWithNamedParameters(_ name: String, body: @escaping (O, [String: Any]) -> T?) -> Function {
return Function([Variable("lhs", shortest: true), Keyword("."), Variable("rhs", interpreted: false) { value,_ in
guard let value = value as? String, value == name else { return nil }
return value
}, Keyword("("), Variable("arguments", interpreted: false), Keyword(")")]) { variables, interpreter, _ in
guard let object = variables["lhs"] as? O, variables["rhs"] != nil, let arguments = variables["arguments"] as? String else { return nil }
var interpretedArguments: [String: Any] = [:]
for argument in arguments.split(separator: ",") {
let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=")
if let key = parts.first, let value = parts.last {
interpretedArguments[String(key)] = interpreter.evaluate(String(value))
}
}
return body(object, interpretedArguments)
}
}
================================================
FILE: Eval.playground/Sources/TypesAndFunctions.swift
================================================
import Foundation
import Eval
//MARK: Double
public let numberDataType = DataType(type: Double.self, literals:[
Literal { Double($0.value) },
Literal("pi", convertsTo: Double.pi)
]) { value, _ in String(describing: value) }
//MARK: Bool
public let booleanDataType = DataType(type: Bool.self, literals: [
Literal("false", convertsTo: false),
Literal("true", convertsTo: true)
]) { $0.value ? "true" : "false" }
//MARK: String
let singleQuotesLiteral = Literal { (input, _) -> String? in
guard let first = input.first, let last = input.last, first == last, first == "'" else { return nil }
let trimmed = input.trimmingCharacters(in: CharacterSet(charactersIn: "'"))
return trimmed.contains("'") ? nil : trimmed
}
public let stringDataType = DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value }
//MARK: Date
public let dateDataType = DataType(type: Date.self, literals: [Literal("now", convertsTo: Date())]) {
let dateFormatter = DateFormatter()
dateFormatter.calendar = Calendar(identifier: .gregorian)
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.string(from: $0)
}
//MARK: Array
let arrayLiteral = Literal { (input, interpreter) -> [CustomStringConvertible]? in
guard let first = input.first, let last = input.last, first == "[", last == "]" else { return nil }
return input
.trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
.split(separator: ",")
.map{ $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.map{ interpreter.evaluate(String($0)) as? CustomStringConvertible ?? String($0) }
}
public let arrayDataType = DataType(type: [CustomStringConvertible].self, literals: [arrayLiteral]) { $0.value.map{ $0.description }.joined(separator: ",") }
//MARK: Operators
public let max = objectFunction("max") { (object: [Double]) -> Double? in object.max() }
public let min = objectFunction("min") { (object: [Double]) -> Double? in object.min() }
public let formatDate = objectFunctionWithParameters("format") { (object: Date, arguments: [Any]) -> String? in
guard let format = arguments.first as? String else { return nil }
let dateFormatter = DateFormatter()
dateFormatter.calendar = Calendar(identifier: .gregorian)
dateFormatter.dateFormat = format
return dateFormatter.string(from: object)
}
public let not = prefixOperator("!") { (value: Bool) in !value }
public let dateFactory = function("Date") { (arguments: [Any]) -> Date? in
guard let arguments = arguments as? [Double], arguments.count >= 3 else { return nil }
var components = DateComponents()
components.calendar = Calendar(identifier: .gregorian)
components.year = Int(arguments[0])
components.month = Int(arguments[1])
components.day = Int(arguments[2])
components.hour = arguments.count > 3 ? Int(arguments[3]) : 0
components.minute = arguments.count > 4 ? Int(arguments[4]) : 0
components.second = arguments.count > 5 ? Int(arguments[5]) : 0
return components.date
}
public let parentheses = Function([Keyword("("), Variable("body"), Keyword(")")]) { $0.variables["body"] }
public let addition = infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs }
public let multipication = infixOperator("*") { (lhs: Double, rhs: Double) in lhs * rhs }
public let concat = infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs }
public let inNumberArray = infixOperator("in") { (lhs: Double, rhs: [Double]) in rhs.contains(lhs) }
public let inStringArray = infixOperator("in") { (lhs: String, rhs: [String]) in rhs.contains(lhs) }
public let range = infixOperator("...") { (lhs: Double, rhs: Double) in CountableClosedRange(uncheckedBounds: (lower: Int(lhs), upper: Int(rhs))).map { Double($0) } }
public let prefix = infixOperator("starts with") { (lhs: String, rhs: String) in lhs.hasPrefix(lhs) }
public let isOdd = suffixOperator("is odd") { (value: Double) in Int(value) % 2 == 1 }
public let isEven = suffixOperator("is even") { (value: Double) in Int(value) % 2 == 0 }
public let lessThan = infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs }
public let greaterThan = infixOperator(">") { (lhs: Double, rhs: Double) in lhs > rhs }
public let equals = infixOperator("==") { (lhs: Double, rhs: Double) in lhs == rhs }
//MARK: Template elements
public let ifStatement = Matcher([Keyword("{%"), Keyword("if"), Variable("condition"), Keyword("%}"), TemplateVariable("body"), Keyword("{%"), Keyword("endif"), Keyword("%}")]) { (variables, interpreter: StringTemplateInterpreter, _) -> String? in
guard let condition = variables["condition"] as? Bool, let body = variables["body"] as? String else { return nil }
if condition {
return body
}
return nil
}
public let printStatement = Matcher([Keyword("{{"), Variable("body"), Keyword("}}")]) { (variables, interpreter: StringTemplateInterpreter, _) -> String? in
guard let body = variables["body"] else { return nil }
return interpreter.typedInterpreter.print(body)
}
================================================
FILE: Eval.playground/contents.xcplayground
================================================
================================================
FILE: Eval.playground/playground.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: Eval.playground/xcshareddata/xcschemes/Playground.xcscheme
================================================
================================================
FILE: Eval.podspec
================================================
Pod::Spec.new do |s|
s.name = "Eval"
s.version = "1.5.0"
s.summary = "Eval is a lightweight interpreter framework written in Swift, evaluating expressions at runtime"
s.description = <<-DESC
Eval is a lightweight interpreter framework written in Swift, for 📱iOS, 🖥 macOS, and 🐧Linux platforms.
It evaluates expressions at runtime, with operators and data types you define.
DESC
s.homepage = "https://tevelee.github.io/Eval/"
s.license = { :type => "Apache 2.0", :file => "LICENSE.txt" }
s.author = { "Laszlo Teveli" => "tevelee@gmail.com" }
s.social_media_url = "http://twitter.com/tevelee"
s.source = { :git => "https://github.com/tevelee/Eval.git", :tag => "#{s.version}" }
s.source_files = "Sources/**/*.{h,swift}"
s.ios.deployment_target = "8.0"
s.osx.deployment_target = "10.10"
s.watchos.deployment_target = "2.0"
s.tvos.deployment_target = "9.0"
end
================================================
FILE: Eval.xcodeproj/EvalTests_Info.plist
================================================
CFBundleDevelopmentRegion
en
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
BNDL
CFBundleShortVersionString
1.0
CFBundleSignature
????
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSPrincipalClass
================================================
FILE: Eval.xcodeproj/Eval_Info.plist
================================================
CFBundleDevelopmentRegion
en
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
FMWK
CFBundleShortVersionString
1.0
CFBundleSignature
????
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
NSPrincipalClass
================================================
FILE: Eval.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXAggregateTarget section */
"Eval::EvalPackageTests::ProductTarget" /* EvalPackageTests */ = {
isa = PBXAggregateTarget;
buildConfigurationList = OBJ_67 /* Build configuration list for PBXAggregateTarget "EvalPackageTests" */;
buildPhases = (
);
dependencies = (
OBJ_70 /* PBXTargetDependency */,
);
name = EvalPackageTests;
productName = EvalPackageTests;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
3AED32EA232D382D00FA2596 /* PerformanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AED32E9232D382D00FA2596 /* PerformanceTest.swift */; };
3AED32ED232D3D4D00FA2596 /* Suffix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AED32EB232D3D3500FA2596 /* Suffix.swift */; };
OBJ_51 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Common.swift */; };
OBJ_52 /* Elements.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Elements.swift */; };
OBJ_53 /* TemplateInterpreter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* TemplateInterpreter.swift */; };
OBJ_54 /* TypedInterpreter.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* TypedInterpreter.swift */; };
OBJ_55 /* MatchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* MatchResult.swift */; };
OBJ_56 /* Matcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* Matcher.swift */; };
OBJ_57 /* Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* Pattern.swift */; };
OBJ_58 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Utils.swift */; };
OBJ_65 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
OBJ_76 /* InterpreterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_21 /* InterpreterTests.swift */; };
OBJ_77 /* TemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* TemplateTests.swift */; };
OBJ_78 /* DataTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* DataTypeTests.swift */; };
OBJ_79 /* FunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* FunctionTests.swift */; };
OBJ_80 /* InterpreterContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* InterpreterContextTests.swift */; };
OBJ_81 /* KeywordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* KeywordTests.swift */; };
OBJ_82 /* LiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* LiteralTests.swift */; };
OBJ_83 /* MatchResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_29 /* MatchResultTests.swift */; };
OBJ_84 /* MatchStatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_30 /* MatchStatementTests.swift */; };
OBJ_85 /* MatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_31 /* MatcherTests.swift */; };
OBJ_86 /* PatternTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_32 /* PatternTests.swift */; };
OBJ_87 /* TemplateInterpreterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* TemplateInterpreterTests.swift */; };
OBJ_88 /* TypedInterpreterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_34 /* TypedInterpreterTests.swift */; };
OBJ_89 /* UtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* UtilTests.swift */; };
OBJ_90 /* VariableProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_36 /* VariableProcessor.swift */; };
OBJ_91 /* VariableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_37 /* VariableTests.swift */; };
OBJ_92 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_38 /* Utils.swift */; };
OBJ_94 /* Eval.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "Eval::Eval::Product" /* Eval.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
3A0B80062294367A008925A6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = OBJ_1 /* Project object */;
proxyType = 1;
remoteGlobalIDString = "Eval::Eval";
remoteInfo = Eval;
};
3A0B80072294367B008925A6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = OBJ_1 /* Project object */;
proxyType = 1;
remoteGlobalIDString = "Eval::EvalTests";
remoteInfo = EvalTests;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
3AED32E9232D382D00FA2596 /* PerformanceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceTest.swift; sourceTree = ""; };
3AED32EB232D3D3500FA2596 /* Suffix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suffix.swift; sourceTree = ""; };
"Eval::Eval::Product" /* Eval.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Eval.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"Eval::EvalTests::Product" /* EvalTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = EvalTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
OBJ_10 /* Elements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Elements.swift; sourceTree = ""; };
OBJ_11 /* TemplateInterpreter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateInterpreter.swift; sourceTree = ""; };
OBJ_12 /* TypedInterpreter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedInterpreter.swift; sourceTree = ""; };
OBJ_14 /* MatchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchResult.swift; sourceTree = ""; };
OBJ_15 /* Matcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matcher.swift; sourceTree = ""; };
OBJ_16 /* Pattern.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pattern.swift; sourceTree = ""; };
OBJ_17 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; };
OBJ_21 /* InterpreterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpreterTests.swift; sourceTree = ""; };
OBJ_22 /* TemplateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateTests.swift; sourceTree = ""; };
OBJ_24 /* DataTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTypeTests.swift; sourceTree = ""; };
OBJ_25 /* FunctionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionTests.swift; sourceTree = ""; };
OBJ_26 /* InterpreterContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterpreterContextTests.swift; sourceTree = ""; };
OBJ_27 /* KeywordTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordTests.swift; sourceTree = ""; };
OBJ_28 /* LiteralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiteralTests.swift; sourceTree = ""; };
OBJ_29 /* MatchResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchResultTests.swift; sourceTree = ""; };
OBJ_30 /* MatchStatementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchStatementTests.swift; sourceTree = ""; };
OBJ_31 /* MatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatcherTests.swift; sourceTree = ""; };
OBJ_32 /* PatternTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatternTests.swift; sourceTree = ""; };
OBJ_33 /* TemplateInterpreterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateInterpreterTests.swift; sourceTree = ""; };
OBJ_34 /* TypedInterpreterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedInterpreterTests.swift; sourceTree = ""; };
OBJ_35 /* UtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilTests.swift; sourceTree = ""; };
OBJ_36 /* VariableProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariableProcessor.swift; sourceTree = ""; };
OBJ_37 /* VariableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariableTests.swift; sourceTree = ""; };
OBJ_38 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; };
OBJ_39 /* Documentation */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Documentation; sourceTree = SOURCE_ROOT; };
OBJ_40 /* Eval.xcworkspace */ = {isa = PBXFileReference; lastKnownFileType = wrapper.workspace; path = Eval.xcworkspace; sourceTree = SOURCE_ROOT; };
OBJ_41 /* Examples */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Examples; sourceTree = SOURCE_ROOT; };
OBJ_42 /* Scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Scripts; sourceTree = SOURCE_ROOT; };
OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; };
OBJ_9 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
OBJ_59 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
OBJ_93 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 0;
files = (
OBJ_94 /* Eval.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
OBJ_13 /* Utilities */ = {
isa = PBXGroup;
children = (
OBJ_14 /* MatchResult.swift */,
OBJ_15 /* Matcher.swift */,
OBJ_16 /* Pattern.swift */,
OBJ_17 /* Utils.swift */,
);
path = Utilities;
sourceTree = "";
};
OBJ_18 /* Tests */ = {
isa = PBXGroup;
children = (
OBJ_19 /* EvalTests */,
);
name = Tests;
sourceTree = SOURCE_ROOT;
};
OBJ_19 /* EvalTests */ = {
isa = PBXGroup;
children = (
OBJ_20 /* IntegrationTests */,
OBJ_23 /* UnitTests */,
OBJ_38 /* Utils.swift */,
);
name = EvalTests;
path = Tests/EvalTests;
sourceTree = SOURCE_ROOT;
};
OBJ_20 /* IntegrationTests */ = {
isa = PBXGroup;
children = (
OBJ_21 /* InterpreterTests.swift */,
OBJ_22 /* TemplateTests.swift */,
3AED32E9232D382D00FA2596 /* PerformanceTest.swift */,
3AED32EB232D3D3500FA2596 /* Suffix.swift */,
);
path = IntegrationTests;
sourceTree = "";
};
OBJ_23 /* UnitTests */ = {
isa = PBXGroup;
children = (
OBJ_24 /* DataTypeTests.swift */,
OBJ_25 /* FunctionTests.swift */,
OBJ_26 /* InterpreterContextTests.swift */,
OBJ_27 /* KeywordTests.swift */,
OBJ_28 /* LiteralTests.swift */,
OBJ_29 /* MatchResultTests.swift */,
OBJ_30 /* MatchStatementTests.swift */,
OBJ_31 /* MatcherTests.swift */,
OBJ_32 /* PatternTests.swift */,
OBJ_33 /* TemplateInterpreterTests.swift */,
OBJ_34 /* TypedInterpreterTests.swift */,
OBJ_35 /* UtilTests.swift */,
OBJ_36 /* VariableProcessor.swift */,
OBJ_37 /* VariableTests.swift */,
);
path = UnitTests;
sourceTree = "";
};
OBJ_43 /* Products */ = {
isa = PBXGroup;
children = (
"Eval::EvalTests::Product" /* EvalTests.xctest */,
"Eval::Eval::Product" /* Eval.framework */,
);
name = Products;
sourceTree = BUILT_PRODUCTS_DIR;
};
OBJ_5 = {
isa = PBXGroup;
children = (
OBJ_6 /* Package.swift */,
OBJ_7 /* Sources */,
OBJ_18 /* Tests */,
OBJ_39 /* Documentation */,
OBJ_40 /* Eval.xcworkspace */,
OBJ_41 /* Examples */,
OBJ_42 /* Scripts */,
OBJ_43 /* Products */,
);
sourceTree = "";
};
OBJ_7 /* Sources */ = {
isa = PBXGroup;
children = (
OBJ_8 /* Eval */,
);
name = Sources;
sourceTree = SOURCE_ROOT;
};
OBJ_8 /* Eval */ = {
isa = PBXGroup;
children = (
OBJ_9 /* Common.swift */,
OBJ_10 /* Elements.swift */,
OBJ_11 /* TemplateInterpreter.swift */,
OBJ_12 /* TypedInterpreter.swift */,
OBJ_13 /* Utilities */,
);
name = Eval;
path = Sources/Eval;
sourceTree = SOURCE_ROOT;
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
"Eval::Eval" /* Eval */ = {
isa = PBXNativeTarget;
buildConfigurationList = OBJ_47 /* Build configuration list for PBXNativeTarget "Eval" */;
buildPhases = (
OBJ_50 /* Sources */,
OBJ_59 /* Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Eval;
productName = Eval;
productReference = "Eval::Eval::Product" /* Eval.framework */;
productType = "com.apple.product-type.framework";
};
"Eval::EvalTests" /* EvalTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = OBJ_72 /* Build configuration list for PBXNativeTarget "EvalTests" */;
buildPhases = (
OBJ_75 /* Sources */,
OBJ_93 /* Frameworks */,
);
buildRules = (
);
dependencies = (
OBJ_95 /* PBXTargetDependency */,
);
name = EvalTests;
productName = EvalTests;
productReference = "Eval::EvalTests::Product" /* EvalTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
"Eval::SwiftPMPackageDescription" /* EvalPackageDescription */ = {
isa = PBXNativeTarget;
buildConfigurationList = OBJ_61 /* Build configuration list for PBXNativeTarget "EvalPackageDescription" */;
buildPhases = (
OBJ_64 /* Sources */,
);
buildRules = (
);
dependencies = (
);
name = EvalPackageDescription;
productName = EvalPackageDescription;
productType = "com.apple.product-type.framework";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
OBJ_1 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
TargetAttributes = {
"Eval::EvalTests" = {
LastSwiftMigration = 1020;
};
};
};
buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Eval" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = OBJ_5;
productRefGroup = OBJ_43 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
"Eval::Eval" /* Eval */,
"Eval::SwiftPMPackageDescription" /* EvalPackageDescription */,
"Eval::EvalPackageTests::ProductTarget" /* EvalPackageTests */,
"Eval::EvalTests" /* EvalTests */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
OBJ_50 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 0;
files = (
OBJ_51 /* Common.swift in Sources */,
OBJ_52 /* Elements.swift in Sources */,
OBJ_53 /* TemplateInterpreter.swift in Sources */,
OBJ_54 /* TypedInterpreter.swift in Sources */,
OBJ_55 /* MatchResult.swift in Sources */,
OBJ_56 /* Matcher.swift in Sources */,
OBJ_57 /* Pattern.swift in Sources */,
OBJ_58 /* Utils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
OBJ_64 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 0;
files = (
OBJ_65 /* Package.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
OBJ_75 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 0;
files = (
OBJ_76 /* InterpreterTests.swift in Sources */,
OBJ_77 /* TemplateTests.swift in Sources */,
OBJ_78 /* DataTypeTests.swift in Sources */,
3AED32ED232D3D4D00FA2596 /* Suffix.swift in Sources */,
OBJ_79 /* FunctionTests.swift in Sources */,
OBJ_80 /* InterpreterContextTests.swift in Sources */,
OBJ_81 /* KeywordTests.swift in Sources */,
3AED32EA232D382D00FA2596 /* PerformanceTest.swift in Sources */,
OBJ_82 /* LiteralTests.swift in Sources */,
OBJ_83 /* MatchResultTests.swift in Sources */,
OBJ_84 /* MatchStatementTests.swift in Sources */,
OBJ_85 /* MatcherTests.swift in Sources */,
OBJ_86 /* PatternTests.swift in Sources */,
OBJ_87 /* TemplateInterpreterTests.swift in Sources */,
OBJ_88 /* TypedInterpreterTests.swift in Sources */,
OBJ_89 /* UtilTests.swift in Sources */,
OBJ_90 /* VariableProcessor.swift in Sources */,
OBJ_91 /* VariableTests.swift in Sources */,
OBJ_92 /* Utils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
OBJ_70 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = "Eval::EvalTests" /* EvalTests */;
targetProxy = 3A0B80072294367B008925A6 /* PBXContainerItemProxy */;
};
OBJ_95 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = "Eval::Eval" /* Eval */;
targetProxy = 3A0B80062294367A008925A6 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
OBJ_3 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_NS_ASSERTIONS = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
ONLY_ACTIVE_ARCH = YES;
OTHER_SWIFT_FLAGS = "-DXcode";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "SWIFT_PACKAGE DEBUG";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
USE_HEADERMAP = NO;
};
name = Debug;
};
OBJ_4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = s;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
OTHER_SWIFT_FLAGS = "-DXcode";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
USE_HEADERMAP = NO;
};
name = Release;
};
OBJ_48 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Eval.xcodeproj/Eval_Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = Eval;
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = Eval;
};
name = Debug;
};
OBJ_49 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Eval.xcodeproj/Eval_Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx";
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = Eval;
PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)";
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = Eval;
};
name = Release;
};
OBJ_62 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD = /usr/bin/true;
OTHER_SWIFT_FLAGS = "-swift-version 4.2 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
OBJ_63 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD = /usr/bin/true;
OTHER_SWIFT_FLAGS = "-swift-version 4.2 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk";
SWIFT_VERSION = 5.0;
};
name = Release;
};
OBJ_68 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
};
name = Debug;
};
OBJ_69 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
};
name = Release;
};
OBJ_73 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ENABLE_MODULES = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Eval.xcodeproj/EvalTests_Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks";
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = EvalTests;
};
name = Debug;
};
OBJ_74 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CLANG_ENABLE_MODULES = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PLATFORM_DIR)/Developer/Library/Frameworks",
);
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = Eval.xcodeproj/EvalTests_Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks";
OTHER_CFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_VERSION = 5.0;
TARGET_NAME = EvalTests;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
OBJ_2 /* Build configuration list for PBXProject "Eval" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_3 /* Debug */,
OBJ_4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
OBJ_47 /* Build configuration list for PBXNativeTarget "Eval" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_48 /* Debug */,
OBJ_49 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
OBJ_61 /* Build configuration list for PBXNativeTarget "EvalPackageDescription" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_62 /* Debug */,
OBJ_63 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
OBJ_67 /* Build configuration list for PBXAggregateTarget "EvalPackageTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_68 /* Debug */,
OBJ_69 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
OBJ_72 /* Build configuration list for PBXNativeTarget "EvalTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
OBJ_73 /* Debug */,
OBJ_74 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = OBJ_1 /* Project object */;
}
================================================
FILE: Eval.xcodeproj/xcshareddata/xcschemes/Eval-Package.xcscheme
================================================
================================================
FILE: Eval.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist
================================================
SchemeUserState
Eval-Package.xcscheme
SuppressBuildableAutocreation
================================================
FILE: Eval.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: Eval.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: Examples/.swiftlint.yml
================================================
disabled_rules:
- file_header
================================================
FILE: Examples/AttributedStringExample/.gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
================================================
FILE: Examples/AttributedStringExample/Package.swift
================================================
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "AttributedStringExample",
products: [
.library(
name: "AttributedStringExample",
targets: ["AttributedStringExample"])
],
dependencies: [
.package(url: "../../", from: "1.4.0")
],
targets: [
.target(
name: "AttributedStringExample",
dependencies: ["Eval"]),
.testTarget(
name: "AttributedStringExampleTests",
dependencies: ["AttributedStringExample"])
]
)
================================================
FILE: Examples/AttributedStringExample/README.md
================================================
# TemplateExample
A description of this package.
================================================
FILE: Examples/AttributedStringExample/Sources/AttributedStringExample/TemplateExample.swift
================================================
import AppKit
@_exported import Eval
import Foundation
@_exported import class Eval.Pattern
// swiftlint:disable:next type_name
public class AttributedStringTemplateInterpreter: TemplateInterpreter {
typealias EvaluatedType = NSAttributedString
override public func evaluate(_ expression: String, context: Context = Context()) -> NSAttributedString {
return evaluate(expression, context: context, reducer: (initialValue: NSAttributedString(), reduceValue: { existing, next in
existing.appending(next)
}, reduceCharacter: { existing, next in
existing.appending(NSAttributedString(string: String(next)))
}))
}
}
// swiftlint:disable:next type_name
public class AttributedStringInterpreter: EvaluatorWithLocalContext {
public typealias EvaluatedType = NSAttributedString
let interpreter: AttributedStringTemplateInterpreter
init() {
let context = Context()
let center = NSMutableParagraphStyle()
center.alignment = .center
interpreter = AttributedStringTemplateInterpreter(statements: [AttributedStringInterpreter.attributeMatcher(name: "bold", attributes: [.font: NSFont.boldSystemFont(ofSize: 12)]),
AttributedStringInterpreter.attributeMatcher(name: "red", attributes: [.foregroundColor: NSColor.red]),
AttributedStringInterpreter.attributeMatcher(name: "center", attributes: [.paragraphStyle: center])],
interpreter: TypedInterpreter(context: context),
context: context)
}
public func evaluate(_ expression: String) -> AttributedStringInterpreter.EvaluatedType {
return interpreter.evaluate(expression)
}
public func evaluate(_ expression: String, context: Context) -> AttributedStringInterpreter.EvaluatedType {
return interpreter.evaluate(expression, context: context)
}
static func attributeMatcher(name: String, attributes: [NSAttributedString.Key: Any]) -> Pattern> {
return Pattern([OpenKeyword("<\(name)>"), GenericVariable("body", options: .notInterpreted), CloseKeyword("\(name)>")]) {
guard let body = $0.variables["body"] as? String else { return nil }
return NSAttributedString(string: body, attributes: attributes)
}
}
}
public extension NSAttributedString {
func appending(_ other: NSAttributedString) -> NSAttributedString {
let mutable = NSMutableAttributedString(attributedString: self)
mutable.append(other)
return mutable
}
}
================================================
FILE: Examples/AttributedStringExample/Tests/.swiftlint.yml
================================================
disabled_rules:
- force_cast
- force_try
- type_name
- file_header
- explicit_top_level_acl
================================================
FILE: Examples/AttributedStringExample/Tests/AttributedStringExampleTests/AttributedStringExampleTests.swift
================================================
@testable import AttributedStringExample
import Eval
import XCTest
class AttributedStringExampleTests: XCTestCase {
let interpreter: AttributedStringInterpreter = AttributedStringInterpreter()
func testExample() {
let interpreter = AttributedStringInterpreter()
XCTAssertEqual(interpreter.evaluate("Hello"), NSAttributedString(string: "Hello", attributes: [.font: NSFont.boldSystemFont(ofSize: 12)]))
XCTAssertEqual(interpreter.evaluate("It's red"), NSAttributedString(string: "It's ").appending(NSAttributedString(string: "red", attributes: [.foregroundColor: NSColor.red])))
let style = interpreter.evaluate("Centered text").attribute(.paragraphStyle, at: 0, effectiveRange: nil) as! NSParagraphStyle
XCTAssertEqual(style.alignment, .center)
}
}
================================================
FILE: Examples/AttributedStringExample/Tests/LinuxMain.swift
================================================
@testable import TemplateExampleTests
import XCTest
XCTMain([
testCase(TemplateExampleTests.allTests)
])
================================================
FILE: Examples/ColorParserExample/.gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
================================================
FILE: Examples/ColorParserExample/Package.swift
================================================
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "ColorParserExample",
products: [
.library(
name: "ColorParserExample",
targets: ["ColorParserExample"])
],
dependencies: [
.package(url: "../../", from: "1.4.0")
],
targets: [
.target(
name: "ColorParserExample",
dependencies: ["Eval"]),
.testTarget(
name: "ColorParserExampleTests",
dependencies: ["ColorParserExample"])
]
)
================================================
FILE: Examples/ColorParserExample/README.md
================================================
# TemplateExample
A description of this package.
================================================
FILE: Examples/ColorParserExample/Sources/ColorParserExample/ColorParserExample.swift
================================================
import AppKit
@_exported import Eval
@_exported import class Eval.Pattern
import Foundation
public class ColorParser: EvaluatorWithLocalContext {
let interpreter: TypedInterpreter
init() {
interpreter = TypedInterpreter(dataTypes: [ColorParser.colorDataType()], functions: [ColorParser.mixFunction()])
}
static func colorDataType() -> DataType {
let hex = Literal {
guard $0.value.first == "#", $0.value.count == 7,
let red = Int($0.value[1...2], radix: 16),
let green = Int($0.value[3...4], radix: 16),
let blue = Int($0.value[5...6], radix: 16) else { return nil }
return NSColor(calibratedRed: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: 1)
}
let red = Literal("red", convertsTo: NSColor.red)
return DataType(type: NSColor.self, literals: [hex, red]) { $0.value.description }
}
static func mixFunction() -> Function {
return Function([Variable("lhs"), Keyword("mixed with"), Variable("rhs")]) {
guard let lhs = $0.variables["lhs"] as? NSColor, let rhs = $0.variables["rhs"] as? NSColor else { return nil }
return lhs.blend(with: rhs)
}
}
public func evaluate(_ expression: String) -> Any? {
return interpreter.evaluate(expression)
}
public func evaluate(_ expression: String, context: Context) -> Any? {
return interpreter.evaluate(expression, context: context)
}
}
extension String {
subscript (range: CountableClosedRange) -> Substring {
return self[index(startIndex, offsetBy: range.lowerBound) ..< index(startIndex, offsetBy: range.upperBound)]
}
}
extension NSColor {
func blend(with other: NSColor, using factor: CGFloat = 0.5) -> NSColor {
let inverseFactor = 1.0 - factor
var leftRed: CGFloat = 0
var leftGreen: CGFloat = 0
var leftBlue: CGFloat = 0
var leftAlpha: CGFloat = 0
getRed(&leftRed, green: &leftGreen, blue: &leftBlue, alpha: &leftAlpha)
var rightRed: CGFloat = 0
var rightGreen: CGFloat = 0
var rightBlue: CGFloat = 0
var rightAlpha: CGFloat = 0
other.getRed(&rightRed, green: &rightGreen, blue: &rightBlue, alpha: &rightAlpha)
return NSColor(calibratedRed: leftRed * factor + rightRed * inverseFactor,
green: leftGreen * factor + rightGreen * inverseFactor,
blue: leftBlue * factor + rightBlue * inverseFactor,
alpha: leftAlpha * factor + rightAlpha * inverseFactor)
}
}
================================================
FILE: Examples/ColorParserExample/Tests/.swiftlint.yml
================================================
disabled_rules:
- force_cast
- force_try
- type_name
- file_header
- explicit_top_level_acl
================================================
FILE: Examples/ColorParserExample/Tests/ColorParserExampleTests/ColorParserExampleTests.swift
================================================
@testable import ColorParserExample
import Eval
import XCTest
class ColorParserExampleTests: XCTestCase {
let colorParser: ColorParser = ColorParser()
func testExample() {
XCTAssertEqual(colorParser.evaluate("#00ff00") as! NSColor, NSColor(calibratedRed: 0, green: 1, blue: 0, alpha: 1))
XCTAssertEqual(colorParser.evaluate("red") as! NSColor, .red)
XCTAssertEqual(colorParser.evaluate("#ff0000 mixed with #0000ff") as! NSColor, NSColor(calibratedRed: 0.5, green: 0, blue: 0.5, alpha: 1))
}
}
================================================
FILE: Examples/ColorParserExample/Tests/LinuxMain.swift
================================================
@testable import TemplateExampleTests
import XCTest
XCTMain([
testCase(TemplateExampleTests.allTests)
])
================================================
FILE: Examples/TemplateExample/.gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
================================================
FILE: Examples/TemplateExample/Package.swift
================================================
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "TemplateExample",
products: [
.library(
name: "TemplateExample",
targets: ["TemplateExample"])
],
dependencies: [
.package(url: "../../", from: "1.4.0")
],
targets: [
.target(
name: "TemplateExample",
dependencies: ["Eval"]),
.testTarget(
name: "TemplateExampleTests",
dependencies: ["TemplateExample"])
]
)
================================================
FILE: Examples/TemplateExample/README.md
================================================
# TemplateExample
A description of this package.
================================================
FILE: Examples/TemplateExample/Sources/TemplateExample/TemplateExample.swift
================================================
@_exported import Eval
@_exported import class Eval.Pattern
import Foundation
public class TemplateLanguage: EvaluatorWithLocalContext {
public typealias EvaluatedType = String
let language: StringTemplateInterpreter
let macroReplacer: StringTemplateInterpreter
init(dataTypes: [DataTypeProtocol] = StandardLibrary.dataTypes,
functions: [FunctionProtocol] = StandardLibrary.functions,
templates: [Pattern>] = TemplateLibrary.templates,
context: Context = Context()) {
TemplateLanguage.preprocess(context)
let interpreter = TypedInterpreter(dataTypes: dataTypes, functions: functions, context: context)
let language = StringTemplateInterpreter(statements: templates, interpreter: interpreter, context: context)
self.language = language
let block = Pattern>([OpenKeyword("{{{"), TemplateVariable("name", options: .notInterpreted), CloseKeyword("}}}")]) {
guard let name = $0.variables["name"] as? String else { return nil }
return language.context.blocks[name]?.last?(language.context)
}
macroReplacer = StringTemplateInterpreter(statements: [block])
}
public func evaluate(_ expression: String) -> String {
return evaluate(expression, context: Context())
}
public func evaluate(_ expression: String, context: Context) -> String {
TemplateLanguage.preprocess(context)
let input = replaceWhitespaces(expression)
let result = language.evaluate(input, context: context)
let finalResult = macroReplacer.evaluate(result)
return finalResult.contains(TemplateLibrary.tagPrefix) ? language.evaluate(finalResult, context: context) : finalResult
}
public func evaluate(template from: URL) throws -> String {
let expression = try String(contentsOf: from)
return evaluate(expression)
}
public func evaluate(template from: URL, context: Context) throws -> String {
let expression = try String(contentsOf: from)
return evaluate(expression, context: context)
}
static func preprocess(_ context: Context) {
context.variables = context.variables.mapValues { value in
convert(value) {
if let integerValue = $0 as? Int {
return Double(integerValue)
}
return $0
}
}
}
static func convert(_ value: Any, recursively: Bool = true, convert: @escaping (Any) -> Any) -> Any {
if recursively, let array = value as? [Any] {
return array.map { convert($0) }
}
if recursively, let dictionary = value as? [String: Any] {
return dictionary.mapValues { convert($0) }
}
return convert(value)
}
func replaceWhitespaces(_ input: String) -> String {
let tag = "{-}"
var input = input
repeat {
if var range = input.range(of: tag) {
searchForward: while true {
if range.upperBound < input.index(before: input.endIndex) {
let nextIndex = range.upperBound
if let unicodeScalar = input[nextIndex].unicodeScalars.first,
CharacterSet.whitespacesAndNewlines.contains(unicodeScalar) {
range = Range(uncheckedBounds: (lower: range.lowerBound, upper: input.index(after: range.upperBound)))
} else {
break searchForward
}
} else {
break searchForward
}
}
searchBackward: while true {
if range.lowerBound > input.startIndex {
let nextIndex = input.index(before: range.lowerBound)
if let unicodeScalar = input[nextIndex].unicodeScalars.first,
CharacterSet.whitespacesAndNewlines.contains(unicodeScalar) {
range = Range(uncheckedBounds: (lower: input.index(before: range.lowerBound), upper: range.upperBound))
} else {
break searchBackward
}
} else {
break searchBackward
}
}
input.replaceSubrange(range, with: "")
}
} while input.contains(tag)
return input
}
}
internal typealias Macro = (arguments: [String], body: String)
internal typealias BlockRenderer = (_ context: Context) -> String
extension Context {
static let macrosKey: String = "__macros"
var macros: [String: Macro] {
get {
return variables[Context.macrosKey] as? [String: Macro] ?? [:]
}
set {
variables[Context.macrosKey] = macros.merging(newValue) { _, new in new }
}
}
static let blocksKey: String = "__blocks"
var blocks: [String: [BlockRenderer]] {
get {
return variables[Context.blocksKey] as? [String: [BlockRenderer]] ?? [:]
}
set {
variables[Context.blocksKey] = blocks.merging(newValue) { _, new in new }
}
}
}
public class TemplateLibrary {
public static var standardLibrary: StandardLibrary = StandardLibrary()
public static var templates: [Pattern>] {
return [
ifElseStatement,
ifStatement,
printStatement,
forInStatement,
setUsingBodyStatement,
setStatement,
blockStatement,
macroStatement,
commentStatement,
importStatement,
spacelessStatement
]
}
public static var tagPrefix: String = "{%"
public static var tagSuffix: String = "%}"
public static var ifStatement: Pattern> {
return Pattern([Keyword(tagPrefix + " if"), Variable("condition"), Keyword(tagSuffix), TemplateVariable("body", options: .notTrimmed) {
guard let content = $0.value as? String, !content.contains(tagPrefix + " else " + tagSuffix) else { return nil }
return content
}, Keyword("{%"), Keyword("endif"), Keyword("%}")]) {
guard let condition = $0.variables["condition"] as? Bool, let body = $0.variables["body"] as? String else { return nil }
return condition ? body : ""
}
}
public static var ifElseStatement: Pattern> {
return Pattern([OpenKeyword(tagPrefix + " if"), Variable("condition"), Keyword(tagSuffix), TemplateVariable("body", options: .notTrimmed) {
guard let content = $0.value as? String, !content.contains(tagPrefix + " else " + tagSuffix) else { return nil }
return content
}, Keyword(tagPrefix + " else " + tagSuffix), TemplateVariable("else", options: .notTrimmed) {
guard let content = $0.value as? String, !content.contains(tagPrefix + " else " + tagSuffix) else { return nil }
return content
}, CloseKeyword(tagPrefix + " endif " + tagSuffix)]) {
guard let condition = $0.variables["condition"] as? Bool, let body = $0.variables["body"] as? String else { return nil }
return condition ? body : $0.variables["else"] as? String
}
}
public static var printStatement: Pattern> {
return Pattern([OpenKeyword("{{"), Variable("body"), CloseKeyword("}}")]) {
guard let body = $0.variables["body"] else { return nil }
return $0.interpreter.typedInterpreter.print(body)
}
}
public static var forInStatement: Pattern> {
return Pattern([OpenKeyword(tagPrefix + " for"),
GenericVariable("variable", options: .notInterpreted), Keyword("in"),
Variable<[Any]>("items"),
Keyword(tagSuffix),
GenericVariable("body", options: [.notInterpreted, .notTrimmed]),
CloseKeyword(tagPrefix + " endfor " + tagSuffix)]) {
guard let variableName = $0.variables["variable"] as? String,
let items = $0.variables["items"] as? [Any],
let body = $0.variables["body"] as? String else { return nil }
var result = ""
$0.context.push()
$0.context.variables["__loop"] = items
for (index, item) in items.enumerated() {
$0.context.variables["__first"] = index == items.startIndex
$0.context.variables["__last"] = index == items.index(before: items.endIndex)
$0.context.variables[variableName] = item
result += $0.interpreter.evaluate(body, context: $0.context)
}
$0.context.pop()
return result
}
}
public static var setStatement: Pattern> {
return Pattern([OpenKeyword(tagPrefix + " set"), TemplateVariable("variable"), Keyword(tagSuffix), TemplateVariable("body"), CloseKeyword(tagPrefix + " endset " + tagSuffix)]) {
guard let variableName = $0.variables["variable"] as? String, let body = $0.variables["body"] as? String else { return nil }
$0.interpreter.context.variables[variableName] = body
return ""
}
}
public static var setUsingBodyStatement: Pattern> {
return Pattern([OpenKeyword(tagPrefix + " set"), TemplateVariable("variable"), Keyword("="), Variable("value"), CloseKeyword(tagSuffix)]) {
guard let variableName = $0.variables["variable"] as? String else { return nil }
$0.interpreter.context.variables[variableName] = $0.variables["value"]
return ""
}
}
public static var blockStatement: Pattern> {
return Pattern([OpenKeyword(tagPrefix + " block"),
GenericVariable("name", options: .notInterpreted),
Keyword(tagSuffix),
GenericVariable("body", options: .notInterpreted),
CloseKeyword(tagPrefix + " endblock " + tagSuffix)]) { match in
guard let name = match.variables["name"] as? String, let body = match.variables["body"] as? String else { return nil }
let block: BlockRenderer = { context in
context.push()
context.merge(with: match.context) { existing, _ in existing }
context.variables["__block"] = name
if let last = context.blocks[name] {
context.blocks[name] = Array(last.dropLast())
}
let result = match.interpreter.evaluate(body, context: context)
context.pop()
return result
}
if let last = match.interpreter.context.blocks[name] {
match.interpreter.context.blocks[name] = last + [block]
return ""
} else {
match.interpreter.context.blocks[name] = [block]
return "{{{\(name)}}}"
}
}
}
public static var macroStatement: Pattern> {
return Pattern([OpenKeyword(tagPrefix + " macro"), GenericVariable("name", options: .notInterpreted), Keyword("("), GenericVariable<[String], StringTemplateInterpreter>("arguments", options: .notInterpreted) {
guard let arguments = $0.value as? String else { return nil }
return arguments.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
}, Keyword(")"), Keyword(tagSuffix), GenericVariable("body", options: .notInterpreted), CloseKeyword(tagPrefix + " endmacro " + tagSuffix)]) {
guard let name = $0.variables["name"] as? String,
let arguments = $0.variables["arguments"] as? [String],
let body = $0.variables["body"] as? String else { return nil }
$0.interpreter.context.macros[name] = (arguments: arguments, body: body)
return ""
}
}
public static var commentStatement: Pattern> {
return Pattern([OpenKeyword("{#"), GenericVariable("body", options: .notInterpreted), CloseKeyword("#}")]) { _ in "" }
}
public static var importStatement: Pattern> {
return Pattern([OpenKeyword(tagPrefix + " import"), Variable("file"), CloseKeyword(tagSuffix)]) {
guard let file = $0.variables["file"] as? String,
let url = Bundle.allBundles.compactMap({ $0.url(forResource: file, withExtension: nil) }).first,
let expression = try? String(contentsOf: url) else { return nil }
return $0.interpreter.evaluate(expression, context: $0.context)
}
}
public static var spacelessStatement: Pattern> {
return Pattern([OpenKeyword(tagPrefix + " spaceless " + tagSuffix), TemplateVariable("body"), CloseKeyword(tagPrefix + " endspaceless " + tagSuffix)]) {
guard let body = $0.variables["body"] as? String else { return nil }
return body.self.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }.joined()
}
}
}
// swiftlint:disable:next type_body_length
public class StandardLibrary {
public static var dataTypes: [DataTypeProtocol] {
return [
stringType,
booleanType,
arrayType,
dictionaryType,
dateType,
numericType,
emptyType
]
}
public static var functions: [FunctionProtocol] {
return [
parentheses,
macro,
blockParent,
ternaryOperator,
rangeFunction,
rangeOfStringFunction,
rangeBySteps,
loopIsFirst,
loopIsLast,
loopIsNotFirst,
loopIsNotLast,
startsWithOperator,
endsWithOperator,
containsOperator,
matchesOperator,
capitalise,
lowercase,
uppercase,
lowercaseFirst,
uppercaseFirst,
trim,
urlEncode,
urlDecode,
escape,
nl2br,
stringConcatenationOperator,
multiplicationOperator,
divisionOperator,
additionOperator,
subtractionOperator,
moduloOperator,
powOperator,
lessThanOperator,
lessThanOrEqualsOperator,
moreThanOperator,
moreThanOrEqualsOperator,
equalsOperator,
notEqualsOperator,
stringEqualsOperator,
stringNotEqualsOperator,
inNumericArrayOperator,
inStringArrayOperator,
incrementOperator,
decrementOperator,
negationOperator,
notOperator,
orOperator,
andOperator,
absoluteValue,
defaultValue,
isEvenOperator,
isOddOperator,
minFunction,
maxFunction,
sumFunction,
sqrtFunction,
roundFunction,
averageFunction,
arraySubscript,
arrayCountFunction,
arrayMapFunction,
arrayFilterFunction,
arraySortFunction,
arrayReverseFunction,
arrayMinFunction,
arrayMaxFunction,
arrayFirstFunction,
arrayLastFunction,
arrayJoinFunction,
arraySplitFunction,
arrayMergeFunction,
arraySumFunction,
arrayAverageFunction,
dictionarySubscript,
dictionaryCountFunction,
dictionaryFilterFunction,
dictionaryKeys,
dictionaryValues,
dateFactory,
dateFormat,
stringFactory
]
}
// MARK: Types
public static var numericType: DataType {
let numberLiteral = Literal { Double($0.value) }
let piLiteral = Literal("pi", convertsTo: Double.pi)
return DataType(type: Double.self, literals: [numberLiteral, piLiteral]) { String(format: "%g", $0.value) }
}
public static var stringType: DataType {
let singleQuotesLiteral = literal(opening: "'", closing: "'") { $0.value }
return DataType(type: String.self, literals: [singleQuotesLiteral]) { $0.value }
}
public static var dateType: DataType {
let dateFormatter = DateFormatter(with: "yyyy-MM-dd HH:mm:ss")
let now = Literal("now", convertsTo: Date())
return DataType(type: Date.self, literals: [now]) { dateFormatter.string(from: $0.value) }
}
public static var arrayType: DataType<[CustomStringConvertible]> {
let arrayLiteral = literal(opening: "[", closing: "]") { literal -> [CustomStringConvertible]? in
literal.value
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.map { literal.interpreter.evaluate(String($0)) as? CustomStringConvertible ?? String($0) }
}
return DataType(type: [CustomStringConvertible].self, literals: [arrayLiteral]) { dataType in dataType.value.map { dataType.printer.print($0) }.joined(separator: ",") }
}
public static var dictionaryType: DataType<[String: CustomStringConvertible?]> {
let dictionaryLiteral = literal(opening: "{", closing: "}") { body -> [String: CustomStringConvertible?]? in
let values = body.value
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
let parsedValues : [(key: String, value: CustomStringConvertible?)] = values
.map { $0.split(separator: ":").map { body.interpreter.evaluate(String($0)) } }
.compactMap {
guard let first = $0.first, let key = first as? String, let value = $0.last else { return nil }
return (key: key, value: value as? CustomStringConvertible)
}
return Dictionary(grouping: parsedValues) { $0.key }.mapValues { $0.first?.value }
}
return DataType(type: [String: CustomStringConvertible?].self, literals: [dictionaryLiteral]) { dataType in
let items = dataType.value.map { key, value in
if let value = value {
return "\(dataType.printer.print(key)): \(dataType.printer.print(value))"
} else {
return "\(dataType.printer.print(key)): nil"
}
}.sorted().joined(separator: ", ")
return "[\(items)]"
}
}
public static var booleanType: DataType {
let trueLiteral = Literal("true", convertsTo: true)
let falseLiteral = Literal("false", convertsTo: false)
return DataType(type: Bool.self, literals: [trueLiteral, falseLiteral]) { $0.value ? "true" : "false" }
}
public static var emptyType: DataType {
let nullLiteral = Literal("null", convertsTo: nil)
let nilLiteral = Literal("nil", convertsTo: nil)
return DataType(type: Any?.self, literals: [nullLiteral, nilLiteral]) { _ in "null" }
}
// MARK: Functions
public static var parentheses: Function {
return Function([OpenKeyword("("), Variable("body"), CloseKeyword(")")]) { $0.variables["body"] as? Double }
}
public static var macro: Function {
return Function([Variable("name", options: .notInterpreted) {
guard let value = $0.value as? String else { return nil }
return $0.interpreter.context.macros.keys.contains(value) ? value : nil
}, Keyword("("), Variable("arguments", options: .notInterpreted), Keyword(")")]) { match in
guard let arguments = match.variables["arguments"] as? String,
let name = match.variables["name"] as? String,
let macro = match.interpreter.context.macros[name.trimmingCharacters(in: .whitespacesAndNewlines)] else { return nil }
let interpretedArguments = arguments.split(separator: ",").compactMap { match.interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) }
match.context.push()
for (key, value) in zip(macro.arguments, interpretedArguments) {
match.context.variables[key] = value
}
let result = match.interpreter.evaluate(macro.body, context: match.context)
match.context.pop()
return result
}
}
public static var blockParent: Function {
return Function([Keyword("parent"), Keyword("("), Variable("arguments", options: .notInterpreted), Keyword(")")]) {
guard let arguments = $0.variables["arguments"] as? String else { return nil }
var interpretedArguments: [String: Any] = [:]
for argument in arguments.split(separator: ",") {
let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=")
if let key = parts.first, let value = parts.last {
interpretedArguments[String(key)] = $0.interpreter.evaluate(String(value))
}
}
guard let name = $0.context.variables["__block"] as? String, let block = $0.context.blocks[name]?.last else { return nil }
$0.context.push()
$0.context.variables.merge(interpretedArguments) { _, new in new }
let result = block($0.context)
$0.context.pop()
return result
}
}
public static var ternaryOperator: Function {
return Function([Variable("condition"), Keyword("?"), Variable("body"), Keyword(": "), Variable("else")]) {
guard let condition = $0.variables["condition"] as? Bool else { return nil }
return condition ? $0.variables["body"] : $0.variables["else"]
}
}
public static var rangeFunction: Function<[Double]> {
return infixOperator("...") { (lhs: Double, rhs: Double) in
CountableClosedRange(uncheckedBounds: (lower: Int(lhs), upper: Int(rhs))).map { Double($0) }
}
}
public static var rangeOfStringFunction: Function<[String]> {
return infixOperator("...") { (lhs: String, rhs: String) in
CountableClosedRange(uncheckedBounds: (lower: Character(lhs), upper: Character(rhs))).map { String($0) }
}
}
public static var startsWithOperator: Function {
return infixOperator("starts with") { (lhs: String, rhs: String) in lhs.hasPrefix(rhs) }
}
public static var endsWithOperator: Function {
return infixOperator("ends with") { (lhs: String, rhs: String) in lhs.hasSuffix(rhs) }
}
public static var containsOperator: Function {
return infixOperator("contains") { (lhs: String, rhs: String) in lhs.contains(rhs) }
}
public static var matchesOperator: Function {
return infixOperator("matches") { (lhs: String, rhs: String) in
if let regex = try? NSRegularExpression(pattern: rhs) {
let matches = regex.numberOfMatches(in: lhs, range: NSRange(lhs.startIndex..., in: lhs))
return matches > 0
}
return false
}
}
public static var capitalise: Function {
return objectFunction("capitalise") { (value: String) -> String? in value.capitalized }
}
public static var lowercase: Function {
return objectFunction("lower") { (value: String) -> String? in value.lowercased() }
}
public static var uppercase: Function {
return objectFunction("upper") { (value: String) -> String? in value.uppercased() }
}
public static var lowercaseFirst: Function {
return objectFunction("lowerFirst") { (value: String) -> String? in
guard let first = value.first else { return nil }
return String(first).lowercased() + value[value.index(value.startIndex, offsetBy: 1)...]
}
}
public static var uppercaseFirst: Function {
return objectFunction("upperFirst") { (value: String) -> String? in
guard let first = value.first else { return nil }
return String(first).uppercased() + value[value.index(value.startIndex, offsetBy: 1)...]
}
}
public static var trim: Function {
return objectFunction("trim") { (value: String) -> String? in value.trimmingCharacters(in: .whitespacesAndNewlines) }
}
public static var urlEncode: Function {
return objectFunction("urlEncode") { (value: String) -> String? in value.addingPercentEncoding(withAllowedCharacters: .alphanumerics) }
}
public static var urlDecode: Function {
return objectFunction("urlDecode") { (value: String) -> String? in value.removingPercentEncoding }
}
public static var escape: Function {
return objectFunction("escape") { (value: String) -> String? in value.html }
}
public static var nl2br: Function {
return objectFunction("nl2br") { (value: String) -> String? in value
.replacingOccurrences(of: "\r\n", with: "
")
.replacingOccurrences(of: "\n", with: "
")
}
}
public static var stringConcatenationOperator: Function {
return infixOperator("+") { (lhs: String, rhs: String) in lhs + rhs }
}
public static var additionOperator: Function {
return infixOperator("+") { (lhs: Double, rhs: Double) in lhs + rhs }
}
public static var subtractionOperator: Function {
return infixOperator("-") { (lhs: Double, rhs: Double) in lhs - rhs }
}
public static var multiplicationOperator: Function {
return infixOperator("*") { (lhs: Double, rhs: Double) in lhs * rhs }
}
public static var divisionOperator: Function {
return infixOperator("/") { (lhs: Double, rhs: Double) in lhs / rhs }
}
public static var moduloOperator: Function {
return infixOperator("%") { (lhs: Double, rhs: Double) in Double(Int(lhs) % Int(rhs)) }
}
public static var powOperator: Function {
return infixOperator("**") { (lhs: Double, rhs: Double) in pow(lhs, rhs) }
}
public static var lessThanOperator: Function {
return infixOperator("<") { (lhs: Double, rhs: Double) in lhs < rhs }
}
public static var moreThanOperator: Function {
return infixOperator("<=") { (lhs: Double, rhs: Double) in lhs <= rhs }
}
public static var lessThanOrEqualsOperator: Function {
return infixOperator(">") { (lhs: Double, rhs: Double) in lhs > rhs }
}
public static var moreThanOrEqualsOperator: Function {
return infixOperator(">=") { (lhs: Double, rhs: Double) in lhs >= rhs }
}
public static var equalsOperator: Function {
return infixOperator("==") { (lhs: Double, rhs: Double) in lhs == rhs }
}
public static var notEqualsOperator: Function {
return infixOperator("!=") { (lhs: Double, rhs: Double) in lhs != rhs }
}
public static var stringEqualsOperator: Function {
return infixOperator("==") { (lhs: String, rhs: String) in lhs == rhs }
}
public static var stringNotEqualsOperator: Function {
return infixOperator("!=") { (lhs: String, rhs: String) in lhs != rhs }
}
public static var inStringArrayOperator: Function {
return infixOperator("in") { (lhs: String, rhs: [String]) in rhs.contains(lhs) }
}
public static var inNumericArrayOperator: Function {
return infixOperator("in") { (lhs: Double, rhs: [Double]) in rhs.contains(lhs) }
}
public static var negationOperator: Function {
return prefixOperator("!") { (expression: Bool) in !expression }
}
public static var notOperator: Function {
return prefixOperator("not") { (expression: Bool) in !expression }
}
public static var andOperator: Function {
return infixOperator("and") { (lhs: Bool, rhs: Bool) in lhs && rhs }
}
public static var orOperator: Function {
return infixOperator("or") { (lhs: Bool, rhs: Bool) in lhs || rhs }
}
public static var absoluteValue: Function {
return objectFunction("abs") { (value: Double) -> Double? in abs(value) }
}
public static var defaultValue: Function {
return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) {
guard let value = $0.value as? String, value == "default" else { return nil }
return value
}, Keyword("("), Variable("fallback"), Keyword(")")], options: .backwardMatch) {
guard let value = $0.variables["lhs"], $0.variables["rhs"] != nil else { return nil }
return isNilOrWrappedNil(value: value) ? $0.variables["fallback"] : value
}
}
public static var incrementOperator: Function {
return suffixOperator("++") { (expression: Double) in expression + 1 }
}
public static var decrementOperator: Function {
return suffixOperator("--") { (expression: Double) in expression - 1 }
}
public static var isEvenOperator: Function {
return suffixOperator("is even") { (expression: Double) in Int(expression) % 2 == 0 }
}
public static var isOddOperator: Function {
return suffixOperator("is odd") { (expression: Double) in abs(Int(expression) % 2) == 1 }
}
public static var minFunction: Function {
return function("min") { (arguments: [Any]) -> Double? in
guard let arguments = arguments as? [Double] else { return nil }
return arguments.min()
}
}
public static var maxFunction: Function {
return function("max") { (arguments: [Any]) -> Double? in
guard let arguments = arguments as? [Double] else { return nil }
return arguments.max()
}
}
public static var arraySortFunction: Function<[Double]> {
return objectFunction("sort") { (object: [Double]) -> [Double]? in object.sorted() }
}
public static var arrayReverseFunction: Function<[Double]> {
return objectFunction("reverse") { (object: [Double]) -> [Double]? in object.reversed() }
}
public static var arrayMinFunction: Function {
return objectFunction("min") { (object: [Double]) -> Double? in object.min() }
}
public static var arrayMaxFunction: Function {
return objectFunction("max") { (object: [Double]) -> Double? in object.max() }
}
public static var arrayFirstFunction: Function {
return objectFunction("first") { (object: [Double]) -> Double? in object.first }
}
public static var arrayLastFunction: Function {
return objectFunction("last") { (object: [Double]) -> Double? in object.last }
}
public static var arrayJoinFunction: Function {
return objectFunctionWithParameters("join") { (object: [String], arguments: [Any]) -> String? in
guard let separator = arguments.first as? String else { return nil }
return object.joined(separator: separator)
}
}
public static var arraySplitFunction: Function<[String]> {
return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) {
guard let value = $0.value as? String, value == "split" else { return nil }
return value
}, Keyword("("), Variable("separator"), Keyword(")")]) {
guard let object = $0.variables["lhs"] as? String, $0.variables["rhs"] != nil, let separator = $0.variables["separator"] as? String else { return nil }
return object.split(separator: Character(separator)).map { String($0) }
}
}
public static var arrayMergeFunction: Function<[Any]> {
return Function([Variable<[Any]>("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) {
guard let value = $0.value as? String, value == "merge" else { return nil }
return value
}, Keyword("("), Variable<[Any]>("other"), Keyword(")")]) {
guard let object = $0.variables["lhs"] as? [Any], $0.variables["rhs"] != nil, let other = $0.variables["other"] as? [Any] else { return nil }
return object + other
}
}
public static var arraySumFunction: Function {
return objectFunction("sum") { (object: [Double]) -> Double? in object.reduce(0, +) }
}
public static var arrayAverageFunction: Function {
return objectFunction("avg") { (object: [Double]) -> Double? in object.reduce(0, +) / Double(object.count) }
}
public static var arrayCountFunction: Function {
return objectFunction("count") { (object: [Double]) -> Double? in Double(object.count) }
}
public static var dictionaryCountFunction: Function {
return objectFunction("count") { (object: [String: Any]) -> Double? in Double(object.count) }
}
public static var arrayMapFunction: Function<[Any]> {
return Function([Variable<[Any]>("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) {
guard let value = $0.value as? String, value == "map" else { return nil }
return value
}, Keyword("("), Variable("variable", options: .notInterpreted), Keyword("=>"), Variable("body", options: .notInterpreted), Keyword(")")]) { match in
guard let object = match.variables["lhs"] as? [Any], match.variables["rhs"] != nil,
let variable = match.variables["variable"] as? String,
let body = match.variables["body"] as? String else { return nil }
match.context.push()
let result: [Any] = object.compactMap { item in
match.context.variables[variable] = item
return match.interpreter.evaluate(body, context: match.context)
}
match.context.pop()
return result
}
}
public static var arrayFilterFunction: Function<[Any]> {
return Function([Variable<[Any]>("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) {
guard let value = $0.value as? String, value == "filter" else { return nil }
return value
}, Keyword("("), Variable("variable", options: .notInterpreted), Keyword("=>"), Variable("body", options: .notInterpreted), Keyword(")")]) { match in
guard let object = match.variables["lhs"] as? [Any], match.variables["rhs"] != nil,
let variable = match.variables["variable"] as? String,
let body = match.variables["body"] as? String else { return nil }
match.context.push()
let result: [Any] = object.filter { item in
match.context.variables[variable] = item
if let result = match.interpreter.evaluate(body, context: match.context) as? Bool {
return result
}
return false
}
match.context.pop()
return result
}
}
public static var dictionaryFilterFunction: Function<[String: Any]> {
return Function([Variable<[String: Any]>("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted) {
guard let value = $0.value as? String, value == "filter" else { return nil }
return value
}, Keyword("("), Variable("key", options: .notInterpreted), Keyword(","), Variable("value", options: .notInterpreted), Keyword("=>"), Variable("body", options: .notInterpreted), Keyword(")")]) { match in
guard let object = match.variables["lhs"] as? [String: Any], match.variables["rhs"] != nil,
let keyVariable = match.variables["key"] as? String,
let valueVariable = match.variables["value"] as? String,
let body = match.variables["body"] as? String else { return nil }
match.context.push()
let result: [String: Any] = object.filter { key, value in
match.context.variables[keyVariable] = key
match.context.variables[valueVariable] = value
if let result = match.interpreter.evaluate(body, context: match.context) as? Bool {
return result
}
return false
}
match.context.pop()
return result
}
}
public static var sumFunction: Function {
return function("sum") { (arguments: [Any]) -> Double? in
guard let arguments = arguments as? [Double] else { return nil }
return arguments.reduce(0, +)
}
}
public static var averageFunction: Function {
return function("avg") { (arguments: [Any]) -> Double? in
guard let arguments = arguments as? [Double] else { return nil }
return arguments.reduce(0, +) / Double(arguments.count)
}
}
public static var sqrtFunction: Function {
return function("sqrt") { (arguments: [Any]) -> Double? in
guard let value = arguments.first as? Double else { return nil }
return sqrt(value)
}
}
public static var roundFunction: Function {
return function("round") { (arguments: [Any]) -> Double? in
guard let value = arguments.first as? Double else { return nil }
return round(value)
}
}
public static var dateFactory: Function {
return function("Date") { (arguments: [Any]) -> Date? in
guard let arguments = arguments as? [Double], arguments.count >= 3 else { return nil }
var components = DateComponents()
components.calendar = Calendar(identifier: .gregorian)
components.year = Int(arguments[0])
components.month = Int(arguments[1])
components.day = Int(arguments[2])
components.hour = arguments.count > 3 ? Int(arguments[3]) : 0
components.minute = arguments.count > 4 ? Int(arguments[4]) : 0
components.second = arguments.count > 5 ? Int(arguments[5]) : 0
return components.date
}
}
public static var stringFactory: Function {
return function("String") { (arguments: [Any]) -> String? in
guard let argument = arguments.first as? Double else { return nil }
return String(format: "%g", argument)
}
}
public static var rangeBySteps: Function<[Double]> {
return functionWithNamedParameters("range") { (arguments: [String: Any]) -> [Double]? in
guard let start = arguments["start"] as? Double, let end = arguments["end"] as? Double, let step = arguments["step"] as? Double else { return nil }
var result = [start]
var value = start
while value <= end - step {
value += step
result.append(value)
}
return result
}
}
public static var loopIsFirst: Function {
return Function([Variable("value"), Keyword("is first")]) {
$0.context.variables["__first"] as? Bool
}
}
public static var loopIsLast: Function {
return Function([Variable("value"), Keyword("is last")]) {
$0.context.variables["__last"] as? Bool
}
}
public static var loopIsNotFirst: Function {
return Function([Variable("value"), Keyword("is not first")]) {
guard let isFirst = $0.context.variables["__first"] as? Bool else { return nil }
return !isFirst
}
}
public static var loopIsNotLast: Function {
return Function([Variable("value"), Keyword("is not last")]) {
guard let isLast = $0.context.variables["__last"] as? Bool else { return nil }
return !isLast
}
}
public static var dateFormat: Function {
return objectFunctionWithParameters("format") { (object: Date, arguments: [Any]) -> String? in
guard let format = arguments.first as? String else { return nil }
let dateFormatter = DateFormatter(with: format)
return dateFormatter.string(from: object)
}
}
public static var arraySubscript: Function {
return Function([Variable<[Any]>("array"), Keyword("."), Variable("index")]) {
guard let array = $0.variables["array"] as? [Any], let index = $0.variables["index"] as? Double, index > 0, Int(index) < array.count else { return nil }
return array[Int(index)]
}
}
public static var dictionarySubscript: Function {
return Function([Variable<[String: Any]>("dictionary"), Keyword("."), Variable("key", options: .notInterpreted)]) {
guard let dictionary = $0.variables["dictionary"] as? [String: Any], let key = $0.variables["key"] as? String else { return nil }
return dictionary[key]
}
}
public static var dictionaryKeys: Function<[String]> {
return objectFunction("keys") { (object: [String: Any?]) -> [String] in
object.keys.sorted()
}
}
public static var dictionaryValues: Function<[Any?]> {
return objectFunction("values") { (object: [String: Any?]) -> [Any?] in
if let values = object as? [String: Double] {
return values.values.sorted()
}
if let values = object as? [String: String] {
return values.values.sorted()
}
return Array(object.values)
}
}
public static var methodCallWithIntResult: Function {
return Function([Variable("lhs"), Keyword("."), Variable("rhs", options: .notInterpreted)]) {
if let lhs = $0.variables["lhs"] as? NSObjectProtocol,
let rhs = $0.variables["rhs"] as? String,
let result = lhs.perform(Selector(rhs)) {
return Double(Int(bitPattern: result.toOpaque()))
}
return nil
}
}
// MARK: Literal helpers
public static func literal(opening: String, closing: String, convert: @escaping (_ literal: LiteralBody) -> T?) -> Literal {
return Literal { literal -> T? in
guard literal.value.hasPrefix(opening), literal.value.hasSuffix(closing), literal.value.count > 1 else { return nil }
let inputWithoutOpening = String(literal.value.suffix(from: literal.value.index(literal.value.startIndex, offsetBy: opening.count)))
let inputWithoutSides = String(inputWithoutOpening.prefix(upTo: inputWithoutOpening.index(inputWithoutOpening.endIndex, offsetBy: -closing.count)))
guard !inputWithoutSides.contains(opening) && !inputWithoutSides.contains(closing) else { return nil }
return convert(LiteralBody(value: inputWithoutSides, interpreter: literal.interpreter))
}
}
// MARK: Operator helpers
public static func infixOperator(_ symbol: String, body: @escaping (A, B) -> T) -> Function {
return Function([Variable("lhs"), Keyword(symbol), Variable("rhs")], options: .backwardMatch) {
guard let lhs = $0.variables["lhs"] as? A, let rhs = $0.variables["rhs"] as? B else { return nil }
return body(lhs, rhs)
}
}
public static func prefixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function {
return Function([Keyword(symbol), Variable("value")]) {
guard let value = $0.variables["value"] as? A else { return nil }
return body(value)
}
}
public static func suffixOperator(_ symbol: String, body: @escaping (A) -> T) -> Function {
return Function([Variable("value"), Keyword(symbol)]) {
guard let value = $0.variables["value"] as? A else { return nil }
return body(value)
}
}
// MARK: Function helpers
public static func function(_ name: String, body: @escaping ([Any]) -> T?) -> Function {
return Function([Keyword(name), OpenKeyword("("), Variable("arguments", options: .notInterpreted), CloseKeyword(")")]) { match in
guard let arguments = match.variables["arguments"] as? String else { return nil }
let interpretedArguments = arguments.split(separator: ",").compactMap { match.interpreter.evaluate(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) }
return body(interpretedArguments)
}
}
public static func functionWithNamedParameters(_ name: String, body: @escaping ([String: Any]) -> T?) -> Function {
return Function([Keyword(name), OpenKeyword("("), Variable("arguments", options: .notInterpreted), CloseKeyword(")")]) {
guard let arguments = $0.variables["arguments"] as? String else { return nil }
var interpretedArguments: [String: Any] = [:]
for argument in arguments.split(separator: ",") {
let parts = String(argument).trimmingCharacters(in: .whitespacesAndNewlines).split(separator: "=")
if let key = parts.first, let value = parts.last {
interpretedArguments[String(key)] = $0.interpreter.evaluate(String(value))
}
}
return body(interpretedArguments)
}
}
public static func objectFunction(_ name: String, body: @escaping (O) -> T?) -> Function