Skip to content
This repository has been archived by the owner on Aug 4, 2022. It is now read-only.

Commit

Permalink
Merge pull request #7 from g-Off/include-tag
Browse files Browse the repository at this point in the history
adds the Include tag
  • Loading branch information
g-Off authored Oct 1, 2019
2 parents 9c35230 + 3d650c4 commit 2262502
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Sources/Liquid/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

final class Parser {
private let tokens: [Lexer.Token]
let tokens: [Lexer.Token]
private var index: Array<Lexer.Token>.Index
init(tokens: [Lexer.Token]) {
self.tokens = tokens
Expand Down
91 changes: 91 additions & 0 deletions Sources/Liquid/Tags/Include.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// Include.swift
//
//
// Created by Geoffrey Foster on 2019-09-27.
//

import Foundation

final class Include: Tag {
private let templateName: Expression
private let variable: Expression?
private let attributes: [String: Expression]
init(name: String, markup: String?, context: ParseContext) throws {
guard let markup = markup else { throw SyntaxError.missingMarkup }
let parser = try Parser(string: markup)

self.templateName = Expression.parse(parser)

if parser.consumeId("for") || parser.consumeId("with") {
self.variable = Expression.parse(parser)
} else {
self.variable = nil
}

var attributes: [String: Expression] = [:]
while let attribute = parser.consume(.id) {
parser.consume(.colon)
let value = Expression.parse(parser)
attributes[attribute] = value
parser.consume(.comma)
}

parser.consume(.endOfString)

self.attributes = attributes
}

func parse(_ tokenizer: Tokenizer, context: ParseContext) throws {}

func render(context: Context) throws -> [String] {
var result: [String] = []
let templateName = self.templateName.evaluate(context: context).toString()
let template = try loadTemplate(path: templateName, context: context)
let variableName = templateName.components(separatedBy: "/").last!

let value = variable?.evaluate(context: context) ?? context.value(named: variableName)

try context.withScope {
attributes.forEach { (key, expression) in
let value = expression.evaluate(context: context)
context.setValue(value, named: key)
}

if let value = value, value.isArray {
for vv in value.toArray() {
context.setValue(vv, named: variableName)
result.append(try template.render(context: context))
}
} else {
if let value = value {
context.setValue(value, named: variableName)
}
result.append(try template.render(context: context))
}
}

return result
}

private func loadTemplate(path: String, context: Context) throws -> Template {
guard !path.isEmpty else {
throw FileSystemError(reason: "")
}
let cacheKey = RegisterKey("CachedPartials")
var cached = (context[cacheKey] as? [String: Template]) ?? [:]

if let template = cached[path] {
return template
}

let source = try context.fileSystem.read(path: path)
let template = Template(source: source, context: context)
try template.parse()

cached[path] = template
context[cacheKey] = cached

return template
}
}
1 change: 1 addition & 0 deletions Sources/Liquid/Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public final class Template {
tags["decrement"] = Decrement.init
tags["for"] = For.init
tags["if"] = If.init
tags["include"] = Include.init
tags["increment"] = Increment.init
tags["unless"] = If.unless

Expand Down
151 changes: 151 additions & 0 deletions Tests/LiquidTests/IncludeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// IncludeTests.swift
//
//
// Created by Geoffrey Foster on 2019-09-28.
//

import XCTest
@testable import Liquid

final class IncludeTests: XCTestCase {
private final class TestFileSystem: FileSystem {
func read(path: String) throws -> String {
switch path {
case "product":
return "Product: {{ product.title }} "
case "locale_variables":
return "Locale: {{echo1}} {{echo2}}"
case "variant":
return "Variant: {{ variant.title }}"
case "nested_template":
return "{% include 'header' %} {% include 'body' %} {% include 'footer' %}"
case "body":
return "body {% include 'body_detail' %}"
case "nested_product_template":
return "Product: {{ nested_product_template.title }} {%include 'details'%} "
case "recursively_nested_template":
return "-{% include 'recursively_nested_template' %}"
case "pick_a_source":
return "from TestFileSystem"
case "assignments":
return "{% assign foo = 'bar' %}"
case "break":
return "{% break %}"
default:
return path
}
}
}

private final class CountingFileSystem: FileSystem {
var count: Int = 0
func read(path: String) throws -> String {
defer { count += 1 }
return "from CountingFileSystem"
}
}

func test_include_tag_with() throws {
XCTAssertTemplate("{% include 'product' with products[0] %}", "Product: Draft 151cm ", ["products": [["title": "Draft 151cm"], ["title": "Element 155cm"]]], fileSystem: TestFileSystem())
}

func test_include_tag_with_default_name() throws {
XCTAssertTemplate("{% include 'product' %}", "Product: Draft 151cm ", ["product": ["title": "Draft 151cm"]], fileSystem: TestFileSystem())
}

func test_include_tag_for() throws {
XCTAssertTemplate("{% include 'product' for products %}", "Product: Draft 151cm Product: Element 155cm ", ["products": [["title": "Draft 151cm"], ["title": "Element 155cm"]]], fileSystem: TestFileSystem())
}

func test_include_tag_with_local_variables() throws {
XCTAssertTemplate("{% include 'locale_variables' echo1: 'test123' %}", "Locale: test123 ", fileSystem: TestFileSystem())
}

func test_include_tag_with_multiple_local_variables() throws {
XCTAssertTemplate("{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}", "Locale: test123 test321", fileSystem: TestFileSystem())
}

func test_include_tag_with_multiple_local_variables_from_context() throws {
XCTAssertTemplate(
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
"Locale: test123 test321",
["echo1": "test123", "more_echos": ["echo2": "test321"]],
fileSystem: TestFileSystem()
)
}

func test_included_templates_assigns_variables() throws {
XCTAssertTemplate("{% include 'assignments' %}{{ foo }}", "bar", fileSystem: TestFileSystem())
}

func test_nested_include_tag() throws {
XCTAssertTemplate("{% include 'body' %}", "body body_detail", fileSystem: TestFileSystem())
XCTAssertTemplate("{% include 'nested_template' %}", "header body body_detail footer", fileSystem: TestFileSystem())
}

func test_nested_include_with_variable() throws {
XCTAssertTemplate("{% include 'nested_product_template' with product %}", "Product: Draft 151cm details ", ["product": ["title": "Draft 151cm"]], fileSystem: TestFileSystem())
XCTAssertTemplate("{% include 'nested_product_template' for products %}", "Product: Draft 151cm details Product: Element 155cm details ", ["products": [["title": "Draft 151cm"], ["title": "Element 155cm"]]], fileSystem: TestFileSystem())
}

// func test_recursively_included_template_does_not_produce_endless_loop() throws {
// infinite_file_system = Class.new do
// func read_template_file(template_path)() throws {
// "-{% include 'loop' %}"
// }
// end
//
// Liquid::Template.file_system = infinite_file_system.new
//
// assert_raises(Liquid::StackLevelError) do
// Template.parse("{% include 'loop' %}").render!
// end
// }

func test_dynamically_choosen_template() throws {
XCTAssertTemplate("{% include template %}", "Test123", ["template": "Test123"], fileSystem: TestFileSystem())
XCTAssertTemplate("{% include template %}", "Test321", ["template": "Test321"], fileSystem: TestFileSystem())
XCTAssertTemplate("{% include template for product %}", "Product: Draft 151cm ", ["template": "product", "product": ["title": "Draft 151cm"]], fileSystem: TestFileSystem())
}

func test_include_tag_caches_second_read_of_same_partial() throws {
let fileSystem = CountingFileSystem()
XCTAssertTemplate("{% include 'pick_a_source' %}{% include 'pick_a_source' %}", "from CountingFileSystemfrom CountingFileSystem", fileSystem: fileSystem)
XCTAssertEqual(fileSystem.count, 1)
}

func test_include_tag_doesnt_cache_partials_across_renders() throws {
let fileSystem = CountingFileSystem()
XCTAssertTemplate("{% include 'pick_a_source' %}", "from CountingFileSystem", fileSystem: fileSystem)
XCTAssertEqual(fileSystem.count, 1)

XCTAssertTemplate("{% include 'pick_a_source' %}", "from CountingFileSystem", fileSystem: fileSystem)
XCTAssertEqual(fileSystem.count, 2)
}

func test_include_tag_within_if_statement() throws {
XCTAssertTemplate("{% if true %}{% include 'foo_if_true' %}{% endif %}", "foo_if_true", fileSystem: TestFileSystem())
}

func test_render_raise_argument_error_when_template_is_undefined() throws {
let undefinedVariableTemplate = Template(source: "{% include undefined_variable %}", fileSystem: TestFileSystem())
XCTAssertNoThrow(try undefinedVariableTemplate.parse())
XCTAssertThrowsError(try undefinedVariableTemplate.render())

let nilTemplate = Template(source: "{% include nil %}", fileSystem: TestFileSystem())
XCTAssertNoThrow(try nilTemplate.parse())
XCTAssertThrowsError(try nilTemplate.render())
}

func test_including_via_variable_value() throws {
XCTAssertTemplate("{% assign page = 'pick_a_source' %}{% include page %}", "from TestFileSystem", fileSystem: TestFileSystem())
XCTAssertTemplate("{% assign page = 'product' %}{% include page %}", "Product: Draft 151cm ", ["product": ["title": "Draft 151cm"]], fileSystem: TestFileSystem())
XCTAssertTemplate("{% assign page = 'product' %}{% include page for foo %}", "Product: Draft 151cm ", ["foo": ["title": "Draft 151cm"]], fileSystem: TestFileSystem())
}

func test_break_through_include() throws {
XCTAssertTemplate("{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}", "1", fileSystem: TestFileSystem())
XCTAssertTemplate("{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}", "1", fileSystem: TestFileSystem())
}
}

0 comments on commit 2262502

Please sign in to comment.