Fix issues in Sources
Sources sources
This commit is contained in:
@@ -4,8 +4,8 @@ public class Context {
|
||||
|
||||
public let environment: Environment
|
||||
|
||||
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
||||
if let dictionary = dictionary {
|
||||
init(dictionary: [String: Any] = [:], environment: Environment? = nil) {
|
||||
if !dictionary.isEmpty {
|
||||
dictionaries = [dictionary]
|
||||
} else {
|
||||
dictionaries = []
|
||||
@@ -28,17 +28,16 @@ public class Context {
|
||||
|
||||
/// Set a variable in the current context, deleting the variable if it's nil
|
||||
set(value) {
|
||||
if let dictionary = dictionaries.popLast() {
|
||||
var mutable_dictionary = dictionary
|
||||
mutable_dictionary[key] = value
|
||||
dictionaries.append(mutable_dictionary)
|
||||
if var dictionary = dictionaries.popLast() {
|
||||
dictionary[key] = value
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a new level into the Context
|
||||
fileprivate func push(_ dictionary: [String: Any]? = nil) {
|
||||
dictionaries.append(dictionary ?? [:])
|
||||
fileprivate func push(_ dictionary: [String: Any] = [:]) {
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
|
||||
/// Pop the last level off of the Context
|
||||
@@ -47,7 +46,7 @@ public class Context {
|
||||
}
|
||||
|
||||
/// Push a new level onto the context for the duration of the execution of the given closure
|
||||
public func push<Result>(dictionary: [String: Any]? = nil, closure: (() throws -> Result)) rethrows -> Result {
|
||||
public func push<Result>(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result {
|
||||
push(dictionary)
|
||||
defer { _ = pop() }
|
||||
return try closure()
|
||||
|
||||
@@ -5,12 +5,12 @@ public struct Environment {
|
||||
public var loader: Loader?
|
||||
|
||||
public init(loader: Loader? = nil,
|
||||
extensions: [Extension]? = nil,
|
||||
extensions: [Extension] = [],
|
||||
templateClass: Template.Type = Template.self) {
|
||||
|
||||
self.templateClass = templateClass
|
||||
self.loader = loader
|
||||
self.extensions = (extensions ?? []) + [DefaultExtension()]
|
||||
self.extensions = extensions + [DefaultExtension()]
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String) throws -> Template {
|
||||
@@ -29,17 +29,17 @@ public struct Environment {
|
||||
}
|
||||
}
|
||||
|
||||
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
||||
public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String {
|
||||
let template = try loadTemplate(name: name)
|
||||
return try render(template: template, context: context)
|
||||
}
|
||||
|
||||
public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String {
|
||||
public func renderTemplate(string: String, context: [String: Any] = [:]) throws -> String {
|
||||
let template = templateClass.init(templateString: string, environment: self)
|
||||
return try render(template: template, context: context)
|
||||
}
|
||||
|
||||
func render(template: Template, context: [String: Any]?) throws -> String {
|
||||
func render(template: Template, context: [String: Any]) throws -> String {
|
||||
// update template environment as it can be created from string literal with default environment
|
||||
template.environment = self
|
||||
return try template.render(context)
|
||||
|
||||
@@ -18,14 +18,14 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||
public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible {
|
||||
public let reason: String
|
||||
public var description: String { return reason }
|
||||
public internal(set) var token: Token?
|
||||
public internal(set) var stackTrace: [Token]
|
||||
public var templateName: String? { return token?.sourceMap.filename }
|
||||
var allTokens: [Token] {
|
||||
return stackTrace + (token.map({ [$0] }) ?? [])
|
||||
return stackTrace + (token.map { [$0] } ?? [])
|
||||
}
|
||||
|
||||
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
||||
@@ -50,7 +50,7 @@ extension Error {
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ErrorReporter: class {
|
||||
public protocol ErrorReporter: AnyObject {
|
||||
func renderError(_ error: Error) -> String
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,14 @@ public protocol Expression: CustomStringConvertible {
|
||||
func evaluate(context: Context) throws -> Bool
|
||||
}
|
||||
|
||||
|
||||
protocol InfixOperator: Expression {
|
||||
init(lhs: Expression, rhs: Expression)
|
||||
}
|
||||
|
||||
|
||||
protocol PrefixOperator: Expression {
|
||||
init(expression: Expression)
|
||||
}
|
||||
|
||||
|
||||
final class StaticExpression: Expression, CustomStringConvertible {
|
||||
let value: Bool
|
||||
|
||||
@@ -29,7 +26,6 @@ final class StaticExpression: Expression, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class VariableExpression: Expression, CustomStringConvertible {
|
||||
let variable: Resolvable
|
||||
|
||||
@@ -48,7 +44,7 @@ final class VariableExpression: Expression, CustomStringConvertible {
|
||||
|
||||
if let result = result as? [Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if let result = result as? [String:Any] {
|
||||
} else if let result = result as? [String: Any] {
|
||||
truthy = !result.isEmpty
|
||||
} else if let result = result as? Bool {
|
||||
truthy = result
|
||||
@@ -68,7 +64,6 @@ final class VariableExpression: Expression, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||
let expression: Expression
|
||||
|
||||
@@ -144,7 +139,6 @@ final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
@@ -168,7 +162,6 @@ final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
@@ -204,7 +197,6 @@ class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
@@ -215,7 +207,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) \(op) \(rhs))"
|
||||
return "(\(lhs) \(symbol) \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
@@ -233,7 +225,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
return false
|
||||
}
|
||||
|
||||
var op: String {
|
||||
var symbol: String {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -242,9 +234,8 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MoreThanExpression: NumericExpression {
|
||||
override var op: String {
|
||||
override var symbol: String {
|
||||
return ">"
|
||||
}
|
||||
|
||||
@@ -253,9 +244,8 @@ class MoreThanExpression: NumericExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MoreThanEqualExpression: NumericExpression {
|
||||
override var op: String {
|
||||
override var symbol: String {
|
||||
return ">="
|
||||
}
|
||||
|
||||
@@ -264,9 +254,8 @@ class MoreThanEqualExpression: NumericExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LessThanExpression: NumericExpression {
|
||||
override var op: String {
|
||||
override var symbol: String {
|
||||
return "<"
|
||||
}
|
||||
|
||||
@@ -275,9 +264,8 @@ class LessThanExpression: NumericExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LessThanEqualExpression: NumericExpression {
|
||||
override var op: String {
|
||||
override var symbol: String {
|
||||
return "<="
|
||||
}
|
||||
|
||||
@@ -286,7 +274,6 @@ class LessThanEqualExpression: NumericExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class InequalityExpression: EqualityExpression {
|
||||
override var description: String {
|
||||
return "(\(lhs) != \(rhs))"
|
||||
@@ -297,7 +284,7 @@ class InequalityExpression: EqualityExpression {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func toNumber(value: Any) -> Number? {
|
||||
if let value = value as? Float {
|
||||
return Number(value)
|
||||
|
||||
@@ -14,12 +14,13 @@ open class Extension {
|
||||
|
||||
/// Registers a simple template tag with a name and a handler
|
||||
public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) {
|
||||
registerTag(name, parser: { parser, token in
|
||||
return SimpleNode(token: token, handler: handler)
|
||||
})
|
||||
registerTag(name) { _, token in
|
||||
SimpleNode(token: token, handler: handler)
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers boolean filter with it's negative counterpart
|
||||
// swiftlint:disable:next discouraged_optional_boolean
|
||||
public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) {
|
||||
filters[name] = .simple(filter)
|
||||
filters[negativeFilterName] = .simple {
|
||||
@@ -44,7 +45,6 @@ open class Extension {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DefaultExtension: Extension {
|
||||
override init() {
|
||||
super.init()
|
||||
@@ -77,7 +77,6 @@ class DefaultExtension: Extension {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protocol FilterType {
|
||||
func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class FilterNode : NodeType {
|
||||
class FilterNode: NodeType {
|
||||
let resolvable: Resolvable
|
||||
let nodes: [NodeType]
|
||||
let token: Token?
|
||||
@@ -30,8 +30,7 @@ class FilterNode : NodeType {
|
||||
let value = try renderNodes(nodes, context)
|
||||
|
||||
return try context.push(dictionary: ["filter_value": value]) {
|
||||
return try VariableNode(variable: resolvable, token: token).render(context)
|
||||
try VariableNode(variable: resolvable, token: token).render(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
}
|
||||
|
||||
var indentWidth = 4
|
||||
if arguments.count > 0 {
|
||||
if !arguments.isEmpty {
|
||||
guard let value = arguments[0] as? Int else {
|
||||
throw TemplateSyntaxError("""
|
||||
'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))
|
||||
@@ -99,18 +99,17 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
indentFirst = value
|
||||
}
|
||||
|
||||
let indentation = [String](repeating: indentationChar, count: indentWidth).joined(separator: "")
|
||||
let indentation = [String](repeating: indentationChar, count: indentWidth).joined()
|
||||
return indent(stringify(value), indentation: indentation, indentFirst: indentFirst)
|
||||
}
|
||||
|
||||
|
||||
func indent(_ content: String, indentation: String, indentFirst: Bool) -> String {
|
||||
guard !indentation.isEmpty else { return content }
|
||||
|
||||
var lines = content.components(separatedBy: .newlines)
|
||||
let firstLine = (indentFirst ? indentation : "") + lines.removeFirst()
|
||||
let result = lines.reduce([firstLine]) { (result, line) in
|
||||
return result + [(line.isEmpty ? "" : "\(indentation)\(line)")]
|
||||
let result = lines.reduce([firstLine]) { result, line in
|
||||
result + [(line.isEmpty ? "" : "\(indentation)\(line)")]
|
||||
}
|
||||
return result.joined(separator: "\n")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import Foundation
|
||||
|
||||
class ForNode : NodeType {
|
||||
class ForNode: NodeType {
|
||||
let resolvable: Resolvable
|
||||
let loopVariables: [String]
|
||||
let nodes:[NodeType]
|
||||
let nodes: [NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
let `where`: Expression?
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let components = token.components
|
||||
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
@@ -46,10 +46,24 @@ class ForNode : NodeType {
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes: emptyNodes, where: `where`, token: token)
|
||||
return ForNode(
|
||||
resolvable: resolvable,
|
||||
loopVariables: loopVariables,
|
||||
nodes: forNodes,
|
||||
emptyNodes: emptyNodes,
|
||||
where: `where`,
|
||||
token: token
|
||||
)
|
||||
}
|
||||
|
||||
init(resolvable: Resolvable, loopVariables: [String], nodes: [NodeType], emptyNodes: [NodeType], where: Expression? = nil, token: Token? = nil) {
|
||||
init(
|
||||
resolvable: Resolvable,
|
||||
loopVariables: [String],
|
||||
nodes: [NodeType],
|
||||
emptyNodes: [NodeType],
|
||||
where: Expression? = nil,
|
||||
token: Token? = nil
|
||||
) {
|
||||
self.resolvable = resolvable
|
||||
self.loopVariables = loopVariables
|
||||
self.nodes = nodes
|
||||
@@ -58,10 +72,48 @@ class ForNode : NodeType {
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||
func render(_ context: Context) throws -> String {
|
||||
var values = try resolve(context)
|
||||
|
||||
if let `where` = self.where {
|
||||
values = try values.filter { item -> Bool in
|
||||
try push(value: item, context: context) {
|
||||
try `where`.evaluate(context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !values.isEmpty {
|
||||
let count = values.count
|
||||
|
||||
return try zip(0..., values)
|
||||
.map { index, item in
|
||||
let forContext: [String: Any] = [
|
||||
"first": index == 0,
|
||||
"last": index == (count - 1),
|
||||
"counter": index + 1,
|
||||
"counter0": index,
|
||||
"length": count
|
||||
]
|
||||
|
||||
return try context.push(dictionary: ["forloop": forContext]) {
|
||||
try push(value: item, context: context) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
.joined()
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
try renderNodes(emptyNodes, context)
|
||||
}
|
||||
}
|
||||
|
||||
private func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||
if loopVariables.isEmpty {
|
||||
return try context.push() {
|
||||
return try closure()
|
||||
return try context.push {
|
||||
try closure()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,27 +123,26 @@ class ForNode : NodeType {
|
||||
throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables")
|
||||
}
|
||||
var variablesContext = [String: Any]()
|
||||
valueMirror.children.prefix(loopVariables.count).enumerated().forEach({ (offset, element) in
|
||||
valueMirror.children.prefix(loopVariables.count).enumerated().forEach { offset, element in
|
||||
if loopVariables[offset] != "_" {
|
||||
variablesContext[loopVariables[offset]] = element.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return try context.push(dictionary: variablesContext) {
|
||||
return try closure()
|
||||
try closure()
|
||||
}
|
||||
}
|
||||
|
||||
return try context.push(dictionary: [loopVariables.first!: value]) {
|
||||
return try closure()
|
||||
return try context.push(dictionary: [loopVariables.first ?? "": value]) {
|
||||
try closure()
|
||||
}
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
private func resolve(_ context: Context) throws -> [Any] {
|
||||
let resolved = try resolvable.resolve(context)
|
||||
|
||||
var values: [Any]
|
||||
|
||||
if let dictionary = resolved as? [String: Any], !dictionary.isEmpty {
|
||||
values = dictionary.sorted { $0.key < $1.key }
|
||||
} else if let array = resolved as? [Any] {
|
||||
@@ -120,36 +171,6 @@ class ForNode : NodeType {
|
||||
values = []
|
||||
}
|
||||
|
||||
if let `where` = self.where {
|
||||
values = try values.filter({ item -> Bool in
|
||||
return try push(value: item, context: context) {
|
||||
try `where`.evaluate(context: context)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if !values.isEmpty {
|
||||
let count = values.count
|
||||
|
||||
return try values.enumerated().map { index, item in
|
||||
let forContext: [String: Any] = [
|
||||
"first": index == 0,
|
||||
"last": index == (count - 1),
|
||||
"counter": index + 1,
|
||||
"counter0": index,
|
||||
"length": count
|
||||
]
|
||||
|
||||
return try context.push(dictionary: ["forloop": forContext]) {
|
||||
return try push(value: item, context: context) {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}.joined(separator: "")
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
try renderNodes(emptyNodes, context)
|
||||
}
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ enum Operator {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let operators: [Operator] = [
|
||||
.infix("in", 5, InExpression.self),
|
||||
.infix("or", 6, OrExpression.self),
|
||||
@@ -23,21 +22,17 @@ let operators: [Operator] = [
|
||||
.infix(">", 10, MoreThanExpression.self),
|
||||
.infix(">=", 10, MoreThanEqualExpression.self),
|
||||
.infix("<", 10, LessThanExpression.self),
|
||||
.infix("<=", 10, LessThanEqualExpression.self),
|
||||
.infix("<=", 10, LessThanEqualExpression.self)
|
||||
]
|
||||
|
||||
|
||||
func findOperator(name: String) -> Operator? {
|
||||
for op in operators {
|
||||
if op.name == name {
|
||||
return op
|
||||
}
|
||||
for `operator` in operators where `operator`.name == name {
|
||||
return `operator`
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
indirect enum IfToken {
|
||||
case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type)
|
||||
case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type)
|
||||
@@ -51,9 +46,9 @@ indirect enum IfToken {
|
||||
return bindingPower
|
||||
case .prefix(_, let bindingPower, _):
|
||||
return bindingPower
|
||||
case .variable(_):
|
||||
case .variable:
|
||||
return 0
|
||||
case .subExpression(_):
|
||||
case .subExpression:
|
||||
return 0
|
||||
case .end:
|
||||
return 0
|
||||
@@ -64,9 +59,9 @@ indirect enum IfToken {
|
||||
switch self {
|
||||
case .infix(let name, _, _):
|
||||
throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side")
|
||||
case .prefix(_, let bindingPower, let op):
|
||||
case .prefix(_, let bindingPower, let operatorType):
|
||||
let expression = try parser.expression(bindingPower: bindingPower)
|
||||
return op.init(expression: expression)
|
||||
return operatorType.init(expression: expression)
|
||||
case .variable(let variable):
|
||||
return VariableExpression(variable: variable)
|
||||
case .subExpression(let expression):
|
||||
@@ -78,14 +73,14 @@ indirect enum IfToken {
|
||||
|
||||
func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression {
|
||||
switch self {
|
||||
case .infix(_, let bindingPower, let op):
|
||||
case .infix(_, let bindingPower, let operatorType):
|
||||
let right = try parser.expression(bindingPower: bindingPower)
|
||||
return op.init(lhs: left, rhs: right)
|
||||
return operatorType.init(lhs: left, rhs: right)
|
||||
case .prefix(let name, _, _):
|
||||
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
|
||||
case .variable(let variable):
|
||||
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
|
||||
case .subExpression(_):
|
||||
case .subExpression:
|
||||
throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side")
|
||||
case .end:
|
||||
throw TemplateSyntaxError("'if' expression error: end")
|
||||
@@ -102,7 +97,6 @@ indirect enum IfToken {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class IfExpressionParser {
|
||||
let tokens: [IfToken]
|
||||
var position: Int = 0
|
||||
@@ -118,7 +112,7 @@ final class IfExpressionParser {
|
||||
private init(components: ArraySlice<String>, environment: Environment, token: Token) throws {
|
||||
var parsedComponents = Set<Int>()
|
||||
var bracketsBalance = 0
|
||||
self.tokens = try zip(components.indices, components).compactMap { (index, component) in
|
||||
self.tokens = try zip(components.indices, components).compactMap { index, component in
|
||||
guard !parsedComponents.contains(index) else { return nil }
|
||||
|
||||
if component == "(" {
|
||||
@@ -139,8 +133,8 @@ final class IfExpressionParser {
|
||||
return nil
|
||||
} else {
|
||||
parsedComponents.insert(index)
|
||||
if let op = findOperator(name: component) {
|
||||
switch op {
|
||||
if let `operator` = findOperator(name: component) {
|
||||
switch `operator` {
|
||||
case .infix(let name, let bindingPower, let operatorType):
|
||||
return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||
case .prefix(let name, let bindingPower, let operatorType):
|
||||
@@ -152,17 +146,20 @@ final class IfExpressionParser {
|
||||
}
|
||||
}
|
||||
|
||||
private static func subExpression(from components: ArraySlice<String>, environment: Environment, token: Token) throws -> (Expression, Int) {
|
||||
private static func subExpression(
|
||||
from components: ArraySlice<String>,
|
||||
environment: Environment,
|
||||
token: Token
|
||||
) throws -> (Expression, Int) {
|
||||
var bracketsBalance = 1
|
||||
let subComponents = components
|
||||
.prefix(while: {
|
||||
let subComponents = components.prefix {
|
||||
if $0 == "(" {
|
||||
bracketsBalance += 1
|
||||
} else if $0 == ")" {
|
||||
bracketsBalance -= 1
|
||||
}
|
||||
return bracketsBalance != 0
|
||||
})
|
||||
}
|
||||
if bracketsBalance > 0 {
|
||||
throw TemplateSyntaxError("'if' expression error: missing closing bracket")
|
||||
}
|
||||
@@ -211,7 +208,6 @@ final class IfExpressionParser {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Represents an if condition and the associated nodes when the condition
|
||||
/// evaluates
|
||||
final class IfCondition {
|
||||
@@ -225,13 +221,12 @@ final class IfCondition {
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
return try context.push {
|
||||
return try renderNodes(nodes, context)
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class IfNode : NodeType {
|
||||
class IfNode: NodeType {
|
||||
let conditions: [IfCondition]
|
||||
let token: Token?
|
||||
|
||||
@@ -291,7 +286,7 @@ class IfNode : NodeType {
|
||||
|
||||
return IfNode(conditions: [
|
||||
IfCondition(expression: expression, nodes: trueNodes),
|
||||
IfCondition(expression: nil, nodes: falseNodes),
|
||||
IfCondition(expression: nil, nodes: falseNodes)
|
||||
], token: token)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import PathKit
|
||||
|
||||
|
||||
class IncludeNode : NodeType {
|
||||
class IncludeNode: NodeType {
|
||||
let templateName: Variable
|
||||
let includeContext: String?
|
||||
let token: Token?
|
||||
@@ -34,9 +33,9 @@ class IncludeNode : NodeType {
|
||||
let template = try context.environment.loadTemplate(name: templateName)
|
||||
|
||||
do {
|
||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] } ?? [:]
|
||||
return try context.push(dictionary: subContext) {
|
||||
return try template.render(context)
|
||||
try template.render(context)
|
||||
}
|
||||
} catch {
|
||||
if let error = error as? TemplateSyntaxError {
|
||||
@@ -47,4 +46,3 @@ class IncludeNode : NodeType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ class BlockContext {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Collection {
|
||||
func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? {
|
||||
for element in self {
|
||||
@@ -46,10 +45,9 @@ extension Collection {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExtendsNode : NodeType {
|
||||
class ExtendsNode: NodeType {
|
||||
let templateName: Variable
|
||||
let blocks: [String:BlockNode]
|
||||
let blocks: [String: BlockNode]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
@@ -66,7 +64,7 @@ class ExtendsNode : NodeType {
|
||||
|
||||
let blockNodes = parsedNodes.compactMap { $0 as? BlockNode }
|
||||
|
||||
let nodes = blockNodes.reduce([String: BlockNode]()) { (accumulator, node) -> [String: BlockNode] in
|
||||
let nodes = blockNodes.reduce([String: BlockNode]()) { accumulator, node -> [String: BlockNode] in
|
||||
var dict = accumulator
|
||||
dict[node.name] = node
|
||||
return dict
|
||||
@@ -102,7 +100,7 @@ class ExtendsNode : NodeType {
|
||||
// pushes base template and renders it's content
|
||||
// block_context contains all blocks from child templates
|
||||
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||
return try baseTemplate.render(context)
|
||||
try baseTemplate.render(context)
|
||||
}
|
||||
} catch {
|
||||
// if error template is already set (see catch in BlockNode)
|
||||
@@ -117,8 +115,7 @@ class ExtendsNode : NodeType {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BlockNode : NodeType {
|
||||
class BlockNode: NodeType {
|
||||
let name: String
|
||||
let nodes: [NodeType]
|
||||
let token: Token?
|
||||
@@ -133,7 +130,7 @@ class BlockNode : NodeType {
|
||||
let blockName = bits[1]
|
||||
let nodes = try parser.parse(until(["endblock"]))
|
||||
_ = parser.nextToken()
|
||||
return BlockNode(name:blockName, nodes:nodes, token: token)
|
||||
return BlockNode(name: blockName, nodes: nodes, token: token)
|
||||
}
|
||||
|
||||
init(name: String, nodes: [NodeType], token: Token) {
|
||||
@@ -148,7 +145,7 @@ class BlockNode : NodeType {
|
||||
// render extension node
|
||||
do {
|
||||
return try context.push(dictionary: childContext) {
|
||||
return try child.render(context)
|
||||
try child.render(context)
|
||||
}
|
||||
} catch {
|
||||
throw error.withToken(child.token)
|
||||
@@ -163,8 +160,11 @@ class BlockNode : NodeType {
|
||||
var childContext: [String: Any] = [BlockContext.contextKey: blockContext]
|
||||
|
||||
if let blockSuperNode = child.nodes.first(where: {
|
||||
if let token = $0.token, case .variable = token.kind, token.contents == "block.super" { return true }
|
||||
else { return false}
|
||||
if let token = $0.token, case .variable = token.kind, token.contents == "block.super" {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}) {
|
||||
do {
|
||||
// render base node so that its content can be used as part of child node that extends it
|
||||
|
||||
@@ -24,8 +24,8 @@ final class KeyPath {
|
||||
subscriptLevel = 0
|
||||
}
|
||||
|
||||
for c in variable {
|
||||
switch c {
|
||||
for character in variable {
|
||||
switch character {
|
||||
case "." where subscriptLevel == 0:
|
||||
try foundSeparator()
|
||||
case "[":
|
||||
@@ -33,7 +33,7 @@ final class KeyPath {
|
||||
case "]":
|
||||
try closeBracket()
|
||||
default:
|
||||
try addCharacter(c)
|
||||
try addCharacter(character)
|
||||
}
|
||||
}
|
||||
try finish()
|
||||
@@ -90,12 +90,12 @@ final class KeyPath {
|
||||
subscriptLevel -= 1
|
||||
}
|
||||
|
||||
private func addCharacter(_ c: Character) throws {
|
||||
private func addCharacter(_ character: Character) throws {
|
||||
guard partialComponents.isEmpty || subscriptLevel > 0 else {
|
||||
throw TemplateSyntaxError("Unexpected character '\(c)' in variable '\(variable)'")
|
||||
throw TemplateSyntaxError("Unexpected character '\(character)' in variable '\(variable)'")
|
||||
}
|
||||
|
||||
current.append(c)
|
||||
current.append(character)
|
||||
}
|
||||
|
||||
private func finish() throws {
|
||||
|
||||
@@ -24,8 +24,9 @@ struct Lexer {
|
||||
self.templateString = templateString
|
||||
|
||||
self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap {
|
||||
guard !$0.element.isEmpty else { return nil }
|
||||
return (content: $0.element, number: UInt($0.offset + 1), templateString.range(of: $0.element)!)
|
||||
guard !$0.element.isEmpty,
|
||||
let range = templateString.range(of: $0.element) else { return nil }
|
||||
return (content: $0.element, number: UInt($0.offset + 1), range)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +44,8 @@ struct Lexer {
|
||||
guard string.count > 4 else { return "" }
|
||||
let trimmed = String(string.dropFirst(2).dropLast(2))
|
||||
.components(separatedBy: "\n")
|
||||
.filter({ !$0.isEmpty })
|
||||
.map({ $0.trim(character: " ") })
|
||||
.filter { !$0.isEmpty }
|
||||
.map { $0.trim(character: " ") }
|
||||
.joined(separator: " ")
|
||||
return trimmed
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
|
||||
public protocol Loader {
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template
|
||||
func loadTemplate(names: [String], environment: Environment) throws -> Template
|
||||
}
|
||||
|
||||
|
||||
extension Loader {
|
||||
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||
for name in names {
|
||||
@@ -24,7 +22,6 @@ extension Loader {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// A class for loading a template from disk
|
||||
public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
public let paths: [Path]
|
||||
@@ -35,7 +32,7 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
|
||||
public init(bundle: [Bundle]) {
|
||||
self.paths = bundle.map {
|
||||
return Path($0.bundlePath)
|
||||
Path($0.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +71,6 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class DictionaryLoader: Loader {
|
||||
public let templates: [String: String]
|
||||
|
||||
@@ -101,7 +97,6 @@ public class DictionaryLoader: Loader {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Path {
|
||||
func safeJoin(path: Path) throws -> Path {
|
||||
let newPath = self + path
|
||||
@@ -114,7 +109,6 @@ extension Path {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SuspiciousFileOperation: Error {
|
||||
let basePath: Path
|
||||
let path: Path
|
||||
|
||||
@@ -2,26 +2,27 @@ import Foundation
|
||||
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
func render(_ context:Context) throws -> String
|
||||
func render(_ context: Context) throws -> String
|
||||
|
||||
/// Reference to this node's token
|
||||
var token: Token? { get }
|
||||
}
|
||||
|
||||
|
||||
/// Render the collection of nodes in the given context
|
||||
public func renderNodes(_ nodes:[NodeType], _ context:Context) throws -> String {
|
||||
return try nodes.map {
|
||||
public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String {
|
||||
return try nodes
|
||||
.map {
|
||||
do {
|
||||
return try $0.render(context)
|
||||
} catch {
|
||||
throw error.withToken($0.token)
|
||||
}
|
||||
}.joined(separator: "")
|
||||
}
|
||||
.joined()
|
||||
}
|
||||
|
||||
public class SimpleNode : NodeType {
|
||||
public let handler:(Context) throws -> String
|
||||
public class SimpleNode: NodeType {
|
||||
public let handler: (Context) throws -> String
|
||||
public let token: Token?
|
||||
|
||||
public init(token: Token, handler: @escaping (Context) throws -> String) {
|
||||
@@ -34,34 +35,31 @@ public class SimpleNode : NodeType {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class TextNode : NodeType {
|
||||
public let text:String
|
||||
public class TextNode: NodeType {
|
||||
public let text: String
|
||||
public let token: Token?
|
||||
|
||||
public init(text:String) {
|
||||
public init(text: String) {
|
||||
self.text = text
|
||||
self.token = nil
|
||||
}
|
||||
|
||||
public func render(_ context:Context) throws -> String {
|
||||
public func render(_ context: Context) throws -> String {
|
||||
return self.text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public protocol Resolvable {
|
||||
func resolve(_ context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
|
||||
public class VariableNode : NodeType {
|
||||
public class VariableNode: NodeType {
|
||||
public let variable: Resolvable
|
||||
public var token: Token?
|
||||
let condition: Expression?
|
||||
let elseExpression: Resolvable?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components
|
||||
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
@@ -121,7 +119,6 @@ public class VariableNode : NodeType {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func stringify(_ result: Any?) -> String {
|
||||
if let result = result as? String {
|
||||
return result
|
||||
@@ -144,7 +141,6 @@ func unwrap(_ array: [Any?]) -> [Any] {
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
else { return item as Any }
|
||||
} else { return item as Any }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#if !os(Linux)
|
||||
import Foundation
|
||||
|
||||
|
||||
class NowNode : NodeType {
|
||||
let format:Variable
|
||||
class NowNode: NodeType {
|
||||
let format: Variable
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var format: Variable?
|
||||
|
||||
let components = token.components
|
||||
guard components.count <= 2 else {
|
||||
@@ -17,10 +16,10 @@ class NowNode : NodeType {
|
||||
format = Variable(components[1])
|
||||
}
|
||||
|
||||
return NowNode(format:format, token: token)
|
||||
return NowNode(format: format, token: token)
|
||||
}
|
||||
|
||||
init(format:Variable?, token: Token? = nil) {
|
||||
init(format: Variable?, token: Token? = nil) {
|
||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
self.token = token
|
||||
}
|
||||
@@ -28,18 +27,18 @@ class NowNode : NodeType {
|
||||
func render(_ context: Context) throws -> String {
|
||||
let date = Date()
|
||||
let format = try self.format.resolve(context)
|
||||
var formatter:DateFormatter?
|
||||
|
||||
var formatter: DateFormatter
|
||||
if let format = format as? DateFormatter {
|
||||
formatter = format
|
||||
} else if let format = format as? String {
|
||||
formatter = DateFormatter()
|
||||
formatter!.dateFormat = format
|
||||
formatter.dateFormat = format
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatter!.string(from: date)
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||
return { parser, token in
|
||||
if let name = token.components.first {
|
||||
for tag in tags {
|
||||
if name == tag {
|
||||
for tag in tags where name == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A class for parsing an array of tokens and converts them into a collection of Node's
|
||||
public class TokenParser {
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
@@ -30,11 +27,11 @@ public class TokenParser {
|
||||
return try parse(nil)
|
||||
}
|
||||
|
||||
public func parse(_ parse_until:((_ parser:TokenParser, _ token:Token) -> (Bool))?) throws -> [NodeType] {
|
||||
public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] {
|
||||
var nodes = [NodeType]()
|
||||
|
||||
while tokens.count > 0 {
|
||||
let token = nextToken()!
|
||||
while !tokens.isEmpty {
|
||||
guard let token = nextToken() else { break }
|
||||
|
||||
switch token.kind {
|
||||
case .text:
|
||||
@@ -42,7 +39,7 @@ public class TokenParser {
|
||||
case .variable:
|
||||
try nodes.append(VariableNode.parse(self, token: token))
|
||||
case .block:
|
||||
if let parse_until = parse_until , parse_until(self, token) {
|
||||
if let parseUntil = parseUntil, parseUntil(self, token) {
|
||||
prependToken(token)
|
||||
return nodes
|
||||
}
|
||||
@@ -65,14 +62,14 @@ public class TokenParser {
|
||||
}
|
||||
|
||||
public func nextToken() -> Token? {
|
||||
if tokens.count > 0 {
|
||||
if !tokens.isEmpty {
|
||||
return tokens.remove(at: 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public func prependToken(_ token:Token) {
|
||||
public func prependToken(_ token: Token) {
|
||||
tokens.insert(token, at: 0)
|
||||
}
|
||||
|
||||
@@ -94,7 +91,6 @@ public class TokenParser {
|
||||
}
|
||||
|
||||
extension Environment {
|
||||
|
||||
func findTag(name: String) throws -> Extension.TagParser {
|
||||
for ext in extensions {
|
||||
if let filter = ext.tags[name] {
|
||||
@@ -118,23 +114,23 @@ extension Environment {
|
||||
} else {
|
||||
throw TemplateSyntaxError("""
|
||||
Unknown filter '\(name)'. \
|
||||
Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")).
|
||||
Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")).
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
private func suggestedFilters(for name: String) -> [String] {
|
||||
let allFilters = extensions.flatMap({ $0.filters.keys })
|
||||
let allFilters = extensions.flatMap { $0.filters.keys }
|
||||
|
||||
let filtersWithDistance = allFilters
|
||||
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
|
||||
.map { (filterName: $0, distance: $0.levenshteinDistance(name)) }
|
||||
// do not suggest filters which names are shorter than the distance
|
||||
.filter({ $0.filterName.count > $0.distance })
|
||||
.filter { $0.filterName.count > $0.distance }
|
||||
guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else {
|
||||
return []
|
||||
}
|
||||
// suggest all filters with the same distance
|
||||
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
||||
return filtersWithDistance.filter { $0.distance == minDistance }.map { $0.filterName }
|
||||
}
|
||||
|
||||
/// Create filter expression from a string
|
||||
@@ -153,8 +149,14 @@ extension Environment {
|
||||
// find offset of filter in the containing token so that only filter is highligted, not the whole token
|
||||
if let filterTokenRange = containingToken.contents.range(of: filterToken) {
|
||||
var location = containingToken.sourceMap.location
|
||||
location.lineOffset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound)
|
||||
syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, location: location))
|
||||
location.lineOffset += containingToken.contents.distance(
|
||||
from: containingToken.contents.startIndex,
|
||||
to: filterTokenRange.lowerBound
|
||||
)
|
||||
syntaxError.token = .variable(
|
||||
value: filterToken,
|
||||
at: SourceMap(filename: containingToken.sourceMap.filename, location: location)
|
||||
)
|
||||
} else {
|
||||
syntaxError.token = containingToken
|
||||
}
|
||||
@@ -183,9 +185,8 @@ extension Environment {
|
||||
|
||||
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||
extension String {
|
||||
|
||||
subscript(_ i: Int) -> Character {
|
||||
return self[self.index(self.startIndex, offsetBy: i)]
|
||||
subscript(_ index: Int) -> Character {
|
||||
return self[self.index(self.startIndex, offsetBy: index)]
|
||||
}
|
||||
|
||||
func levenshteinDistance(_ target: String) -> Int {
|
||||
@@ -198,19 +199,19 @@ extension String {
|
||||
last = [Int](0...target.count)
|
||||
current = [Int](repeating: 0, count: target.count + 1)
|
||||
|
||||
for i in 0..<self.count {
|
||||
for selfIndex in 0..<self.count {
|
||||
// calculate v1 (current row distances) from the previous row v0
|
||||
|
||||
// first element of v1 is A[i+1][0]
|
||||
// edit distance is delete (i+1) chars from s to match empty t
|
||||
current[0] = i + 1
|
||||
current[0] = selfIndex + 1
|
||||
|
||||
// use formula to fill in the rest of the row
|
||||
for j in 0..<target.count {
|
||||
current[j+1] = Swift.min(
|
||||
last[j+1] + 1,
|
||||
current[j] + 1,
|
||||
last[j] + (self[i] == target[j] ? 0 : 1)
|
||||
for targetIndex in 0..<target.count {
|
||||
current[targetIndex + 1] = Swift.min(
|
||||
last[targetIndex + 1] + 1,
|
||||
current[targetIndex] + 1,
|
||||
last[targetIndex] + (self[selfIndex] == target[targetIndex] ? 0 : 1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -220,5 +221,4 @@ extension String {
|
||||
|
||||
return current[target.count]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ let NSFileNoSuchFileError = 4
|
||||
/// A class representing a template
|
||||
open class Template: ExpressibleByStringLiteral {
|
||||
let templateString: String
|
||||
internal(set) var environment: Environment
|
||||
var environment: Environment
|
||||
let tokens: [Token]
|
||||
|
||||
/// The name of the loaded Template if the Template was loaded from a Loader
|
||||
@@ -26,18 +26,18 @@ open class Template: ExpressibleByStringLiteral {
|
||||
|
||||
/// Create a template with the given name inside the given bundle
|
||||
@available(*, deprecated, message: "Use Environment/FileSystemLoader instead")
|
||||
public convenience init(named:String, inBundle bundle:Bundle? = nil) throws {
|
||||
public convenience init(named: String, inBundle bundle: Bundle? = nil) throws {
|
||||
let useBundle = bundle ?? Bundle.main
|
||||
guard let url = useBundle.url(forResource: named, withExtension: nil) else {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
|
||||
}
|
||||
|
||||
try self.init(URL:url)
|
||||
try self.init(URL: url)
|
||||
}
|
||||
|
||||
/// Create a template with a file found at the given URL
|
||||
@available(*, deprecated, message: "Use Environment/FileSystemLoader instead")
|
||||
public convenience init(URL:Foundation.URL) throws {
|
||||
public convenience init(URL: Foundation.URL) throws {
|
||||
try self.init(path: Path(URL.path))
|
||||
}
|
||||
|
||||
@@ -72,8 +72,9 @@ open class Template: ExpressibleByStringLiteral {
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
|
||||
// swiftlint:disable discouraged_optional_collection
|
||||
/// Render the given template
|
||||
open func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||
return try render(Context(dictionary: dictionary, environment: environment))
|
||||
return try render(Context(dictionary: dictionary ?? [:], environment: environment))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
extension String {
|
||||
/// Split a string by a separator leaving quoted phrases together
|
||||
func smartSplit(separator: Character = " ") -> [String] {
|
||||
@@ -10,37 +9,18 @@ extension String {
|
||||
var singleQuoteCount = 0
|
||||
var doubleQuoteCount = 0
|
||||
|
||||
let specialCharacters = ",|:"
|
||||
func appendWord(_ word: String) {
|
||||
if components.count > 0 {
|
||||
if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) {
|
||||
components[components.count-1] += word
|
||||
} else if specialCharacters.contains(word) {
|
||||
components[components.count-1] += word
|
||||
} else if word != "(" && word.first == "(" || word != ")" && word.first == ")" {
|
||||
components.append(String(word.prefix(1)))
|
||||
appendWord(String(word.dropFirst()))
|
||||
} else if word != "(" && word.last == "(" || word != ")" && word.last == ")" {
|
||||
appendWord(String(word.dropLast()))
|
||||
components.append(String(word.suffix(1)))
|
||||
} else {
|
||||
components.append(word)
|
||||
}
|
||||
} else {
|
||||
components.append(word)
|
||||
}
|
||||
}
|
||||
|
||||
for character in self {
|
||||
if character == "'" { singleQuoteCount += 1 }
|
||||
else if character == "\"" { doubleQuoteCount += 1 }
|
||||
if character == "'" {
|
||||
singleQuoteCount += 1
|
||||
} else if character == "\"" {
|
||||
doubleQuoteCount += 1
|
||||
}
|
||||
|
||||
if character == separate {
|
||||
|
||||
if separate != separator {
|
||||
word.append(separate)
|
||||
} else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty {
|
||||
appendWord(word)
|
||||
appendWord(word, to: &components)
|
||||
word = ""
|
||||
}
|
||||
|
||||
@@ -54,11 +34,33 @@ extension String {
|
||||
}
|
||||
|
||||
if !word.isEmpty {
|
||||
appendWord(word)
|
||||
appendWord(word, to: &components)
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
private func appendWord(_ word: String, to components: inout [String]) {
|
||||
let specialCharacters = ",|:"
|
||||
|
||||
if !components.isEmpty {
|
||||
if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) {
|
||||
components[components.count - 1] += word
|
||||
} else if specialCharacters.contains(word) {
|
||||
components[components.count - 1] += word
|
||||
} else if word != "(" && word.first == "(" || word != ")" && word.first == ")" {
|
||||
components.append(String(word.prefix(1)))
|
||||
appendWord(String(word.dropFirst()), to: &components)
|
||||
} else if word != "(" && word.last == "(" || word != ")" && word.last == ")" {
|
||||
appendWord(String(word.dropLast()), to: &components)
|
||||
components.append(String(word.suffix(1)))
|
||||
} else {
|
||||
components.append(word)
|
||||
}
|
||||
} else {
|
||||
components.append(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct SourceMap: Equatable {
|
||||
@@ -72,7 +74,7 @@ public struct SourceMap: Equatable {
|
||||
|
||||
static let unknown = SourceMap()
|
||||
|
||||
public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool {
|
||||
public static func == (lhs: SourceMap, rhs: SourceMap) -> Bool {
|
||||
return lhs.filename == rhs.filename && lhs.location == rhs.location
|
||||
}
|
||||
}
|
||||
@@ -125,5 +127,4 @@ public class Token: Equatable {
|
||||
public static func == (lhs: Token, rhs: Token) -> Bool {
|
||||
return lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
typealias Number = Float
|
||||
|
||||
|
||||
class FilterExpression : Resolvable {
|
||||
class FilterExpression: Resolvable {
|
||||
let filters: [(FilterType, [Variable])]
|
||||
let variable: Variable
|
||||
|
||||
init(token: String, environment: Environment) throws {
|
||||
let bits = token.smartSplit(separator: "|").map({ String($0).trim(character: " ") })
|
||||
let bits = token.smartSplit(separator: "|").map { String($0).trim(character: " ") }
|
||||
if bits.isEmpty {
|
||||
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
||||
}
|
||||
@@ -32,15 +30,15 @@ class FilterExpression : Resolvable {
|
||||
func resolve(_ context: Context) throws -> Any? {
|
||||
let result = try variable.resolve(context)
|
||||
|
||||
return try filters.reduce(result) { x, y in
|
||||
let arguments = try y.1.map { try $0.resolve(context) }
|
||||
return try y.0.invoke(value: x, arguments: arguments, context: context)
|
||||
return try filters.reduce(result) { value, filter in
|
||||
let arguments = try filter.1.map { try $0.resolve(context) }
|
||||
return try filter.0.invoke(value: value, arguments: arguments, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure used to represent a template variable, and to resolve it in a given context.
|
||||
public struct Variable : Equatable, Resolvable {
|
||||
public struct Variable: Equatable, Resolvable {
|
||||
public let variable: String
|
||||
|
||||
/// Create a variable with a string representing the variable
|
||||
@@ -48,16 +46,8 @@ public struct Variable : Equatable, Resolvable {
|
||||
self.variable = variable
|
||||
}
|
||||
|
||||
// Split the lookup string and resolve references if possible
|
||||
fileprivate func lookup(_ context: Context) throws -> [String] {
|
||||
let keyPath = KeyPath(variable, in: context)
|
||||
return try keyPath.parse()
|
||||
}
|
||||
|
||||
/// Resolve the variable in the given context
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
var current: Any? = context
|
||||
|
||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||
// String literal
|
||||
return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)])
|
||||
@@ -75,37 +65,13 @@ public struct Variable : Equatable, Resolvable {
|
||||
return bool
|
||||
}
|
||||
|
||||
var current: Any? = context
|
||||
for bit in try lookup(context) {
|
||||
current = normalize(current)
|
||||
current = resolve(bit: bit, context: current)
|
||||
|
||||
if let context = current as? Context {
|
||||
current = context[bit]
|
||||
} else if let dictionary = current as? [String: Any] {
|
||||
if bit == "count" {
|
||||
current = dictionary.count
|
||||
} else {
|
||||
current = dictionary[bit]
|
||||
}
|
||||
} else if let array = current as? [Any] {
|
||||
current = resolveCollection(array, bit: bit)
|
||||
} else if let string = current as? String {
|
||||
current = resolveCollection(string, bit: bit)
|
||||
} else if let object = current as? NSObject { // NSKeyValueCoding
|
||||
#if os(Linux)
|
||||
return nil
|
||||
#else
|
||||
if object.responds(to: Selector(bit)) {
|
||||
current = object.value(forKey: bit)
|
||||
}
|
||||
#endif
|
||||
} else if let value = current {
|
||||
current = Mirror(reflecting: value).getValue(for: bit)
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if let resolvable = current as? Resolvable {
|
||||
@@ -116,9 +82,51 @@ public struct Variable : Equatable, Resolvable {
|
||||
|
||||
return normalize(current)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveCollection<T: Collection>(_ collection: T, bit: String) -> Any? {
|
||||
// Split the lookup string and resolve references if possible
|
||||
private func lookup(_ context: Context) throws -> [String] {
|
||||
let keyPath = KeyPath(variable, in: context)
|
||||
return try keyPath.parse()
|
||||
}
|
||||
|
||||
// Try to resolve a partial keypath for the given context
|
||||
private func resolve(bit: String, context: Any?) -> Any? {
|
||||
let context = normalize(context)
|
||||
|
||||
if let context = context as? Context {
|
||||
return context[bit]
|
||||
} else if let dictionary = context as? [String: Any] {
|
||||
return resolve(bit: bit, dictionary: dictionary)
|
||||
} else if let array = context as? [Any] {
|
||||
return resolve(bit: bit, collection: array)
|
||||
} else if let string = context as? String {
|
||||
return resolve(bit: bit, collection: string)
|
||||
} else if let object = context as? NSObject { // NSKeyValueCoding
|
||||
#if os(Linux)
|
||||
return nil
|
||||
#else
|
||||
if object.responds(to: Selector(bit)) {
|
||||
return object.value(forKey: bit)
|
||||
}
|
||||
#endif
|
||||
} else if let value = context {
|
||||
return Mirror(reflecting: value).getValue(for: bit)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to resolve a partial keypath for the given dictionary
|
||||
private func resolve(bit: String, dictionary: [String: Any]) -> Any? {
|
||||
if bit == "count" {
|
||||
return dictionary.count
|
||||
} else {
|
||||
return dictionary[bit]
|
||||
}
|
||||
}
|
||||
|
||||
// Try to resolve a partial keypath for the given collection
|
||||
private func resolve<T: Collection>(bit: String, collection: T) -> Any? {
|
||||
if let index = Int(bit) {
|
||||
if index >= 0 && index < collection.count {
|
||||
return collection[collection.index(collection.startIndex, offsetBy: index)]
|
||||
@@ -134,6 +142,7 @@ private func resolveCollection<T: Collection>(_ collection: T, bit: String) -> A
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure used to represet range of two integer values expressed as `from...to`.
|
||||
@@ -142,6 +151,7 @@ private func resolveCollection<T: Collection>(_ collection: T, bit: String) -> A
|
||||
/// If `from` is more than `to` array will contain values of reversed range.
|
||||
public struct RangeVariable: Resolvable {
|
||||
public let from: Resolvable
|
||||
// swiftlint:disable:next identifier_name
|
||||
public let to: Resolvable
|
||||
|
||||
public init?(_ token: String, environment: Environment) throws {
|
||||
@@ -165,24 +175,23 @@ public struct RangeVariable: Resolvable {
|
||||
}
|
||||
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
let fromResolved = try from.resolve(context)
|
||||
let toResolved = try to.resolve(context)
|
||||
let lowerResolved = try from.resolve(context)
|
||||
let upperResolved = try to.resolve(context)
|
||||
|
||||
guard let from = fromResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'from' value is not an Integer (\(fromResolved ?? "nil"))")
|
||||
guard let lower = lowerResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'from' value is not an Integer (\(lowerResolved ?? "nil"))")
|
||||
}
|
||||
|
||||
guard let to = toResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'to' value is not an Integer (\(toResolved ?? "nil") )")
|
||||
guard let upper = upperResolved.flatMap(toNumber(value:)).flatMap(Int.init) else {
|
||||
throw TemplateSyntaxError("'to' value is not an Integer (\(upperResolved ?? "nil") )")
|
||||
}
|
||||
|
||||
let range = min(from, to)...max(from, to)
|
||||
return from > to ? Array(range.reversed()) : Array(range)
|
||||
let range = min(lower, upper)...max(lower, upper)
|
||||
return lower > upper ? Array(range.reversed()) : Array(range)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func normalize(_ current: Any?) -> Any? {
|
||||
if let current = current as? Normalizable {
|
||||
return current.normalize()
|
||||
@@ -195,19 +204,19 @@ protocol Normalizable {
|
||||
func normalize() -> Any?
|
||||
}
|
||||
|
||||
extension Array : Normalizable {
|
||||
extension Array: Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
extension NSArray : Normalizable {
|
||||
extension NSArray: Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary : Normalizable {
|
||||
extension Dictionary: Normalizable {
|
||||
func normalize() -> Any? {
|
||||
var dictionary: [String: Any] = [:]
|
||||
|
||||
@@ -235,7 +244,7 @@ func parseFilterComponents(token: String) -> (String, [Variable]) {
|
||||
|
||||
extension Mirror {
|
||||
func getValue(for key: String) -> Any? {
|
||||
let result = descendant(key) ?? Int(key).flatMap({ descendant($0) })
|
||||
let result = descendant(key) ?? Int(key).flatMap { descendant($0) }
|
||||
if result == nil {
|
||||
// go through inheritance chain to reach superclass properties
|
||||
return superclassMirror?.getValue(for: key)
|
||||
@@ -267,5 +276,3 @@ extension Optional: AnyOptional {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,16 +10,16 @@ import Foundation
|
||||
|
||||
#if !swift(>=4.1)
|
||||
public extension Collection {
|
||||
func index(_ i: Self.Index, offsetBy n: Int) -> Self.Index {
|
||||
let indexDistance = Self.IndexDistance(n)
|
||||
return index(i, offsetBy: indexDistance)
|
||||
func index(_ index: Self.Index, offsetBy offset: Int) -> Self.Index {
|
||||
let indexDistance = Self.IndexDistance(offset)
|
||||
return self.index(index, offsetBy: indexDistance)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !swift(>=4.1)
|
||||
public extension TemplateSyntaxError {
|
||||
public static func ==(lhs: TemplateSyntaxError, rhs: TemplateSyntaxError) -> Bool {
|
||||
public static func == (lhs: TemplateSyntaxError, rhs: TemplateSyntaxError) -> Bool {
|
||||
return lhs.reason == rhs.reason &&
|
||||
lhs.description == rhs.description &&
|
||||
lhs.token == rhs.token &&
|
||||
@@ -31,7 +31,7 @@ public extension TemplateSyntaxError {
|
||||
|
||||
#if !swift(>=4.1)
|
||||
public extension Variable {
|
||||
public static func ==(lhs: Variable, rhs: Variable) -> Bool {
|
||||
public static func == (lhs: Variable, rhs: Variable) -> Bool {
|
||||
return lhs.variable == rhs.variable
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user