Warnings--
This commit is contained in:
@@ -2,8 +2,14 @@
|
||||
public class Context {
|
||||
var dictionaries: [[String: Any?]]
|
||||
|
||||
/// The context's environment, such as registered extensions, classes, …
|
||||
public let environment: Environment
|
||||
|
||||
/// Create a context from a dictionary (and an env.)
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dictionary: The context's data
|
||||
/// - environment: Environment such as extensions, …
|
||||
public init(dictionary: [String: Any] = [:], environment: Environment? = nil) {
|
||||
if !dictionary.isEmpty {
|
||||
dictionaries = [dictionary]
|
||||
@@ -14,6 +20,7 @@ public class Context {
|
||||
self.environment = environment ?? Environment()
|
||||
}
|
||||
|
||||
/// Access variables in this context by name
|
||||
public subscript(key: String) -> Any? {
|
||||
/// Retrieves a variable's value, starting at the current context and going upwards
|
||||
get {
|
||||
@@ -36,22 +43,35 @@ public class Context {
|
||||
}
|
||||
|
||||
/// Push a new level into the Context
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dictionary: The new level data
|
||||
fileprivate func push(_ dictionary: [String: Any] = [:]) {
|
||||
dictionaries.append(dictionary)
|
||||
}
|
||||
|
||||
/// Pop the last level off of the Context
|
||||
///
|
||||
/// - returns: The popped level
|
||||
fileprivate func pop() -> [String: Any?]? {
|
||||
return dictionaries.popLast()
|
||||
dictionaries.popLast()
|
||||
}
|
||||
|
||||
/// Push a new level onto the context for the duration of the execution of the given closure
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dictionary: The new level data
|
||||
/// - closure: The closure to execute
|
||||
/// - returns: Return value of the closure
|
||||
public func push<Result>(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result {
|
||||
push(dictionary)
|
||||
defer { _ = pop() }
|
||||
return try closure()
|
||||
}
|
||||
|
||||
/// Flatten all levels of context data into 1, merging duplicate variables
|
||||
///
|
||||
/// - returns: All collected variables
|
||||
public func flatten() -> [String: Any] {
|
||||
var accumulator: [String: Any] = [:]
|
||||
|
||||
|
||||
@@ -6,10 +6,13 @@ public protocol DynamicMemberLookup {
|
||||
}
|
||||
|
||||
public extension DynamicMemberLookup where Self: RawRepresentable {
|
||||
/// Get a value for a given `String` key
|
||||
subscript(dynamicMember member: String) -> Any? {
|
||||
switch member {
|
||||
case "rawValue": return rawValue
|
||||
default: return nil
|
||||
case "rawValue":
|
||||
return rawValue
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
/// Container for environment data, such as registered extensions
|
||||
public struct Environment {
|
||||
/// The class for loading new templates
|
||||
public let templateClass: Template.Type
|
||||
/// List of registered extensions
|
||||
public var extensions: [Extension]
|
||||
/// Mechanism for loading new files
|
||||
public var loader: Loader?
|
||||
|
||||
/// Basic initializer
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - loader: Mechanism for loading new files
|
||||
/// - extensions: List of extension containers
|
||||
/// - templateClass: Class for newly loaded templates
|
||||
public init(
|
||||
loader: Loader? = nil,
|
||||
extensions: [Extension] = [],
|
||||
@@ -13,6 +23,11 @@ public struct Environment {
|
||||
self.extensions = extensions + [DefaultExtension()]
|
||||
}
|
||||
|
||||
/// Load a template with the given name
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: Name of the template
|
||||
/// - returns: Loaded template instance
|
||||
public func loadTemplate(name: String) throws -> Template {
|
||||
if let loader = loader {
|
||||
return try loader.loadTemplate(name: name, environment: self)
|
||||
@@ -21,6 +36,11 @@ public struct Environment {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a template with the given names
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - names: Names of the template
|
||||
/// - returns: Loaded template instance
|
||||
public func loadTemplate(names: [String]) throws -> Template {
|
||||
if let loader = loader {
|
||||
return try loader.loadTemplate(names: names, environment: self)
|
||||
@@ -29,11 +49,23 @@ public struct Environment {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a template with the given name, providing some data
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: Name of the template
|
||||
/// - context: Data for rendering
|
||||
/// - returns: Rendered output
|
||||
public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String {
|
||||
let template = try loadTemplate(name: name)
|
||||
return try render(template: template, context: context)
|
||||
}
|
||||
|
||||
/// Render the given template string, providing some data
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - string: Template string
|
||||
/// - context: Data for rendering
|
||||
/// - returns: Rendered output
|
||||
public func renderTemplate(string: String, context: [String: Any] = [:]) throws -> String {
|
||||
let template = templateClass.init(templateString: string, environment: self)
|
||||
return try render(template: template, context: context)
|
||||
|
||||
@@ -20,12 +20,12 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
||||
|
||||
public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible {
|
||||
public let reason: String
|
||||
public var description: String { return reason }
|
||||
public var description: String { reason }
|
||||
public internal(set) var token: Token?
|
||||
public internal(set) var stackTrace: [Token]
|
||||
public var templateName: String? { return token?.sourceMap.filename }
|
||||
public var templateName: String? { token?.sourceMap.filename }
|
||||
var allTokens: [Token] {
|
||||
return stackTrace + (token.map { [$0] } ?? [])
|
||||
stackTrace + (token.map { [$0] } ?? [])
|
||||
}
|
||||
|
||||
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
||||
|
||||
@@ -18,11 +18,11 @@ final class StaticExpression: Expression, CustomStringConvertible {
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
return value
|
||||
value
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "\(value)"
|
||||
"\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ final class VariableExpression: Expression, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(variable: \(variable))"
|
||||
"(variable: \(variable))"
|
||||
}
|
||||
|
||||
/// Resolves a variable in the given context as boolean
|
||||
@@ -60,7 +60,7 @@ final class VariableExpression: Expression, CustomStringConvertible {
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
return try resolve(context: context, variable: variable)
|
||||
try resolve(context: context, variable: variable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +72,11 @@ final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "not \(expression)"
|
||||
"not \(expression)"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
return try !expression.evaluate(context: context)
|
||||
try !expression.evaluate(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) in \(rhs))"
|
||||
"(\(lhs) in \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
@@ -125,7 +125,7 @@ final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) or \(rhs))"
|
||||
"(\(lhs) or \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
@@ -148,7 +148,7 @@ final class AndExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) and \(rhs))"
|
||||
"(\(lhs) and \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
@@ -171,7 +171,7 @@ class EqualityExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) == \(rhs))"
|
||||
"(\(lhs) == \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
@@ -206,7 +206,7 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) \(symbol) \(rhs))"
|
||||
"(\(lhs) \(symbol) \(rhs))"
|
||||
}
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
@@ -225,97 +225,97 @@ class NumericExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
}
|
||||
|
||||
var symbol: String {
|
||||
return ""
|
||||
""
|
||||
}
|
||||
|
||||
func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return false
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
class MoreThanExpression: NumericExpression {
|
||||
override var symbol: String {
|
||||
return ">"
|
||||
">"
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs > rhs
|
||||
lhs > rhs
|
||||
}
|
||||
}
|
||||
|
||||
class MoreThanEqualExpression: NumericExpression {
|
||||
override var symbol: String {
|
||||
return ">="
|
||||
">="
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs >= rhs
|
||||
lhs >= rhs
|
||||
}
|
||||
}
|
||||
|
||||
class LessThanExpression: NumericExpression {
|
||||
override var symbol: String {
|
||||
return "<"
|
||||
"<"
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs < rhs
|
||||
lhs < rhs
|
||||
}
|
||||
}
|
||||
|
||||
class LessThanEqualExpression: NumericExpression {
|
||||
override var symbol: String {
|
||||
return "<="
|
||||
"<="
|
||||
}
|
||||
|
||||
override func compare(lhs: Number, rhs: Number) -> Bool {
|
||||
return lhs <= rhs
|
||||
lhs <= rhs
|
||||
}
|
||||
}
|
||||
|
||||
class InequalityExpression: EqualityExpression {
|
||||
override var description: String {
|
||||
return "(\(lhs) != \(rhs))"
|
||||
"(\(lhs) != \(rhs))"
|
||||
}
|
||||
|
||||
override func evaluate(context: Context) throws -> Bool {
|
||||
return try !super.evaluate(context: context)
|
||||
try !super.evaluate(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func toNumber(value: Any) -> Number? {
|
||||
if let value = value as? Float {
|
||||
return Number(value)
|
||||
} else if let value = value as? Double {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int8 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int16 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int32 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int64 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt8 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt16 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt32 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt64 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Number {
|
||||
return value
|
||||
} else if let value = value as? Float64 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Float32 {
|
||||
return Number(value)
|
||||
}
|
||||
if let value = value as? Float {
|
||||
return Number(value)
|
||||
} else if let value = value as? Double {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int8 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int16 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int32 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Int64 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt8 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt16 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt32 {
|
||||
return Number(value)
|
||||
} else if let value = value as? UInt64 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Number {
|
||||
return value
|
||||
} else if let value = value as? Float64 {
|
||||
return Number(value)
|
||||
} else if let value = value as? Float32 {
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/// Container for registered tags and filters
|
||||
open class Extension {
|
||||
typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
var tags = [String: TagParser]()
|
||||
|
||||
var tags = [String: TagParser]()
|
||||
var filters = [String: Filter]()
|
||||
|
||||
/// Simple initializer
|
||||
public init() {
|
||||
}
|
||||
|
||||
@@ -20,11 +22,11 @@ open class Extension {
|
||||
}
|
||||
|
||||
/// 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?) {
|
||||
// swiftlint:disable:previous discouraged_optional_boolean
|
||||
filters[name] = .simple(filter)
|
||||
filters[negativeFilterName] = .simple {
|
||||
guard let result = try filter($0) else { return nil }
|
||||
filters[negativeFilterName] = .simple { value in
|
||||
guard let result = try filter(value) else { return nil }
|
||||
return !result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,9 +74,11 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
var indentWidth = 4
|
||||
if !arguments.isEmpty {
|
||||
guard let value = arguments[0] as? Int else {
|
||||
throw TemplateSyntaxError("""
|
||||
throw TemplateSyntaxError(
|
||||
"""
|
||||
'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))
|
||||
""")
|
||||
"""
|
||||
)
|
||||
}
|
||||
indentWidth = value
|
||||
}
|
||||
@@ -84,9 +86,11 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
var indentationChar = " "
|
||||
if arguments.count > 1 {
|
||||
guard let value = arguments[1] as? String else {
|
||||
throw TemplateSyntaxError("""
|
||||
throw TemplateSyntaxError(
|
||||
"""
|
||||
'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))
|
||||
""")
|
||||
"""
|
||||
)
|
||||
}
|
||||
indentationChar = value
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ class ForNode: NodeType {
|
||||
let components = token.components
|
||||
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
return components.count > (index + 1) && components[index] == token
|
||||
components.count > (index + 1) && components[index] == token
|
||||
}
|
||||
|
||||
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
|
||||
return components.count == index || hasToken(token, at: index)
|
||||
components.count == index || hasToken(token, at: index)
|
||||
}
|
||||
|
||||
guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
|
||||
@@ -154,9 +154,9 @@ class ForNode: NodeType {
|
||||
} else if let resolved = resolved {
|
||||
let mirror = Mirror(reflecting: resolved)
|
||||
switch mirror.displayStyle {
|
||||
case .struct?, .tuple?:
|
||||
case .struct, .tuple:
|
||||
values = Array(mirror.children)
|
||||
case .class?:
|
||||
case .class:
|
||||
var children = Array(mirror.children)
|
||||
var currentMirror: Mirror? = mirror
|
||||
while let superclassMirror = currentMirror?.superclassMirror {
|
||||
|
||||
@@ -10,23 +10,23 @@ enum Operator {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
static let all: [Operator] = [
|
||||
.infix("in", 5, InExpression.self),
|
||||
.infix("or", 6, OrExpression.self),
|
||||
.infix("and", 7, AndExpression.self),
|
||||
.prefix("not", 8, NotExpression.self),
|
||||
.infix("==", 10, EqualityExpression.self),
|
||||
.infix("!=", 10, InequalityExpression.self),
|
||||
.infix(">", 10, MoreThanExpression.self),
|
||||
.infix(">=", 10, MoreThanEqualExpression.self),
|
||||
.infix("<", 10, LessThanExpression.self),
|
||||
.infix("<=", 10, LessThanEqualExpression.self)
|
||||
]
|
||||
}
|
||||
|
||||
let operators: [Operator] = [
|
||||
.infix("in", 5, InExpression.self),
|
||||
.infix("or", 6, OrExpression.self),
|
||||
.infix("and", 7, AndExpression.self),
|
||||
.prefix("not", 8, NotExpression.self),
|
||||
.infix("==", 10, EqualityExpression.self),
|
||||
.infix("!=", 10, InequalityExpression.self),
|
||||
.infix(">", 10, MoreThanExpression.self),
|
||||
.infix(">=", 10, MoreThanEqualExpression.self),
|
||||
.infix("<", 10, LessThanExpression.self),
|
||||
.infix("<=", 10, LessThanEqualExpression.self)
|
||||
]
|
||||
|
||||
func findOperator(name: String) -> Operator? {
|
||||
for `operator` in operators where `operator`.name == name {
|
||||
for `operator` in Operator.all where `operator`.name == name {
|
||||
return `operator`
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ final class IfExpressionParser {
|
||||
}
|
||||
|
||||
static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser {
|
||||
return try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token)
|
||||
try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token)
|
||||
}
|
||||
|
||||
private init(components: ArraySlice<String>, environment: Environment, token: Token) throws {
|
||||
@@ -117,7 +117,7 @@ final class IfExpressionParser {
|
||||
|
||||
if component == "(" {
|
||||
bracketsBalance += 1
|
||||
let (expression, parsedCount) = try IfExpressionParser.subExpression(
|
||||
let (expression, parsedCount) = try Self.subExpression(
|
||||
from: components.suffix(from: index + 1),
|
||||
environment: environment,
|
||||
token: token
|
||||
@@ -152,11 +152,11 @@ final class IfExpressionParser {
|
||||
token: Token
|
||||
) throws -> (Expression, Int) {
|
||||
var bracketsBalance = 1
|
||||
let subComponents = components.prefix {
|
||||
if $0 == "(" {
|
||||
bracketsBalance += 1
|
||||
} else if $0 == ")" {
|
||||
bracketsBalance -= 1
|
||||
let subComponents = components.prefix { component in
|
||||
if component == "(" {
|
||||
bracketsBalance += 1
|
||||
} else if component == ")" {
|
||||
bracketsBalance -= 1
|
||||
}
|
||||
return bracketsBalance != 0
|
||||
}
|
||||
@@ -220,7 +220,7 @@ final class IfCondition {
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
return try context.push {
|
||||
try context.push {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ class IncludeNode: NodeType {
|
||||
let bits = token.components
|
||||
|
||||
guard bits.count == 2 || bits.count == 3 else {
|
||||
throw TemplateSyntaxError("""
|
||||
throw TemplateSyntaxError(
|
||||
"""
|
||||
'include' tag requires one argument, the template file to be included. \
|
||||
A second optional argument can be used to specify the context that will \
|
||||
be passed to the included file
|
||||
""")
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class BlockContext {
|
||||
class var contextKey: String { return "block_context" }
|
||||
class var contextKey: String { "block_context" }
|
||||
|
||||
// contains mapping of block names to their nodes and templates where they are defined
|
||||
var blocks: [String: [BlockNode]]
|
||||
|
||||
@@ -23,10 +23,10 @@ struct Lexer {
|
||||
self.templateName = templateName
|
||||
self.templateString = templateString
|
||||
|
||||
self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap {
|
||||
guard !$0.element.isEmpty,
|
||||
let range = templateString.range(of: $0.element) else { return nil }
|
||||
return (content: $0.element, number: UInt($0.offset + 1), range)
|
||||
self.lines = zip(1..., templateString.components(separatedBy: .newlines)).compactMap { index, line in
|
||||
guard !line.isEmpty,
|
||||
let range = templateString.range(of: line) else { return nil }
|
||||
return (content: line, number: UInt(index), range)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,12 +79,12 @@ struct Lexer {
|
||||
|
||||
let scanner = Scanner(templateString)
|
||||
while !scanner.isEmpty {
|
||||
if let (char, text) = scanner.scanForTokenStart(Lexer.tokenChars) {
|
||||
if let (char, text) = scanner.scanForTokenStart(Self.tokenChars) {
|
||||
if !text.isEmpty {
|
||||
tokens.append(createToken(string: text, at: scanner.range))
|
||||
}
|
||||
|
||||
guard let end = Lexer.tokenCharMap[char] else { continue }
|
||||
guard let end = Self.tokenCharMap[char] else { continue }
|
||||
let result = scanner.scanForTokenEnd(end)
|
||||
tokens.append(createToken(string: result, at: scanner.range))
|
||||
} else {
|
||||
@@ -127,7 +127,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return content.isEmpty
|
||||
content.isEmpty
|
||||
}
|
||||
|
||||
/// Scans for the end of a token, with a specific ending character. If we're
|
||||
@@ -144,8 +144,8 @@ class Scanner {
|
||||
func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String {
|
||||
var foundChar = false
|
||||
|
||||
for (index, char) in content.unicodeScalars.enumerated() {
|
||||
if foundChar && char == Scanner.tokenEndDelimiter {
|
||||
for (index, char) in zip(0..., content.unicodeScalars) {
|
||||
if foundChar && char == Self.tokenEndDelimiter {
|
||||
let result = String(content.unicodeScalars.prefix(index + 1))
|
||||
content = String(content.unicodeScalars.dropFirst(index + 1))
|
||||
range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index + 1)
|
||||
@@ -178,14 +178,14 @@ class Scanner {
|
||||
var foundBrace = false
|
||||
|
||||
range = range.upperBound..<range.upperBound
|
||||
for (index, char) in content.unicodeScalars.enumerated() {
|
||||
for (index, char) in zip(0..., content.unicodeScalars) {
|
||||
if foundBrace && tokenChars.contains(char) {
|
||||
let result = String(content.unicodeScalars.prefix(index - 1))
|
||||
content = String(content.unicodeScalars.dropFirst(index - 1))
|
||||
range = range.upperBound..<originalContent.unicodeScalars.index(range.upperBound, offsetBy: index - 1)
|
||||
return (char, result)
|
||||
} else {
|
||||
foundBrace = (char == Scanner.tokenStartDelimiter)
|
||||
foundBrace = (char == Self.tokenStartDelimiter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,4 +227,5 @@ extension String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Location in some content (text)
|
||||
public typealias ContentLocation = (content: String, lineNumber: UInt, lineOffset: Int)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
/// Type used for loading a template
|
||||
public protocol Loader {
|
||||
/// Load a template with the given name
|
||||
func loadTemplate(name: String, environment: Environment) throws -> Template
|
||||
/// Load a template with the given list of names
|
||||
func loadTemplate(names: [String], environment: Environment) throws -> Template
|
||||
}
|
||||
|
||||
extension Loader {
|
||||
/// Default implementation, tries to load the first template that exists from the list of given names
|
||||
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||
for name in names {
|
||||
do {
|
||||
@@ -31,13 +35,13 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
||||
}
|
||||
|
||||
public init(bundle: [Bundle]) {
|
||||
self.paths = bundle.map {
|
||||
Path($0.bundlePath)
|
||||
self.paths = bundle.map { bundle in
|
||||
Path(bundle.bundlePath)
|
||||
}
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return "FileSystemLoader(\(paths))"
|
||||
"FileSystemLoader(\(paths))"
|
||||
}
|
||||
|
||||
public func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||
@@ -119,6 +123,6 @@ class SuspiciousFileOperation: Error {
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "Path `\(path)` is located outside of base path `\(basePath)`"
|
||||
"Path `\(path)` is located outside of base path `\(basePath)`"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
/// Represents a parsed node
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
func render(_ context: Context) throws -> String
|
||||
@@ -10,17 +11,18 @@ public protocol NodeType {
|
||||
|
||||
/// Render the collection of nodes in the given context
|
||||
public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String {
|
||||
return try nodes
|
||||
.map {
|
||||
try nodes
|
||||
.map { node in
|
||||
do {
|
||||
return try $0.render(context)
|
||||
return try node.render(context)
|
||||
} catch {
|
||||
throw error.withToken($0.token)
|
||||
throw error.withToken(node.token)
|
||||
}
|
||||
}
|
||||
.joined()
|
||||
}
|
||||
|
||||
/// Simple node, used for triggering a closure during rendering
|
||||
public class SimpleNode: NodeType {
|
||||
public let handler: (Context) throws -> String
|
||||
public let token: Token?
|
||||
@@ -31,10 +33,11 @@ public class SimpleNode: NodeType {
|
||||
}
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
return try handler(context)
|
||||
try handler(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a block of text, renders the text
|
||||
public class TextNode: NodeType {
|
||||
public let text: String
|
||||
public let token: Token?
|
||||
@@ -45,14 +48,17 @@ public class TextNode: NodeType {
|
||||
}
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
return self.text
|
||||
self.text
|
||||
}
|
||||
}
|
||||
|
||||
/// Representing something that can be resolved in a context
|
||||
public protocol Resolvable {
|
||||
/// Try to resolve this with the given context
|
||||
func resolve(_ context: Context) throws -> Any?
|
||||
}
|
||||
|
||||
/// Represents a variable, renders the variable, may have conditional expressions.
|
||||
public class VariableNode: NodeType {
|
||||
public let variable: Resolvable
|
||||
public var token: Token?
|
||||
@@ -63,7 +69,7 @@ public class VariableNode: NodeType {
|
||||
let components = token.components
|
||||
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
return components.count > (index + 1) && components[index] == token
|
||||
components.count > (index + 1) && components[index] == token
|
||||
}
|
||||
|
||||
let condition: Expression?
|
||||
@@ -137,7 +143,7 @@ func stringify(_ result: Any?) -> String {
|
||||
}
|
||||
|
||||
func unwrap(_ array: [Any?]) -> [Any] {
|
||||
return array.map { (item: Any?) -> Any in
|
||||
array.map { (item: Any?) -> Any in
|
||||
if let item = item {
|
||||
if let items = item as? [Any?] {
|
||||
return unwrap(items)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/// Creates a checker that will stop parsing if it encounters a list of tags.
|
||||
/// Useful for example for scanning until a given "end"-node.
|
||||
public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||
return { _, token in
|
||||
{ _, token in
|
||||
if let name = token.components.first {
|
||||
for tag in tags where name == tag {
|
||||
return true
|
||||
@@ -12,11 +14,13 @@ public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) {
|
||||
|
||||
/// A class for parsing an array of tokens and converts them into a collection of Node's
|
||||
public class TokenParser {
|
||||
/// Parser for finding a kind of node
|
||||
public typealias TagParser = (TokenParser, Token) throws -> NodeType
|
||||
|
||||
fileprivate var tokens: [Token]
|
||||
fileprivate let environment: Environment
|
||||
|
||||
/// Simple initializer
|
||||
public init(tokens: [Token], environment: Environment) {
|
||||
self.tokens = tokens
|
||||
self.environment = environment
|
||||
@@ -24,9 +28,11 @@ public class TokenParser {
|
||||
|
||||
/// Parse the given tokens into nodes
|
||||
public func parse() throws -> [NodeType] {
|
||||
return try parse(nil)
|
||||
try parse(nil)
|
||||
}
|
||||
|
||||
/// Parse nodes until a specific "something" is detected, determined by the provided closure.
|
||||
/// Combine this with the `until(:)` function above to scan nodes until a given token.
|
||||
public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] {
|
||||
var nodes = [NodeType]()
|
||||
|
||||
@@ -61,6 +67,7 @@ public class TokenParser {
|
||||
return nodes
|
||||
}
|
||||
|
||||
/// Pop the next token (returning it)
|
||||
public func nextToken() -> Token? {
|
||||
if !tokens.isEmpty {
|
||||
return tokens.remove(at: 0)
|
||||
@@ -69,23 +76,24 @@ public class TokenParser {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Insert a token
|
||||
public func prependToken(_ token: Token) {
|
||||
tokens.insert(token, at: 0)
|
||||
}
|
||||
|
||||
/// Create filter expression from a string contained in provided token
|
||||
public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable {
|
||||
return try environment.compileFilter(filterToken, containedIn: token)
|
||||
try environment.compileFilter(filterToken, containedIn: token)
|
||||
}
|
||||
|
||||
/// Create boolean expression from components contained in provided token
|
||||
public func compileExpression(components: [String], token: Token) throws -> Expression {
|
||||
return try environment.compileExpression(components: components, containedIn: token)
|
||||
try environment.compileExpression(components: components, containedIn: token)
|
||||
}
|
||||
|
||||
/// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token
|
||||
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
return try environment.compileResolvable(token, containedIn: containingToken)
|
||||
try environment.compileResolvable(token, containedIn: containingToken)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,10 +119,12 @@ extension Environment {
|
||||
if suggestedFilters.isEmpty {
|
||||
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
||||
} else {
|
||||
throw TemplateSyntaxError("""
|
||||
throw TemplateSyntaxError(
|
||||
"""
|
||||
Unknown filter '\(name)'. \
|
||||
Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")).
|
||||
""")
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +132,9 @@ extension Environment {
|
||||
let allFilters = extensions.flatMap { $0.filters.keys }
|
||||
|
||||
let filtersWithDistance = allFilters
|
||||
.map { (filterName: $0, distance: $0.levenshteinDistance(name)) }
|
||||
// do not suggest filters which names are shorter than the distance
|
||||
.filter { $0.filterName.count > $0.distance }
|
||||
.map { (filterName: $0, distance: $0.levenshteinDistance(name)) }
|
||||
// do not suggest filters which names are shorter than the distance
|
||||
.filter { $0.filterName.count > $0.distance }
|
||||
guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else {
|
||||
return []
|
||||
}
|
||||
@@ -134,7 +144,7 @@ extension Environment {
|
||||
|
||||
/// Create filter expression from a string
|
||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||
return try FilterExpression(token: token, environment: self)
|
||||
try FilterExpression(token: token, environment: self)
|
||||
}
|
||||
|
||||
/// Create filter expression from a string contained in provided token
|
||||
@@ -165,26 +175,26 @@ extension Environment {
|
||||
|
||||
/// Create resolvable (i.e. range variable or filter expression) from a string
|
||||
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||
return try RangeVariable(token, environment: self)
|
||||
try RangeVariable(token, environment: self)
|
||||
?? compileFilter(token)
|
||||
}
|
||||
|
||||
/// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token
|
||||
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
return try RangeVariable(token, environment: self, containedIn: containingToken)
|
||||
?? compileFilter(token, containedIn: containingToken)
|
||||
try RangeVariable(token, environment: self, containedIn: containingToken)
|
||||
?? compileFilter(token, containedIn: containingToken)
|
||||
}
|
||||
|
||||
/// Create boolean expression from components contained in provided token
|
||||
public func compileExpression(components: [String], containedIn token: Token) throws -> Expression {
|
||||
return try IfExpressionParser.parser(components: components, environment: self, token: token).parse()
|
||||
try IfExpressionParser.parser(components: components, environment: self, token: token).parse()
|
||||
}
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||
extension String {
|
||||
subscript(_ index: Int) -> Character {
|
||||
return self[self.index(self.startIndex, offsetBy: index)]
|
||||
self[self.index(self.startIndex, offsetBy: index)]
|
||||
}
|
||||
|
||||
func levenshteinDistance(_ target: String) -> Int {
|
||||
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
import PathKit
|
||||
|
||||
#if os(Linux)
|
||||
// swiftlint:disable:next prefixed_toplevel_constant
|
||||
let NSFileNoSuchFileError = 4
|
||||
#endif
|
||||
|
||||
@@ -77,6 +78,6 @@ open class Template: ExpressibleByStringLiteral {
|
||||
// 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))
|
||||
try render(Context(dictionary: dictionary ?? [:], environment: environment))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ extension String {
|
||||
if character == separate {
|
||||
if separate != separator {
|
||||
word.append(separate)
|
||||
} else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty {
|
||||
} else if (singleQuoteCount.isMultiple(of: 2) || doubleQuoteCount.isMultiple(of: 2)) && !word.isEmpty {
|
||||
appendWord(word, to: &components)
|
||||
word = ""
|
||||
}
|
||||
@@ -75,7 +75,7 @@ public struct SourceMap: Equatable {
|
||||
static let unknown = SourceMap()
|
||||
|
||||
public static func == (lhs: SourceMap, rhs: SourceMap) -> Bool {
|
||||
return lhs.filename == rhs.filename && lhs.location == rhs.location
|
||||
lhs.filename == rhs.filename && lhs.location == rhs.location
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,25 +106,25 @@ public class Token: Equatable {
|
||||
|
||||
/// A token representing a piece of text.
|
||||
public static func text(value: String, at sourceMap: SourceMap) -> Token {
|
||||
return Token(contents: value, kind: .text, sourceMap: sourceMap)
|
||||
Token(contents: value, kind: .text, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
/// A token representing a variable.
|
||||
public static func variable(value: String, at sourceMap: SourceMap) -> Token {
|
||||
return Token(contents: value, kind: .variable, sourceMap: sourceMap)
|
||||
Token(contents: value, kind: .variable, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
/// A token representing a comment.
|
||||
public static func comment(value: String, at sourceMap: SourceMap) -> Token {
|
||||
return Token(contents: value, kind: .comment, sourceMap: sourceMap)
|
||||
Token(contents: value, kind: .comment, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
/// A token representing a template block.
|
||||
public static func block(value: String, at sourceMap: SourceMap) -> Token {
|
||||
return Token(contents: value, kind: .block, sourceMap: sourceMap)
|
||||
Token(contents: value, kind: .block, sourceMap: sourceMap)
|
||||
}
|
||||
|
||||
public static func == (lhs: Token, rhs: Token) -> Bool {
|
||||
return lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap
|
||||
lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ class FilterExpression: Resolvable {
|
||||
let filterBits = bits[bits.indices.suffix(from: 1)]
|
||||
|
||||
do {
|
||||
filters = try filterBits.map {
|
||||
let (name, arguments) = parseFilterComponents(token: $0)
|
||||
filters = try filterBits.map { bit in
|
||||
let (name, arguments) = parseFilterComponents(token: bit)
|
||||
let filter = try environment.findFilter(name)
|
||||
return (filter, arguments)
|
||||
}
|
||||
@@ -208,13 +208,14 @@ protocol Normalizable {
|
||||
|
||||
extension Array: Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next legacy_objc_type
|
||||
extension NSArray: Normalizable {
|
||||
func normalize() -> Any? {
|
||||
return map { $0 as Any }
|
||||
map { $0 as Any }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,8 +274,10 @@ protocol AnyOptional {
|
||||
extension Optional: AnyOptional {
|
||||
var wrapped: Any? {
|
||||
switch self {
|
||||
case let .some(value): return value
|
||||
case .none: return nil
|
||||
case let .some(value):
|
||||
return value
|
||||
case .none:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user