Warnings--

This commit is contained in:
David Jennes
2022-07-27 04:47:19 +02:00
parent e6ee27f64e
commit f32c772b99
39 changed files with 772 additions and 614 deletions

View File

@@ -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] = [:]

View File

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

View File

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

View File

@@ -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] = []) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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