From c4866178541aa63bdda1e848cd9860b1619082e4 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Mon, 25 Dec 2017 00:34:13 +0100 Subject: [PATCH] fixed reporting errors in child templates --- Sources/Errors.swift | 2 +- Sources/Inheritence.swift | 79 +++++++++++++++--------- Tests/StencilTests/EnvironmentSpec.swift | 65 +++++++++++++------ 3 files changed, 95 insertions(+), 51 deletions(-) diff --git a/Sources/Errors.swift b/Sources/Errors.swift index 0cb40eb..ab5eda3 100644 --- a/Sources/Errors.swift +++ b/Sources/Errors.swift @@ -99,7 +99,7 @@ open class SimpleErrorReporter: ErrorReporter { return TemplateSyntaxError(reason: (error as? TemplateSyntaxError)?.reason ?? "\(error)", lexeme: (error as? TemplateSyntaxError)?.lexeme, - template: context.template, + template: (error as? TemplateSyntaxError)?.template ?? context.template, parentError: (error as? TemplateSyntaxError)?.parentError ) } diff --git a/Sources/Inheritence.swift b/Sources/Inheritence.swift index 7457c37..55f94ef 100644 --- a/Sources/Inheritence.swift +++ b/Sources/Inheritence.swift @@ -1,25 +1,23 @@ class BlockContext { class var contextKey: String { return "block_context" } - var blocks: [String: [BlockNode]] + // contains mapping of block names to their nodes and templates where they are defined + var blocks: [String: [(BlockNode, Template?)]] - init(blocks: [String: BlockNode]) { - self.blocks = [:] - blocks.forEach { (key, value) in - self.blocks[key] = [value] - } + init(blocks: [String: (BlockNode, Template?)]) { + self.blocks = blocks.mapValues({ [$0] }) } - func push(_ block: BlockNode, forKey blockName: String) { + func pushBlock(_ block: BlockNode, named blockName: String, definedIn template: Template?) { if var blocks = blocks[blockName] { - blocks.append(block) + blocks.append((block, template)) self.blocks[blockName] = blocks } else { - self.blocks[blockName] = [block] + self.blocks[blockName] = [(block, template)] } } - func pop(_ blockName: String) -> BlockNode? { + func popBlock(named blockName: String) -> (node: BlockNode, template: Template?)? { if var blocks = blocks[blockName] { let block = blocks.removeFirst() if blocks.isEmpty { @@ -87,28 +85,33 @@ class ExtendsNode : NodeType { throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string") } - let template = try context.environment.loadTemplate(name: templateName) - + let baseTemplate = try context.environment.loadTemplate(name: templateName) + let template = context.environment.template + let blockContext: BlockContext - if let context = context[BlockContext.contextKey] as? BlockContext { - blockContext = context - - for (key, value) in blocks { - blockContext.push(value, forKey: key) + if let _blockContext = context[BlockContext.contextKey] as? BlockContext { + blockContext = _blockContext + for (name, block) in blocks { + blockContext.pushBlock(block, named: name, definedIn: template) } } else { - blockContext = BlockContext(blocks: blocks) + blockContext = BlockContext(blocks: blocks.mapValues({ ($0, template) })) } do { - return try context.environment.pushTemplate(template, token: token) { + // pushes base template and renders it's content + // block_context contains all blocks from child templates + return try context.environment.pushTemplate(baseTemplate, token: token) { try context.push(dictionary: [BlockContext.contextKey: blockContext]) { - return try template.render(context) + return try baseTemplate.render(context) } } } catch { - if let parentError = error as? TemplateSyntaxError { - throw TemplateSyntaxError(reason: parentError.reason, parentError: parentError) + // if error template is already set (see catch in BlockNode) + // and it happend in the same template as current template + // there is no need to wrap it in another error + if let error = error as? TemplateSyntaxError, error.template !== context.environment.template { + throw TemplateSyntaxError(reason: error.reason, parentError: error) } else { throw error } @@ -142,15 +145,31 @@ class BlockNode : NodeType { } func render(_ context: Context) throws -> String { - if let blockContext = context[BlockContext.contextKey] as? BlockContext, let node = blockContext.pop(name) { + if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.popBlock(named: name) { + // node is a block node from child template that extends this node (has the same name) let newContext: [String: Any] - newContext = [ - BlockContext.contextKey: blockContext, - "block": ["super": try self.render(context)] - ] - - return try context.push(dictionary: newContext) { - return try node.render(context) + newContext = [ + BlockContext.contextKey: blockContext, + // render current node so that it's content can be used as part of node that extends it + "block": ["super": try self.render(context)] + ] + // render extension node + do { + return try context.push(dictionary: newContext) { + return try child.node.render(context) + } + } catch { + // child node belongs to child template, which is currently not on top of stack + // so we need to use node's template to report errors, not current template + // unless it's already set + if var error = error as? TemplateSyntaxError { + error.template = error.template ?? child.template + error.lexeme = error.lexeme ?? child.node.token + + throw error + } else { + throw error + } } } diff --git a/Tests/StencilTests/EnvironmentSpec.swift b/Tests/StencilTests/EnvironmentSpec.swift index 738ed86..4d98f7b 100644 --- a/Tests/StencilTests/EnvironmentSpec.swift +++ b/Tests/StencilTests/EnvironmentSpec.swift @@ -228,18 +228,8 @@ func testEnvironment() { var error = expectedSyntaxError(token: "include \"invalid-include.html\"", template: template, description: "Unknown filter 'unknown'") error.parentError = parentError - try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) - } - - $0.it("reports syntax error in extended template") { - let template = try environment.loadTemplate(name: "invalid-child-super.html") - let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") - - let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'") - var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "Unknown filter 'unknown'") - error.parentError = parentError - try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) } $0.it("reports runtime error in included template") { @@ -259,24 +249,59 @@ func testEnvironment() { try expect(environment.renderTemplate(string: template.templateString, context: ["target": "World"])).toThrow(error) } - - $0.it("reports runtime error in extended template") { + + $0.it("reports syntax error in base template") { + let template = try environment.loadTemplate(name: "invalid-child-super.html") + let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "Unknown filter 'unknown'") + var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "Unknown filter 'unknown'") + error.parentError = parentError + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports runtime error in base template") { var environment = environment let filterExtension = Extension() - filterExtension.registerFilter("throw", filter: { (_: Any?) in + filterExtension.registerFilter("unknown", filter: { (_: Any?) in + throw TemplateSyntaxError("filter error") + }) + environment.extensions += [filterExtension] + + let template = try environment.loadTemplate(name: "invalid-child-super.html") + let baseTemplate = try environment.loadTemplate(name: "invalid-base.html") + + let parentError = expectedSyntaxError(token: "target|unknown", template: baseTemplate, description: "filter error") + var error = expectedSyntaxError(token: "extends \"invalid-base.html\"", template: template, description: "filter error") + error.parentError = parentError + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports syntax error in child template") { + let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) + let error = expectedSyntaxError(token: "target|unknown", template: template, description: "Unknown filter 'unknown'") + + try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) + } + + $0.it("reports runtime error in child template") { + var environment = environment + let filterExtension = Extension() + filterExtension.registerFilter("unknown", filter: { (_: Any?) in throw TemplateSyntaxError("filter error") }) environment.extensions += [filterExtension] - let template = try environment.loadTemplate(name: "invalid-runtime-child-super.html") - let baseTemplate = try environment.loadTemplate(name: "invalid-runtime-base.html") - - let parentError = expectedSyntaxError(token: "target|throw", template: baseTemplate, description: "filter error") - var error = expectedSyntaxError(token: "extends \"invalid-runtime-base.html\"", template: template, description: "filter error") - error.parentError = parentError + let template = Template.init(templateString: "{% extends \"base.html\" %}\n" + + "{% block body %}Child {{ target|unknown }}{% endblock %}", environment: environment, name: nil) + let error = expectedSyntaxError(token: "{{ target|unknown }}", template: template, description: "filter error") try expect(environment.render(template: template, context: ["target": "World"])).toThrow(error) } + } }