Merge branch 'master' into dynamic-filter
# Conflicts: # CHANGELOG.md # Sources/ForTag.swift # Sources/IfTag.swift # Sources/Parser.swift # Sources/Variable.swift # Tests/StencilTests/ExpressionSpec.swift # Tests/StencilTests/FilterSpec.swift # Tests/StencilTests/ForNodeSpec.swift # Tests/StencilTests/VariableSpec.swift
This commit is contained in:
@@ -3,7 +3,7 @@ public class Context {
|
||||
var dictionaries: [[String: Any?]]
|
||||
|
||||
public let environment: Environment
|
||||
|
||||
|
||||
init(dictionary: [String: Any]? = nil, environment: Environment? = nil) {
|
||||
if let dictionary = dictionary {
|
||||
dictionaries = [dictionary]
|
||||
|
||||
@@ -4,7 +4,10 @@ public struct Environment {
|
||||
|
||||
public var loader: Loader?
|
||||
|
||||
public init(loader: Loader? = nil, extensions: [Extension]? = nil, templateClass: Template.Type = Template.self) {
|
||||
public init(loader: Loader? = nil,
|
||||
extensions: [Extension]? = nil,
|
||||
templateClass: Template.Type = Template.self) {
|
||||
|
||||
self.templateClass = templateClass
|
||||
self.loader = loader
|
||||
self.extensions = (extensions ?? []) + [DefaultExtension()]
|
||||
@@ -28,11 +31,18 @@ public struct Environment {
|
||||
|
||||
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
||||
let template = try loadTemplate(name: name)
|
||||
return try template.render(context)
|
||||
return try render(template: template, context: context)
|
||||
}
|
||||
|
||||
public func renderTemplate(string: String, context: [String: Any]? = nil) 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 {
|
||||
// update template environment as it can be created from string literal with default environment
|
||||
template.environment = self
|
||||
return try template.render(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,3 +17,67 @@ public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
||||
return "Template named `\(templates)` does not exist. No loaders found"
|
||||
}
|
||||
}
|
||||
|
||||
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] }) ?? [])
|
||||
}
|
||||
|
||||
public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) {
|
||||
self.reason = reason
|
||||
self.stackTrace = stackTrace
|
||||
self.token = token
|
||||
}
|
||||
|
||||
public init(_ description: String) {
|
||||
self.init(reason: description)
|
||||
}
|
||||
}
|
||||
|
||||
extension Error {
|
||||
func withToken(_ token: Token?) -> Error {
|
||||
if var error = self as? TemplateSyntaxError {
|
||||
error.token = error.token ?? token
|
||||
return error
|
||||
} else {
|
||||
return TemplateSyntaxError(reason: "\(self)", token: token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ErrorReporter: class {
|
||||
func renderError(_ error: Error) -> String
|
||||
}
|
||||
|
||||
open class SimpleErrorReporter: ErrorReporter {
|
||||
|
||||
open func renderError(_ error: Error) -> String {
|
||||
guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription }
|
||||
|
||||
func describe(token: Token) -> String {
|
||||
let templateName = token.sourceMap.filename ?? ""
|
||||
let location = token.sourceMap.location
|
||||
let highlight = """
|
||||
\(String(Array(repeating: " ", count: location.lineOffset)))\
|
||||
^\(String(Array(repeating: "~", count: max(token.contents.count - 1, 0))))
|
||||
"""
|
||||
|
||||
return """
|
||||
\(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason)
|
||||
\(location.content)
|
||||
\(highlight)
|
||||
"""
|
||||
}
|
||||
|
||||
var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] }
|
||||
let description = templateError.token.map(describe(token:)) ?? templateError.reason
|
||||
descriptions.append(description)
|
||||
return descriptions.joined(separator: "\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,21 +88,21 @@ final class NotExpression: Expression, PrefixOperator, CustomStringConvertible {
|
||||
final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
let lhs: Expression
|
||||
let rhs: Expression
|
||||
|
||||
|
||||
init(lhs: Expression, rhs: Expression) {
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
}
|
||||
|
||||
|
||||
var description: String {
|
||||
return "(\(lhs) in \(rhs))"
|
||||
}
|
||||
|
||||
|
||||
func evaluate(context: Context) throws -> Bool {
|
||||
if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression {
|
||||
let lhsValue = try lhs.variable.resolve(context)
|
||||
let rhsValue = try rhs.variable.resolve(context)
|
||||
|
||||
|
||||
if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] {
|
||||
return rhs.contains(lhs)
|
||||
} else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableClosedRange<Int> {
|
||||
@@ -115,10 +115,10 @@ final class InExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
final class OrExpression: Expression, InfixOperator, CustomStringConvertible {
|
||||
|
||||
@@ -15,9 +15,18 @@ 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(handler: handler)
|
||||
return SimpleNode(token: token, handler: handler)
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers boolean filter with it's negative counterpart
|
||||
public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) {
|
||||
filters[name] = .simple(filter)
|
||||
filters[negativeFilterName] = .simple {
|
||||
guard let result = try filter($0) else { return nil }
|
||||
return !result
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a template filter with the given name
|
||||
public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) {
|
||||
@@ -46,9 +55,9 @@ class DefaultExtension: Extension {
|
||||
registerTag("for", parser: ForNode.parse)
|
||||
registerTag("if", parser: IfNode.parse)
|
||||
registerTag("ifnot", parser: IfNode.parse_ifnot)
|
||||
#if !os(Linux)
|
||||
registerTag("now", parser: NowNode.parse)
|
||||
#endif
|
||||
#if !os(Linux)
|
||||
registerTag("now", parser: NowNode.parse)
|
||||
#endif
|
||||
registerTag("include", parser: IncludeNode.parse)
|
||||
registerTag("extends", parser: ExtendsNode.parse)
|
||||
registerTag("block", parser: BlockNode.parse)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class FilterNode : NodeType {
|
||||
let resolvable: Resolvable
|
||||
let nodes: [NodeType]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
@@ -15,20 +16,21 @@ class FilterNode : NodeType {
|
||||
throw TemplateSyntaxError("`endfilter` was not found.")
|
||||
}
|
||||
|
||||
let resolvable = try parser.compileFilter("filter_value|\(bits[1])")
|
||||
return FilterNode(nodes: blocks, resolvable: resolvable)
|
||||
let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token)
|
||||
return FilterNode(nodes: blocks, resolvable: resolvable, token: token)
|
||||
}
|
||||
|
||||
init(nodes: [NodeType], resolvable: Resolvable) {
|
||||
init(nodes: [NodeType], resolvable: Resolvable, token: Token) {
|
||||
self.nodes = nodes
|
||||
self.resolvable = resolvable
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
let value = try renderNodes(nodes, context)
|
||||
|
||||
return try context.push(dictionary: ["filter_value": value]) {
|
||||
return try VariableNode(variable: resolvable).render(context)
|
||||
return try VariableNode(variable: resolvable, token: token).render(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,9 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
var indentWidth = 4
|
||||
if arguments.count > 0 {
|
||||
guard let value = arguments[0] as? Int else {
|
||||
throw TemplateSyntaxError("'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))")
|
||||
throw TemplateSyntaxError("""
|
||||
'indent' filter width argument must be an Integer (\(String(describing: arguments[0])))
|
||||
""")
|
||||
}
|
||||
indentWidth = value
|
||||
}
|
||||
@@ -82,7 +84,9 @@ func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? {
|
||||
var indentationChar = " "
|
||||
if arguments.count > 1 {
|
||||
guard let value = arguments[1] as? String else {
|
||||
throw TemplateSyntaxError("'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))")
|
||||
throw TemplateSyntaxError("""
|
||||
'indent' filter indentation argument must be a String (\(String(describing: arguments[1]))
|
||||
""")
|
||||
}
|
||||
indentationChar = value
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ class ForNode : NodeType {
|
||||
let nodes:[NodeType]
|
||||
let emptyNodes: [NodeType]
|
||||
let `where`: Expression?
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
let components = token.components()
|
||||
@@ -13,20 +14,25 @@ class ForNode : NodeType {
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
return components.count > (index + 1) && components[index] == token
|
||||
}
|
||||
|
||||
func endsOrHasToken(_ token: String, at index: Int) -> Bool {
|
||||
return components.count == index || hasToken(token, at: index)
|
||||
}
|
||||
|
||||
guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else {
|
||||
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]")
|
||||
throw TemplateSyntaxError("'for' statements should use the syntax: `for <x> in <y> [where <condition>]`.")
|
||||
}
|
||||
|
||||
let loopVariables = components[1].characters
|
||||
let loopVariables = components[1]
|
||||
.split(separator: ",")
|
||||
.map(String.init)
|
||||
.map { $0.trim(character: " ") }
|
||||
|
||||
var emptyNodes = [NodeType]()
|
||||
let resolvable = try parser.compileResolvable(components[3], containedIn: token)
|
||||
|
||||
let `where` = hasToken("where", at: 4)
|
||||
? try parseExpression(components: Array(components.suffix(from: 5)), tokenParser: parser, token: token)
|
||||
: nil
|
||||
|
||||
let forNodes = try parser.parse(until(["endfor", "empty"]))
|
||||
|
||||
@@ -34,26 +40,22 @@ class ForNode : NodeType {
|
||||
throw TemplateSyntaxError("`endfor` was not found.")
|
||||
}
|
||||
|
||||
var emptyNodes = [NodeType]()
|
||||
if token.contents == "empty" {
|
||||
emptyNodes = try parser.parse(until(["endfor"]))
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
let resolvable = try parser.compileResolvable(components[3])
|
||||
|
||||
let `where` = hasToken("where", at: 4)
|
||||
? try parser.compileExpression(components: Array(components.suffix(from: 5)))
|
||||
: nil
|
||||
|
||||
return ForNode(resolvable: resolvable, loopVariables: loopVariables, nodes: forNodes, emptyNodes:emptyNodes, where: `where`)
|
||||
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) {
|
||||
init(resolvable: Resolvable, loopVariables: [String], nodes: [NodeType], emptyNodes: [NodeType], where: Expression? = nil, token: Token? = nil) {
|
||||
self.resolvable = resolvable
|
||||
self.loopVariables = loopVariables
|
||||
self.nodes = nodes
|
||||
self.emptyNodes = emptyNodes
|
||||
self.where = `where`
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func push<Result>(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result {
|
||||
@@ -91,7 +93,7 @@ class ForNode : NodeType {
|
||||
var values: [Any]
|
||||
|
||||
if let dictionary = resolved as? [String: Any], !dictionary.isEmpty {
|
||||
values = dictionary.map { ($0.key, $0.value) }
|
||||
values = dictionary.sorted { $0.key < $1.key }
|
||||
} else if let array = resolved as? [Any] {
|
||||
values = array
|
||||
} else if let range = resolved as? CountableClosedRange<Int> {
|
||||
@@ -143,7 +145,7 @@ class ForNode : NodeType {
|
||||
try renderNodes(nodes, context)
|
||||
}
|
||||
}
|
||||
}.joined(separator: "")
|
||||
}.joined(separator: "")
|
||||
}
|
||||
|
||||
return try context.push {
|
||||
|
||||
@@ -38,10 +38,11 @@ func findOperator(name: String) -> Operator? {
|
||||
}
|
||||
|
||||
|
||||
enum IfToken {
|
||||
case infix(name: String, bindingPower: Int, op: InfixOperator.Type)
|
||||
case prefix(name: String, bindingPower: Int, op: PrefixOperator.Type)
|
||||
indirect enum IfToken {
|
||||
case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type)
|
||||
case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type)
|
||||
case variable(Resolvable)
|
||||
case subExpression(Expression)
|
||||
case end
|
||||
|
||||
var bindingPower: Int {
|
||||
@@ -52,8 +53,10 @@ enum IfToken {
|
||||
return bindingPower
|
||||
case .variable(_):
|
||||
return 0
|
||||
case .subExpression(_):
|
||||
return 0
|
||||
case .end:
|
||||
return 0
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +69,8 @@ enum IfToken {
|
||||
return op.init(expression: expression)
|
||||
case .variable(let variable):
|
||||
return VariableExpression(variable: variable)
|
||||
case .subExpression(let expression):
|
||||
return expression
|
||||
case .end:
|
||||
throw TemplateSyntaxError("'if' expression error: end")
|
||||
}
|
||||
@@ -80,6 +85,8 @@ enum IfToken {
|
||||
throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side")
|
||||
case .variable(let variable):
|
||||
throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side")
|
||||
case .subExpression(_):
|
||||
throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side")
|
||||
case .end:
|
||||
throw TemplateSyntaxError("'if' expression error: end")
|
||||
}
|
||||
@@ -100,21 +107,71 @@ final class IfExpressionParser {
|
||||
let tokens: [IfToken]
|
||||
var position: Int = 0
|
||||
|
||||
init(components: [String], environment: Environment) throws {
|
||||
self.tokens = try components.map { component in
|
||||
if let op = findOperator(name: component) {
|
||||
switch op {
|
||||
case .infix(let name, let bindingPower, let cls):
|
||||
return .infix(name: name, bindingPower: bindingPower, op: cls)
|
||||
case .prefix(let name, let bindingPower, let cls):
|
||||
return .prefix(name: name, bindingPower: bindingPower, op: cls)
|
||||
}
|
||||
}
|
||||
private init(tokens: [IfToken]) {
|
||||
self.tokens = tokens
|
||||
}
|
||||
|
||||
static func parser(components: [String], tokenParser: TokenParser, token: Token) throws -> IfExpressionParser {
|
||||
return try IfExpressionParser(components: ArraySlice(components), tokenParser: tokenParser, token: token)
|
||||
}
|
||||
|
||||
return .variable(try environment.compileResolvable(component))
|
||||
private init(components: ArraySlice<String>, tokenParser: TokenParser, token: Token) throws {
|
||||
var parsedComponents = Set<Int>()
|
||||
var bracketsBalance = 0
|
||||
self.tokens = try zip(components.indices, components).compactMap { (index, component) in
|
||||
guard !parsedComponents.contains(index) else { return nil }
|
||||
|
||||
if component == "(" {
|
||||
bracketsBalance += 1
|
||||
let (expression, parsedCount) = try IfExpressionParser.subExpression(
|
||||
from: components.suffix(from: index + 1),
|
||||
tokenParser: tokenParser,
|
||||
token: token
|
||||
)
|
||||
parsedComponents.formUnion(Set(index...(index + parsedCount)))
|
||||
return .subExpression(expression)
|
||||
} else if component == ")" {
|
||||
bracketsBalance -= 1
|
||||
if bracketsBalance < 0 {
|
||||
throw TemplateSyntaxError("'if' expression error: missing opening bracket")
|
||||
}
|
||||
parsedComponents.insert(index)
|
||||
return nil
|
||||
} else {
|
||||
parsedComponents.insert(index)
|
||||
if let op = findOperator(name: component) {
|
||||
switch op {
|
||||
case .infix(let name, let bindingPower, let operatorType):
|
||||
return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||
case .prefix(let name, let bindingPower, let operatorType):
|
||||
return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType)
|
||||
}
|
||||
}
|
||||
return .variable(try tokenParser.compileResolvable(component, containedIn: token))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func subExpression(from components: ArraySlice<String>, tokenParser: TokenParser, token: Token) throws -> (Expression, Int) {
|
||||
var bracketsBalance = 1
|
||||
let subComponents = components
|
||||
.prefix(while: {
|
||||
if $0 == "(" {
|
||||
bracketsBalance += 1
|
||||
} else if $0 == ")" {
|
||||
bracketsBalance -= 1
|
||||
}
|
||||
return bracketsBalance != 0
|
||||
})
|
||||
if bracketsBalance > 0 {
|
||||
throw TemplateSyntaxError("'if' expression error: missing closing bracket")
|
||||
}
|
||||
|
||||
let expressionParser = try IfExpressionParser(components: subComponents, tokenParser: tokenParser, token: token)
|
||||
let expression = try expressionParser.parse()
|
||||
return (expression, subComponents.count)
|
||||
}
|
||||
|
||||
var currentToken: IfToken {
|
||||
if tokens.count > position {
|
||||
return tokens[position]
|
||||
@@ -176,49 +233,51 @@ final class IfCondition {
|
||||
|
||||
class IfNode : NodeType {
|
||||
let conditions: [IfCondition]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components()
|
||||
components.removeFirst()
|
||||
|
||||
let expression = try parser.compileExpression(components: components)
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||
var conditions: [IfCondition] = [
|
||||
IfCondition(expression: expression, nodes: nodes)
|
||||
]
|
||||
|
||||
var token = parser.nextToken()
|
||||
while let current = token, current.contents.hasPrefix("elif") {
|
||||
var nextToken = parser.nextToken()
|
||||
while let current = nextToken, current.contents.hasPrefix("elif") {
|
||||
var components = current.components()
|
||||
components.removeFirst()
|
||||
let expression = try parser.compileExpression(components: components)
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: current)
|
||||
|
||||
let nodes = try parser.parse(until(["endif", "elif", "else"]))
|
||||
token = parser.nextToken()
|
||||
nextToken = parser.nextToken()
|
||||
conditions.append(IfCondition(expression: expression, nodes: nodes))
|
||||
}
|
||||
|
||||
if let current = token, current.contents == "else" {
|
||||
if let current = nextToken, current.contents == "else" {
|
||||
conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"]))))
|
||||
token = parser.nextToken()
|
||||
nextToken = parser.nextToken()
|
||||
}
|
||||
|
||||
guard let current = token, current.contents == "endif" else {
|
||||
guard let current = nextToken, current.contents == "endif" else {
|
||||
throw TemplateSyntaxError("`endif` was not found.")
|
||||
}
|
||||
|
||||
return IfNode(conditions: conditions)
|
||||
return IfNode(conditions: conditions, token: token)
|
||||
}
|
||||
|
||||
class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
var components = token.components()
|
||||
guard components.count == 2 else {
|
||||
throw TemplateSyntaxError("'ifnot' statements should use the following 'ifnot condition' `\(token.contents)`.")
|
||||
throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.")
|
||||
}
|
||||
components.removeFirst()
|
||||
var trueNodes = [NodeType]()
|
||||
var falseNodes = [NodeType]()
|
||||
|
||||
let expression = try parseExpression(components: components, tokenParser: parser, token: token)
|
||||
falseNodes = try parser.parse(until(["endif", "else"]))
|
||||
|
||||
guard let token = parser.nextToken() else {
|
||||
@@ -230,15 +289,15 @@ class IfNode : NodeType {
|
||||
_ = parser.nextToken()
|
||||
}
|
||||
|
||||
let expression = try parser.compileExpression(components: components)
|
||||
return IfNode(conditions: [
|
||||
IfCondition(expression: expression, nodes: trueNodes),
|
||||
IfCondition(expression: nil, nodes: falseNodes),
|
||||
])
|
||||
], token: token)
|
||||
}
|
||||
|
||||
init(conditions: [IfCondition]) {
|
||||
init(conditions: [IfCondition], token: Token? = nil) {
|
||||
self.conditions = conditions
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
|
||||
@@ -4,20 +4,26 @@ import PathKit
|
||||
class IncludeNode : NodeType {
|
||||
let templateName: Variable
|
||||
let includeContext: String?
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
|
||||
guard bits.count == 2 || bits.count == 3 else {
|
||||
throw TemplateSyntaxError("'include' tag requires one argument, the template file to be included. A second optional argument can be used to specify the context that will be passed to the included file")
|
||||
throw TemplateSyntaxError("""
|
||||
'include' tag requires one argument, the template file to be included. \
|
||||
A second optional argument can be used to specify the context that will \
|
||||
be passed to the included file
|
||||
""")
|
||||
}
|
||||
|
||||
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil)
|
||||
return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token)
|
||||
}
|
||||
|
||||
init(templateName: Variable, includeContext: String? = nil) {
|
||||
init(templateName: Variable, includeContext: String? = nil, token: Token) {
|
||||
self.templateName = templateName
|
||||
self.includeContext = includeContext
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
@@ -27,9 +33,17 @@ class IncludeNode : NodeType {
|
||||
|
||||
let template = try context.environment.loadTemplate(name: templateName)
|
||||
|
||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
||||
return try context.push(dictionary: subContext) {
|
||||
return try template.render(context)
|
||||
do {
|
||||
let subContext = includeContext.flatMap { context[$0] as? [String: Any] }
|
||||
return try context.push(dictionary: subContext) {
|
||||
return try template.render(context)
|
||||
}
|
||||
} catch {
|
||||
if let error = error as? TemplateSyntaxError {
|
||||
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
class BlockContext {
|
||||
class var contextKey: String { return "block_context" }
|
||||
|
||||
// contains mapping of block names to their nodes and templates where they are defined
|
||||
var blocks: [String: [BlockNode]]
|
||||
|
||||
init(blocks: [String: BlockNode]) {
|
||||
self.blocks = [:]
|
||||
blocks.forEach { (key, value) in
|
||||
self.blocks[key] = [value]
|
||||
}
|
||||
blocks.forEach { self.blocks[$0.key] = [$0.value] }
|
||||
}
|
||||
|
||||
func push(_ block: BlockNode, forKey blockName: String) {
|
||||
@@ -18,7 +17,7 @@ class BlockContext {
|
||||
self.blocks[blockName] = [block]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func pop(_ blockName: String) -> BlockNode? {
|
||||
if var blocks = blocks[blockName] {
|
||||
let block = blocks.removeFirst()
|
||||
@@ -51,6 +50,7 @@ extension Collection {
|
||||
class ExtendsNode : NodeType {
|
||||
let templateName: Variable
|
||||
let blocks: [String:BlockNode]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
@@ -64,7 +64,7 @@ class ExtendsNode : NodeType {
|
||||
throw TemplateSyntaxError("'extends' cannot appear more than once in the same template")
|
||||
}
|
||||
|
||||
let blockNodes = parsedNodes.flatMap { $0 as? BlockNode }
|
||||
let blockNodes = parsedNodes.compactMap { $0 as? BlockNode }
|
||||
|
||||
let nodes = blockNodes.reduce([String: BlockNode]()) { (accumulator, node) -> [String: BlockNode] in
|
||||
var dict = accumulator
|
||||
@@ -72,12 +72,13 @@ class ExtendsNode : NodeType {
|
||||
return dict
|
||||
}
|
||||
|
||||
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes)
|
||||
return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes, token: token)
|
||||
}
|
||||
|
||||
init(templateName: Variable, blocks: [String: BlockNode]) {
|
||||
init(templateName: Variable, blocks: [String: BlockNode], token: Token) {
|
||||
self.templateName = templateName
|
||||
self.blocks = blocks
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
@@ -85,21 +86,33 @@ class ExtendsNode : NodeType {
|
||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||
}
|
||||
|
||||
let template = try context.environment.loadTemplate(name: templateName)
|
||||
let baseTemplate = try context.environment.loadTemplate(name: templateName)
|
||||
|
||||
let blockContext: BlockContext
|
||||
if let context = context[BlockContext.contextKey] as? BlockContext {
|
||||
blockContext = context
|
||||
|
||||
for (key, value) in blocks {
|
||||
blockContext.push(value, forKey: key)
|
||||
if let currentBlockContext = context[BlockContext.contextKey] as? BlockContext {
|
||||
blockContext = currentBlockContext
|
||||
for (name, block) in blocks {
|
||||
blockContext.push(block, forKey: name)
|
||||
}
|
||||
} else {
|
||||
blockContext = BlockContext(blocks: blocks)
|
||||
}
|
||||
|
||||
return try context.push(dictionary: [BlockContext.contextKey: blockContext]) {
|
||||
return try template.render(context)
|
||||
do {
|
||||
// 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)
|
||||
}
|
||||
} catch {
|
||||
// if error template is already set (see catch in BlockNode)
|
||||
// and it happend in the same template as current template
|
||||
// there is no need to wrap it in another error
|
||||
if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename {
|
||||
throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +121,7 @@ class ExtendsNode : NodeType {
|
||||
class BlockNode : NodeType {
|
||||
let name: String
|
||||
let nodes: [NodeType]
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser: TokenParser, token: Token) throws -> NodeType {
|
||||
let bits = token.components()
|
||||
@@ -119,25 +133,57 @@ class BlockNode : NodeType {
|
||||
let blockName = bits[1]
|
||||
let nodes = try parser.parse(until(["endblock"]))
|
||||
_ = parser.nextToken()
|
||||
return BlockNode(name:blockName, nodes:nodes)
|
||||
return BlockNode(name:blockName, nodes:nodes, token: token)
|
||||
}
|
||||
|
||||
init(name: String, nodes: [NodeType]) {
|
||||
init(name: String, nodes: [NodeType], token: Token) {
|
||||
self.name = name
|
||||
self.nodes = nodes
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) {
|
||||
let newContext: [String: Any] = [
|
||||
BlockContext.contextKey: blockContext,
|
||||
"block": ["super": try self.render(context)]
|
||||
]
|
||||
return try context.push(dictionary: newContext) {
|
||||
return try node.render(context)
|
||||
if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.pop(name) {
|
||||
let childContext = try self.childContext(child, blockContext: blockContext, context: context)
|
||||
// render extension node
|
||||
do {
|
||||
return try context.push(dictionary: childContext) {
|
||||
return try child.render(context)
|
||||
}
|
||||
} catch {
|
||||
throw error.withToken(child.token)
|
||||
}
|
||||
}
|
||||
|
||||
return try renderNodes(nodes, context)
|
||||
}
|
||||
|
||||
// child node is a block node from child template that extends this node (has the same name)
|
||||
func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any] {
|
||||
var childContext: [String: Any] = [BlockContext.contextKey: blockContext]
|
||||
|
||||
if let blockSuperNode = child.nodes.first(where: {
|
||||
if case .variable(let variable, _)? = $0.token, variable == "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
|
||||
childContext["block"] = ["super": try self.render(context)]
|
||||
} catch {
|
||||
if let error = error as? TemplateSyntaxError {
|
||||
throw TemplateSyntaxError(
|
||||
reason: error.reason,
|
||||
token: blockSuperNode.token,
|
||||
stackTrace: error.allTokens)
|
||||
} else {
|
||||
throw TemplateSyntaxError(
|
||||
reason: "\(error)",
|
||||
token: blockSuperNode.token,
|
||||
stackTrace: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
return childContext
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ final class KeyPath {
|
||||
subscriptLevel = 0
|
||||
}
|
||||
|
||||
for c in variable.characters {
|
||||
for c in variable {
|
||||
switch c {
|
||||
case "." where subscriptLevel == 0:
|
||||
try foundSeparator()
|
||||
|
||||
@@ -1,16 +1,47 @@
|
||||
struct Lexer {
|
||||
let templateString: String
|
||||
import Foundation
|
||||
|
||||
init(templateString: String) {
|
||||
typealias Line = (content: String, number: UInt, range: Range<String.Index>)
|
||||
|
||||
struct Lexer {
|
||||
let templateName: String?
|
||||
let templateString: String
|
||||
let lines: [Line]
|
||||
|
||||
/// The potential token start characters. In a template these appear after a
|
||||
/// `{` character, for example `{{`, `{%`, `{#`, ...
|
||||
private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"]
|
||||
|
||||
/// The token end characters, corresponding to their token start characters.
|
||||
/// For example, a variable token starts with `{{` and ends with `}}`
|
||||
private static let tokenCharMap: [Unicode.Scalar: Unicode.Scalar] = [
|
||||
"{": "}",
|
||||
"%": "%",
|
||||
"#": "#"
|
||||
]
|
||||
|
||||
init(templateName: String? = nil, templateString: String) {
|
||||
self.templateName = templateName
|
||||
self.templateString = templateString
|
||||
|
||||
self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap {
|
||||
guard !$0.element.isEmpty else { return nil }
|
||||
return (content: $0.element, number: UInt($0.offset + 1), templateString.range(of: $0.element)!)
|
||||
}
|
||||
}
|
||||
|
||||
func createToken(string: String) -> Token {
|
||||
/// Create a token that will be passed on to the parser, with the given
|
||||
/// content and a range. The content will be tested to see if it's a
|
||||
/// `variable`, a `block` or a `comment`, otherwise it'll default to a simple
|
||||
/// `text` token.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - string: The content string of the token
|
||||
/// - range: The range within the template content, used for smart
|
||||
/// error reporting
|
||||
func createToken(string: String, at range: Range<String.Index>) -> Token {
|
||||
func strip() -> String {
|
||||
guard string.characters.count > 4 else { return "" }
|
||||
let start = string.index(string.startIndex, offsetBy: 2)
|
||||
let end = string.index(string.endIndex, offsetBy: -2)
|
||||
let trimmed = String(string[start..<end])
|
||||
guard string.count > 4 else { return "" }
|
||||
let trimmed = String(string.dropFirst(2).dropLast(2))
|
||||
.components(separatedBy: "\n")
|
||||
.filter({ !$0.isEmpty })
|
||||
.map({ $0.trim(character: " ") })
|
||||
@@ -18,112 +49,150 @@ struct Lexer {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return .variable(value: strip())
|
||||
} else if string.hasPrefix("{%") {
|
||||
return .block(value: strip())
|
||||
} else if string.hasPrefix("{#") {
|
||||
return .comment(value: strip())
|
||||
if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") {
|
||||
let value = strip()
|
||||
let range = templateString.range(of: value, range: range) ?? range
|
||||
let location = rangeLocation(range)
|
||||
let sourceMap = SourceMap(filename: templateName, location: location)
|
||||
|
||||
if string.hasPrefix("{{") {
|
||||
return .variable(value: value, at: sourceMap)
|
||||
} else if string.hasPrefix("{%") {
|
||||
return .block(value: value, at: sourceMap)
|
||||
} else if string.hasPrefix("{#") {
|
||||
return .comment(value: value, at: sourceMap)
|
||||
}
|
||||
}
|
||||
|
||||
return .text(value: string)
|
||||
let location = rangeLocation(range)
|
||||
let sourceMap = SourceMap(filename: templateName, location: location)
|
||||
return .text(value: string, at: sourceMap)
|
||||
}
|
||||
|
||||
/// Returns an array of tokens from a given template string.
|
||||
/// Transforms the template into a list of tokens, that will eventually be
|
||||
/// passed on to the parser.
|
||||
///
|
||||
/// - Returns: The list of tokens (see `createToken(string: at:)`).
|
||||
func tokenize() -> [Token] {
|
||||
var tokens: [Token] = []
|
||||
|
||||
let scanner = Scanner(templateString)
|
||||
|
||||
let map = [
|
||||
"{{": "}}",
|
||||
"{%": "%}",
|
||||
"{#": "#}",
|
||||
]
|
||||
|
||||
while !scanner.isEmpty {
|
||||
if let text = scanner.scan(until: ["{{", "{%", "{#"]) {
|
||||
if !text.1.isEmpty {
|
||||
tokens.append(createToken(string: text.1))
|
||||
if let (char, text) = scanner.scanForTokenStart(Lexer.tokenChars) {
|
||||
if !text.isEmpty {
|
||||
tokens.append(createToken(string: text, at: scanner.range))
|
||||
}
|
||||
|
||||
let end = map[text.0]!
|
||||
let result = scanner.scan(until: end, returnUntil: true)
|
||||
tokens.append(createToken(string: result))
|
||||
guard let end = Lexer.tokenCharMap[char] else { continue }
|
||||
let result = scanner.scanForTokenEnd(end)
|
||||
tokens.append(createToken(string: result, at: scanner.range))
|
||||
} else {
|
||||
tokens.append(createToken(string: scanner.content))
|
||||
tokens.append(createToken(string: scanner.content, at: scanner.range))
|
||||
scanner.content = ""
|
||||
}
|
||||
}
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
/// Finds the line matching the given range (for a token)
|
||||
///
|
||||
/// - Parameter range: The range to search for.
|
||||
/// - Returns: The content for that line, the line number and offset within
|
||||
/// the line.
|
||||
func rangeLocation(_ range: Range<String.Index>) -> ContentLocation {
|
||||
guard let line = self.lines.first(where: { $0.range.contains(range.lowerBound) }) else {
|
||||
return ("", 0, 0)
|
||||
}
|
||||
let offset = templateString.distance(from: line.range.lowerBound, to: range.lowerBound)
|
||||
return (line.content, line.number, offset)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class Scanner {
|
||||
let originalContent: String
|
||||
var content: String
|
||||
var range: Range<String.Index>
|
||||
|
||||
/// The start delimiter for a token.
|
||||
private static let tokenStartDelimiter: Unicode.Scalar = "{"
|
||||
/// And the corresponding end delimiter for a token.
|
||||
private static let tokenEndDelimiter: Unicode.Scalar = "}"
|
||||
|
||||
init(_ content: String) {
|
||||
self.originalContent = content
|
||||
self.content = content
|
||||
range = content.startIndex..<content.startIndex
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return content.isEmpty
|
||||
}
|
||||
|
||||
func scan(until: String, returnUntil: Bool = false) -> String {
|
||||
if until.isEmpty {
|
||||
return ""
|
||||
}
|
||||
|
||||
var index = content.startIndex
|
||||
while index != content.endIndex {
|
||||
let substring = content.substring(from: index)
|
||||
|
||||
if substring.hasPrefix(until) {
|
||||
let result = content.substring(to: index)
|
||||
content = substring
|
||||
|
||||
if returnUntil {
|
||||
content = content.substring(from: until.endIndex)
|
||||
return result + until
|
||||
}
|
||||
/// Scans for the end of a token, with a specific ending character. If we're
|
||||
/// searching for the end of a block token `%}`, this method receives a `%`.
|
||||
/// The scanner will search for that `%` followed by a `}`.
|
||||
///
|
||||
/// Note: if the end of a token is found, the `content` and `range`
|
||||
/// properties are updated to reflect this. `content` will be set to what
|
||||
/// remains of the template after the token. `range` will be set to the range
|
||||
/// of the token within the template.
|
||||
///
|
||||
/// - Parameter tokenChar: The token end character to search for.
|
||||
/// - Returns: The content of a token, or "" if no token end was found.
|
||||
func scanForTokenEnd(_ tokenChar: Unicode.Scalar) -> String {
|
||||
var foundChar = false
|
||||
|
||||
for (index, char) in content.unicodeScalars.enumerated() {
|
||||
if foundChar && char == Scanner.tokenEndDelimiter {
|
||||
let result = String(content.prefix(index + 1))
|
||||
content = String(content.dropFirst(index + 1))
|
||||
range = range.upperBound..<originalContent.index(range.upperBound, offsetBy: index + 1)
|
||||
return result
|
||||
} else {
|
||||
foundChar = (char == tokenChar)
|
||||
}
|
||||
|
||||
index = content.index(after: index)
|
||||
}
|
||||
|
||||
content = ""
|
||||
return ""
|
||||
}
|
||||
|
||||
func scan(until: [String]) -> (String, String)? {
|
||||
if until.isEmpty {
|
||||
return nil
|
||||
}
|
||||
/// Scans for the start of a token, with a list of potential starting
|
||||
/// characters. To scan for the start of variables (`{{`), blocks (`{%`) and
|
||||
/// comments (`{#`), this method receives the characters `{`, `%` and `#`.
|
||||
/// The scanner will search for a `{`, followed by one of the search
|
||||
/// characters. It will give the found character, and the content that came
|
||||
/// before the token.
|
||||
///
|
||||
/// Note: if the start of a token is found, the `content` and `range`
|
||||
/// properties are updated to reflect this. `content` will be set to what
|
||||
/// remains of the template starting with the token. `range` will be set to
|
||||
/// the start of the token within the template.
|
||||
///
|
||||
/// - Parameter tokenChars: List of token start characters to search for.
|
||||
/// - Returns: The found token start character, together with the content
|
||||
/// before the token, or nil of no token start was found.
|
||||
func scanForTokenStart(_ tokenChars: [Unicode.Scalar]) -> (Unicode.Scalar, String)? {
|
||||
var foundBrace = false
|
||||
|
||||
var index = content.startIndex
|
||||
while index != content.endIndex {
|
||||
let substring = content.substring(from: index)
|
||||
for string in until {
|
||||
if substring.hasPrefix(string) {
|
||||
let result = content.substring(to: index)
|
||||
content = substring
|
||||
return (string, result)
|
||||
}
|
||||
range = range.upperBound..<range.upperBound
|
||||
for (index, char) in content.unicodeScalars.enumerated() {
|
||||
if foundBrace && tokenChars.contains(char) {
|
||||
let result = String(content.prefix(index - 1))
|
||||
content = String(content.dropFirst(index - 1))
|
||||
range = range.upperBound..<originalContent.index(range.upperBound, offsetBy: index - 1)
|
||||
return (char, result)
|
||||
} else {
|
||||
foundBrace = (char == Scanner.tokenStartDelimiter)
|
||||
}
|
||||
|
||||
index = content.index(after: index)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension String {
|
||||
func findFirstNot(character: Character) -> String.Index? {
|
||||
var index = startIndex
|
||||
@@ -157,3 +226,5 @@ extension String {
|
||||
return String(self[first..<last])
|
||||
}
|
||||
}
|
||||
|
||||
public typealias ContentLocation = (content: String, lineNumber: UInt, lineOffset: Int)
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
public struct TemplateSyntaxError : Error, Equatable, CustomStringConvertible {
|
||||
public let description:String
|
||||
|
||||
public init(_ description:String) {
|
||||
self.description = description
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func ==(lhs:TemplateSyntaxError, rhs:TemplateSyntaxError) -> Bool {
|
||||
return lhs.description == rhs.description
|
||||
}
|
||||
|
||||
|
||||
public protocol NodeType {
|
||||
/// Render the node in the given context
|
||||
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 { try $0.render(context) }.joined(separator: "")
|
||||
return try nodes.map {
|
||||
do {
|
||||
return try $0.render(context)
|
||||
} catch {
|
||||
throw error.withToken($0.token)
|
||||
}
|
||||
}.joined(separator: "")
|
||||
}
|
||||
|
||||
public class SimpleNode : NodeType {
|
||||
public let handler:(Context) throws -> String
|
||||
public let token: Token?
|
||||
|
||||
public init(handler: @escaping (Context) throws -> String) {
|
||||
public init(token: Token, handler: @escaping (Context) throws -> String) {
|
||||
self.token = token
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
@@ -41,9 +37,11 @@ public class SimpleNode : NodeType {
|
||||
|
||||
public class TextNode : NodeType {
|
||||
public let text:String
|
||||
public let token: Token?
|
||||
|
||||
public init(text:String) {
|
||||
self.text = text
|
||||
self.token = nil
|
||||
}
|
||||
|
||||
public func render(_ context:Context) throws -> String {
|
||||
@@ -59,16 +57,65 @@ public protocol Resolvable {
|
||||
|
||||
public class VariableNode : NodeType {
|
||||
public let variable: Resolvable
|
||||
public var token: Token?
|
||||
let condition: Expression?
|
||||
let elseExpression: Resolvable?
|
||||
|
||||
public init(variable: Resolvable) {
|
||||
self.variable = variable
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var components = token.components()
|
||||
|
||||
func hasToken(_ token: String, at index: Int) -> Bool {
|
||||
return components.count > (index + 1) && components[index] == token
|
||||
}
|
||||
|
||||
let condition: Expression?
|
||||
let elseExpression: Resolvable?
|
||||
|
||||
if hasToken("if", at: 1) {
|
||||
let components = components.suffix(from: 2)
|
||||
if let elseIndex = components.index(of: "else") {
|
||||
condition = try parseExpression(components: Array(components.prefix(upTo: elseIndex)), tokenParser: parser, token: token)
|
||||
let elseToken = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ")
|
||||
elseExpression = try parser.compileResolvable(elseToken, containedIn: token)
|
||||
} else {
|
||||
condition = try parseExpression(components: Array(components), tokenParser: parser, token: token)
|
||||
elseExpression = nil
|
||||
}
|
||||
} else {
|
||||
condition = nil
|
||||
elseExpression = nil
|
||||
}
|
||||
|
||||
let filter = try parser.compileResolvable(components[0], containedIn: token)
|
||||
return VariableNode(variable: filter, token: token, condition: condition, elseExpression: elseExpression)
|
||||
}
|
||||
|
||||
public init(variable: String) {
|
||||
public init(variable: Resolvable, token: Token? = nil) {
|
||||
self.variable = variable
|
||||
self.token = token
|
||||
self.condition = nil
|
||||
self.elseExpression = nil
|
||||
}
|
||||
|
||||
init(variable: Resolvable, token: Token? = nil, condition: Expression?, elseExpression: Resolvable?) {
|
||||
self.variable = variable
|
||||
self.token = token
|
||||
self.condition = condition
|
||||
self.elseExpression = elseExpression
|
||||
}
|
||||
|
||||
public init(variable: String, token: Token? = nil) {
|
||||
self.variable = Variable(variable)
|
||||
self.token = token
|
||||
self.condition = nil
|
||||
self.elseExpression = nil
|
||||
}
|
||||
|
||||
public func render(_ context: Context) throws -> String {
|
||||
if let condition = self.condition, try condition.evaluate(context: context) == false {
|
||||
return try elseExpression?.resolve(context).map(stringify) ?? ""
|
||||
}
|
||||
|
||||
let result = try variable.resolve(context)
|
||||
return stringify(result)
|
||||
}
|
||||
|
||||
@@ -4,23 +4,25 @@ import Foundation
|
||||
|
||||
class NowNode : NodeType {
|
||||
let format:Variable
|
||||
let token: Token?
|
||||
|
||||
class func parse(_ parser:TokenParser, token:Token) throws -> NodeType {
|
||||
var format:Variable?
|
||||
|
||||
let components = token.components()
|
||||
guard components.count <= 2 else {
|
||||
throw TemplateSyntaxError("'now' tags may only have one argument: the format string `\(token.contents)`.")
|
||||
throw TemplateSyntaxError("'now' tags may only have one argument: the format string.")
|
||||
}
|
||||
if components.count == 2 {
|
||||
format = Variable(components[1])
|
||||
}
|
||||
|
||||
return NowNode(format:format)
|
||||
return NowNode(format:format, token: token)
|
||||
}
|
||||
|
||||
init(format:Variable?) {
|
||||
init(format:Variable?, token: Token? = nil) {
|
||||
self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"")
|
||||
self.token = token
|
||||
}
|
||||
|
||||
func render(_ context: Context) throws -> String {
|
||||
|
||||
@@ -37,10 +37,10 @@ public class TokenParser {
|
||||
let token = nextToken()!
|
||||
|
||||
switch token {
|
||||
case .text(let text):
|
||||
case .text(let text, _):
|
||||
nodes.append(TextNode(text: text))
|
||||
case .variable:
|
||||
nodes.append(VariableNode(variable: try compileResolvable(token.contents)))
|
||||
try nodes.append(VariableNode.parse(self, token: token))
|
||||
case .block:
|
||||
if let parse_until = parse_until , parse_until(self, token) {
|
||||
prependToken(token)
|
||||
@@ -48,8 +48,13 @@ public class TokenParser {
|
||||
}
|
||||
|
||||
if let tag = token.components().first {
|
||||
let parser = try environment.findTag(name: tag)
|
||||
nodes.append(try parser(self, token))
|
||||
do {
|
||||
let parser = try findTag(name: tag)
|
||||
let node = try parser(self, token)
|
||||
nodes.append(node)
|
||||
} catch {
|
||||
throw error.withToken(token)
|
||||
}
|
||||
}
|
||||
case .comment:
|
||||
continue
|
||||
@@ -108,7 +113,10 @@ extension Environment {
|
||||
if suggestedFilters.isEmpty {
|
||||
throw TemplateSyntaxError("Unknown filter '\(name)'.")
|
||||
} else {
|
||||
throw TemplateSyntaxError("Unknown filter '\(name)'. Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", "))")
|
||||
throw TemplateSyntaxError("""
|
||||
Unknown filter '\(name)'. \
|
||||
Found similar filters: \(suggestedFilters.map({ "'\($0)'" }).joined(separator: ", ")).
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +126,7 @@ extension Environment {
|
||||
let filtersWithDistance = allFilters
|
||||
.map({ (filterName: $0, distance: $0.levenshteinDistance(name)) })
|
||||
// do not suggest filters which names are shorter than the distance
|
||||
.filter({ $0.filterName.characters.count > $0.distance })
|
||||
.filter({ $0.filterName.count > $0.distance })
|
||||
guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else {
|
||||
return []
|
||||
}
|
||||
@@ -126,6 +134,26 @@ extension Environment {
|
||||
return filtersWithDistance.filter({ $0.distance == minDistance }).map({ $0.filterName })
|
||||
}
|
||||
|
||||
public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
do {
|
||||
return try FilterExpression(token: filterToken, parser: self)
|
||||
} catch {
|
||||
guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else {
|
||||
throw error
|
||||
}
|
||||
// find offset of filter in the containing token so that only filter is highligted, not the whole token
|
||||
if let filterTokenRange = containingToken.contents.range(of: filterToken) {
|
||||
var location = containingToken.sourceMap.location
|
||||
location.lineOffset += containingToken.contents.distance(from: containingToken.contents.startIndex, to: filterTokenRange.lowerBound)
|
||||
syntaxError.token = .variable(value: filterToken, at: SourceMap(filename: containingToken.sourceMap.filename, location: location))
|
||||
} else {
|
||||
syntaxError.token = containingToken
|
||||
}
|
||||
throw syntaxError
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use compileFilter(_:containedIn:)")
|
||||
public func compileFilter(_ token: String) throws -> Resolvable {
|
||||
return try FilterExpression(token: token, environment: self)
|
||||
}
|
||||
@@ -134,11 +162,17 @@ extension Environment {
|
||||
return try IfExpressionParser(components: components, environment: self).parse()
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Use compileResolvable(_:containedIn:)")
|
||||
public func compileResolvable(_ token: String) throws -> Resolvable {
|
||||
return try RangeVariable(token, environment: self)
|
||||
?? compileFilter(token)
|
||||
}
|
||||
|
||||
public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable {
|
||||
return try RangeVariable(token, parser: self, containedIn: containingToken)
|
||||
?? compileFilter(token, containedIn: containingToken)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||
@@ -155,10 +189,10 @@ extension String {
|
||||
// initialize v0 (the previous row of distances)
|
||||
// this row is A[0][i]: edit distance for an empty s
|
||||
// the distance is just the number of characters to delete from t
|
||||
last = [Int](0...target.characters.count)
|
||||
current = [Int](repeating: 0, count: target.characters.count + 1)
|
||||
last = [Int](0...target.count)
|
||||
current = [Int](repeating: 0, count: target.count + 1)
|
||||
|
||||
for i in 0..<self.characters.count {
|
||||
for i in 0..<self.count {
|
||||
// calculate v1 (current row distances) from the previous row v0
|
||||
|
||||
// first element of v1 is A[i+1][0]
|
||||
@@ -166,7 +200,7 @@ extension String {
|
||||
current[0] = i + 1
|
||||
|
||||
// use formula to fill in the rest of the row
|
||||
for j in 0..<target.characters.count {
|
||||
for j in 0..<target.count {
|
||||
current[j+1] = Swift.min(
|
||||
last[j+1] + 1,
|
||||
current[j] + 1,
|
||||
@@ -178,7 +212,7 @@ extension String {
|
||||
last = current
|
||||
}
|
||||
|
||||
return current[target.characters.count]
|
||||
return current[target.count]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ let NSFileNoSuchFileError = 4
|
||||
|
||||
/// A class representing a template
|
||||
open class Template: ExpressibleByStringLiteral {
|
||||
let environment: Environment
|
||||
let templateString: String
|
||||
internal(set) var environment: Environment
|
||||
let tokens: [Token]
|
||||
|
||||
/// The name of the loaded Template if the Template was loaded from a Loader
|
||||
@@ -17,8 +18,9 @@ open class Template: ExpressibleByStringLiteral {
|
||||
public required init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
||||
self.environment = environment ?? Environment()
|
||||
self.name = name
|
||||
self.templateString = templateString
|
||||
|
||||
let lexer = Lexer(templateString: templateString)
|
||||
let lexer = Lexer(templateName: name, templateString: templateString)
|
||||
tokens = lexer.tokenize()
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ extension String {
|
||||
let specialCharacters = ",|:"
|
||||
func appendWord(_ word: String) {
|
||||
if components.count > 0 {
|
||||
if let precedingChar = components.last?.characters.last, specialCharacters.characters.contains(precedingChar) {
|
||||
if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) {
|
||||
components[components.count-1] += word
|
||||
} else if specialCharacters.contains(word) {
|
||||
components[components.count-1] += word
|
||||
@@ -25,7 +25,7 @@ extension String {
|
||||
}
|
||||
}
|
||||
|
||||
for character in self.characters {
|
||||
for character in self {
|
||||
if character == "'" { singleQuoteCount += 1 }
|
||||
else if character == "\"" { doubleQuoteCount += 1 }
|
||||
|
||||
@@ -55,60 +55,63 @@ extension String {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SourceMap: Equatable {
|
||||
public let filename: String?
|
||||
public let location: ContentLocation
|
||||
|
||||
init(filename: String? = nil, location: ContentLocation = ("", 0, 0)) {
|
||||
self.filename = filename
|
||||
self.location = location
|
||||
}
|
||||
|
||||
static let unknown = SourceMap()
|
||||
|
||||
public static func ==(lhs: SourceMap, rhs: SourceMap) -> Bool {
|
||||
return lhs.filename == rhs.filename && lhs.location == rhs.location
|
||||
}
|
||||
}
|
||||
|
||||
public enum Token : Equatable {
|
||||
/// A token representing a piece of text.
|
||||
case text(value: String)
|
||||
case text(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a variable.
|
||||
case variable(value: String)
|
||||
case variable(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a comment.
|
||||
case comment(value: String)
|
||||
case comment(value: String, at: SourceMap)
|
||||
|
||||
/// A token representing a template block.
|
||||
case block(value: String)
|
||||
case block(value: String, at: SourceMap)
|
||||
|
||||
/// Returns the underlying value as an array seperated by spaces
|
||||
public func components() -> [String] {
|
||||
switch self {
|
||||
case .block(let value):
|
||||
return value.smartSplit()
|
||||
case .variable(let value):
|
||||
return value.smartSplit()
|
||||
case .text(let value):
|
||||
return value.smartSplit()
|
||||
case .comment(let value):
|
||||
case .block(let value, _),
|
||||
.variable(let value, _),
|
||||
.text(let value, _),
|
||||
.comment(let value, _):
|
||||
return value.smartSplit()
|
||||
}
|
||||
}
|
||||
|
||||
public var contents: String {
|
||||
switch self {
|
||||
case .block(let value):
|
||||
return value
|
||||
case .variable(let value):
|
||||
return value
|
||||
case .text(let value):
|
||||
return value
|
||||
case .comment(let value):
|
||||
case .block(let value, _),
|
||||
.variable(let value, _),
|
||||
.text(let value, _),
|
||||
.comment(let value, _):
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func == (lhs: Token, rhs: Token) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.text(let lhsValue), .text(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.variable(let lhsValue), .variable(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.block(let lhsValue), .block(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
case (.comment(let lhsValue), .comment(let rhsValue)):
|
||||
return lhsValue == rhsValue
|
||||
default:
|
||||
return false
|
||||
public var sourceMap: SourceMap {
|
||||
switch self {
|
||||
case .block(_, let sourceMap),
|
||||
.variable(_, let sourceMap),
|
||||
.text(_, let sourceMap),
|
||||
.comment(_, let sourceMap):
|
||||
return sourceMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,9 @@ 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: " ") })
|
||||
init(token: String, parser: TokenParser) throws {
|
||||
let bits = token.split(separator: "|").map({ String($0).trim(character: " ") })
|
||||
if bits.isEmpty {
|
||||
filters = []
|
||||
variable = Variable("")
|
||||
throw TemplateSyntaxError("Variable tags must include at least 1 argument")
|
||||
}
|
||||
|
||||
@@ -52,7 +50,7 @@ public struct Variable : Equatable, Resolvable {
|
||||
|
||||
// Split the lookup string and resolve references if possible
|
||||
fileprivate func lookup(_ context: Context) throws -> [String] {
|
||||
var keyPath = KeyPath(variable, in: context)
|
||||
let keyPath = KeyPath(variable, in: context)
|
||||
return try keyPath.parse()
|
||||
}
|
||||
|
||||
@@ -62,7 +60,7 @@ public struct Variable : Equatable, Resolvable {
|
||||
|
||||
if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) {
|
||||
// String literal
|
||||
return String(variable[variable.characters.index(after: variable.startIndex) ..< variable.characters.index(before: variable.endIndex)])
|
||||
return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)])
|
||||
}
|
||||
|
||||
// Number literal
|
||||
@@ -89,25 +87,17 @@ public struct Variable : Equatable, Resolvable {
|
||||
current = dictionary[bit]
|
||||
}
|
||||
} else if let array = current as? [Any] {
|
||||
if let index = Int(bit) {
|
||||
if index >= 0 && index < array.count {
|
||||
current = array[index]
|
||||
} else {
|
||||
current = nil
|
||||
}
|
||||
} else if bit == "first" {
|
||||
current = array.first
|
||||
} else if bit == "last" {
|
||||
current = array.last
|
||||
} else if bit == "count" {
|
||||
current = array.count
|
||||
}
|
||||
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
|
||||
current = object.value(forKey: bit)
|
||||
#endif
|
||||
#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 {
|
||||
@@ -128,8 +118,22 @@ public struct Variable : Equatable, Resolvable {
|
||||
}
|
||||
}
|
||||
|
||||
public func ==(lhs: Variable, rhs: Variable) -> Bool {
|
||||
return lhs.variable == rhs.variable
|
||||
private func resolveCollection<T: Collection>(_ collection: T, bit: String) -> Any? {
|
||||
if let index = Int(bit) {
|
||||
if index >= 0 && index < collection.count {
|
||||
return collection[collection.index(collection.startIndex, offsetBy: index)]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else if bit == "first" {
|
||||
return collection.first
|
||||
} else if bit == "last" {
|
||||
return collection[collection.index(collection.endIndex, offsetBy: -1)]
|
||||
} else if bit == "count" {
|
||||
return collection.count
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure used to represet range of two integer values expressed as `from...to`.
|
||||
@@ -140,7 +144,8 @@ public struct RangeVariable: Resolvable {
|
||||
public let from: Resolvable
|
||||
public let to: Resolvable
|
||||
|
||||
public init?(_ token: String, environment: Environment) throws {
|
||||
@available(*, deprecated, message: "Use init?(_:parser:containedIn:)")
|
||||
public init?(_ token: String, parser: TokenParser) throws {
|
||||
let components = token.components(separatedBy: "...")
|
||||
guard components.count == 2 else {
|
||||
return nil
|
||||
@@ -150,6 +155,16 @@ public struct RangeVariable: Resolvable {
|
||||
self.to = try environment.compileFilter(components[1])
|
||||
}
|
||||
|
||||
public init?(_ token: String, parser: TokenParser, containedIn containingToken: Token) throws {
|
||||
let components = token.components(separatedBy: "...")
|
||||
guard components.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.from = try parser.compileFilter(components[0], containedIn: containingToken)
|
||||
self.to = try parser.compileFilter(components[1], containedIn: containingToken)
|
||||
}
|
||||
|
||||
public func resolve(_ context: Context) throws -> Any? {
|
||||
let fromResolved = try from.resolve(context)
|
||||
let toResolved = try to.resolve(context)
|
||||
|
||||
Reference in New Issue
Block a user