Merge pull request #24 from hummingbird-project/2.x.x

Merge 2.x.x into main
This commit is contained in:
Adam Fowler
2024-03-08 10:53:44 +00:00
committed by GitHub
13 changed files with 72 additions and 66 deletions

View File

@@ -3,6 +3,8 @@ name: API breaking changes
on: on:
pull_request: pull_request:
branches:
- main
jobs: jobs:
linux: linux:

View File

@@ -10,6 +10,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- 2.x.x
paths: paths:
- '**.swift' - '**.swift'
- '**.yml' - '**.yml'
@@ -39,9 +40,10 @@ jobs:
strategy: strategy:
matrix: matrix:
image: image:
- 'swift:5.6'
- 'swift:5.7' - 'swift:5.7'
- 'swift:5.8' - 'swift:5.8'
- 'swift:5.9'
- 'swiftlang/swift:nightly-5.10-jammy'
container: container:
image: ${{ matrix.image }} image: ${{ matrix.image }}

View File

@@ -4,6 +4,7 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- 2.x.x
jobs: jobs:
validate: validate:
@@ -16,6 +17,6 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: | run: |
brew install mint brew install mint
mint install NickLockwood/SwiftFormat@0.48.17 --no-link mint install NickLockwood/SwiftFormat@0.51.15 --no-link
- name: run script - name: run script
run: ./scripts/validate.sh run: ./scripts/validate.sh

View File

@@ -1,10 +1,11 @@
// swift-tools-version:5.3 // swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "hummingbird-mustache", name: "hummingbird-mustache",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
products: [ products: [
.library(name: "HummingbirdMustache", targets: ["HummingbirdMustache"]), .library(name: "HummingbirdMustache", targets: ["HummingbirdMustache"]),
], ],

View File

@@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
/// Protocol for content types /// Protocol for content types
public protocol HBMustacheContentType { public protocol HBMustacheContentType: Sendable {
/// escape text for this content type eg for HTML replace "<" with "&lt;" /// escape text for this content type eg for HTML replace "<" with "&lt;"
func escapeText(_ text: String) -> String func escapeText(_ text: String) -> String
} }

View File

@@ -19,14 +19,16 @@ struct HBMustacheContext {
let indentation: String? let indentation: String?
let inherited: [String: HBMustacheTemplate]? let inherited: [String: HBMustacheTemplate]?
let contentType: HBMustacheContentType let contentType: HBMustacheContentType
let library: HBMustacheLibrary?
/// initialize context with a single objectt /// initialize context with a single objectt
init(_ object: Any) { init(_ object: Any, library: HBMustacheLibrary? = nil) {
self.stack = [object] self.stack = [object]
self.sequenceContext = nil self.sequenceContext = nil
self.indentation = nil self.indentation = nil
self.inherited = nil self.inherited = nil
self.contentType = HBHTMLContentType() self.contentType = HBHTMLContentType()
self.library = library
} }
private init( private init(
@@ -34,13 +36,15 @@ struct HBMustacheContext {
sequenceContext: HBMustacheSequenceContext?, sequenceContext: HBMustacheSequenceContext?,
indentation: String?, indentation: String?,
inherited: [String: HBMustacheTemplate]?, inherited: [String: HBMustacheTemplate]?,
contentType: HBMustacheContentType contentType: HBMustacheContentType,
library: HBMustacheLibrary? = nil
) { ) {
self.stack = stack self.stack = stack
self.sequenceContext = sequenceContext self.sequenceContext = sequenceContext
self.indentation = indentation self.indentation = indentation
self.inherited = inherited self.inherited = inherited
self.contentType = contentType self.contentType = contentType
self.library = library
} }
/// return context with object add to stack /// return context with object add to stack
@@ -52,7 +56,8 @@ struct HBMustacheContext {
sequenceContext: nil, sequenceContext: nil,
indentation: self.indentation, indentation: self.indentation,
inherited: self.inherited, inherited: self.inherited,
contentType: self.contentType contentType: self.contentType,
library: self.library
) )
} }
@@ -79,7 +84,8 @@ struct HBMustacheContext {
sequenceContext: nil, sequenceContext: nil,
indentation: indentation, indentation: indentation,
inherited: inherits, inherited: inherits,
contentType: HBHTMLContentType() contentType: HBHTMLContentType(),
library: self.library
) )
} }
@@ -92,7 +98,8 @@ struct HBMustacheContext {
sequenceContext: sequenceContext, sequenceContext: sequenceContext,
indentation: self.indentation, indentation: self.indentation,
inherited: self.inherited, inherited: self.inherited,
contentType: self.contentType contentType: self.contentType,
library: self.library
) )
} }
@@ -103,7 +110,8 @@ struct HBMustacheContext {
sequenceContext: self.sequenceContext, sequenceContext: self.sequenceContext,
indentation: self.indentation, indentation: self.indentation,
inherited: self.inherited, inherited: self.inherited,
contentType: contentType contentType: contentType,
library: self.library
) )
} }
} }

