Rename package to swift-mustache (#27)
* Rename package to swift-mustache * Update CI
This commit is contained in:
57
Sources/Mustache/ContentType.swift
Normal file
57
Sources/Mustache/ContentType.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Protocol for content types
|
||||
public protocol MustacheContentType: Sendable {
|
||||
/// escape text for this content type eg for HTML replace "<" with "<"
|
||||
func escapeText(_ text: String) -> String
|
||||
}
|
||||
|
||||
/// Text content type where no character is escaped
|
||||
struct TextContentType: MustacheContentType {
|
||||
func escapeText(_ text: String) -> String {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/// HTML content where text is escaped for HTML output
|
||||
struct HTMLContentType: MustacheContentType {
|
||||
func escapeText(_ text: String) -> String {
|
||||
return text.htmlEscape()
|
||||
}
|
||||
}
|
||||
|
||||
/// Map of strings to content types.
|
||||
///
|
||||
/// The string is read from the "CONTENT_TYPE" pragma `{{% CONTENT_TYPE: type}}`. Replace type with
|
||||
/// the content type required. The default available types are `TEXT` and `HTML`. You can register your own
|
||||
/// with `MustacheContentTypes.register`.
|
||||
public enum MustacheContentTypes {
|
||||
static func get(_ name: String) -> MustacheContentType? {
|
||||
return self.types[name]
|
||||
}
|
||||
|
||||
/// Register new content type
|
||||
/// - Parameters:
|
||||
/// - contentType: Content type
|
||||
/// - name: String to identify it
|
||||
public static func register(_ contentType: MustacheContentType, named name: String) {
|
||||
self.types[name] = contentType
|
||||
}
|
||||
|
||||
static var types: [String: MustacheContentType] = [
|
||||
"HTML": HTMLContentType(),
|
||||
"TEXT": TextContentType(),
|
||||
]
|
||||
}
|
||||
115
Sources/Mustache/Context.swift
Normal file
115
Sources/Mustache/Context.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Context while rendering mustache tokens
|
||||
struct MustacheContext {
|
||||
let stack: [Any]
|
||||
let sequenceContext: MustacheSequenceContext?
|
||||
let indentation: String?
|
||||
let inherited: [String: MustacheTemplate]?
|
||||
let contentType: MustacheContentType
|
||||
let library: MustacheLibrary?
|
||||
|
||||
/// initialize context with a single objectt
|
||||
init(_ object: Any, library: MustacheLibrary? = nil) {
|
||||
self.stack = [object]
|
||||
self.sequenceContext = nil
|
||||
self.indentation = nil
|
||||
self.inherited = nil
|
||||
self.contentType = HTMLContentType()
|
||||
self.library = library
|
||||
}
|
||||
|
||||
private init(
|
||||
stack: [Any],
|
||||
sequenceContext: MustacheSequenceContext?,
|
||||
indentation: String?,
|
||||
inherited: [String: MustacheTemplate]?,
|
||||
contentType: MustacheContentType,
|
||||
library: MustacheLibrary? = nil
|
||||
) {
|
||||
self.stack = stack
|
||||
self.sequenceContext = sequenceContext
|
||||
self.indentation = indentation
|
||||
self.inherited = inherited
|
||||
self.contentType = contentType
|
||||
self.library = library
|
||||
}
|
||||
|
||||
/// return context with object add to stack
|
||||
func withObject(_ object: Any) -> MustacheContext {
|
||||
var stack = self.stack
|
||||
stack.append(object)
|
||||
return .init(
|
||||
stack: stack,
|
||||
sequenceContext: nil,
|
||||
indentation: self.indentation,
|
||||
inherited: self.inherited,
|
||||
contentType: self.contentType,
|
||||
library: self.library
|
||||
)
|
||||
}
|
||||
|
||||
/// return context with indent and parameter information for invoking a partial
|
||||
func withPartial(indented: String?, inheriting: [String: MustacheTemplate]?) -> MustacheContext {
|
||||
let indentation: String? = if let indented {
|
||||
(self.indentation ?? "") + indented
|
||||
} else {
|
||||
self.indentation
|
||||
}
|
||||
let inherits: [String: MustacheTemplate]? = if let inheriting {
|
||||
if let originalInherits = self.inherited {
|
||||
originalInherits.merging(inheriting) { value, _ in value }
|
||||
} else {
|
||||
inheriting
|
||||
}
|
||||
} else {
|
||||
self.inherited
|
||||
}
|
||||
return .init(
|
||||
stack: self.stack,
|
||||
sequenceContext: nil,
|
||||
indentation: indentation,
|
||||
inherited: inherits,
|
||||
contentType: HTMLContentType(),
|
||||
library: self.library
|
||||
)
|
||||
}
|
||||
|
||||
/// return context with sequence info and sequence element added to stack
|
||||
func withSequence(_ object: Any, sequenceContext: MustacheSequenceContext) -> MustacheContext {
|
||||
var stack = self.stack
|
||||
stack.append(object)
|
||||
return .init(
|
||||
stack: stack,
|
||||
sequenceContext: sequenceContext,
|
||||
indentation: self.indentation,
|
||||
inherited: self.inherited,
|
||||
contentType: self.contentType,
|
||||
library: self.library
|
||||
)
|
||||
}
|
||||
|
||||
/// return context with sequence info and sequence element added to stack
|
||||
func withContentType(_ contentType: MustacheContentType) -> MustacheContext {
|
||||
return .init(
|
||||
stack: self.stack,
|
||||
sequenceContext: self.sequenceContext,
|
||||
indentation: self.indentation,
|
||||
inherited: self.inherited,
|
||||
contentType: contentType,
|
||||
library: self.library
|
||||
)
|
||||
}
|
||||
}
|
||||
38
Sources/Mustache/CustomRenderable.swift
Normal file
38
Sources/Mustache/CustomRenderable.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Allow object to override standard hummingbird type rendering which uses
|
||||
/// `String(describing)`.
|
||||
public protocol MustacheCustomRenderable {
|
||||
/// Custom rendered version of object
|
||||
var renderText: String { get }
|
||||
/// Whether the object is a null object. Used when scoping sections
|
||||
var isNull: Bool { get }
|
||||
}
|
||||
|
||||
extension MustacheCustomRenderable {
|
||||
/// default version returning the standard rendering
|
||||
var renderText: String { String(describing: self) }
|
||||
/// default version returning false
|
||||
var isNull: Bool { false }
|
||||
}
|
||||
|
||||
/// Extend NSNull to conform to `MustacheCustomRenderable` to avoid outputting `<null>` and returning
|
||||
/// a valid response for `isNull`
|
||||
extension NSNull: MustacheCustomRenderable {
|
||||
public var renderText: String { "" }
|
||||
public var isNull: Bool { true }
|
||||
}
|
||||
38
Sources/Mustache/Deprecations.swift
Normal file
38
Sources/Mustache/Deprecations.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2024 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
// Below is a list of deprecated symbols with the "HB" prefix. These are available
|
||||
// temporarily to ease transition from the old symbols that included the "HB"
|
||||
// prefix to the new ones.
|
||||
//
|
||||
// This file will be removed before we do a 2.0 release
|
||||
|
||||
@_documentation(visibility: internal) @available(*, deprecated, renamed: "MustacheContentType")
|
||||
public typealias HBMustacheContentType = MustacheContentType
|
||||
@_documentation(visibility: internal) @available(*, deprecated, renamed: "MustacheContentTypes")
|
||||
public typealias HBMustacheContentTypes = MustacheContentTypes
|
||||
@_documentation(visibility: internal) @available(*, deprecated, renamed: "MustacheCustomRenderable")
|
||||
public typealias HBMustacheCustomRenderable = MustacheCustomRenderable
|
||||
@_documentation(visibility: internal) @available(*, deprecated, renamed: "MustacheLambda")
|
||||
public typealias HBMustacheLambda = MustacheLambda
|
||||
@_documentation(visibility: internal) @available(*, deprecated, renamed: "MustacheLibrary")
|
||||
public typealias HBMustacheLibrary = MustacheLibrary
|
||||
@_documentation(visibility: internal) @available(*, deprecated, renamed: "MustacheParent")
|
||||
public typealias HBMustacheParent = MustacheParent
|
||||
@_documentation(visibility: internal) @available(*, deprecated, renamed: "MustacheParserContext")
|
||||
public typealias HBMustacheParserContext = MustacheParserContext
|
||||
@_documentation(visibility: internal) @available(*, deprecated, renamed: "MustacheTemplate")
|
||||
public typealias HBMustacheTemplate = MustacheTemplate
|
||||
@_documentation(visibility: internal) @available(*, deprecated, renamed: "MustacheTransformable")
|
||||
public typealias HBMustacheTransformable = MustacheTransformable
|
||||
50
Sources/Mustache/Lambda.swift
Normal file
50
Sources/Mustache/Lambda.swift
Normal file
@@ -0,0 +1,50 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Lambda function. Can add this to object being rendered to filter contents of objects.
|
||||
///
|
||||
/// See http://mustache.github.io/mustache.5.html for more details on
|
||||
/// mustache lambdas. Lambdas work slightly differently in HummingbirdMustache though
|
||||
/// as they are passed a template representing the contained text and not the raw text
|
||||
/// e.g
|
||||
/// ```
|
||||
/// struct Object {
|
||||
/// let name: String
|
||||
/// let wrapped: MustacheLambda
|
||||
/// }
|
||||
/// let willy = Object(name: "Willy", wrapped: .init({ object, template in
|
||||
/// return "<b>\(template.render(object))</b>"
|
||||
/// }))
|
||||
/// let mustache = "{{#wrapped}}{{name}} is awesome.{{/wrapped}}"
|
||||
/// let template = try MustacheTemplate(string: mustache)
|
||||
/// let output = template.render(willy)
|
||||
/// print(output) // <b>Willy is awesome</b>
|
||||
/// ```
|
||||
///
|
||||
public struct MustacheLambda {
|
||||
/// lambda callback
|
||||
public typealias Callback = (Any, MustacheTemplate) -> String
|
||||
|
||||
let callback: Callback
|
||||
|
||||
/// Initialize `MustacheLambda`
|
||||
/// - Parameter cb: function to be called by lambda
|
||||
public init(_ cb: @escaping Callback) {
|
||||
self.callback = cb
|
||||
}
|
||||
|
||||
internal func run(_ object: Any, _ template: MustacheTemplate) -> String {
|
||||
return self.callback(object, template)
|
||||
}
|
||||
}
|
||||
44
Sources/Mustache/Library+FileSystem.swift
Normal file
44
Sources/Mustache/Library+FileSystem.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension MustacheLibrary {
|
||||
/// Load templates from a folder
|
||||
static func loadTemplates(from directory: String, withExtension extension: String = "mustache") async throws -> [String: MustacheTemplate] {
|
||||
var directory = directory
|
||||
if !directory.hasSuffix("/") {
|
||||
directory += "/"
|
||||
}
|
||||
let extWithDot = ".\(`extension`)"
|
||||
let fs = FileManager()
|
||||
guard let enumerator = fs.enumerator(atPath: directory) else { return [:] }
|
||||
var templates: [String: MustacheTemplate] = [:]
|
||||
for case let path as String in enumerator {
|
||||
guard path.hasSuffix(extWithDot) else { continue }
|
||||
guard let data = fs.contents(atPath: directory + path) else { continue }
|
||||
let string = String(decoding: data, as: Unicode.UTF8.self)
|
||||
var template: MustacheTemplate
|
||||
do {
|
||||
template = try MustacheTemplate(string: string)
|
||||
} catch let error as MustacheTemplate.ParserError {
|
||||
throw ParserError(filename: path, context: error.context, error: error.error)
|
||||
}
|
||||
// drop ".mustache" from path to get name
|
||||
let name = String(path.dropLast(extWithDot.count))
|
||||
templates[name] = template
|
||||
}
|
||||
return templates
|
||||
}
|
||||
}
|
||||
92
Sources/Mustache/Library.swift
Normal file
92
Sources/Mustache/Library.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Class holding a collection of mustache templates.
|
||||
///
|
||||
/// Each template can reference the others via a partial using the name the template is registered under
|
||||
/// ```
|
||||
/// {{#sequence}}{{>entry}}{{/sequence}}
|
||||
/// ```
|
||||
public struct MustacheLibrary: Sendable {
|
||||
/// Initialize empty library
|
||||
public init() {
|
||||
self.templates = [:]
|
||||
}
|
||||
|
||||
/// Initialize library with contents of folder.
|
||||
///
|
||||
/// Each template is registered with the name of the file minus its extension. The search through
|
||||
/// the folder is recursive and templates in subfolders will be registered with the name `subfolder/template`.
|
||||
/// - Parameter directory: Directory to look for mustache templates
|
||||
/// - Parameter extension: Extension of files to look for
|
||||
public init(templates: [String: MustacheTemplate]) {
|
||||
self.templates = templates
|
||||
}
|
||||
|
||||
/// Initialize library with contents of folder.
|
||||
///
|
||||
/// Each template is registered with the name of the file minus its extension. The search through
|
||||
/// the folder is recursive and templates in subfolders will be registered with the name `subfolder/template`.
|
||||
/// - Parameter directory: Directory to look for mustache templates
|
||||
/// - Parameter extension: Extension of files to look for
|
||||
public init(directory: String, withExtension extension: String = "mustache") async throws {
|
||||
self.templates = try await Self.loadTemplates(from: directory, withExtension: `extension`)
|
||||
}
|
||||
|
||||
/// Register template under name
|
||||
/// - Parameters:
|
||||
/// - template: Template
|
||||
/// - name: Name of template
|
||||
public mutating func register(_ template: MustacheTemplate, named name: String) {
|
||||
self.templates[name] = template
|
||||
}
|
||||
|
||||
/// Register template under name
|
||||
/// - Parameters:
|
||||
/// - mustache: Mustache text
|
||||
/// - name: Name of template
|
||||
public mutating func register(_ mustache: String, named name: String) throws {
|
||||
let template = try MustacheTemplate(string: mustache)
|
||||
self.templates[name] = template
|
||||
}
|
||||
|
||||
/// Return template registed with name
|
||||
/// - Parameter name: name to search for
|
||||
/// - Returns: Template
|
||||
public func getTemplate(named name: String) -> MustacheTemplate? {
|
||||
self.templates[name]
|
||||
}
|
||||
|
||||
/// Render object using templated with name
|
||||
/// - Parameters:
|
||||
/// - object: Object to render
|
||||
/// - name: Name of template
|
||||
/// - Returns: Rendered text
|
||||
public func render(_ object: Any, withTemplate name: String) -> String? {
|
||||
guard let template = templates[name] else { return nil }
|
||||
return template.render(object, library: self)
|
||||
}
|
||||
|
||||
/// Error returned by init() when parser fails
|
||||
public struct ParserError: Swift.Error {
|
||||
/// File error occurred in
|
||||
public let filename: String
|
||||
/// Context (line, linenumber and column number)
|
||||
public let context: MustacheParserContext
|
||||
/// Actual error that occurred
|
||||
public let error: Error
|
||||
}
|
||||
|
||||
private var templates: [String: MustacheTemplate]
|
||||
}
|
||||
31
Sources/Mustache/Mirror.swift
Normal file
31
Sources/Mustache/Mirror.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension Mirror {
|
||||
/// Return value from Mirror given name
|
||||
func getValue(forKey key: String) -> Any? {
|
||||
guard let matched = children.filter({ $0.label == key }).first else {
|
||||
return nil
|
||||
}
|
||||
return unwrapOptional(matched.value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return object and if it is an Optional return object Optional holds
|
||||
private func unwrapOptional(_ object: Any) -> Any? {
|
||||
let mirror = Mirror(reflecting: object)
|
||||
guard mirror.displayStyle == .optional else { return object }
|
||||
guard let first = mirror.children.first else { return nil }
|
||||
return first.value
|
||||
}
|
||||
25
Sources/Mustache/Parent.swift
Normal file
25
Sources/Mustache/Parent.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Protocol for object that has a custom method for accessing their children, instead
|
||||
/// of using Mirror
|
||||
public protocol MustacheParent {
|
||||
func child(named: String) -> Any?
|
||||
}
|
||||
|
||||
/// Extend dictionary where the key is a string so that it uses the key values to access
|
||||
/// it values
|
||||
extension Dictionary: MustacheParent where Key == String {
|
||||
public func child(named: String) -> Any? { return self[named] }
|
||||
}
|
||||
394
Sources/Mustache/Parser.swift
Normal file
394
Sources/Mustache/Parser.swift
Normal file
@@ -0,0 +1,394 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Reader object for parsing String buffers
|
||||
struct Parser {
|
||||
enum Error: Swift.Error {
|
||||
case overflow
|
||||
}
|
||||
|
||||
/// internal storage used to store String
|
||||
private class Storage {
|
||||
init(_ buffer: String) {
|
||||
self.buffer = buffer
|
||||
}
|
||||
|
||||
let buffer: String
|
||||
}
|
||||
|
||||
private let _storage: Storage
|
||||
|
||||
/// Create a Reader object
|
||||
/// - Parameter string: String to parse
|
||||
init(_ string: String) {
|
||||
self._storage = Storage(string)
|
||||
self.position = string.startIndex
|
||||
}
|
||||
|
||||
var buffer: String { return self._storage.buffer }
|
||||
private(set) var position: String.Index
|
||||
}
|
||||
|
||||
extension Parser {
|
||||
/// Return current character
|
||||
/// - Throws: .overflow
|
||||
/// - Returns: Current character
|
||||
mutating func character() throws -> Character {
|
||||
guard !self.reachedEnd() else { throw Parser.Error.overflow }
|
||||
let c = unsafeCurrent()
|
||||
unsafeAdvance()
|
||||
return c
|
||||
}
|
||||
|
||||
/// Read the current character and return if it is as intended. If character test returns true then move forward 1
|
||||
/// - Parameter char: character to compare against
|
||||
/// - Throws: .overflow
|
||||
/// - Returns: If current character was the one we expected
|
||||
mutating func read(_ char: Character) throws -> Bool {
|
||||
let c = try character()
|
||||
guard c == char else { unsafeRetreat(); return false }
|
||||
return true
|
||||
}
|
||||
|
||||
/// Read the current character and return if it is as intended. If character test returns true then move forward 1
|
||||
/// - Parameter char: character to compare against
|
||||
/// - Throws: .overflow
|
||||
/// - Returns: If current character was the one we expected
|
||||
mutating func read(string: String) throws -> Bool {
|
||||
let initialPosition = self.position
|
||||
guard string.count > 0 else { return true }
|
||||
let subString = try read(count: string.count)
|
||||
guard subString == string else {
|
||||
self.position = initialPosition
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/// Read the current character and check if it is in a set of characters If character test returns true then move forward 1
|
||||
/// - Parameter characterSet: Set of characters to compare against
|
||||
/// - Throws: .overflow
|
||||
/// - Returns: If current character is in character set
|
||||
mutating func read(_ characterSet: Set<Character>) throws -> Bool {
|
||||
let c = try character()
|
||||
guard characterSet.contains(c) else { unsafeRetreat(); return false }
|
||||
return true
|
||||
}
|
||||
|
||||
/// Read next so many characters from buffer
|
||||
/// - Parameter count: Number of characters to read
|
||||
/// - Throws: .overflow
|
||||
/// - Returns: The string read from the buffer
|
||||
mutating func read(count: Int) throws -> Substring {
|
||||
guard self.buffer.distance(from: self.position, to: self.buffer.endIndex) >= count else { throw Parser.Error.overflow }
|
||||
let end = self.buffer.index(self.position, offsetBy: count)
|
||||
let subString = self.buffer[self.position..<end]
|
||||
unsafeAdvance(by: count)
|
||||
return subString
|
||||
}
|
||||
|
||||
/// Read from buffer until we hit a character. Position after this is of the character we were checking for
|
||||
/// - Parameter until: Character to read until
|
||||
/// - Throws: .overflow if we hit the end of the buffer before reading character
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(until: Character, throwOnOverflow: Bool = true) throws -> Substring {
|
||||
let startIndex = self.position
|
||||
while !self.reachedEnd() {
|
||||
if unsafeCurrent() == until {
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
unsafeAdvance()
|
||||
}
|
||||
if throwOnOverflow {
|
||||
unsafeSetPosition(startIndex)
|
||||
throw Parser.Error.overflow
|
||||
}
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read from buffer until we hit a string. By default the position after this is of the beginning of the string we were checking for
|
||||
/// - Parameter untilString: String to check for
|
||||
/// - Parameter throwOnOverflow: Throw errors if we hit the end of the buffer
|
||||
/// - Parameter skipToEnd: Should we set the position to after the found string
|
||||
/// - Throws: .overflow, .emptyString
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(untilString: String, throwOnOverflow: Bool = true, skipToEnd: Bool = false) throws -> Substring {
|
||||
guard untilString.count > 0 else { return "" }
|
||||
let startIndex = self.position
|
||||
var foundIndex = self.position
|
||||
var untilIndex = untilString.startIndex
|
||||
while !self.reachedEnd() {
|
||||
if unsafeCurrent() == untilString[untilIndex] {
|
||||
if untilIndex == untilString.startIndex {
|
||||
foundIndex = self.position
|
||||
}
|
||||
untilIndex = untilString.index(after: untilIndex)
|
||||
if untilIndex == untilString.endIndex {
|
||||
unsafeAdvance()
|
||||
if skipToEnd == false {
|
||||
self.position = foundIndex
|
||||
}
|
||||
let result = self.buffer[startIndex..<foundIndex]
|
||||
return result
|
||||
}
|
||||
} else {
|
||||
untilIndex = untilString.startIndex
|
||||
}
|
||||
unsafeAdvance()
|
||||
}
|
||||
if throwOnOverflow {
|
||||
self.position = startIndex
|
||||
throw Error.overflow
|
||||
}
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read from buffer until we hit a character in supplied set. Position after this is of the character we were checking for
|
||||
/// - Parameter characterSet: Character set to check against
|
||||
/// - Throws: .overflow
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(until characterSet: Set<Character>, throwOnOverflow: Bool = true) throws -> Substring {
|
||||
let startIndex = self.position
|
||||
while !self.reachedEnd() {
|
||||
if characterSet.contains(unsafeCurrent()) {
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
unsafeAdvance()
|
||||
}
|
||||
if throwOnOverflow {
|
||||
unsafeSetPosition(startIndex)
|
||||
throw Parser.Error.overflow
|
||||
}
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read from buffer until keyPath on character returns true. Position after this is of the character we were checking for
|
||||
/// - Parameter keyPath: keyPath to check
|
||||
/// - Throws: .overflow
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(until keyPath: KeyPath<Character, Bool>, throwOnOverflow: Bool = true) throws -> Substring {
|
||||
let startIndex = self.position
|
||||
while !self.reachedEnd() {
|
||||
if current()[keyPath: keyPath] {
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
unsafeAdvance()
|
||||
}
|
||||
if throwOnOverflow {
|
||||
self.position = startIndex
|
||||
throw Error.overflow
|
||||
}
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read from buffer until keyPath on character returns true. Position after this is of the character we were checking for
|
||||
/// - Parameter keyPath: keyPath to check
|
||||
/// - Throws: .overflow
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(until cb: (Character) -> Bool, throwOnOverflow: Bool = true) throws -> Substring {
|
||||
let startIndex = self.position
|
||||
while !self.reachedEnd() {
|
||||
if cb(current()) {
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
unsafeAdvance()
|
||||
}
|
||||
if throwOnOverflow {
|
||||
self.position = startIndex
|
||||
throw Error.overflow
|
||||
}
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read from buffer from current position until the end of the buffer
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func readUntilTheEnd() -> Substring {
|
||||
let startIndex = self.position
|
||||
self.position = self.buffer.endIndex
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read while character at current position is the one supplied
|
||||
/// - Parameter while: Character to check against
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(while: Character) -> Int {
|
||||
var count = 0
|
||||
while !self.reachedEnd(),
|
||||
unsafeCurrent() == `while`
|
||||
{
|
||||
unsafeAdvance()
|
||||
count += 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/// Read while keyPath on character at current position returns true is the one supplied
|
||||
/// - Parameter while: keyPath to check
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(while keyPath: KeyPath<Character, Bool>) -> Substring {
|
||||
let startIndex = self.position
|
||||
while !self.reachedEnd(),
|
||||
unsafeCurrent()[keyPath: keyPath]
|
||||
{
|
||||
unsafeAdvance()
|
||||
}
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read while closure returns true
|
||||
/// - Parameter while: closure
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(while cb: (Character) -> Bool) -> Substring {
|
||||
let startIndex = self.position
|
||||
while !self.reachedEnd(),
|
||||
cb(unsafeCurrent())
|
||||
{
|
||||
unsafeAdvance()
|
||||
}
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Read while character at current position is in supplied set
|
||||
/// - Parameter while: character set to check
|
||||
/// - Returns: String read from buffer
|
||||
@discardableResult mutating func read(while characterSet: Set<Character>) -> Substring {
|
||||
let startIndex = self.position
|
||||
while !self.reachedEnd(),
|
||||
characterSet.contains(unsafeCurrent())
|
||||
{
|
||||
unsafeAdvance()
|
||||
}
|
||||
return self.buffer[startIndex..<self.position]
|
||||
}
|
||||
|
||||
/// Return whether we have reached the end of the buffer
|
||||
/// - Returns: Have we reached the end
|
||||
func reachedEnd() -> Bool {
|
||||
return self.position == self.buffer.endIndex
|
||||
}
|
||||
|
||||
/// Return whether we are at the start of the buffer
|
||||
/// - Returns: Are we are the start
|
||||
func atStart() -> Bool {
|
||||
return self.position == self.buffer.startIndex
|
||||
}
|
||||
}
|
||||
|
||||
/// context used in parser error
|
||||
public struct MustacheParserContext {
|
||||
public let line: String
|
||||
public let lineNumber: Int
|
||||
public let columnNumber: Int
|
||||
}
|
||||
|
||||
extension Parser {
|
||||
/// Return context of current position (line, lineNumber, columnNumber)
|
||||
func getContext() -> MustacheParserContext {
|
||||
var parser = self
|
||||
var columnNumber = 0
|
||||
while !parser.atStart() {
|
||||
try? parser.retreat()
|
||||
if parser.current() == "\n" {
|
||||
break
|
||||
}
|
||||
columnNumber += 1
|
||||
}
|
||||
if parser.current() == "\n" {
|
||||
try? parser.advance()
|
||||
}
|
||||
// read line from parser
|
||||
let line = try! parser.read(until: Character("\n"), throwOnOverflow: false)
|
||||
// count new lines up to this current position
|
||||
let buffer = parser.buffer
|
||||
let textBefore = buffer[buffer.startIndex..<self.position]
|
||||
let lineNumber = textBefore.filter(\.isNewline).count
|
||||
|
||||
return MustacheParserContext(line: String(line), lineNumber: lineNumber + 1, columnNumber: columnNumber + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// versions of internal functions which include tests for overflow
|
||||
extension Parser {
|
||||
/// Return the character at the current position
|
||||
/// - Throws: .overflow
|
||||
/// - Returns: Character
|
||||
func current() -> Character {
|
||||
guard !self.reachedEnd() else { return "\0" }
|
||||
return unsafeCurrent()
|
||||
}
|
||||
|
||||
/// Move forward one character
|
||||
/// - Throws: .overflow
|
||||
mutating func advance() throws {
|
||||
guard !self.reachedEnd() else { throw Parser.Error.overflow }
|
||||
return unsafeAdvance()
|
||||
}
|
||||
|
||||
/// Move back one character
|
||||
/// - Throws: .overflow
|
||||
mutating func retreat() throws {
|
||||
guard self.position != self.buffer.startIndex else { throw Parser.Error.overflow }
|
||||
return unsafeRetreat()
|
||||
}
|
||||
|
||||
/// Move forward so many character
|
||||
/// - Parameter amount: number of characters to move forward
|
||||
/// - Throws: .overflow
|
||||
mutating func advance(by amount: Int) throws {
|
||||
guard self.buffer.distance(from: self.position, to: self.buffer.endIndex) >= amount else { throw Parser.Error.overflow }
|
||||
return unsafeAdvance(by: amount)
|
||||
}
|
||||
|
||||
/// Move back so many characters
|
||||
/// - Parameter amount: number of characters to move back
|
||||
/// - Throws: .overflow
|
||||
mutating func retreat(by amount: Int) throws {
|
||||
guard self.buffer.distance(from: self.buffer.startIndex, to: self.position) >= amount else { throw Parser.Error.overflow }
|
||||
return unsafeRetreat(by: amount)
|
||||
}
|
||||
|
||||
mutating func setPosition(_ position: String.Index) throws {
|
||||
guard position <= self.buffer.endIndex else { throw Parser.Error.overflow }
|
||||
unsafeSetPosition(position)
|
||||
}
|
||||
}
|
||||
|
||||
// unsafe versions without checks
|
||||
extension Parser {
|
||||
func unsafeCurrent() -> Character {
|
||||
return self.buffer[self.position]
|
||||
}
|
||||
|
||||
mutating func unsafeAdvance() {
|
||||
self.position = self.buffer.index(after: self.position)
|
||||
}
|
||||
|
||||
mutating func unsafeRetreat() {
|
||||
self.position = self.buffer.index(before: self.position)
|
||||
}
|
||||
|
||||
mutating func unsafeAdvance(by amount: Int) {
|
||||
self.position = self.buffer.index(self.position, offsetBy: amount)
|
||||
}
|
||||
|
||||
mutating func unsafeRetreat(by amount: Int) {
|
||||
self.position = self.buffer.index(self.position, offsetBy: -amount)
|
||||
}
|
||||
|
||||
mutating func unsafeSetPosition(_ position: String.Index) {
|
||||
self.position = position
|
||||
}
|
||||
}
|
||||
57
Sources/Mustache/Sequence.swift
Normal file
57
Sources/Mustache/Sequence.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Protocol for objects that can be rendered as a sequence in Mustache
|
||||
protocol MustacheSequence {
|
||||
/// Render section using template
|
||||
func renderSection(with template: MustacheTemplate, context: MustacheContext) -> String
|
||||
/// Render inverted section using template
|
||||
func renderInvertedSection(with template: MustacheTemplate, context: MustacheContext) -> String
|
||||
}
|
||||
|
||||
extension Sequence {
|
||||
/// Render section using template
|
||||
func renderSection(with template: MustacheTemplate, context: MustacheContext) -> String {
|
||||
var string = ""
|
||||
var sequenceContext = MustacheSequenceContext(first: true)
|
||||
|
||||
var iterator = makeIterator()
|
||||
guard var currentObject = iterator.next() else { return "" }
|
||||
|
||||
while let object = iterator.next() {
|
||||
string += template.render(context: context.withSequence(currentObject, sequenceContext: sequenceContext))
|
||||
currentObject = object
|
||||
sequenceContext.first = false
|
||||
sequenceContext.index += 1
|
||||
}
|
||||
|
||||
sequenceContext.last = true
|
||||
string += template.render(context: context.withSequence(currentObject, sequenceContext: sequenceContext))
|
||||
|
||||
return string
|
||||
}
|
||||
|
||||
/// Render inverted section using template
|
||||
func renderInvertedSection(with template: MustacheTemplate, context: MustacheContext) -> String {
|
||||
var iterator = makeIterator()
|
||||
if iterator.next() == nil {
|
||||
return template.render(context: context.withObject(self))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: MustacheSequence {}
|
||||
extension Set: MustacheSequence {}
|
||||
extension ReversedCollection: MustacheSequence {}
|
||||
54
Sources/Mustache/SequenceContext.swift
Normal file
54
Sources/Mustache/SequenceContext.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Context that current object inside a sequence is being rendered in. Only relevant when rendering a sequence
|
||||
struct MustacheSequenceContext: MustacheTransformable {
|
||||
var first: Bool
|
||||
var last: Bool
|
||||
var index: Int
|
||||
|
||||
init(first: Bool = false, last: Bool = false) {
|
||||
self.first = first
|
||||
self.last = last
|
||||
self.index = 0
|
||||
}
|
||||
|
||||
/// Transform `MustacheContext`. These are available when processing elements
|
||||
/// of a sequence.
|
||||
///
|
||||
/// Format your mustache as follows to accept them. They look like a function without any arguments
|
||||
/// ```
|
||||
/// {{#sequence}}{{index()}}{{/sequence}}
|
||||
/// ```
|
||||
///
|
||||
/// Transforms available are `first`, `last`, `index`, `even` and `odd`
|
||||
/// - Parameter name: transform name
|
||||
/// - Returns: Result
|
||||
func transform(_ name: String) -> Any? {
|
||||
switch name {
|
||||
case "first":
|
||||
return self.first
|
||||
case "last":
|
||||
return self.last
|
||||
case "index":
|
||||
return self.index
|
||||
case "even":
|
||||
return (self.index & 1) == 0
|
||||
case "odd":
|
||||
return (self.index & 1) == 1
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Sources/Mustache/String.swift
Normal file
38
Sources/Mustache/String.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension String {
|
||||
private static let htmlEscapedCharacters: [Character: String] = [
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&": "&",
|
||||
"\"": """,
|
||||
"'": "'",
|
||||
]
|
||||
/// HTML escape string. Replace '<', '>' and '&' with HTML escaped versions
|
||||
func htmlEscape() -> String {
|
||||
var newString = ""
|
||||
newString.reserveCapacity(count)
|
||||
// currently doing this by going through each character could speed
|
||||
// this us by treating as an array of UInt8's
|
||||
for c in self {
|
||||
if let replacement = Self.htmlEscapedCharacters[c] {
|
||||
newString += replacement
|
||||
} else {
|
||||
newString.append(c)
|
||||
}
|
||||
}
|
||||
return newString
|
||||
}
|
||||
}
|
||||
403
Sources/Mustache/Template+Parser.swift
Normal file
403
Sources/Mustache/Template+Parser.swift
Normal file
@@ -0,0 +1,403 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
extension MustacheTemplate {
|
||||
/// Error return by `MustacheTemplate.parse`. Includes information about where error occurred
|
||||
public struct ParserError: Swift.Error {
|
||||
public let context: MustacheParserContext
|
||||
public let error: Swift.Error
|
||||
}
|
||||
|
||||
/// Error generated by `MustacheTemplate.parse`
|
||||
public enum Error: Swift.Error {
|
||||
/// the end section does not match the name of the start section
|
||||
case sectionCloseNameIncorrect
|
||||
/// tag was badly formatted
|
||||
case unfinishedName
|
||||
/// was not expecting a section end
|
||||
case expectedSectionEnd
|
||||
/// set delimiter tag badly formatted
|
||||
case invalidSetDelimiter
|
||||
/// cannot apply transform to inherited section
|
||||
case transformAppliedToInheritanceSection
|
||||
/// illegal token inside inherit section of partial
|
||||
case illegalTokenInsideInheritSection
|
||||
/// text found inside inherit section of partial
|
||||
case textInsideInheritSection
|
||||
/// config variable syntax is wrong
|
||||
case invalidConfigVariableSyntax
|
||||
/// unrecognised config variable
|
||||
case unrecognisedConfigVariable
|
||||
}
|
||||
|
||||
struct ParserState {
|
||||
var sectionName: String?
|
||||
var sectionTransform: String?
|
||||
var newLine: Bool
|
||||
var startDelimiter: String
|
||||
var endDelimiter: String
|
||||
|
||||
init() {
|
||||
self.sectionName = nil
|
||||
self.newLine = true
|
||||
self.startDelimiter = "{{"
|
||||
self.endDelimiter = "}}"
|
||||
}
|
||||
|
||||
func withSectionName(_ name: String, transform: String? = nil) -> ParserState {
|
||||
var newValue = self
|
||||
newValue.sectionName = name
|
||||
newValue.sectionTransform = transform
|
||||
return newValue
|
||||
}
|
||||
|
||||
func withDelimiters(start: String, end: String) -> ParserState {
|
||||
var newValue = self
|
||||
newValue.startDelimiter = start
|
||||
newValue.endDelimiter = end
|
||||
return newValue
|
||||
}
|
||||
}
|
||||
|
||||
/// parse mustache text to generate a list of tokens
|
||||
static func parse(_ string: String) throws -> [Token] {
|
||||
var parser = Parser(string)
|
||||
do {
|
||||
return try self.parse(&parser, state: .init())
|
||||
} catch {
|
||||
throw ParserError(context: parser.getContext(), error: error)
|
||||
}
|
||||
}
|
||||
|
||||
/// parse section in mustache text
|
||||
static func parse(_ parser: inout Parser, state: ParserState) throws -> [Token] {
|
||||
var tokens: [Token] = []
|
||||
var state = state
|
||||
var whiteSpaceBefore: Substring = ""
|
||||
while !parser.reachedEnd() {
|
||||
// if new line read whitespace
|
||||
if state.newLine {
|
||||
whiteSpaceBefore = parser.read(while: Set(" \t"))
|
||||
}
|
||||
let text = try readUntilDelimiterOrNewline(&parser, state: state)
|
||||
// if we hit a newline add text
|
||||
if parser.current().isNewline {
|
||||
tokens.append(.text(whiteSpaceBefore + text + String(parser.current())))
|
||||
state.newLine = true
|
||||
parser.unsafeAdvance()
|
||||
continue
|
||||
}
|
||||
// we have found a tag
|
||||
// whatever text we found before the tag should be added as a token
|
||||
if text.count > 0 {
|
||||
tokens.append(.text(whiteSpaceBefore + text))
|
||||
whiteSpaceBefore = ""
|
||||
state.newLine = false
|
||||
}
|
||||
// have we reached the end of the text
|
||||
if parser.reachedEnd() {
|
||||
break
|
||||
}
|
||||
var setNewLine = false
|
||||
switch parser.current() {
|
||||
case "#":
|
||||
// section
|
||||
parser.unsafeAdvance()
|
||||
let (name, transform) = try parseName(&parser, state: state)
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
} else if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transform: transform))
|
||||
tokens.append(.section(name: name, transform: transform, template: MustacheTemplate(sectionTokens)))
|
||||
|
||||
case "^":
|
||||
// inverted section
|
||||
parser.unsafeAdvance()
|
||||
let (name, transform) = try parseName(&parser, state: state)
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
} else if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transform: transform))
|
||||
tokens.append(.invertedSection(name: name, transform: transform, template: MustacheTemplate(sectionTokens)))
|
||||
|
||||
case "$":
|
||||
// inherited section
|
||||
parser.unsafeAdvance()
|
||||
let (name, transform) = try parseName(&parser, state: state)
|
||||
// ERROR: can't have transform applied to inherited sections
|
||||
guard transform == nil else { throw Error.transformAppliedToInheritanceSection }
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
} else if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transform: transform))
|
||||
tokens.append(.inheritedSection(name: name, template: MustacheTemplate(sectionTokens)))
|
||||
|
||||
case "/":
|
||||
// end of section
|
||||
parser.unsafeAdvance()
|
||||
let position = parser.position
|
||||
let (name, transform) = try parseName(&parser, state: state)
|
||||
guard name == state.sectionName, transform == state.sectionTransform else {
|
||||
parser.unsafeSetPosition(position)
|
||||
throw Error.sectionCloseNameIncorrect
|
||||
}
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
} else if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
return tokens
|
||||
|
||||
case "!":
|
||||
// comment
|
||||
parser.unsafeAdvance()
|
||||
_ = try self.parseComment(&parser, state: state)
|
||||
setNewLine = self.isStandalone(&parser, state: state)
|
||||
|
||||
case "{":
|
||||
// unescaped variable
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
parser.unsafeAdvance()
|
||||
let (name, transform) = try parseName(&parser, state: state)
|
||||
guard try parser.read("}") else { throw Error.unfinishedName }
|
||||
tokens.append(.unescapedVariable(name: name, transform: transform))
|
||||
|
||||
case "&":
|
||||
// unescaped variable
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
parser.unsafeAdvance()
|
||||
let (name, transform) = try parseName(&parser, state: state)
|
||||
tokens.append(.unescapedVariable(name: name, transform: transform))
|
||||
|
||||
case ">":
|
||||
// partial
|
||||
parser.unsafeAdvance()
|
||||
let name = try parsePartialName(&parser, state: state)
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
}
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: nil))
|
||||
} else {
|
||||
tokens.append(.partial(name, indentation: nil, inherits: nil))
|
||||
}
|
||||
whiteSpaceBefore = ""
|
||||
|
||||
case "<":
|
||||
// partial with inheritance
|
||||
parser.unsafeAdvance()
|
||||
let name = try parsePartialName(&parser, state: state)
|
||||
var indent: String?
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
} else if whiteSpaceBefore.count > 0 {
|
||||
indent = String(whiteSpaceBefore)
|
||||
tokens.append(.text(indent!))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name))
|
||||
var inherit: [String: MustacheTemplate] = [:]
|
||||
// parse tokens in section to extract inherited sections
|
||||
for token in sectionTokens {
|
||||
switch token {
|
||||
case .inheritedSection(let name, let template):
|
||||
inherit[name] = template
|
||||
case .text:
|
||||
break
|
||||
default:
|
||||
throw Error.illegalTokenInsideInheritSection
|
||||
}
|
||||
}
|
||||
tokens.append(.partial(name, indentation: indent, inherits: inherit))
|
||||
|
||||
case "=":
|
||||
// set delimiter
|
||||
parser.unsafeAdvance()
|
||||
state = try self.parserSetDelimiter(&parser, state: state)
|
||||
setNewLine = self.isStandalone(&parser, state: state)
|
||||
|
||||
case "%":
|
||||
// read config variable
|
||||
parser.unsafeAdvance()
|
||||
if let token = try self.readConfigVariable(&parser, state: state) {
|
||||
tokens.append(token)
|
||||
}
|
||||
setNewLine = self.isStandalone(&parser, state: state)
|
||||
|
||||
default:
|
||||
// variable
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let (name, transform) = try parseName(&parser, state: state)
|
||||
tokens.append(.variable(name: name, transform: transform))
|
||||
}
|
||||
state.newLine = setNewLine
|
||||
}
|
||||
// should never get here if reading section
|
||||
guard state.sectionName == nil else {
|
||||
throw Error.expectedSectionEnd
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
/// read until we hit either the start delimiter of a tag or a newline
|
||||
static func readUntilDelimiterOrNewline(_ parser: inout Parser, state: ParserState) throws -> String {
|
||||
var untilSet: Set<Character> = ["\n", "\r\n"]
|
||||
guard let delimiterFirstChar = state.startDelimiter.first else { return "" }
|
||||
var totalText = ""
|
||||
untilSet.insert(delimiterFirstChar)
|
||||
|
||||
while !parser.reachedEnd() {
|
||||
// read until we hit either a newline or "{"
|
||||
let text = try parser.read(until: untilSet, throwOnOverflow: false)
|
||||
totalText += text
|
||||
// if new line append all text read plus newline
|
||||
if parser.current().isNewline {
|
||||
break
|
||||
} else if parser.current() == delimiterFirstChar {
|
||||
if try parser.read(string: state.startDelimiter) {
|
||||
break
|
||||
}
|
||||
totalText += String(delimiterFirstChar)
|
||||
parser.unsafeAdvance()
|
||||
}
|
||||
}
|
||||
return totalText
|
||||
}
|
||||
|
||||
/// parse variable name
|
||||
static func parseName(_ parser: inout Parser, state: ParserState) throws -> (String, String?) {
|
||||
parser.read(while: \.isWhitespace)
|
||||
let text = String(parser.read(while: self.sectionNameChars))
|
||||
parser.read(while: \.isWhitespace)
|
||||
guard try parser.read(string: state.endDelimiter) else { throw Error.unfinishedName }
|
||||
|
||||
// does the name include brackets. If so this is a transform call
|
||||
var nameParser = Parser(String(text))
|
||||
let string = nameParser.read(while: self.sectionNameCharsWithoutBrackets)
|
||||
if nameParser.reachedEnd() {
|
||||
return (text, nil)
|
||||
} else {
|
||||
// parse function parameter, as we have just parsed a function name
|
||||
guard nameParser.current() == "(" else { throw Error.unfinishedName }
|
||||
nameParser.unsafeAdvance()
|
||||
let string2 = nameParser.read(while: self.sectionNameCharsWithoutBrackets)
|
||||
guard nameParser.current() == ")" else { throw Error.unfinishedName }
|
||||
nameParser.unsafeAdvance()
|
||||
guard nameParser.reachedEnd() else { throw Error.unfinishedName }
|
||||
return (String(string2), String(string))
|
||||
}
|
||||
}
|
||||
|
||||
/// parse partial name
|
||||
static func parsePartialName(_ parser: inout Parser, state: ParserState) throws -> String {
|
||||
parser.read(while: \.isWhitespace)
|
||||
let text = String(parser.read(while: self.sectionNameChars))
|
||||
parser.read(while: \.isWhitespace)
|
||||
guard try parser.read(string: state.endDelimiter) else { throw Error.unfinishedName }
|
||||
return text
|
||||
}
|
||||
|
||||
static func parseComment(_ parser: inout Parser, state: ParserState) throws -> String {
|
||||
let text = try parser.read(untilString: state.endDelimiter, throwOnOverflow: true, skipToEnd: true)
|
||||
return String(text)
|
||||
}
|
||||
|
||||
static func parserSetDelimiter(_ parser: inout Parser, state: ParserState) throws -> ParserState {
|
||||
let startDelimiter: Substring
|
||||
let endDelimiter: Substring
|
||||
|
||||
do {
|
||||
parser.read(while: \.isWhitespace)
|
||||
startDelimiter = try parser.read(until: \.isWhitespace)
|
||||
parser.read(while: \.isWhitespace)
|
||||
endDelimiter = try parser.read(until: { $0 == "=" || $0.isWhitespace })
|
||||
parser.read(while: \.isWhitespace)
|
||||
} catch {
|
||||
throw Error.invalidSetDelimiter
|
||||
}
|
||||
guard try parser.read("=") else { throw Error.invalidSetDelimiter }
|
||||
guard try parser.read(string: state.endDelimiter) else { throw Error.invalidSetDelimiter }
|
||||
guard startDelimiter.count > 0, endDelimiter.count > 0 else { throw Error.invalidSetDelimiter }
|
||||
return state.withDelimiters(start: String(startDelimiter), end: String(endDelimiter))
|
||||
}
|
||||
|
||||
static func readConfigVariable(_ parser: inout Parser, state: ParserState) throws -> Token? {
|
||||
let variable: Substring
|
||||
let value: Substring
|
||||
|
||||
do {
|
||||
parser.read(while: \.isWhitespace)
|
||||
variable = parser.read(while: self.sectionNameCharsWithoutBrackets)
|
||||
parser.read(while: \.isWhitespace)
|
||||
guard try parser.read(":") else { throw Error.invalidConfigVariableSyntax }
|
||||
parser.read(while: \.isWhitespace)
|
||||
value = parser.read(while: self.sectionNameCharsWithoutBrackets)
|
||||
parser.read(while: \.isWhitespace)
|
||||
guard try parser.read(string: state.endDelimiter) else { throw Error.invalidConfigVariableSyntax }
|
||||
} catch {
|
||||
throw Error.invalidConfigVariableSyntax
|
||||
}
|
||||
|
||||
// do both variable and value have content
|
||||
guard variable.count > 0, value.count > 0 else { throw Error.invalidConfigVariableSyntax }
|
||||
|
||||
switch variable {
|
||||
case "CONTENT_TYPE":
|
||||
guard let contentType = MustacheContentTypes.get(String(value)) else { throw Error.unrecognisedConfigVariable }
|
||||
return .contentType(contentType)
|
||||
default:
|
||||
throw Error.unrecognisedConfigVariable
|
||||
}
|
||||
}
|
||||
|
||||
static func hasLineFinished(_ parser: inout Parser) -> Bool {
|
||||
var parser2 = parser
|
||||
if parser.reachedEnd() { return true }
|
||||
parser2.read(while: Set(" \t"))
|
||||
if parser2.current().isNewline {
|
||||
parser2.unsafeAdvance()
|
||||
try! parser.setPosition(parser2.position)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
static func isStandalone(_ parser: inout Parser, state: ParserState) -> Bool {
|
||||
return state.newLine && self.hasLineFinished(&parser)
|
||||
}
|
||||
|
||||
private static let sectionNameCharsWithoutBrackets = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?")
|
||||
private static let sectionNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_?()")
|
||||
private static let partialNameChars = Set<Character>("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-_()")
|
||||
}
|
||||
193
Sources/Mustache/Template+Render.swift
Normal file
193
Sources/Mustache/Template+Render.swift
Normal file
@@ -0,0 +1,193 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension MustacheTemplate {
|
||||
/// Render template using object
|
||||
/// - Parameters:
|
||||
/// - stack: Object
|
||||
/// - context: Context that render is occurring in. Contains information about position in sequence
|
||||
/// - indentation: indentation of partial
|
||||
/// - Returns: Rendered text
|
||||
func render(context: MustacheContext) -> String {
|
||||
var string = ""
|
||||
var context = context
|
||||
|
||||
if let indentation = context.indentation, indentation != "" {
|
||||
for token in tokens {
|
||||
let renderedString = self.renderToken(token, context: &context)
|
||||
if renderedString != "", string.last == "\n" {
|
||||
string += indentation
|
||||
}
|
||||
string += renderedString
|
||||
}
|
||||
} else {
|
||||
for token in tokens {
|
||||
let result = self.renderToken(token, context: &context)
|
||||
string += result
|
||||
}
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
func renderToken(_ token: Token, context: inout MustacheContext) -> String {
|
||||
switch token {
|
||||
case .text(let text):
|
||||
return text
|
||||
case .variable(let variable, let transform):
|
||||
if let child = getChild(named: variable, transform: transform, context: context) {
|
||||
if let template = child as? MustacheTemplate {
|
||||
return template.render(context: context)
|
||||
} else if let renderable = child as? MustacheCustomRenderable {
|
||||
return context.contentType.escapeText(renderable.renderText)
|
||||
} else {
|
||||
return context.contentType.escapeText(String(describing: child))
|
||||
}
|
||||
}
|
||||
case .unescapedVariable(let variable, let transform):
|
||||
if let child = getChild(named: variable, transform: transform, context: context) {
|
||||
if let renderable = child as? MustacheCustomRenderable {
|
||||
return renderable.renderText
|
||||
} else {
|
||||
return String(describing: child)
|
||||
}
|
||||
}
|
||||
case .section(let variable, let transform, let template):
|
||||
let child = self.getChild(named: variable, transform: transform, context: context)
|
||||
return self.renderSection(child, with: template, context: context)
|
||||
|
||||
case .invertedSection(let variable, let transform, let template):
|
||||
let child = self.getChild(named: variable, transform: transform, context: context)
|
||||
return self.renderInvertedSection(child, with: template, context: context)
|
||||
|
||||
case .inheritedSection(let name, let template):
|
||||
if let override = context.inherited?[name] {
|
||||
return override.render(context: context)
|
||||
} else {
|
||||
return template.render(context: context)
|
||||
}
|
||||
|
||||
case .partial(let name, let indentation, let overrides):
|
||||
if let template = context.library?.getTemplate(named: name) {
|
||||
return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
|
||||
}
|
||||
|
||||
case .contentType(let contentType):
|
||||
context = context.withContentType(contentType)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// Render a section
|
||||
/// - Parameters:
|
||||
/// - child: Object to render section for
|
||||
/// - parent: Current object being rendered
|
||||
/// - template: Template to render with
|
||||
/// - Returns: Rendered text
|
||||
func renderSection(_ child: Any?, with template: MustacheTemplate, context: MustacheContext) -> String {
|
||||
switch child {
|
||||
case let array as MustacheSequence:
|
||||
return array.renderSection(with: template, context: context)
|
||||
case let bool as Bool:
|
||||
return bool ? template.render(context: context) : ""
|
||||
case let lambda as MustacheLambda:
|
||||
return lambda.run(context.stack.last!, template)
|
||||
case let null as MustacheCustomRenderable where null.isNull == true:
|
||||
return ""
|
||||
case .some(let value):
|
||||
return template.render(context: context.withObject(value))
|
||||
case .none:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/// Render an inverted section
|
||||
/// - Parameters:
|
||||
/// - child: Object to render section for
|
||||
/// - parent: Current object being rendered
|
||||
/// - template: Template to render with
|
||||
/// - Returns: Rendered text
|
||||
func renderInvertedSection(_ child: Any?, with template: MustacheTemplate, context: MustacheContext) -> String {
|
||||
switch child {
|
||||
case let array as MustacheSequence:
|
||||
return array.renderInvertedSection(with: template, context: context)
|
||||
case let bool as Bool:
|
||||
return bool ? "" : template.render(context: context)
|
||||
case let null as MustacheCustomRenderable where null.isNull == true:
|
||||
return template.render(context: context)
|
||||
case .some:
|
||||
return ""
|
||||
case .none:
|
||||
return template.render(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get child object from variable name
|
||||
func getChild(named name: String, transform: String?, context: MustacheContext) -> Any? {
|
||||
func _getImmediateChild(named name: String, from object: Any) -> Any? {
|
||||
if let customBox = object as? MustacheParent {
|
||||
return customBox.child(named: name)
|
||||
} else {
|
||||
let mirror = Mirror(reflecting: object)
|
||||
return mirror.getValue(forKey: name)
|
||||
}
|
||||
}
|
||||
|
||||
func _getChild(named names: ArraySlice<String>, from object: Any) -> Any? {
|
||||
guard let name = names.first else { return object }
|
||||
guard let childObject = _getImmediateChild(named: name, from: object) else { return nil }
|
||||
let names2 = names.dropFirst()
|
||||
return _getChild(named: names2, from: childObject)
|
||||
}
|
||||
|
||||
func _getChildInStack(named names: ArraySlice<String>, from stack: [Any]) -> Any? {
|
||||
guard let name = names.first else { return stack.last }
|
||||
for object in stack.reversed() {
|
||||
if let childObject = _getImmediateChild(named: name, from: object) {
|
||||
let names2 = names.dropFirst()
|
||||
return _getChild(named: names2, from: childObject)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// work out which object to access. "." means the current object, if the variable name is ""
|
||||
// and we have a transform to run on the variable then we need the context object, otherwise
|
||||
// the name is split by "." and we use mirror to get the correct child object. If we cannot find
|
||||
// the root object we look up the context stack until we can find one with a matching name. The
|
||||
// stack climbing can be disabled by prefixing the variable name with a "."
|
||||
let child: Any?
|
||||
if name == "." {
|
||||
child = context.stack.last!
|
||||
} else if name == "", transform != nil {
|
||||
child = context.sequenceContext
|
||||
} else if name.first == "." {
|
||||
let nameSplit = name.split(separator: ".").map { String($0) }
|
||||
child = _getChild(named: nameSplit[...], from: context.stack.last!)
|
||||
} else {
|
||||
let nameSplit = name.split(separator: ".").map { String($0) }
|
||||
child = _getChildInStack(named: nameSplit[...], from: context.stack)
|
||||
}
|
||||
// if we want to run a transform and the current child can have transforms applied to it then
|
||||
// run transform on the current child
|
||||
if let transform {
|
||||
if let runnable = child as? MustacheTransformable {
|
||||
return runnable.transform(transform)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return child
|
||||
}
|
||||
}
|
||||
47
Sources/Mustache/Template.swift
Normal file
47
Sources/Mustache/Template.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Class holding Mustache template
|
||||
public struct MustacheTemplate: Sendable {
|
||||
/// Initialize template
|
||||
/// - Parameter string: Template text
|
||||
/// - Throws: MustacheTemplate.Error
|
||||
public init(string: String) throws {
|
||||
self.tokens = try Self.parse(string)
|
||||
}
|
||||
|
||||
/// Render object using this template
|
||||
/// - Parameter object: Object to render
|
||||
/// - Returns: Rendered text
|
||||
public func render(_ object: Any, library: MustacheLibrary? = nil) -> String {
|
||||
self.render(context: .init(object, library: library))
|
||||
}
|
||||
|
||||
internal init(_ tokens: [Token]) {
|
||||
self.tokens = tokens
|
||||
}
|
||||
|
||||
enum Token: Sendable {
|
||||
case text(String)
|
||||
case variable(name: String, transform: String? = nil)
|
||||
case unescapedVariable(name: String, transform: String? = nil)
|
||||
case section(name: String, transform: String? = nil, template: MustacheTemplate)
|
||||
case invertedSection(name: String, transform: String? = nil, template: MustacheTemplate)
|
||||
case inheritedSection(name: String, template: MustacheTemplate)
|
||||
case partial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
|
||||
case contentType(MustacheContentType)
|
||||
}
|
||||
|
||||
var tokens: [Token]
|
||||
}
|
||||
173
Sources/Mustache/Transform.swift
Normal file
173
Sources/Mustache/Transform.swift
Normal file
@@ -0,0 +1,173 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the Hummingbird server framework project
|
||||
//
|
||||
// Copyright (c) 2021-2021 the Hummingbird authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
/// Objects that can have a transforms run on them. Mustache transforms are specific to this implementation
|
||||
/// of Mustache. They allow you to process objects before they are rendered.
|
||||
///
|
||||
/// The syntax for applying transforms is `{{transform(variable)}}`. Transforms can be applied to both
|
||||
/// variables, sections and inverted sections.
|
||||
///
|
||||
/// A simple example would be ensuring a string is lowercase.
|
||||
/// ```
|
||||
/// {{lowercased(myString)}}
|
||||
/// ```
|
||||
/// If applying a transform to a sequence then the closing element of the sequence should include the
|
||||
/// transform name eg
|
||||
/// ```
|
||||
/// {{#reversed(sequence)}}{{.}}{{\reversed(sequence)}}
|
||||
/// ```
|
||||
public protocol MustacheTransformable {
|
||||
func transform(_ name: String) -> Any?
|
||||
}
|
||||
|
||||
public extension StringProtocol {
|
||||
/// Transform String/Substring
|
||||
///
|
||||
/// Transforms available are `capitalized`, `lowercased`, `uppercased` and `reversed`
|
||||
/// - Parameter name: transform name
|
||||
/// - Returns: Result
|
||||
func transform(_ name: String) -> Any? {
|
||||
switch name {
|
||||
case "empty":
|
||||
return isEmpty
|
||||
case "capitalized":
|
||||
return capitalized
|
||||
case "lowercased":
|
||||
return lowercased()
|
||||
case "uppercased":
|
||||
return uppercased()
|
||||
case "reversed":
|
||||
return reversed()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension String: MustacheTransformable {}
|
||||
extension Substring: MustacheTransformable {}
|
||||
|
||||
/// Protocol for sequence that can be sorted
|
||||
private protocol ComparableSequence {
|
||||
func comparableTransform(_ name: String) -> Any?
|
||||
}
|
||||
|
||||
extension Array: MustacheTransformable {
|
||||
/// Transform Array.
|
||||
///
|
||||
/// Transforms available are `first`, `last`, `reversed`, `count` and for arrays
|
||||
/// with comparable elements `sorted`.
|
||||
/// - Parameter name: transform name
|
||||
/// - Returns: Result
|
||||
public func transform(_ name: String) -> Any? {
|
||||
switch name {
|
||||
case "first":
|
||||
return first
|
||||
case "last":
|
||||
return last
|
||||
case "reversed":
|
||||
return reversed()
|
||||
case "count":
|
||||
return count
|
||||
case "empty":
|
||||
return isEmpty
|
||||
default:
|
||||
if let comparableSeq = self as? ComparableSequence {
|
||||
return comparableSeq.comparableTransform(name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: ComparableSequence where Element: Comparable {
|
||||
func comparableTransform(_ name: String) -> Any? {
|
||||
switch name {
|
||||
case "sorted":
|
||||
return sorted()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary: MustacheTransformable {
|
||||
/// Transform Dictionary
|
||||
///
|
||||
/// Transforms available are `count`, `enumerated` and for dictionaries
|
||||
/// with comparable keys `sorted`.
|
||||
/// - Parameter name: transform name
|
||||
/// - Returns: Result
|
||||
public func transform(_ name: String) -> Any? {
|
||||
switch name {
|
||||
case "count":
|
||||
return count
|
||||
case "empty":
|
||||
return isEmpty
|
||||
case "enumerated":
|
||||
return map { (key: $0.key, value: $0.value) }
|
||||
default:
|
||||
if let comparableSeq = self as? ComparableSequence {
|
||||
return comparableSeq.comparableTransform(name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary: ComparableSequence where Key: Comparable {
|
||||
func comparableTransform(_ name: String) -> Any? {
|
||||
switch name {
|
||||
case "sorted":
|
||||
return map { (key: $0.key, value: $0.value) }.sorted { $0.key < $1.key }
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension FixedWidthInteger {
|
||||
/// Transform FixedWidthInteger
|
||||
///
|
||||
/// Transforms available are `plusone`, `minusone`, `odd`, `even`
|
||||
/// - Parameter name: transform name
|
||||
/// - Returns: Result
|
||||
func transform(_ name: String) -> Any? {
|
||||
switch name {
|
||||
case "equalzero":
|
||||
return self == 0
|
||||
case "plusone":
|
||||
return self + 1
|
||||
case "minusone":
|
||||
return self - 1
|
||||
case "even":
|
||||
return (self & 1) == 0
|
||||
case "odd":
|
||||
return (self & 1) == 1
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Int: MustacheTransformable {}
|
||||
extension Int8: MustacheTransformable {}
|
||||
extension Int16: MustacheTransformable {}
|
||||
extension Int32: MustacheTransformable {}
|
||||
extension Int64: MustacheTransformable {}
|
||||
extension UInt: MustacheTransformable {}
|
||||
extension UInt8: MustacheTransformable {}
|
||||
extension UInt16: MustacheTransformable {}
|
||||
extension UInt32: MustacheTransformable {}
|
||||
extension UInt64: MustacheTransformable {}
|
||||
Reference in New Issue
Block a user