Skip to content

Commit

Permalink
2.x.x Template struct (#22)
Browse files Browse the repository at this point in the history
* Start of turning template into a struct

* Everything is Sendable now, just doesnt work

* Add library to context

* Make sure render is initialized with library

* comment about inheritance spec

* Add register back in

* Re-instate register functions

* Re-instate commented out print

* Fix tabbing in Partial tests

* Make HBMustacheLibrary.loadTemplates async

* Update platforms, swift version
  • Loading branch information
adam-fowler committed Jan 31, 2024
1 parent bec77d1 commit 071b182
Show file tree
Hide file tree
Showing 11 changed files with 67 additions and 65 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ jobs:
strategy:
matrix:
image:
- 'swift:5.6'
- 'swift:5.7'
- 'swift:5.8'
- 'swift:5.9'
- 'swiftlang/swift:nightly-5.10-jammy'

container:
image: ${{ matrix.image }}
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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.

import PackageDescription

let package = Package(
name: "hummingbird-mustache",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
products: [
.library(name: "HummingbirdMustache", targets: ["HummingbirdMustache"]),
],
Expand Down
2 changes: 1 addition & 1 deletion Sources/HummingbirdMustache/ContentType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===//

/// Protocol for content types
public protocol HBMustacheContentType {
public protocol HBMustacheContentType: Sendable {
/// escape text for this content type eg for HTML replace "<" with "&lt;"
func escapeText(_ text: String) -> String
}
Expand Down
20 changes: 14 additions & 6 deletions Sources/HummingbirdMustache/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,32 @@ struct HBMustacheContext {
let indentation: String?
let inherited: [String: HBMustacheTemplate]?
let contentType: HBMustacheContentType
let library: HBMustacheLibrary?

/// initialize context with a single objectt
init(_ object: Any) {
init(_ object: Any, library: HBMustacheLibrary? = nil) {
self.stack = [object]
self.sequenceContext = nil
self.indentation = nil
self.inherited = nil
self.contentType = HBHTMLContentType()
self.library = library
}

private init(
stack: [Any],
sequenceContext: HBMustacheSequenceContext?,
indentation: String?,
inherited: [String: HBMustacheTemplate]?,
contentType: HBMustacheContentType
contentType: HBMustacheContentType,
library: HBMustacheLibrary? = 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
Expand All @@ -52,7 +56,8 @@ struct HBMustacheContext {
sequenceContext: nil,
indentation: self.indentation,
inherited: self.inherited,
contentType: self.contentType
contentType: self.contentType,
library: self.library
)
}

Expand All @@ -79,7 +84,8 @@ struct HBMustacheContext {
sequenceContext: nil,
indentation: indentation,
inherited: inherits,
contentType: HBHTMLContentType()
contentType: HBHTMLContentType(),
library: self.library
)
}

Expand All @@ -92,7 +98,8 @@ struct HBMustacheContext {
sequenceContext: sequenceContext,
indentation: self.indentation,
inherited: self.inherited,
contentType: self.contentType
contentType: self.contentType,
library: self.library
)
}

Expand All @@ -103,7 +110,8 @@ struct HBMustacheContext {
sequenceContext: self.sequenceContext,
indentation: self.indentation,
inherited: self.inherited,
contentType: contentType
contentType: contentType,
library: self.library
)
}
}
10 changes: 6 additions & 4 deletions Sources/HummingbirdMustache/Library+FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,29 @@ import Foundation

extension HBMustacheLibrary {
/// 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
if !directory.hasSuffix("/") {
directory += "/"
}
let extWithDot = ".\(`extension`)"
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 {
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)
let template: HBMustacheTemplate
var template: HBMustacheTemplate
do {
template = try HBMustacheTemplate(string: string)
} catch let error as HBMustacheTemplate.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))
register(template, named: name)
templates[name] = template
}
return templates
}
}
25 changes: 16 additions & 9 deletions Sources/HummingbirdMustache/Library.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/// ```
/// {{#sequence}}{{>entry}}{{/sequence}}
/// ```
public final class HBMustacheLibrary {
public struct HBMustacheLibrary: Sendable {
/// Initialize empty library
public init() {
self.templates = [:]
Expand All @@ -30,27 +30,34 @@ public final class HBMustacheLibrary {
/// 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") throws {
self.templates = [:]
try loadTemplates(from: directory, withExtension: `extension`)
public init(templates: [String: HBMustacheTemplate]) {
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 func register(_ template: HBMustacheTemplate, named name: String) {
template.setLibrary(self)
public mutating func register(_ template: HBMustacheTemplate, named name: String) {
self.templates[name] = template
}

/// Register template under name
/// - Parameters:
/// - mustache: Mustache text
/// - 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)
template.setLibrary(self)
self.templates[name] = template
}

Expand All @@ -68,7 +75,7 @@ public final class HBMustacheLibrary {
/// - Returns: Rendered text
public func render(_ object: Any, withTemplate name: String) -> String? {
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
Expand Down
2 changes: 1 addition & 1 deletion Sources/HummingbirdMustache/Template+Render.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ extension HBMustacheTemplate {
}

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

Expand Down
25 changes: 5 additions & 20 deletions Sources/HummingbirdMustache/Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===//

/// Class holding Mustache template
public final class HBMustacheTemplate {
public struct HBMustacheTemplate: Sendable {
/// Initialize template
/// - Parameter string: Template text
/// - Throws: HBMustacheTemplate.Error
Expand All @@ -24,29 +24,15 @@ public final class HBMustacheTemplate {
/// Render object using this template
/// - Parameter object: Object to render
/// - Returns: Rendered text
public func render(_ object: Any) -> String {
self.render(context: .init(object))
public func render(_ object: Any, library: HBMustacheLibrary? = nil) -> String {
self.render(context: .init(object, library: library))
}

internal init(_ tokens: [Token]) {
self.tokens = tokens
}

internal func setLibrary(_ library: HBMustacheLibrary) {
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 {
enum Token: Sendable {
case text(String)
case variable(name: String, transform: String? = nil)
case unescapedVariable(name: String, transform: String? = nil)
Expand All @@ -57,6 +43,5 @@ public final class HBMustacheTemplate {
case contentType(HBMustacheContentType)
}

let tokens: [Token]
var library: HBMustacheLibrary?
var tokens: [Token]
}
18 changes: 8 additions & 10 deletions Tests/HummingbirdMustacheTests/LibraryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@
import XCTest

final class LibraryTests: XCTestCase {
func testDirectoryLoad() throws {
func testDirectoryLoad() async throws {
let fs = FileManager()
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) }
let mustache = Data("<test>{{#value}}<value>{{.}}</value>{{/value}}</test>".utf8)
try mustache.write(to: URL(fileURLWithPath: "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"]]
XCTAssertEqual(library.render(object, withTemplate: "test"), "<test><value>value1</value><value>value2</value></test>")
}

func testPartial() throws {
func testPartial() async throws {
let fs = FileManager()
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
let mustache = Data("<test>{{#value}}<value>{{.}}</value>{{/value}}</test>".utf8)
Expand All @@ -42,12 +42,12 @@ final class LibraryTests: XCTestCase {
XCTAssertNoThrow(try fs.removeItem(atPath: "templates"))
}

let library = try HBMustacheLibrary(directory: "./templates")
let library = try await HBMustacheLibrary(directory: "./templates")
let object = ["value": ["value1", "value2"]]
XCTAssertEqual(library.render(object, withTemplate: "test"), "<test><value>value1</value><value>value2</value></test>")
}

func testLibraryParserError() throws {
func testLibraryParserError() async throws {
let fs = FileManager()
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) }
Expand All @@ -62,11 +62,9 @@ final class LibraryTests: XCTestCase {
try mustache2.write(to: URL(fileURLWithPath: "templates/error.mustache"))
defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/error.mustache")) }

XCTAssertThrowsError(try HBMustacheLibrary(directory: "./templates")) { error in
guard let parserError = error as? HBMustacheLibrary.ParserError else {
XCTFail("\(error)")
return
}
do {
_ = try await HBMustacheLibrary(directory: "./templates")
} catch let parserError as HBMustacheLibrary.ParserError {
XCTAssertEqual(parserError.filename, "error.mustache")
XCTAssertEqual(parserError.context.line, "{{{name}}")
XCTAssertEqual(parserError.context.lineNumber, 2)
Expand Down
13 changes: 5 additions & 8 deletions Tests/HummingbirdMustacheTests/PartialTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import XCTest
final class PartialTests: XCTestCase {
/// Testing partials
func testMustacheManualExample9() throws {
let library = HBMustacheLibrary()
let template = try HBMustacheTemplate(string: """
<h2>Names</h2>
{{#names}}
Expand All @@ -29,8 +28,7 @@ final class PartialTests: XCTestCase {
<strong>{{.}}</strong>
""")
library.register(template, named: "base")
library.register(template2, named: "user")
let library = HBMustacheLibrary(templates: ["base": template, "user": template2])

let object: [String: Any] = ["names": ["john", "adam", "claire"]]
XCTAssertEqual(library.render(object, withTemplate: "base"), """
Expand All @@ -45,7 +43,6 @@ final class PartialTests: XCTestCase {
/// Test where last line of partial generates no content. It should not add a
/// tab either
func testPartialEmptyLineTabbing() throws {
let library = HBMustacheLibrary()
let template = try HBMustacheTemplate(string: """
<h2>Names</h2>
{{#names}}
Expand All @@ -63,8 +60,9 @@ final class PartialTests: XCTestCase {
{{/empty(.)}}
""")
var library = HBMustacheLibrary()
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"]]
XCTAssertEqual(library.render(object, withTemplate: "base"), """
Expand All @@ -79,7 +77,6 @@ final class PartialTests: XCTestCase {

/// Testing dynamic partials
func testDynamicPartials() throws {
let library = HBMustacheLibrary()
let template = try HBMustacheTemplate(string: """
<h2>Names</h2>
{{partial}}
Expand All @@ -89,7 +86,7 @@ final class PartialTests: XCTestCase {
<strong>{{.}}</strong>
{{/names}}
""")
library.register(template, named: "base")
let library = HBMustacheLibrary(templates: ["base": template])

let object: [String: Any] = ["names": ["john", "adam", "claire"], "partial": template2]
XCTAssertEqual(library.render(object, withTemplate: "base"), """
Expand All @@ -103,7 +100,7 @@ final class PartialTests: XCTestCase {

/// test inheritance
func testInheritance() throws {
let library = HBMustacheLibrary()
var library = HBMustacheLibrary()
try library.register(
"""
<head>
Expand Down
Loading

0 comments on commit 071b182

Please sign in to comment.