Add development support for reloading templates when you render them (#30)
* Add support for reloading templates when you render them * comment * Ensure reload is only available in DEBUG * move preprocessor block * swift format * MustacheTemplate.init?(filename:) internal * Only pass reload flag down in DEBUG builds * Rebase with main
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// This source file is part of the Hummingbird server framework project
|
// 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
|
// Licensed under Apache License v2.0
|
||||||
//
|
//
|
||||||
// See LICENSE.txt for license information
|
// See LICENSE.txt for license information
|
||||||
@@ -20,15 +20,17 @@ struct MustacheContext {
|
|||||||
let inherited: [String: MustacheTemplate]?
|
let inherited: [String: MustacheTemplate]?
|
||||||
let contentType: MustacheContentType
|
let contentType: MustacheContentType
|
||||||
let library: MustacheLibrary?
|
let library: MustacheLibrary?
|
||||||
|
let reloadPartials: Bool
|
||||||
|
|
||||||
/// initialize context with a single objectt
|
/// 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.stack = [object]
|
||||||
self.sequenceContext = nil
|
self.sequenceContext = nil
|
||||||
self.indentation = nil
|
self.indentation = nil
|
||||||
self.inherited = nil
|
self.inherited = nil
|
||||||
self.contentType = HTMLContentType()
|
self.contentType = HTMLContentType()
|
||||||
self.library = library
|
self.library = library
|
||||||
|
self.reloadPartials = reloadPartials
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(
|
private init(
|
||||||
@@ -37,7 +39,8 @@ struct MustacheContext {
|
|||||||
indentation: String?,
|
indentation: String?,
|
||||||
inherited: [String: MustacheTemplate]?,
|
inherited: [String: MustacheTemplate]?,
|
||||||
contentType: MustacheContentType,
|
contentType: MustacheContentType,
|
||||||
library: MustacheLibrary? = nil
|
library: MustacheLibrary? = nil,
|
||||||
|
reloadPartials: Bool
|
||||||
) {
|
) {
|
||||||
self.stack = stack
|
self.stack = stack
|
||||||
self.sequenceContext = sequenceContext
|
self.sequenceContext = sequenceContext
|
||||||
@@ -45,6 +48,7 @@ struct MustacheContext {
|
|||||||
self.inherited = inherited
|
self.inherited = inherited
|
||||||
self.contentType = contentType
|
self.contentType = contentType
|
||||||
self.library = library
|
self.library = library
|
||||||
|
self.reloadPartials = reloadPartials
|
||||||
}
|
}
|
||||||
|
|
||||||
/// return context with object add to stack
|
/// return context with object add to stack
|
||||||
@@ -57,7 +61,8 @@ struct MustacheContext {
|
|||||||
indentation: self.indentation,
|
indentation: self.indentation,
|
||||||
inherited: self.inherited,
|
inherited: self.inherited,
|
||||||
contentType: self.contentType,
|
contentType: self.contentType,
|
||||||
library: self.library
|
library: self.library,
|
||||||
|
reloadPartials: self.reloadPartials
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +88,8 @@ struct MustacheContext {
|
|||||||
indentation: indentation,
|
indentation: indentation,
|
||||||
inherited: inherits,
|
inherited: inherits,
|
||||||
contentType: HTMLContentType(),
|
contentType: HTMLContentType(),
|
||||||
library: self.library
|
library: self.library,
|
||||||
|
reloadPartials: self.reloadPartials
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +106,8 @@ struct MustacheContext {
|
|||||||
indentation: indentation,
|
indentation: indentation,
|
||||||
inherited: self.inherited,
|
inherited: self.inherited,
|
||||||
contentType: self.contentType,
|
contentType: self.contentType,
|
||||||
library: self.library
|
library: self.library,
|
||||||
|
reloadPartials: self.reloadPartials
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +121,8 @@ struct MustacheContext {
|
|||||||
indentation: self.indentation,
|
indentation: self.indentation,
|
||||||
inherited: self.inherited,
|
inherited: self.inherited,
|
||||||
contentType: self.contentType,
|
contentType: self.contentType,
|
||||||
library: self.library
|
library: self.library,
|
||||||
|
reloadPartials: self.reloadPartials
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +134,8 @@ struct MustacheContext {
|
|||||||
indentation: self.indentation,
|
indentation: self.indentation,
|
||||||
inherited: self.inherited,
|
inherited: self.inherited,
|
||||||
contentType: contentType,
|
contentType: contentType,
|
||||||
library: self.library
|
library: self.library,
|
||||||
|
reloadPartials: self.reloadPartials
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// This source file is part of the Hummingbird server framework project
|
// 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
|
// Licensed under Apache License v2.0
|
||||||
//
|
//
|
||||||
// See LICENSE.txt for license information
|
// See LICENSE.txt for license information
|
||||||
@@ -27,17 +27,14 @@ extension MustacheLibrary {
|
|||||||
var templates: [String: MustacheTemplate] = [:]
|
var templates: [String: MustacheTemplate] = [:]
|
||||||
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 }
|
|
||||||
let string = String(decoding: data, as: Unicode.UTF8.self)
|
|
||||||
var template: MustacheTemplate
|
|
||||||
do {
|
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 {
|
} catch let error as MustacheTemplate.ParserError {
|
||||||
throw ParserError(filename: path, context: error.context, error: error.error)
|
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
|
return templates
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// This source file is part of the Hummingbird server framework project
|
// 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
|
// Licensed under Apache License v2.0
|
||||||
//
|
//
|
||||||
// See LICENSE.txt for license information
|
// See LICENSE.txt for license information
|
||||||
@@ -78,6 +78,21 @@ public struct MustacheLibrary: Sendable {
|
|||||||
return template.render(object, library: self)
|
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
|
/// Error returned by init() when parser fails
|
||||||
public struct ParserError: Swift.Error {
|
public struct ParserError: Swift.Error {
|
||||||
/// File error occurred in
|
/// File error occurred in
|
||||||
|
|||||||
30
Sources/Mustache/Template+FileSystem.swift
Normal file
30
Sources/Mustache/Template+FileSystem.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// This source file is part of the Hummingbird server framework project
|
// 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
|
// Licensed under Apache License v2.0
|
||||||
//
|
//
|
||||||
// See LICENSE.txt for license information
|
// See LICENSE.txt for license information
|
||||||
@@ -84,7 +84,20 @@ extension MustacheTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .partial(let name, let indentation, let overrides):
|
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))
|
return template.render(context: context.withPartial(indented: indentation, inheriting: overrides))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// This source file is part of the Hummingbird server framework project
|
// 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
|
// Licensed under Apache License v2.0
|
||||||
//
|
//
|
||||||
// See LICENSE.txt for license information
|
// See LICENSE.txt for license information
|
||||||
@@ -19,17 +19,44 @@ public struct MustacheTemplate: Sendable {
|
|||||||
/// - Throws: MustacheTemplate.Error
|
/// - Throws: MustacheTemplate.Error
|
||||||
public init(string: String) throws {
|
public init(string: String) throws {
|
||||||
self.tokens = try Self.parse(string)
|
self.tokens = try Self.parse(string)
|
||||||
|
self.filename = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render object using this template
|
/// 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
|
/// - Returns: Rendered text
|
||||||
public func render(_ object: Any, library: MustacheLibrary? = nil) -> String {
|
public func render(_ object: Any, library: MustacheLibrary? = nil) -> String {
|
||||||
self.render(context: .init(object, library: library))
|
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]) {
|
internal init(_ tokens: [Token]) {
|
||||||
self.tokens = tokens
|
self.tokens = tokens
|
||||||
|
self.filename = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Token: Sendable {
|
enum Token: Sendable {
|
||||||
@@ -45,4 +72,5 @@ public struct MustacheTemplate: Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tokens: [Token]
|
var tokens: [Token]
|
||||||
|
let filename: String?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//
|
//
|
||||||
// This source file is part of the Hummingbird server framework project
|
// 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
|
// Licensed under Apache License v2.0
|
||||||
//
|
//
|
||||||
// See LICENSE.txt for license information
|
// See LICENSE.txt for license information
|
||||||
@@ -71,4 +71,43 @@ final class LibraryTests: XCTestCase {
|
|||||||
XCTAssertEqual(parserError.context.columnNumber, 10)
|
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("<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 await MustacheLibrary(directory: "./templates")
|
||||||
|
let object = ["value": ["value1", "value2"]]
|
||||||
|
XCTAssertEqual(library.render(object, withTemplate: "test"), "<test><value>value1</value><value>value2</value></test>")
|
||||||
|
let mustache2 = Data("<test2>{{#value}}<value>{{.}}</value>{{/value}}</test2>".utf8)
|
||||||
|
try mustache2.write(to: URL(fileURLWithPath: "templates/test.mustache"))
|
||||||
|
XCTAssertEqual(library.render(object, withTemplate: "test", reload: true), "<test2><value>value1</value><value>value2</value></test2>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReloadPartial() async throws {
|
||||||
|
let fs = FileManager()
|
||||||
|
try? fs.createDirectory(atPath: "templates", withIntermediateDirectories: false)
|
||||||
|
let mustache = Data("<test>{{#value}}<value>{{.}}</value>{{/value}}</test>".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"), "<test><value>value1</value><value>value2</value></test>")
|
||||||
|
let mustache3 = Data("<test2>{{#value}}<value>{{.}}</value>{{/value}}</test2>".utf8)
|
||||||
|
try mustache3.write(to: URL(fileURLWithPath: "templates/test-partial.mustache"))
|
||||||
|
XCTAssertEqual(library.render(object, withTemplate: "test", reload: true), "<test2><value>value1</value><value>value2</value></test2>")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user