Add an include tag

This commit is contained in:
Kyle Fuller
2014-12-29 00:18:58 +00:00
parent 1989c20932
commit fa34c2a98e
7 changed files with 162 additions and 2 deletions

View File

@@ -128,6 +128,22 @@ A for loop allows you to iterate over an array found by variable lookup.
{% endif %} {% endif %}
``` ```
#### include
You can include another template using the `include` tag.
```html+django
{% include "comment.html" %}
```
The `include` tag requires a TemplateLoader to be found inside your context with the paths, or bundles used to lookup the template.
```swift
let context = Context(dictionary: [
"loader": TemplateLoader(bundle:[NSBundle.mainBundle()])
])
```
#### Building custom tags #### Building custom tags
You can build a custom template tag. There are a couple of APIs to allow You can build a custom template tag. There are a couple of APIs to allow

View File

@@ -10,6 +10,8 @@
27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */; }; 27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */; };
27CE0AE01A50BF05004A105B /* TemplateLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */; }; 27CE0AE01A50BF05004A105B /* TemplateLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */; };
27CE0AFA1A50C963004A105B /* test.html in Resources */ = {isa = PBXBuildFile; fileRef = 27CE0AF91A50C963004A105B /* test.html */; }; 27CE0AFA1A50C963004A105B /* test.html in Resources */ = {isa = PBXBuildFile; fileRef = 27CE0AF91A50C963004A105B /* test.html */; };
27CE0B011A50CBD1004A105B /* Include.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B001A50CBD1004A105B /* Include.swift */; };
27CE0B041A50CBEA004A105B /* IncludeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0B031A50CBEA004A105B /* IncludeTests.swift */; };
27E2138D1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 27E2138A1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig */; }; 27E2138D1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 27E2138A1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig */; };
27E2138E1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig */; }; 27E2138E1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig */; };
27E2138F1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */; }; 27E2138F1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */; };
@@ -75,6 +77,8 @@
27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoaderTests.swift; sourceTree = "<group>"; }; 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoaderTests.swift; sourceTree = "<group>"; };
27CE0AE11A50BFAF004A105B /* PathKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PathKit.xcodeproj; path = PathKit/PathKit.xcodeproj; sourceTree = "<group>"; }; 27CE0AE11A50BFAF004A105B /* PathKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PathKit.xcodeproj; path = PathKit/PathKit.xcodeproj; sourceTree = "<group>"; };
27CE0AF91A50C963004A105B /* test.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = test.html; sourceTree = "<group>"; }; 27CE0AF91A50C963004A105B /* test.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = test.html; sourceTree = "<group>"; };
27CE0B001A50CBD1004A105B /* Include.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Include.swift; sourceTree = "<group>"; };
27CE0B031A50CBEA004A105B /* IncludeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncludeTests.swift; sourceTree = "<group>"; };
27E2138A1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Base.xcconfig; sourceTree = "<group>"; }; 27E2138A1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Base.xcconfig; sourceTree = "<group>"; };
27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Framework.xcconfig; sourceTree = "<group>"; }; 27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Framework.xcconfig; sourceTree = "<group>"; };
27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Test.xcconfig; sourceTree = "<group>"; }; 27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Test.xcconfig; sourceTree = "<group>"; };
@@ -128,6 +132,22 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
27CE0AFF1A50CBBF004A105B /* TemplateLoader */ = {
isa = PBXGroup;
children = (
27CE0B001A50CBD1004A105B /* Include.swift */,
);
path = TemplateLoader;
sourceTree = "<group>";
};
27CE0B021A50CBEA004A105B /* TemplateLoader */ = {
isa = PBXGroup;
children = (
27CE0B031A50CBEA004A105B /* IncludeTests.swift */,
);
path = TemplateLoader;
sourceTree = "<group>";
};
27E213891A4CD5F50073E063 /* Configurations */ = { 27E213891A4CD5F50073E063 /* Configurations */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -171,6 +191,7 @@
27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */, 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */,
7725B3CE19F94214002CF74B /* Tokenizer.swift */, 7725B3CE19F94214002CF74B /* Tokenizer.swift */,
7725B3CC19F92B61002CF74B /* Variable.swift */, 7725B3CC19F92B61002CF74B /* Variable.swift */,
27CE0AFF1A50CBBF004A105B /* TemplateLoader */,
77FAAE5519F91E480029DC5E /* Supporting Files */, 77FAAE5519F91E480029DC5E /* Supporting Files */,
); );
path = Stencil; path = Stencil;
@@ -195,6 +216,7 @@
77EB082819FA85F2001870F1 /* LexerTests.swift */, 77EB082819FA85F2001870F1 /* LexerTests.swift */,
77EB082619F96E9C001870F1 /* TemplateTests.swift */, 77EB082619F96E9C001870F1 /* TemplateTests.swift */,
27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */, 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */,
27CE0B021A50CBEA004A105B /* TemplateLoader */,
27CE0AF91A50C963004A105B /* test.html */, 27CE0AF91A50C963004A105B /* test.html */,
77FAAE6219F91E480029DC5E /* Supporting Files */, 77FAAE6219F91E480029DC5E /* Supporting Files */,
); );
@@ -345,6 +367,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
27CE0B011A50CBD1004A105B /* Include.swift in Sources */,
77FAAE6F19F920750029DC5E /* Context.swift in Sources */, 77FAAE6F19F920750029DC5E /* Context.swift in Sources */,
77EB082B19FA8600001870F1 /* Lexer.swift in Sources */, 77EB082B19FA8600001870F1 /* Lexer.swift in Sources */,
7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */, 7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */,
@@ -367,6 +390,7 @@
7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */, 7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */,
77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */, 77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */,
7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */, 7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */,
27CE0B041A50CBEA004A105B /* IncludeTests.swift in Sources */,
77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */, 77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */,
77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */, 77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */,
); );

