Rename package to swift-mustache (#27)

* Rename package to swift-mustache

* Update CI
This commit is contained in:
Adam Fowler
2024-03-15 07:28:57 +00:00
committed by GitHub
parent bdfa05391a
commit 2663d13ea7
30 changed files with 19 additions and 42 deletions

View 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 "&lt;"
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(),
]
}

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

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

View 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

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

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

View 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]
}

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

View 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] }
}

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

View 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 {}

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

View 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] = [
"<": "&lt;",
">": "&gt;",
"&": "&amp;",
"\"": "&quot;",
"'": "&#39;",
]
/// 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
}
}

View 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.-_()")
}

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

View 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]
}

View 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 {}