Skip to content

Commit

Permalink
Include template inheritence
Browse files Browse the repository at this point in the history
Closes #15
  • Loading branch information
kylef committed Jun 30, 2015
1 parent 53d5a4f commit 620154e
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 2 deletions.
16 changes: 16 additions & 0 deletions Stencil.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
27A848E31B42240E004ACA13 /* Inheritence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Inheritence.swift; sourceTree = "<group>"; };
27A848E71B42242C004ACA13 /* base.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = base.html; sourceTree = "<group>"; };
27A848E81B42242C004ACA13 /* child.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = child.html; sourceTree = "<group>"; };
27A848EB1B42247D004ACA13 /* InheritenceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InheritenceTests.swift; sourceTree = "<group>"; };
27CE0ADD1A50BEC3004A105B /* TemplateLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoader.swift; sourceTree = "<group>"; };
27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateLoaderTests.swift; sourceTree = "<group>"; };
27CE0AF91A50C963004A105B /* test.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = test.html; sourceTree = "<group>"; };
Expand Down Expand Up @@ -102,6 +110,7 @@
isa = PBXGroup;
children = (
27CE0B001A50CBD1004A105B /* Include.swift */,
27A848E31B42240E004ACA13 /* Inheritence.swift */,
);
path = TemplateLoader;
sourceTree = "<group>";
Expand All @@ -110,6 +119,7 @@
isa = PBXGroup;
children = (
27CE0B031A50CBEA004A105B /* IncludeTests.swift */,
27A848EB1B42247D004ACA13 /* InheritenceTests.swift */,
);
path = TemplateLoader;
sourceTree = "<group>";
Expand Down Expand Up @@ -176,6 +186,8 @@
27CE0ADF1A50BF05004A105B /* TemplateLoaderTests.swift */,
27CE0B021A50CBEA004A105B /* TemplateLoader */,
27CE0AF91A50C963004A105B /* test.html */,
27A848E71B42242C004ACA13 /* base.html */,
27A848E81B42242C004ACA13 /* child.html */,
77FAAE6219F91E480029DC5E /* Supporting Files */,
);
path = StencilTests;
Expand Down Expand Up @@ -312,6 +324,8 @@
buildActionMask = 2147483647;
files = (
27CE0AFA1A50C963004A105B /* test.html in Resources */,
27A848EA1B42242C004ACA13 /* child.html in Resources */,
27A848E91B42242C004ACA13 /* base.html in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -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;
};
Expand All @@ -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 */,
Expand Down
4 changes: 2 additions & 2 deletions Stencil/Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ public class Context : Equatable {
}

public func push() {
push(Dictionary<String, String>())
push(Dictionary<String, AnyObject>())
}

public func push(dictionary:Dictionary<String, String>) {
public func push(dictionary:Dictionary<String, AnyObject>) {
dictionaries.append(dictionary)
}

Expand Down
2 changes: 2 additions & 0 deletions Stencil/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
124 changes: 124 additions & 0 deletions Stencil/TemplateLoader/Inheritence.swift
Original file line number Diff line number Diff line change
@@ -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<Element>(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)
}
}
51 changes: 51 additions & 0 deletions StencilTests/TemplateLoader/InheritenceTests.swift
Original file line number Diff line number Diff line change
@@ -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")
// }
// }
//}
2 changes: 2 additions & 0 deletions StencilTests/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% block header %}Header{% endblock %}
{% block body %}Body{% endblock %}
2 changes: 2 additions & 0 deletions StencilTests/child.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% extends "base.html" %}
{% block body %}Child{% endblock %}

0 comments on commit 620154e

Please sign in to comment.