diff --git a/README.md b/README.md index 1af26ff..429386c 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,22 @@ A for loop allows you to iterate over an array found by variable lookup. {% 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 You can build a custom template tag. There are a couple of APIs to allow diff --git a/Stencil.xcodeproj/project.pbxproj b/Stencil.xcodeproj/project.pbxproj index f80a21a..5bb7515 100644 --- a/Stencil.xcodeproj/project.pbxproj +++ b/Stencil.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */; }; 27CE0AE01A50BF05004A105B /* TemplateLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */; }; 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 */; }; 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 */; }; @@ -75,6 +77,8 @@ 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoaderTests.swift; sourceTree = ""; }; 27CE0AE11A50BFAF004A105B /* PathKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PathKit.xcodeproj; path = PathKit/PathKit.xcodeproj; sourceTree = ""; }; 27CE0AF91A50C963004A105B /* test.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = test.html; sourceTree = ""; }; + 27CE0B001A50CBD1004A105B /* Include.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Include.swift; sourceTree = ""; }; + 27CE0B031A50CBEA004A105B /* IncludeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncludeTests.swift; sourceTree = ""; }; 27E2138A1A4CD5F50073E063 /* UniversalFramework_Base.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Base.xcconfig; sourceTree = ""; }; 27E2138B1A4CD5F50073E063 /* UniversalFramework_Framework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Framework.xcconfig; sourceTree = ""; }; 27E2138C1A4CD5F50073E063 /* UniversalFramework_Test.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = UniversalFramework_Test.xcconfig; sourceTree = ""; }; @@ -128,6 +132,22 @@ name = Products; sourceTree = ""; }; + 27CE0AFF1A50CBBF004A105B /* TemplateLoader */ = { + isa = PBXGroup; + children = ( + 27CE0B001A50CBD1004A105B /* Include.swift */, + ); + path = TemplateLoader; + sourceTree = ""; + }; + 27CE0B021A50CBEA004A105B /* TemplateLoader */ = { + isa = PBXGroup; + children = ( + 27CE0B031A50CBEA004A105B /* IncludeTests.swift */, + ); + path = TemplateLoader; + sourceTree = ""; + }; 27E213891A4CD5F50073E063 /* Configurations */ = { isa = PBXGroup; children = ( @@ -171,6 +191,7 @@ 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */, 7725B3CE19F94214002CF74B /* Tokenizer.swift */, 7725B3CC19F92B61002CF74B /* Variable.swift */, + 27CE0AFF1A50CBBF004A105B /* TemplateLoader */, 77FAAE5519F91E480029DC5E /* Supporting Files */, ); path = Stencil; @@ -195,6 +216,7 @@ 77EB082819FA85F2001870F1 /* LexerTests.swift */, 77EB082619F96E9C001870F1 /* TemplateTests.swift */, 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */, + 27CE0B021A50CBEA004A105B /* TemplateLoader */, 27CE0AF91A50C963004A105B /* test.html */, 77FAAE6219F91E480029DC5E /* Supporting Files */, ); @@ -345,6 +367,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 27CE0B011A50CBD1004A105B /* Include.swift in Sources */, 77FAAE6F19F920750029DC5E /* Context.swift in Sources */, 77EB082B19FA8600001870F1 /* Lexer.swift in Sources */, 7725B3CF19F94214002CF74B /* Tokenizer.swift in Sources */, @@ -367,6 +390,7 @@ 7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */, 77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */, 7725B3CB19F92B4F002CF74B /* VariableTests.swift in Sources */, + 27CE0B041A50CBEA004A105B /* IncludeTests.swift in Sources */, 77EB082919FA85F2001870F1 /* LexerTests.swift in Sources */, 77FAAE7119F9208C0029DC5E /* ContextTests.swift in Sources */, ); diff --git a/Stencil/Parser.swift b/Stencil/Parser.swift index d22b7ca..021e9f1 100644 --- a/Stencil/Parser.swift +++ b/Stencil/Parser.swift @@ -36,6 +36,7 @@ public class TokenParser { registerTag("if", IfNode.parse) registerTag("ifnot", IfNode.parse_ifnot) registerTag("now", NowNode.parse) + registerTag("include", IncludeNode.parse) } /// Registers a new template tag diff --git a/Stencil/TemplateLoader.swift b/Stencil/TemplateLoader.swift index 9fa18dc..978466e 100644 --- a/Stencil/TemplateLoader.swift +++ b/Stencil/TemplateLoader.swift @@ -11,7 +11,7 @@ import PathKit // A class for loading a template from disk public class TemplateLoader { - let paths:[Path] + public let paths:[Path] public init(paths:[Path]) { self.paths = paths diff --git a/Stencil/TemplateLoader/Include.swift b/Stencil/TemplateLoader/Include.swift new file mode 100644 index 0000000..c730c0c --- /dev/null +++ b/Stencil/TemplateLoader/Include.swift @@ -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) + } +} + diff --git a/StencilTests/TemplateLoader/IncludeTests.swift b/StencilTests/TemplateLoader/IncludeTests.swift new file mode 100644 index 0000000..5fcb5ba --- /dev/null +++ b/StencilTests/TemplateLoader/IncludeTests.swift @@ -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)") + } + } + +} diff --git a/StencilTests/test.html b/StencilTests/test.html index 7414573..d97eeb4 100644 --- a/StencilTests/test.html +++ b/StencilTests/test.html @@ -1 +1 @@ -Hello {{ target }}. \ No newline at end of file +Hello {{ target }}! \ No newline at end of file