diff --git a/Sources/Mustache/Context.swift b/Sources/Mustache/Context.swift index ab48e3e..03488a9 100644 --- a/Sources/Mustache/Context.swift +++ b/Sources/Mustache/Context.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2021-2024 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -20,15 +20,17 @@ struct MustacheContext { let inherited: [String: MustacheTemplate]? let contentType: MustacheContentType let library: MustacheLibrary? + let reloadPartials: Bool /// initialize context with a single objectt - init(_ object: Any, library: MustacheLibrary? = nil) { + init(_ object: Any, library: MustacheLibrary? = nil, reloadPartials: Bool = false) { self.stack = [object] self.sequenceContext = nil self.indentation = nil self.inherited = nil self.contentType = HTMLContentType() self.library = library + self.reloadPartials = reloadPartials } private init( @@ -37,7 +39,8 @@ struct MustacheContext { indentation: String?, inherited: [String: MustacheTemplate]?, contentType: MustacheContentType, - library: MustacheLibrary? = nil + library: MustacheLibrary? = nil, + reloadPartials: Bool ) { self.stack = stack self.sequenceContext = sequenceContext @@ -45,6 +48,7 @@ struct MustacheContext { self.inherited = inherited self.contentType = contentType self.library = library + self.reloadPartials = reloadPartials } /// return context with object add to stack @@ -57,7 +61,8 @@ struct MustacheContext { indentation: self.indentation, inherited: self.inherited, contentType: self.contentType, - library: self.library + library: self.library, + reloadPartials: self.reloadPartials ) } @@ -83,7 +88,8 @@ struct MustacheContext { indentation: indentation, inherited: inherits, contentType: HTMLContentType(), - library: self.library + library: self.library, + reloadPartials: self.reloadPartials ) } @@ -100,7 +106,8 @@ struct MustacheContext { indentation: indentation, inherited: self.inherited, contentType: self.contentType, - library: self.library + library: self.library, + reloadPartials: self.reloadPartials ) } @@ -114,7 +121,8 @@ struct MustacheContext { indentation: self.indentation, inherited: self.inherited, contentType: self.contentType, - library: self.library + library: self.library, + reloadPartials: self.reloadPartials ) } @@ -126,7 +134,8 @@ struct MustacheContext { indentation: self.indentation, inherited: self.inherited, contentType: contentType, - library: self.library + library: self.library, + reloadPartials: self.reloadPartials ) } } diff --git a/Sources/Mustache/Library+FileSystem.swift b/Sources/Mustache/Library+FileSystem.swift index 06a95e8..ccdf0c8 100644 --- a/Sources/Mustache/Library+FileSystem.swift +++ b/Sources/Mustache/Library+FileSystem.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2021-2024 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -27,17 +27,14 @@ extension MustacheLibrary { 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) + guard let template = try MustacheTemplate(filename: directory + path) else { continue } + // drop ".mustache" from path to get name + let name = String(path.dropLast(extWithDot.count)) + templates[name] = template } 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 } diff --git a/Sources/Mustache/Library.swift b/Sources/Mustache/Library.swift index 6e8be10..0cdd408 100644 --- a/Sources/Mustache/Library.swift +++ b/Sources/Mustache/Library.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2021-2024 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -78,6 +78,21 @@ public struct MustacheLibrary: Sendable { return template.render(object, library: self) } + /// Render object using templated with name + /// - Parameters: + /// - object: Object to render + /// - name: Name of template + /// - reload: Reload templates when rendering. This is only available in debug builds + /// - Returns: Rendered text + public func render(_ object: Any, withTemplate name: String, reload: Bool) -> String? { + guard let template = templates[name] else { return nil } + #if DEBUG + return template.render(object, library: self, reload: reload) + #else + return template.render(object, library: self) + #endif + } + /// Error returned by init() when parser fails public struct ParserError: Swift.Error { /// File error occurred in diff --git a/Sources/Mustache/Template+FileSystem.swift b/Sources/Mustache/Template+FileSystem.swift new file mode 100644 index 0000000..5e2afb5 --- /dev/null +++ b/Sources/Mustache/Template+FileSystem.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-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 +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension MustacheTemplate { + /// Internal function to load a template from a file + /// - Parameters + /// - string: Template text + /// - filename: File template was loaded from + /// - Throws: MustacheTemplate.Error + init?(filename: String) throws { + let fs = FileManager() + guard let data = fs.contents(atPath: filename) else { return nil } + let string = String(decoding: data, as: Unicode.UTF8.self) + self.tokens = try Self.parse(string) + self.filename = filename + } +} diff --git a/Sources/Mustache/Template+Render.swift b/Sources/Mustache/Template+Render.swift index 6be73eb..6862d23 100644 --- a/Sources/Mustache/Template+Render.swift +++ b/Sources/Mustache/Template+Render.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2021-2024 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -84,7 +84,20 @@ extension MustacheTemplate { } case .partial(let name, let indentation, let overrides): - if let template = context.library?.getTemplate(named: name) { + if var template = context.library?.getTemplate(named: name) { + #if DEBUG + if context.reloadPartials { + guard let filename = template.filename else { + preconditionFailure("Can only use reload if template was generated from a file") + } + do { + guard let partialTemplate = try MustacheTemplate(filename: filename) else { return "Cannot find template at \(filename)" } + template = partialTemplate + } catch { + return "\(error)" + } + } + #endif return template.render(context: context.withPartial(indented: indentation, inheriting: overrides)) } diff --git a/Sources/Mustache/Template.swift b/Sources/Mustache/Template.swift index fe152ac..26a1211 100644 --- a/Sources/Mustache/Template.swift +++ b/Sources/Mustache/Template.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2021-2024 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -19,17 +19,44 @@ public struct MustacheTemplate: Sendable { /// - Throws: MustacheTemplate.Error public init(string: String) throws { self.tokens = try Self.parse(string) + self.filename = nil } /// Render object using this template - /// - Parameter object: Object to render + /// - Parameters + /// - object: Object to render + /// - library: library template uses to access partials /// - Returns: Rendered text public func render(_ object: Any, library: MustacheLibrary? = nil) -> String { self.render(context: .init(object, library: library)) } + /// Render object using this template + /// - Parameters + /// - object: Object to render + /// - library: library template uses to access partials + /// - reload: Should I reload this template when rendering. This is only available in debug builds + /// - Returns: Rendered text + public func render(_ object: Any, library: MustacheLibrary? = nil, reload: Bool) -> String { + #if DEBUG + if reload { + guard let filename else { + preconditionFailure("Can only use reload if template was generated from a file") + } + do { + guard let template = try MustacheTemplate(filename: filename) else { return "Cannot find template at \(filename)" } + return template.render(context: .init(object, library: library, reloadPartials: reload)) + } catch { + return "\(error)" + } + } + #endif + return self.render(context: .init(object, library: library)) + } + internal init(_ tokens: [Token]) { self.tokens = tokens + self.filename = nil } enum Token: Sendable { @@ -45,4 +72,5 @@ public struct MustacheTemplate: Sendable { } var tokens: [Token] + let filename: String? } diff --git a/Tests/MustacheTests/LibraryTests.swift b/Tests/MustacheTests/LibraryTests.swift index 10f062d..c2cc38f 100644 --- a/Tests/MustacheTests/LibraryTests.swift +++ b/Tests/MustacheTests/LibraryTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2021-2024 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -71,4 +71,43 @@ final class LibraryTests: XCTestCase { XCTAssertEqual(parserError.context.columnNumber, 10) } } + + #if DEBUG + func testReload() async throws { + let fs = FileManager() + try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false) + defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) } + let mustache = Data("{{#value}}{{.}}{{/value}}".utf8) + try mustache.write(to: URL(fileURLWithPath: "templates/test.mustache")) + defer { XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache")) } + + let library = try await MustacheLibrary(directory: "./templates") + let object = ["value": ["value1", "value2"]] + XCTAssertEqual(library.render(object, withTemplate: "test"), "value1value2") + let mustache2 = Data("{{#value}}{{.}}{{/value}}".utf8) + try mustache2.write(to: URL(fileURLWithPath: "templates/test.mustache")) + XCTAssertEqual(library.render(object, withTemplate: "test", reload: true), "value1value2") + } + + func testReloadPartial() async throws { + let fs = FileManager() + try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false) + let mustache = Data("{{#value}}{{.}}{{/value}}".utf8) + try mustache.write(to: URL(fileURLWithPath: "templates/test-partial.mustache")) + let mustache2 = Data("{{>test-partial}}".utf8) + try mustache2.write(to: URL(fileURLWithPath: "templates/test.mustache")) + defer { + XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test-partial.mustache")) + XCTAssertNoThrow(try fs.removeItem(atPath: "templates/test.mustache")) + XCTAssertNoThrow(try fs.removeItem(atPath: "templates")) + } + + let library = try await MustacheLibrary(directory: "./templates") + let object = ["value": ["value1", "value2"]] + XCTAssertEqual(library.render(object, withTemplate: "test"), "value1value2") + let mustache3 = Data("{{#value}}{{.}}{{/value}}".utf8) + try mustache3.write(to: URL(fileURLWithPath: "templates/test-partial.mustache")) + XCTAssertEqual(library.render(object, withTemplate: "test", reload: true), "value1value2") + } + #endif }