refactor: Introducing Environments
This commit is contained in:
28
CHANGELOG.md
28
CHANGELOG.md
@@ -4,11 +4,39 @@
|
|||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|
||||||
|
- It is no longer possible to create `Context` objects. Instead, you can pass a
|
||||||
|
dictionary directly to a `Template`s `render` method.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- try template.render(Context(dictionary: ["name": "Kyle"]))
|
||||||
|
+ try template.render(["name": "Kyle"])
|
||||||
|
```
|
||||||
|
|
||||||
|
- Template loader are no longer passed into a `Context`, instead you will need
|
||||||
|
to pass the `Loader` to an `Environment` and create a template from the
|
||||||
|
`Environment`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
let loader = FileSystemLoader(paths: ["templates/"])
|
||||||
|
|
||||||
|
- let template = loader.loadTemplate(name: "index.html")
|
||||||
|
- try template.render(Context(dictionary: ["loader": loader]))
|
||||||
|
|
||||||
|
+ let environment = Environment(loader: loader)
|
||||||
|
+ try environment.renderTemplate(name: "index.html")
|
||||||
|
```
|
||||||
|
|
||||||
- `Loader`s will now throw a `TemplateDoesNotExist` error when a template
|
- `Loader`s will now throw a `TemplateDoesNotExist` error when a template
|
||||||
is not found.
|
is not found.
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
- `Environment` is a new way to load templates. You can configure an
|
||||||
|
environment with custom template filters, tags and loaders and then create a
|
||||||
|
template from an environment.
|
||||||
|
|
||||||
|
Environment also provides a convenience method to render a template directly.
|
||||||
|
|
||||||
- `FileSystemLoader` will now ensure that template paths are within the base
|
- `FileSystemLoader` will now ensure that template paths are within the base
|
||||||
path. Any template names that try to escape the base path will raise a
|
path. Any template names that try to escape the base path will raise a
|
||||||
`SuspiciousFileOperation` error.
|
`SuspiciousFileOperation` error.
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -19,25 +19,24 @@ There are {{ articles.count }} articles.
|
|||||||
```
|
```
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
import Stencil
|
||||||
|
|
||||||
struct Article {
|
struct Article {
|
||||||
let title: String
|
let title: String
|
||||||
let author: String
|
let author: String
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = Context(dictionary: [
|
let context = [
|
||||||
"articles": [
|
"articles": [
|
||||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||||
]
|
]
|
||||||
])
|
]
|
||||||
|
|
||||||
do {
|
let environment = Environment(loader: FileSystemLoader(paths: ["templates/"])
|
||||||
let template = try Template(named: "template.html")
|
let rendered = try environment.renderTemplate(name: context)
|
||||||
let rendered = try template.render(context)
|
|
||||||
print(rendered)
|
print(rendered)
|
||||||
} catch {
|
|
||||||
print("Failed to render template \(error)")
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
/// A container for template variables.
|
/// A container for template variables.
|
||||||
public class Context {
|
public class Context {
|
||||||
var dictionaries: [[String: Any?]]
|
var dictionaries: [[String: Any?]]
|
||||||
|
|
||||||
|
public let environment: Environment
|
||||||
let namespace: Namespace
|
let namespace: Namespace
|
||||||
|
|
||||||
/// Initialise a Context with an optional dictionary and optional namespace
|
init(dictionary: [String: Any]? = nil, namespace: Namespace? = nil, environment: Environment? = nil) {
|
||||||
public init(dictionary: [String: Any]? = nil, namespace: Namespace = Namespace()) {
|
|
||||||
if let dictionary = dictionary {
|
if let dictionary = dictionary {
|
||||||
dictionaries = [dictionary]
|
dictionaries = [dictionary]
|
||||||
} else {
|
} else {
|
||||||
dictionaries = []
|
dictionaries = []
|
||||||
}
|
}
|
||||||
|
|
||||||
self.namespace = namespace
|
self.namespace = namespace ?? environment?.namespace ?? Namespace()
|
||||||
|
self.environment = environment ?? Environment()
|
||||||
}
|
}
|
||||||
|
|
||||||
public subscript(key: String) -> Any? {
|
public subscript(key: String) -> Any? {
|
||||||
|
|||||||
36
Sources/Environment.swift
Normal file
36
Sources/Environment.swift
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
public struct Environment {
|
||||||
|
var namespace: Namespace
|
||||||
|
|
||||||
|
public var loader: Loader?
|
||||||
|
|
||||||
|
public init(loader: Loader? = nil, namespace: Namespace? = nil) {
|
||||||
|
self.loader = loader
|
||||||
|
self.namespace = namespace ?? Namespace()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func loadTemplate(name: String) throws -> Template {
|
||||||
|
if let loader = loader {
|
||||||
|
return try loader.loadTemplate(name: name, environment: self)
|
||||||
|
} else {
|
||||||
|
throw TemplateDoesNotExist(templateNames: [name], loader: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func loadTemplate(names: [String]) throws -> Template {
|
||||||
|
if let loader = loader {
|
||||||
|
return try loader.loadTemplate(names: names, environment: self)
|
||||||
|
} else {
|
||||||
|
throw TemplateDoesNotExist(templateNames: names, loader: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func renderTemplate(name: String, context: [String: Any]? = nil) throws -> String {
|
||||||
|
let template = try loadTemplate(name: name)
|
||||||
|
return try template.render(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func renderTemplate(string: String, context: [String: Any]? = nil) throws -> String {
|
||||||
|
let template = Template(templateString: string, environment: self)
|
||||||
|
return try template.render(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
public class TemplateDoesNotExist: Error, CustomStringConvertible {
|
||||||
let templateNames: [String]
|
let templateNames: [String]
|
||||||
let loader: Loader
|
let loader: Loader?
|
||||||
|
|
||||||
public init(templateNames: [String], loader: Loader) {
|
public init(templateNames: [String], loader: Loader? = nil) {
|
||||||
self.templateNames = templateNames
|
self.templateNames = templateNames
|
||||||
self.loader = loader
|
self.loader = loader
|
||||||
}
|
}
|
||||||
|
|
||||||
public var description: String {
|
public var description: String {
|
||||||
let templates = templateNames.joined(separator: ", ")
|
let templates = templateNames.joined(separator: ", ")
|
||||||
|
|
||||||
|
if let loader = loader {
|
||||||
return "Template named `\(templates)` does not exist in loader \(loader)"
|
return "Template named `\(templates)` does not exist in loader \(loader)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "Template named `\(templates)` does not exist. No loaders found"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,16 +19,15 @@ class IncludeNode : NodeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
guard let loader = context["loader"] as? Loader else {
|
|
||||||
throw TemplateSyntaxError("Template loader not in context")
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let templateName = try self.templateName.resolve(context) as? String else {
|
guard let templateName = try self.templateName.resolve(context) as? String else {
|
||||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
let template = try loader.loadTemplate(name: templateName)
|
let template = try context.environment.loadTemplate(name: templateName)
|
||||||
|
|
||||||
|
return try context.push {
|
||||||
return try template.render(context)
|
return try template.render(context)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,15 +59,11 @@ class ExtendsNode : NodeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func render(_ context: Context) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
guard let loader = context["loader"] as? Loader else {
|
|
||||||
throw TemplateSyntaxError("Template loader not in context")
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let templateName = try self.templateName.resolve(context) as? String else {
|
guard let templateName = try self.templateName.resolve(context) as? String else {
|
||||||
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
let template = try loader.loadTemplate(name: templateName)
|
let template = try context.environment.loadTemplate(name: templateName)
|
||||||
|
|
||||||
let blockContext: BlockContext
|
let blockContext: BlockContext
|
||||||
if let context = context[BlockContext.contextKey] as? BlockContext {
|
if let context = context[BlockContext.contextKey] as? BlockContext {
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import PathKit
|
|||||||
|
|
||||||
|
|
||||||
public protocol Loader {
|
public protocol Loader {
|
||||||
func loadTemplate(name: String) throws -> Template
|
func loadTemplate(name: String, environment: Environment) throws -> Template
|
||||||
func loadTemplate(names: [String]) throws -> Template
|
func loadTemplate(names: [String], environment: Environment) throws -> Template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension Loader {
|
extension Loader {
|
||||||
func loadTemplate(names: [String]) throws -> Template {
|
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||||
for name in names {
|
for name in names {
|
||||||
do {
|
do {
|
||||||
return try loadTemplate(name: name)
|
return try loadTemplate(name: name, environment: environment)
|
||||||
} catch is TemplateDoesNotExist {
|
} catch is TemplateDoesNotExist {
|
||||||
continue
|
continue
|
||||||
} catch {
|
} catch {
|
||||||
@@ -43,7 +43,7 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
|||||||
return "FileSystemLoader(\(paths))"
|
return "FileSystemLoader(\(paths))"
|
||||||
}
|
}
|
||||||
|
|
||||||
public func loadTemplate(name: String) throws -> Template {
|
public func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let templatePath = try path.safeJoin(path: Path(name))
|
let templatePath = try path.safeJoin(path: Path(name))
|
||||||
|
|
||||||
@@ -51,19 +51,19 @@ public class FileSystemLoader: Loader, CustomStringConvertible {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return try Template(path: templatePath)
|
return try Template(path: templatePath, environment: environment, name: name)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw TemplateDoesNotExist(templateNames: [name], loader: self)
|
throw TemplateDoesNotExist(templateNames: [name], loader: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func loadTemplate(names: [String]) throws -> Template {
|
public func loadTemplate(names: [String], environment: Environment) throws -> Template {
|
||||||
for path in paths {
|
for path in paths {
|
||||||
for templateName in names {
|
for templateName in names {
|
||||||
let templatePath = try path.safeJoin(path: Path(templateName))
|
let templatePath = try path.safeJoin(path: Path(templateName))
|
||||||
|
|
||||||
if templatePath.exists {
|
if templatePath.exists {
|
||||||
return try Template(path: templatePath)
|
return try Template(path: templatePath, environment: environment, name: templateName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,17 @@ let NSFileNoSuchFileError = 4
|
|||||||
|
|
||||||
/// A class representing a template
|
/// A class representing a template
|
||||||
public class Template: ExpressibleByStringLiteral {
|
public class Template: ExpressibleByStringLiteral {
|
||||||
|
let environment: Environment
|
||||||
let tokens: [Token]
|
let tokens: [Token]
|
||||||
|
|
||||||
|
/// The name of the loaded Template if the Template was loaded from a Loader
|
||||||
|
public let name: String?
|
||||||
|
|
||||||
/// Create a template with a template string
|
/// Create a template with a template string
|
||||||
public init(templateString: String) {
|
public init(templateString: String, environment: Environment? = nil, name: String? = nil) {
|
||||||
|
self.environment = environment ?? Environment()
|
||||||
|
self.name = name
|
||||||
|
|
||||||
let lexer = Lexer(templateString: templateString)
|
let lexer = Lexer(templateString: templateString)
|
||||||
tokens = lexer.tokenize()
|
tokens = lexer.tokenize()
|
||||||
}
|
}
|
||||||
@@ -31,11 +38,13 @@ public class Template: ExpressibleByStringLiteral {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a template with a file found at the given path
|
/// Create a template with a file found at the given path
|
||||||
public convenience init(path: Path) throws {
|
public convenience init(path: Path, environment: Environment? = nil, name: String? = nil) throws {
|
||||||
self.init(templateString: try path.read())
|
self.init(templateString: try path.read(), environment: environment, name: name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a template with a template string literal
|
// MARK: ExpressibleByStringLiteral
|
||||||
|
|
||||||
|
// Create a templaVte with a template string literal
|
||||||
public convenience required init(stringLiteral value: String) {
|
public convenience required init(stringLiteral value: String) {
|
||||||
self.init(templateString: value)
|
self.init(templateString: value)
|
||||||
}
|
}
|
||||||
@@ -50,11 +59,16 @@ public class Template: ExpressibleByStringLiteral {
|
|||||||
self.init(stringLiteral: value)
|
self.init(stringLiteral: value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the given template
|
/// Render the given template with a context
|
||||||
public func render(_ context: Context? = nil) throws -> String {
|
func render(_ context: Context) throws -> String {
|
||||||
let context = context ?? Context()
|
let context = context ?? Context(environment: environment)
|
||||||
let parser = TokenParser(tokens: tokens, namespace: context.namespace)
|
let parser = TokenParser(tokens: tokens, namespace: context.namespace)
|
||||||
let nodes = try parser.parse()
|
let nodes = try parser.parse()
|
||||||
return try renderNodes(nodes, context)
|
return try renderNodes(nodes, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render the given template
|
||||||
|
public func render(_ dictionary: [String: Any]? = nil) throws -> String {
|
||||||
|
return try render(Context(dictionary: dictionary, environment: environment))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
func testContext() {
|
func testContext() {
|
||||||
|
|||||||
41
Tests/StencilTests/EnvironmentSpec.swift
Normal file
41
Tests/StencilTests/EnvironmentSpec.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import Spectre
|
||||||
|
import Stencil
|
||||||
|
|
||||||
|
|
||||||
|
func testEnvironment() {
|
||||||
|
describe("Environment") {
|
||||||
|
let environment = Environment(loader: ExampleLoader())
|
||||||
|
|
||||||
|
$0.it("can load a template from a name") {
|
||||||
|
let template = try environment.loadTemplate(name: "example.html")
|
||||||
|
try expect(template.name) == "example.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can load a template from a names") {
|
||||||
|
let template = try environment.loadTemplate(names: ["first.html", "example.html"])
|
||||||
|
try expect(template.name) == "example.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can render a template from a string") {
|
||||||
|
let result = try environment.renderTemplate(string: "Hello World")
|
||||||
|
try expect(result) == "Hello World"
|
||||||
|
}
|
||||||
|
|
||||||
|
$0.it("can render a template from a file") {
|
||||||
|
let result = try environment.renderTemplate(name: "example.html")
|
||||||
|
try expect(result) == "Hello World!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fileprivate class ExampleLoader: Loader {
|
||||||
|
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||||
|
if name == "example.html" {
|
||||||
|
return Template(templateString: "Hello World!", environment: environment, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw TemplateDoesNotExist(templateNames: [name], loader: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
func testFilter() {
|
func testFilter() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ func testInclude() {
|
|||||||
describe("Include") {
|
describe("Include") {
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
let loader = FileSystemLoader(paths: [path])
|
let loader = FileSystemLoader(paths: [path])
|
||||||
|
let environment = Environment(loader: loader)
|
||||||
|
|
||||||
$0.describe("parsing") {
|
$0.describe("parsing") {
|
||||||
$0.it("throws an error when no template is given") {
|
$0.it("throws an error when no template is given") {
|
||||||
@@ -35,7 +36,7 @@ func testInclude() {
|
|||||||
do {
|
do {
|
||||||
_ = try node.render(Context())
|
_ = try node.render(Context())
|
||||||
} catch {
|
} catch {
|
||||||
try expect("\(error)") == "Template loader not in context"
|
try expect("\(error)") == "Template named `test.html` does not exist. No loaders found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ func testInclude() {
|
|||||||
let node = IncludeNode(templateName: Variable("\"unknown.html\""))
|
let node = IncludeNode(templateName: Variable("\"unknown.html\""))
|
||||||
|
|
||||||
do {
|
do {
|
||||||
_ = try node.render(Context(dictionary: ["loader": loader]))
|
_ = try node.render(Context(environment: environment))
|
||||||
} catch {
|
} catch {
|
||||||
try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue()
|
try expect("\(error)".hasPrefix("Template named `unknown.html` does not exist in loader")).to.beTrue()
|
||||||
}
|
}
|
||||||
@@ -51,7 +52,7 @@ func testInclude() {
|
|||||||
|
|
||||||
$0.it("successfully renders a found included template") {
|
$0.it("successfully renders a found included template") {
|
||||||
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
let node = IncludeNode(templateName: Variable("\"test.html\""))
|
||||||
let context = Context(dictionary: ["loader":loader, "target": "World"])
|
let context = Context(dictionary: ["target": "World"], environment: environment)
|
||||||
let value = try node.render(context)
|
let value = try node.render(context)
|
||||||
try expect(value) == "Hello World!"
|
try expect(value) == "Hello World!"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,16 @@ func testInheritence() {
|
|||||||
describe("Inheritence") {
|
describe("Inheritence") {
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
let loader = FileSystemLoader(paths: [path])
|
let loader = FileSystemLoader(paths: [path])
|
||||||
|
let environment = Environment(loader: loader)
|
||||||
|
|
||||||
$0.it("can inherit from another template") {
|
$0.it("can inherit from another template") {
|
||||||
let context = Context(dictionary: ["loader": loader])
|
let template = try environment.loadTemplate(name: "child.html")
|
||||||
let template = try loader.loadTemplate(name: "child.html")
|
try expect(try template.render()) == "Header\nChild"
|
||||||
try expect(try template.render(context)) == "Header\nChild"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can inherit from another template inheriting from another template") {
|
$0.it("can inherit from another template inheriting from another template") {
|
||||||
let context = Context(dictionary: ["loader": loader])
|
let template = try environment.loadTemplate(name: "child-child.html")
|
||||||
let template = try loader.loadTemplate(name: "child-child.html")
|
try expect(try template.render()) == "Child Child Header\nChild"
|
||||||
try expect(try template.render(context)) == "Child Child Header\nChild"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,25 +7,26 @@ func testTemplateLoader() {
|
|||||||
describe("FileSystemLoader") {
|
describe("FileSystemLoader") {
|
||||||
let path = Path(#file) + ".." + "fixtures"
|
let path = Path(#file) + ".." + "fixtures"
|
||||||
let loader = FileSystemLoader(paths: [path])
|
let loader = FileSystemLoader(paths: [path])
|
||||||
|
let environment = Environment(loader: loader)
|
||||||
|
|
||||||
$0.it("errors when a template cannot be found") {
|
$0.it("errors when a template cannot be found") {
|
||||||
try expect(try loader.loadTemplate(name: "unknown.html")).toThrow()
|
try expect(try environment.loadTemplate(name: "unknown.html")).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when an array of templates cannot be found") {
|
$0.it("errors when an array of templates cannot be found") {
|
||||||
try expect(try loader.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
try expect(try environment.loadTemplate(names: ["unknown.html", "unknown2.html"])).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can load a template from a file") {
|
$0.it("can load a template from a file") {
|
||||||
_ = try loader.loadTemplate(name: "test.html")
|
_ = try environment.loadTemplate(name: "test.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when loading absolute file outside of the selected path") {
|
$0.it("errors when loading absolute file outside of the selected path") {
|
||||||
try expect(try loader.loadTemplate(name: "/etc/hosts")).toThrow()
|
try expect(try environment.loadTemplate(name: "/etc/hosts")).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("errors when loading relative file outside of the selected path") {
|
$0.it("errors when loading relative file outside of the selected path") {
|
||||||
try expect(try loader.loadTemplate(name: "../LoaderSpec.swift")).toThrow()
|
try expect(try environment.loadTemplate(name: "../LoaderSpec.swift")).toThrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
class ErrorNode : NodeType {
|
class ErrorNode : NodeType {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
func testTokenParser() {
|
func testTokenParser() {
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ func testStencil() {
|
|||||||
" - {{ article.title }} by {{ article.author }}.\n" +
|
" - {{ article.title }} by {{ article.author }}.\n" +
|
||||||
"{% endfor %}\n"
|
"{% endfor %}\n"
|
||||||
|
|
||||||
let context = Context(dictionary: [
|
let context = [
|
||||||
"articles": [
|
"articles": [
|
||||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||||
]
|
]
|
||||||
])
|
]
|
||||||
|
|
||||||
let template = Template(templateString: templateString)
|
let template = Template(templateString: templateString)
|
||||||
let result = try template.render(context)
|
let result = try template.render(context)
|
||||||
@@ -45,28 +45,27 @@ func testStencil() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a custom template tag") {
|
$0.it("can render a custom template tag") {
|
||||||
let templateString = "{% custom %}"
|
|
||||||
let template = Template(templateString: templateString)
|
|
||||||
|
|
||||||
let namespace = Namespace()
|
let namespace = Namespace()
|
||||||
namespace.registerTag("custom") { parser, token in
|
namespace.registerTag("custom") { parser, token in
|
||||||
return CustomNode()
|
return CustomNode()
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = try template.render(Context(namespace: namespace))
|
let environment = Environment(namespace: namespace)
|
||||||
|
let result = try environment.renderTemplate(string: "{% custom %}")
|
||||||
|
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a simple custom tag") {
|
$0.it("can render a simple custom tag") {
|
||||||
let templateString = "{% custom %}"
|
|
||||||
let template = Template(templateString: templateString)
|
|
||||||
|
|
||||||
let namespace = Namespace()
|
let namespace = Namespace()
|
||||||
namespace.registerSimpleTag("custom") { context in
|
namespace.registerSimpleTag("custom") { context in
|
||||||
return "Hello World"
|
return "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
try expect(try template.render(Context(namespace: namespace))) == "Hello World"
|
let environment = Environment(namespace: namespace)
|
||||||
|
let result = try environment.renderTemplate(string: "{% custom %}")
|
||||||
|
|
||||||
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,14 @@ import Stencil
|
|||||||
func testTemplate() {
|
func testTemplate() {
|
||||||
describe("Template") {
|
describe("Template") {
|
||||||
$0.it("can render a template from a string") {
|
$0.it("can render a template from a string") {
|
||||||
let context = Context(dictionary: [ "name": "Kyle" ])
|
|
||||||
let template = Template(templateString: "Hello World")
|
let template = Template(templateString: "Hello World")
|
||||||
let result = try template.render(context)
|
let result = try template.render([ "name": "Kyle" ])
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
|
|
||||||
$0.it("can render a template from a string literal") {
|
$0.it("can render a template from a string literal") {
|
||||||
let context = Context(dictionary: [ "name": "Kyle" ])
|
|
||||||
let template: Template = "Hello World"
|
let template: Template = "Hello World"
|
||||||
let result = try template.render(context)
|
let result = try template.render([ "name": "Kyle" ])
|
||||||
try expect(result) == "Hello World"
|
try expect(result) == "Hello World"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Spectre
|
import Spectre
|
||||||
import Stencil
|
@testable import Stencil
|
||||||
|
|
||||||
|
|
||||||
#if os(OSX)
|
#if os(OSX)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public func stencilTests() {
|
|||||||
testInclude()
|
testInclude()
|
||||||
testInheritence()
|
testInheritence()
|
||||||
testFilterTag()
|
testFilterTag()
|
||||||
|
testEnvironment()
|
||||||
testStencil()
|
testStencil()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ template. It’s somewhat like a dictionary, however you can push and pop to
|
|||||||
scope variables. So that means that when iterating over a for loop, you can
|
scope variables. So that means that when iterating over a for loop, you can
|
||||||
push a new scope into the context to store any variables local to the scope.
|
push a new scope into the context to store any variables local to the scope.
|
||||||
|
|
||||||
You can initialise a ``Context`` with a ``Dictionary``.
|
|
||||||
|
|
||||||
.. code-block:: swift
|
|
||||||
|
|
||||||
Context(dictionary: [String: Any]? = nil)
|
|
||||||
|
|
||||||
API
|
API
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|||||||
43
docs/api/environment.rst
Normal file
43
docs/api/environment.rst
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
Environment
|
||||||
|
===========
|
||||||
|
|
||||||
|
An environment contains shared configuration such as custom filters and tags
|
||||||
|
along with template loaders.
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
let environment = Environment()
|
||||||
|
|
||||||
|
You can optionally provide a loader or namespace when creating an environment:
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
let environment = Environment(loader: ..., namespace: ...)
|
||||||
|
|
||||||
|
Rendering a Template
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Environment providences coninience methods to render a template either from a
|
||||||
|
string or a template loader.
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
let template = "Hello {{ name }}"
|
||||||
|
let context = ["name": "Kyle"]
|
||||||
|
let rendered = environment.render(templateString: template, context: context)
|
||||||
|
|
||||||
|
Rendering a template from the configured loader:
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
let context = ["name": "Kyle"]
|
||||||
|
let rendered = environment.render(templateName: "example.html", context: context)
|
||||||
|
|
||||||
|
Loading a Template
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Environment provides an API to load a template from the configured loader.
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
let template = try environment.loadTemplate(name: "example.html")
|
||||||
38
docs/api/loader.rst
Normal file
38
docs/api/loader.rst
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
Loader
|
||||||
|
======
|
||||||
|
|
||||||
|
Loaders are responsible for loading templates from a resource such as the file
|
||||||
|
system.
|
||||||
|
|
||||||
|
Stencil provides a ``FileSytemLoader`` which allows you to load a template
|
||||||
|
directly from the file system.
|
||||||
|
|
||||||
|
``Loader`` is a protocol, so you can implement your own compatible loaders. You
|
||||||
|
will need to implement a ``loadTemplate`` method to load the template,
|
||||||
|
throwing a ``TemplateDoesNotExist`` when the template is not found.
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
class ExampleMemoryLoader: Loader {
|
||||||
|
func loadTemplate(name: String, environment: Environment) throws -> Template {
|
||||||
|
if name == "index.html" {
|
||||||
|
return Template(templateString: "Hello", environment: environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw TemplateDoesNotExist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSystemLoader
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Loads templates from the file system. This loader can find templates in folders
|
||||||
|
on the file system.
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
FileSystemLoader(paths: ["./templates"])
|
||||||
|
|
||||||
|
.. code-block:: swift
|
||||||
|
|
||||||
|
FileSystemLoader(bundle: [Bundle.main])
|
||||||
@@ -206,13 +206,13 @@ You can include another template using the `include` tag.
|
|||||||
|
|
||||||
{% include "comment.html" %}
|
{% include "comment.html" %}
|
||||||
|
|
||||||
The `include` tag requires a FileSystemLoader to be found inside your context with the paths, or bundles used to lookup the template.
|
The `include` tag requires you to provide a loader which will be used to lookup
|
||||||
|
the template.
|
||||||
|
|
||||||
.. code-block:: swift
|
.. code-block:: swift
|
||||||
|
|
||||||
let context = Context(dictionary: [
|
let environment = Environment(bundle: [Bundle.main])
|
||||||
"loader": FileSystemLoader(bundle: [NSBundle.mainBundle()])
|
let template = environment.loadTemplate(name: "index.html")
|
||||||
])
|
|
||||||
|
|
||||||
``extends``
|
``extends``
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ namespace which contains all filters and tags available to the template.
|
|||||||
|
|
||||||
let namespace = Namespace()
|
let namespace = Namespace()
|
||||||
// Register your filters and tags with the namespace
|
// Register your filters and tags with the namespace
|
||||||
let rendered = try template.render(context, namespace: namespace)
|
|
||||||
|
let environment = Environment(namespace: namespace)
|
||||||
|
try environment.renderTemplate(name: "example.html")
|
||||||
|
|
||||||
Custom Filters
|
Custom Filters
|
||||||
--------------
|
--------------
|
||||||
|
|||||||
@@ -17,17 +17,19 @@ feel right at home with Stencil.
|
|||||||
|
|
||||||
.. code-block:: swift
|
.. code-block:: swift
|
||||||
|
|
||||||
|
import Stencil
|
||||||
|
|
||||||
struct Article {
|
struct Article {
|
||||||
let title: String
|
let title: String
|
||||||
let author: String
|
let author: String
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = Context(dictionary: [
|
let context = [
|
||||||
"articles": [
|
"articles": [
|
||||||
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
Article(title: "Migrating from OCUnit to XCTest", author: "Kyle Fuller"),
|
||||||
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
Article(title: "Memory Management with ARC", author: "Kyle Fuller"),
|
||||||
]
|
]
|
||||||
])
|
]
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let template = try Template(named: "template.html")
|
let template = try Template(named: "template.html")
|
||||||
|
|||||||
Reference in New Issue
Block a user