View File

@@ -36,6 +36,7 @@ public class TokenParser {
registerTag("if", IfNode.parse) registerTag("if", IfNode.parse)
registerTag("ifnot", IfNode.parse_ifnot) registerTag("ifnot", IfNode.parse_ifnot)
registerTag("now", NowNode.parse) registerTag("now", NowNode.parse)
registerTag("include", IncludeNode.parse)
} }
/// Registers a new template tag /// Registers a new template tag

View File

@@ -11,7 +11,7 @@ import PathKit
// A class for loading a template from disk // A class for loading a template from disk
public class TemplateLoader { public class TemplateLoader {
let paths:[Path] public let paths:[Path]
public init(paths:[Path]) { public init(paths:[Path]) {
self.paths = paths self.paths = paths

View File

@@ -0,0 +1,44 @@
import Foundation
import PathKit
extension String : Error {
public var description:String {
return self
}
}
public class IncludeNode : Node {
public let templateName:String
public class func parse(parser:TokenParser, token:Token) -> TokenParser.Result {
let bits = token.contents.componentsSeparatedByString("\"")
if bits.count != 3 {
return .Error(error:NodeError(token: token, message: "Tag takes one argument, the template file to be included"))
}
return .Success(node:IncludeNode(templateName: bits[1]))
}
public init(templateName:String) {
self.templateName = templateName
}
public func render(context: Context) -> Result {
if let loader = context["loader"] as? TemplateLoader {
if let template = loader.loadTemplate(templateName) {
return template.render(context)
}
let paths:String = join(", ", loader.paths.map { path in
return path.description
})
let error = "Template '\(templateName)' not found in \(paths)"
return .Error(error)
}
let error = "Template loader not in context"
return .Error(error)
}
}

View File

@@ -0,0 +1,75 @@
import Foundation
import XCTest
import Stencil
import PathKit
class IncludeTests: NodeTests {
var loader:TemplateLoader!
override func setUp() {
super.setUp()
let path = (Path(__FILE__) + Path("../..")).absolute()
loader = TemplateLoader(paths: [path])
}
// MARK: Parsing
func testParseMissingTemplate() {
let tokens = [ Token.Block(value: "include") ]
let parser = TokenParser(tokens: tokens)
assertFailure(parser.parse(), "include: Tag takes one argument, the template file to be included")
}
func testParse() {
let tokens = [ Token.Block(value: "include \"test.html\"") ]
let parser = TokenParser(tokens: tokens)
assertSuccess(parser.parse()) { nodes in
let node = nodes.first! as IncludeNode
XCTAssertEqual(nodes.count, 1)
XCTAssertEqual(node.templateName, "test.html")
}
}
// MARK: Render
func testRenderWithoutLoader() {
let node = IncludeNode(templateName: "test.html")
let result = node.render(Context())
switch result {
case .Success(let string):
XCTAssert(false, "Unexpected error")
case .Error(let error):
XCTAssertEqual("\(error)", "Template loader not in context")
}
}
func testRenderWithoutTemplateNamed() {
let node = IncludeNode(templateName: "unknown.html")
let result = node.render(Context(dictionary:["loader":loader]))
switch result {
case .Success(let string):
XCTAssert(false, "Unexpected error")
case .Error(let error):
XCTAssertTrue("\(error)".hasPrefix("Template 'unknown.html' not found"))
}
}
func testRender() {
let node = IncludeNode(templateName: "test.html")
let result = node.render(Context(dictionary:["loader":loader, "target": "World"]))
switch result {
case .Success(let string):
XCTAssertEqual(string, "Hello World!")
case .Error(let error):
XCTAssert(false, "Unexpected error: \(error)")
}
}
}

View File

@@ -1 +1 @@
Hello {{ target }}. Hello {{ target }}!