From 620154e7211403073de9b58cdde281854a52207b Mon Sep 17 00:00:00 2001 From: Kyle Fuller Date: Mon, 29 Jun 2015 18:36:27 -0700 Subject: [PATCH] Include template inheritence Closes #15 --- Stencil.xcodeproj/project.pbxproj | 16 +++ Stencil/Context.swift | 4 +- Stencil/Parser.swift | 2 + Stencil/TemplateLoader/Inheritence.swift | 124 ++++++++++++++++++ .../TemplateLoader/InheritenceTests.swift | 51 +++++++ StencilTests/base.html | 2 + StencilTests/child.html | 2 + 7 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 Stencil/TemplateLoader/Inheritence.swift create mode 100644 StencilTests/TemplateLoader/InheritenceTests.swift create mode 100644 StencilTests/base.html create mode 100644 StencilTests/child.html diff --git a/Stencil.xcodeproj/project.pbxproj b/Stencil.xcodeproj/project.pbxproj index 16ea1913..58837212 100644 --- a/Stencil.xcodeproj/project.pbxproj +++ b/Stencil.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ 1474245D3CE34A8BC76F8D20 /* Pods_StencilTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 40E4E61A4F4EA12FE3FA6E39 /* Pods_StencilTests.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 27A848E41B42240E004ACA13 /* Inheritence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A848E31B42240E004ACA13 /* Inheritence.swift */; }; + 27A848E91B42242C004ACA13 /* base.html in Resources */ = {isa = PBXBuildFile; fileRef = 27A848E71B42242C004ACA13 /* base.html */; }; + 27A848EA1B42242C004ACA13 /* child.html in Resources */ = {isa = PBXBuildFile; fileRef = 27A848E81B42242C004ACA13 /* child.html */; }; + 27A848EC1B42247D004ACA13 /* InheritenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A848EB1B42247D004ACA13 /* InheritenceTests.swift */; }; 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 */; }; @@ -45,6 +49,10 @@ /* Begin PBXFileReference section */ 216AE96E764D5BD92D11049B /* Pods-Stencil.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stencil.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Stencil/Pods-Stencil.debug.xcconfig"; sourceTree = ""; }; + 27A848E31B42240E004ACA13 /* Inheritence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Inheritence.swift; sourceTree = ""; }; + 27A848E71B42242C004ACA13 /* base.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = base.html; sourceTree = ""; }; + 27A848E81B42242C004ACA13 /* child.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = child.html; sourceTree = ""; }; + 27A848EB1B42247D004ACA13 /* InheritenceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InheritenceTests.swift; sourceTree = ""; }; 27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoader.swift; sourceTree = ""; }; 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoaderTests.swift; sourceTree = ""; }; 27CE0AF91A50C963004A105B /* test.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = test.html; sourceTree = ""; }; @@ -102,6 +110,7 @@ isa = PBXGroup; children = ( 27CE0B001A50CBD1004A105B /* Include.swift */, + 27A848E31B42240E004ACA13 /* Inheritence.swift */, ); path = TemplateLoader; sourceTree = ""; @@ -110,6 +119,7 @@ isa = PBXGroup; children = ( 27CE0B031A50CBEA004A105B /* IncludeTests.swift */, + 27A848EB1B42247D004ACA13 /* InheritenceTests.swift */, ); path = TemplateLoader; sourceTree = ""; @@ -176,6 +186,8 @@ 27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */, 27CE0B021A50CBEA004A105B /* TemplateLoader */, 27CE0AF91A50C963004A105B /* test.html */, + 27A848E71B42242C004ACA13 /* base.html */, + 27A848E81B42242C004ACA13 /* child.html */, 77FAAE6219F91E480029DC5E /* Supporting Files */, ); path = StencilTests; @@ -312,6 +324,8 @@ buildActionMask = 2147483647; files = ( 27CE0AFA1A50C963004A105B /* test.html in Resources */, + 27A848EA1B42242C004ACA13 /* child.html in Resources */, + 27A848E91B42242C004ACA13 /* base.html in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -410,6 +424,7 @@ 71CE4C0A19FD29D000B9E0C5 /* Result.swift in Sources */, 7725B3D519F9438F002CF74B /* Node.swift in Sources */, 27CE0ADE1A50BEC3004A105B /* TemplateLoader.swift in Sources */, + 27A848E41B42240E004ACA13 /* Inheritence.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -419,6 +434,7 @@ files = ( 77FAAE6519F91E480029DC5E /* StencilTests.swift in Sources */, 7725B3D319F9437F002CF74B /* NodeTests.swift in Sources */, + 27A848EC1B42247D004ACA13 /* InheritenceTests.swift in Sources */, 27CE0AE01A50BF05004A105B /* TemplateLoaderTests.swift in Sources */, 7725B3D919F94A61002CF74B /* ParserTests.swift in Sources */, 77EB082719F96E9C001870F1 /* TemplateTests.swift in Sources */, diff --git a/Stencil/Context.swift b/Stencil/Context.swift index f2276203..1eeee964 100644 --- a/Stencil/Context.swift +++ b/Stencil/Context.swift @@ -35,10 +35,10 @@ public class Context : Equatable { } public func push() { - push(Dictionary()) + push(Dictionary()) } - public func push(dictionary:Dictionary) { + public func push(dictionary:Dictionary) { dictionaries.append(dictionary) } diff --git a/Stencil/Parser.swift b/Stencil/Parser.swift index 8eebeaab..37ad6d9f 100644 --- a/Stencil/Parser.swift +++ b/Stencil/Parser.swift @@ -37,6 +37,8 @@ public class TokenParser { registerTag("ifnot", parser: IfNode.parse_ifnot) registerTag("now", parser: NowNode.parse) registerTag("include", parser: IncludeNode.parse) + registerTag("extends", parser: ExtendsNode.parse) + registerTag("block", parser: BlockNode.parse) } /// Registers a new template tag diff --git a/Stencil/TemplateLoader/Inheritence.swift b/Stencil/TemplateLoader/Inheritence.swift new file mode 100644 index 00000000..179d11ea --- /dev/null +++ b/Stencil/TemplateLoader/Inheritence.swift @@ -0,0 +1,124 @@ +import Foundation + +class BlockContext { + class var contextKey:String { return "block_context" } + + var blocks:[String:BlockNode] + + init(blocks:[String:BlockNode]) { + self.blocks = blocks + } + + func pop(blockName:String) -> BlockNode? { + return blocks.removeValueForKey(blockName) + } +} + +func any(elements:[Element], closure:(Element -> Bool)) -> Element? { + for element in elements { + if closure(element) { + return element + } + } + + return nil +} + +class ExtendsNode : Node { + let templateName:String + let blocks:[String:BlockNode] + + 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 extended")) + } + + switch parser.parse() { + case .Success(let nodes): + if (any(nodes) { ($0 as? ExtendsNode) != nil }) != nil { + return .Error(error:"'extends' cannot appear more than once in the same template") + } + + let blockNodes = filter(nodes) { node in node is BlockNode } + + let nodes = reduce(blockNodes, [String:BlockNode](), { (accumulator, node:Node) -> [String:BlockNode] in + let node = (node as! BlockNode) + var dict = accumulator + dict[node.name] = node + return dict + }) + + return .Success(node:ExtendsNode(templateName: bits[1], blocks: nodes)) + case .Error(let error): + return .Error(error:error) + } + } + + init(templateName:String, blocks:[String:BlockNode]) { + self.templateName = templateName + self.blocks = blocks + } + + func render(context: Context) -> Result { + if let loader = context["loader"] as? TemplateLoader { + if let template = loader.loadTemplate(templateName) { + let blockContext = BlockContext(blocks: blocks) + context.push([BlockContext.contextKey: blockContext]) + let result = template.render(context) + context.pop() + return result + } + + 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) + } +} + +class BlockNode : Node { + let name:String + let nodes:[Node] + + class func parse(parser:TokenParser, token:Token) -> TokenParser.Result { + let bits = token.components() + + if bits.count != 2 { + return .Error(error:NodeError(token: token, message: "Tag takes one argument, the template file to be included")) + } + + let blockName = bits[1] + var nodes = [Node]() + + switch parser.parse(until(["endblock"])) { + case .Success(let blockNodes): + nodes = blockNodes + case .Error(let error): + return .Error(error: error) + } + + return .Success(node:BlockNode(name:blockName, nodes:nodes)) + } + + init(name:String, nodes:[Node]) { + self.name = name + self.nodes = nodes + } + + func render(context: Context) -> Result { + if let blockContext = context[BlockContext.contextKey] as? BlockContext { + if let node = blockContext.pop(name) { + return node.render(context) + } + } + + return renderNodes(nodes, context) + } +} diff --git a/StencilTests/TemplateLoader/InheritenceTests.swift b/StencilTests/TemplateLoader/InheritenceTests.swift new file mode 100644 index 00000000..f08bb215 --- /dev/null +++ b/StencilTests/TemplateLoader/InheritenceTests.swift @@ -0,0 +1,51 @@ +import Foundation +import XCTest +import Stencil +import PathKit + +class InheritenceTests: NodeTests { + var loader:TemplateLoader! + + override func setUp() { + super.setUp() + + let path = (Path(__FILE__) + Path("../..")).absolute() + loader = TemplateLoader(paths: [path]) + } + + func testInheritence() { + context = Context(dictionary: ["loader": loader]) + let template = loader.loadTemplate("child.html")! + let result = template.render(context) + + switch result { + case .Success(let rendered): + XCTAssertEqual(rendered, "Header\nChild") + case .Error(let error): + XCTAssert(false, "Unexpected error") + } + } +} + +//class BlockNodeTests: NodeTests { +// func testBlockNodeWithoutChildren() { +// let context = Context() +// let block = BlockNode(name:"header", nodes:[TextNode(text: "contents")]) +// let result = block.render(context) +// +// assertSuccess(result) { rendered in +// XCTAssertEqual(rendered, "contents") +// } +// } +// +// func testBlockNodeWithChild() { +// let context = Context() +// let node = BlockNode(name:"header", nodes:[TextNode(text: "contents")]) +// let childBlock = BlockNode(name: "header", nodes: [TextNode(text: "child contents")]) +// let result = node.render(context) +// +// assertSuccess(result) { rendered in +// XCTAssertEqual(rendered, "child contents") +// } +// } +//} diff --git a/StencilTests/base.html b/StencilTests/base.html new file mode 100644 index 00000000..7c74ae0a --- /dev/null +++ b/StencilTests/base.html @@ -0,0 +1,2 @@ +{% block header %}Header{% endblock %} +{% block body %}Body{% endblock %} \ No newline at end of file diff --git a/StencilTests/child.html b/StencilTests/child.html new file mode 100644 index 00000000..30984785 --- /dev/null +++ b/StencilTests/child.html @@ -0,0 +1,2 @@ +{% extends "base.html" %} +{% block body %}Child{% endblock %} \ No newline at end of file