View File

@@ -16,19 +16,20 @@ import Foundation
extension HBMustacheLibrary { extension HBMustacheLibrary {
/// Load templates from a folder /// Load templates from a folder
func loadTemplates(from directory: String, withExtension extension: String = "mustache") throws { static func loadTemplates(from directory: String, withExtension extension: String = "mustache") async throws -> [String: HBMustacheTemplate] {
var directory = directory var directory = directory
if !directory.hasSuffix("/") { if !directory.hasSuffix("/") {
directory += "/" directory += "/"
} }
let extWithDot = ".\(`extension`)" let extWithDot = ".\(`extension`)"
let fs = FileManager() let fs = FileManager()
guard let enumerator = fs.enumerator(atPath: directory) else { return } guard let enumerator = fs.enumerator(atPath: directory) else { return [:] }
var templates: [String: HBMustacheTemplate] = [:]
for case let path as String in enumerator { for case let path as String in enumerator {
guard path.hasSuffix(extWithDot) else { continue } guard path.hasSuffix(extWithDot) else { continue }
guard let data = fs.contents(atPath: directory + path) else { continue } guard let data = fs.contents(atPath: directory + path) else { continue }
let string = String(decoding: data, as: Unicode.UTF8.self) let string = String(decoding: data, as: Unicode.UTF8.self)
let template: HBMustacheTemplate var template: HBMustacheTemplate
do { do {
template = try HBMustacheTemplate(string: string) template = try HBMustacheTemplate(string: string)
} catch let error as HBMustacheTemplate.ParserError { } catch let error as HBMustacheTemplate.ParserError {
@@ -36,7 +37,8 @@ extension HBMustacheLibrary {
} }
// drop ".mustache" from path to get name // drop ".mustache" from path to get name
let name = String(path.dropLast(extWithDot.count)) let name = String(path.dropLast(extWithDot.count))
register(template, named: name) templates[name] = template
} }
return templates
} }
} }

View File

@@ -18,7 +18,7 @@
/// ``` /// ```
/// {{#sequence}}{{>entry}}{{/sequence}} /// {{#sequence}}{{>entry}}{{/sequence}}
/// ``` /// ```
public final class HBMustacheLibrary { public struct HBMustacheLibrary: Sendable {
/// Initialize empty library /// Initialize empty library
public init() { public init() {
self.templates = [:] self.templates = [:]
@@ -30,17 +30,25 @@ public final class HBMustacheLibrary {
/// the folder is recursive and templates in subfolders will be registered with the name `subfolder/template`. /// 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 directory: Directory to look for mustache templates
/// - Parameter extension: Extension of files to look for /// - Parameter extension: Extension of files to look for
public init(directory: String, withExtension extension: String = "mustache") throws { public init(templates: [String: HBMustacheTemplate]) {
self.templates = [:] self.templates = templates
try loadTemplates(from: directory, withExtension: `extension`) }
/// 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 /// Register template under name
/// - Parameters: /// - Parameters:
/// - template: Template /// - template: Template
/// - name: Name of template /// - name: Name of template
public func register(_ template: HBMustacheTemplate, named name: String) { public mutating func register(_ template: HBMustacheTemplate, named name: String) {
template.setLibrary(self)
self.templates[name] = template self.templates[name] = template
} }
@@ -48,9 +56,8 @@ public final class HBMustacheLibrary {
/// - Parameters: /// - Parameters:
/// - mustache: Mustache text /// - mustache: Mustache text
/// - name: Name of template /// - name: Name of template
public func register(_ mustache: String, named name: String) throws { public mutating func register(_ mustache: String, named name: String) throws {
let template = try HBMustacheTemplate(string: mustache) let template = try HBMustacheTemplate(string: mustache)
template.setLibrary(self)
self.templates[name] = template self.templates[name] = template
} }
@@ -68,7 +75,7 @@ public final class HBMustacheLibrary {
/// - Returns: Rendered text /// - Returns: Rendered text
public func render(_ object: Any, withTemplate name: String) -> String? { public func render(_ object: Any, withTemplate name: String) -> String? {
guard let template = templates[name] else { return nil } guard let template = templates[name] else { return nil }
return template.render(object) return template.render(object, library: self)
} }
/// Error returned by init() when parser fails /// Error returned by init() when parser fails

View File

@@ -80,7 +80,7 @@ extension HBMustacheTemplate {
} }
case .partial(let name, let indentation, let overrides): case .partial(let name, let indentation, let overrides):
if let template = library?.getTemplate(named: name) { if let template = context.library?.getTemplate(named: name) {
return template.render(context: context.withPartial(indented: indentation, inheriting: overrides)) return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
} }

View File

@@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
/// Class holding Mustache template /// Class holding Mustache template
public final class HBMustacheTemplate { public struct HBMustacheTemplate: Sendable {
/// Initialize template /// Initialize template
/// - Parameter string: Template text /// - Parameter string: Template text
/// - Throws: HBMustacheTemplate.Error /// - Throws: HBMustacheTemplate.Error
@@ -24,29 +24,15 @@ public final class HBMustacheTemplate {
/// Render object using this template /// Render object using this template
/// - Parameter object: Object to render /// - Parameter object: Object to render
/// - Returns: Rendered text /// - Returns: Rendered text
public func render(_ object: Any) -> String { public func render(_ object: Any, library: HBMustacheLibrary? = nil) -> String {
self.render(context: .init(object)) self.render(context: .init(object, library: library))
} }
internal init(_ tokens: [Token]) { internal init(_ tokens: [Token]) {
self.tokens = tokens self.tokens = tokens
} }
internal func setLibrary(_ library: HBMustacheLibrary) { enum Token: Sendable {
self.library = library
for token in self.tokens {
switch token {
case .section(_, _, let template), .invertedSection(_, _, let template), .inheritedSection(_, let template):
template.setLibrary(library)
case .partial(_, _, let templates):
templates?.forEach { $1.setLibrary(library) }
default:
break
}
}
}
enum Token {
case text(String) case text(String)
case variable(name: String, transform: String? = nil) case variable(name: String, transform: String? = nil)
case unescapedVariable(name: String, transform: String? = nil) case unescapedVariable(name: String, transform: String? = nil)
@@ -57,6 +43,5 @@ public final class HBMustacheTemplate {
case contentType(HBMustacheContentType) case contentType(HBMustacheContentType)
} }
let tokens: [Token] var tokens: [Token]
var library: HBMustacheLibrary?
} }

View File

@@ -16,7 +16,7 @@
import XCTest import XCTest
final class LibraryTests: XCTestCase { final class LibraryTests: XCTestCase {
func testDirectoryLoad() throws { func testDirectoryLoad() async throws {
let fs = FileManager() let fs = FileManager()
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false) try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) } defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) }
@@ -24,12 +24,12 @@ final class LibraryTests: XCTestCase {
try mustache.write(to: URL(fileURLWithPath: "templates/test.mustache")) try mustache.write(to: URL(fileURLWithPath: "templates/test.mustache"))
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache")) } defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache")) }
let library = try HBMustacheLibrary(directory: "./templates") let library = try await HBMustacheLibrary(directory: "./templates")
let object = ["value": ["value1", "value2"]] let object = ["value": ["value1", "value2"]]
XCTAssertEqual(library.render(object, withTemplate: "test"), "<test><value>value1</value><value>value2</value></test>") XCTAssertEqual(library.render(object, withTemplate: "test"), "<test><value>value1</value><value>value2</value></test>")
} }
func testPartial() throws { func testPartial() async throws {
let fs = FileManager() let fs = FileManager()
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false) try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
let mustache = Data("<test>{{#value}}<value>{{.}}</value>{{/value}}</test>".utf8) let mustache = Data("<test>{{#value}}<value>{{.}}</value>{{/value}}</test>".utf8)
@@ -42,12 +42,12 @@ final class LibraryTests: XCTestCase {
XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) XCTAssertNoThrow(try fs.removeItem(atPath: "templates"))
} }
let library = try HBMustacheLibrary(directory: "./templates") let library = try await HBMustacheLibrary(directory: "./templates")
let object = ["value": ["value1", "value2"]] let object = ["value": ["value1", "value2"]]
XCTAssertEqual(library.render(object, withTemplate: "test"), "<test><value>value1</value><value>value2</value></test>") XCTAssertEqual(library.render(object, withTemplate: "test"), "<test><value>value1</value><value>value2</value></test>")
} }
func testLibraryParserError() throws { func testLibraryParserError() async throws {
let fs = FileManager() let fs = FileManager()
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false) try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) } defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) }
@@ -62,11 +62,9 @@ final class LibraryTests: XCTestCase {
try mustache2.write(to: URL(fileURLWithPath: "templates/error.mustache")) try mustache2.write(to: URL(fileURLWithPath: "templates/error.mustache"))
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/error.mustache")) } defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/error.mustache")) }
XCTAssertThrowsError(try HBMustacheLibrary(directory: "./templates")) { error in do {
guard let parserError = error as? HBMustacheLibrary.ParserError else { _ = try await HBMustacheLibrary(directory: "./templates")
XCTFail("\(error)") } catch let parserError as HBMustacheLibrary.ParserError {
return
}
XCTAssertEqual(parserError.filename, "error.mustache") XCTAssertEqual(parserError.filename, "error.mustache")
XCTAssertEqual(parserError.context.line, "{{{name}}") XCTAssertEqual(parserError.context.line, "{{{name}}")
XCTAssertEqual(parserError.context.lineNumber, 2) XCTAssertEqual(parserError.context.lineNumber, 2)

View File

@@ -18,7 +18,6 @@ import XCTest
final class PartialTests: XCTestCase { final class PartialTests: XCTestCase {
/// Testing partials /// Testing partials
func testMustacheManualExample9() throws { func testMustacheManualExample9() throws {
let library = HBMustacheLibrary()
let template = try HBMustacheTemplate(string: """ let template = try HBMustacheTemplate(string: """
<h2>Names</h2> <h2>Names</h2>
{{#names}} {{#names}}
@@ -29,8 +28,7 @@ final class PartialTests: XCTestCase {
<strong>{{.}}</strong> <strong>{{.}}</strong>
""") """)
library.register(template, named: "base") let library = HBMustacheLibrary(templates: ["base": template, "user": template2])
library.register(template2, named: "user")
let object: [String: Any] = ["names": ["john", "adam", "claire"]] let object: [String: Any] = ["names": ["john", "adam", "claire"]]
XCTAssertEqual(library.render(object, withTemplate: "base"), """ XCTAssertEqual(library.render(object, withTemplate: "base"), """
@@ -45,7 +43,6 @@ final class PartialTests: XCTestCase {
/// Test where last line of partial generates no content. It should not add a /// Test where last line of partial generates no content. It should not add a
/// tab either /// tab either
func testPartialEmptyLineTabbing() throws { func testPartialEmptyLineTabbing() throws {
let library = HBMustacheLibrary()
let template = try HBMustacheTemplate(string: """ let template = try HBMustacheTemplate(string: """
<h2>Names</h2> <h2>Names</h2>
{{#names}} {{#names}}
@@ -63,8 +60,9 @@ final class PartialTests: XCTestCase {
{{/empty(.)}} {{/empty(.)}}
""") """)
var library = HBMustacheLibrary()
library.register(template, named: "base") library.register(template, named: "base")
library.register(template2, named: "user") library.register(template2, named: "user") // , withTemplate: String)// = HBMustacheLibrary(templates: ["base": template, "user": template2])
let object: [String: Any] = ["names": ["john", "adam", "claire"]] let object: [String: Any] = ["names": ["john", "adam", "claire"]]
XCTAssertEqual(library.render(object, withTemplate: "base"), """ XCTAssertEqual(library.render(object, withTemplate: "base"), """
@@ -79,7 +77,6 @@ final class PartialTests: XCTestCase {
/// Testing dynamic partials /// Testing dynamic partials
func testDynamicPartials() throws { func testDynamicPartials() throws {
let library = HBMustacheLibrary()
let template = try HBMustacheTemplate(string: """ let template = try HBMustacheTemplate(string: """
<h2>Names</h2> <h2>Names</h2>
{{partial}} {{partial}}
@@ -89,7 +86,7 @@ final class PartialTests: XCTestCase {
<strong>{{.}}</strong> <strong>{{.}}</strong>
{{/names}} {{/names}}
""") """)
library.register(template, named: "base") let library = HBMustacheLibrary(templates: ["base": template])
let object: [String: Any] = ["names": ["john", "adam", "claire"], "partial": template2] let object: [String: Any] = ["names": ["john", "adam", "claire"], "partial": template2]
XCTAssertEqual(library.render(object, withTemplate: "base"), """ XCTAssertEqual(library.render(object, withTemplate: "base"), """
@@ -103,7 +100,7 @@ final class PartialTests: XCTestCase {
/// test inheritance /// test inheritance
func testInheritance() throws { func testInheritance() throws {
let library = HBMustacheLibrary() var library = HBMustacheLibrary()
try library.register( try library.register(
""" """
<head> <head>

View File

@@ -66,15 +66,15 @@ final class MustacheSpecTests: XCTestCase {
let expected: String let expected: String
func run() throws { func run() throws {
print("Test: \(self.name)") // print("Test: \(self.name)")
if let partials = self.partials { if let partials = self.partials {
let library = HBMustacheLibrary()
let template = try HBMustacheTemplate(string: self.template) let template = try HBMustacheTemplate(string: self.template)
library.register(template, named: "__test__") var templates: [String: HBMustacheTemplate] = ["__test__": template]
for (key, value) in partials { for (key, value) in partials {
let template = try HBMustacheTemplate(string: value) let template = try HBMustacheTemplate(string: value)
library.register(template, named: key) templates[key] = template
} }
let library = HBMustacheLibrary(templates: templates)
let result = library.render(self.data.value, withTemplate: "__test__") let result = library.render(self.data.value, withTemplate: "__test__")
self.XCTAssertSpecEqual(result, self) self.XCTAssertSpecEqual(result, self)
} else { } else {
@@ -105,10 +105,12 @@ final class MustacheSpecTests: XCTestCase {
let spec = try JSONDecoder().decode(Spec.self, from: data) let spec = try JSONDecoder().decode(Spec.self, from: data)
print(spec.overview) print(spec.overview)
let date = Date()
for test in spec.tests { for test in spec.tests {
guard !ignoring.contains(test.name) else { continue } guard !ignoring.contains(test.name) else { continue }
XCTAssertNoThrow(try test.run()) XCTAssertNoThrow(try test.run())
} }
print(-date.timeIntervalSinceNow)
} }
func testCommentsSpec() throws { func testCommentsSpec() throws {
@@ -136,6 +138,7 @@ final class MustacheSpecTests: XCTestCase {
} }
func testInheritanceSpec() throws { func testInheritanceSpec() throws {
try XCTSkipIf(true) // inheritance spec has been updated and has added requirements, we don't yet support
try self.testSpec(name: "~inheritance") try self.testSpec(name: "~inheritance")
} }
} }