Fix issues from Inheritance spec (#36)
* Separate inheritance block and expansion * Catch top level partial definition, and block newlines * Add testTrailingNewLines to verify output of trailing newlines in partials * Remove comment * If block,partial has indentation add indent for first line * Re-enable full sections spec * withBlockExpansion * Get indentation of blocks correct
This commit is contained in:
@@ -87,6 +87,23 @@ struct MustacheContext {
|
||||
)
|
||||
}
|
||||
|
||||
/// return context with indent information for invoking an inheritance block
|
||||
func withBlockExpansion(indented: String?) -> MustacheContext {
|
||||
let indentation: String? = if let indented {
|
||||
(self.indentation ?? "") + indented
|
||||
} else {
|
||||
self.indentation
|
||||
}
|
||||
return .init(
|
||||
stack: self.stack,
|
||||
sequenceContext: nil,
|
||||
indentation: indentation,
|
||||
inherited: self.inherited,
|
||||
contentType: self.contentType,
|
||||
library: self.library
|
||||
)
|
||||
}
|
||||
|
||||
/// return context with sequence info and sequence element added to stack
|
||||
func withSequence(_ object: Any, sequenceContext: MustacheSequenceContext) -> MustacheContext {
|
||||
var stack = self.stack
|
||||
|
||||
@@ -42,23 +42,58 @@ extension MustacheTemplate {
|
||||
}
|
||||
|
||||
struct ParserState {
|
||||
struct Flags: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
init(rawValue: Int) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static var newLine: Self { .init(rawValue: 1 << 0) }
|
||||
static var isPartialDefinition: Self { .init(rawValue: 1 << 1) }
|
||||
static var isPartialDefinitionTopLevel: Self { .init(rawValue: 1 << 2) }
|
||||
}
|
||||
|
||||
var sectionName: String?
|
||||
var sectionTransforms: [String] = []
|
||||
var newLine: Bool
|
||||
var flags: Flags
|
||||
var startDelimiter: String
|
||||
var endDelimiter: String
|
||||
var partialDefinitionIndent: Substring?
|
||||
|
||||
var newLine: Bool {
|
||||
get { self.flags.contains(.newLine) }
|
||||
set {
|
||||
if newValue {
|
||||
self.flags.insert(.newLine)
|
||||
} else {
|
||||
self.flags.remove(.newLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
self.sectionName = nil
|
||||
self.newLine = true
|
||||
self.flags = .newLine
|
||||
self.startDelimiter = "{{"
|
||||
self.endDelimiter = "}}"
|
||||
}
|
||||
|
||||
func withSectionName(_ name: String, transforms: [String] = []) -> ParserState {
|
||||
func withSectionName(_ name: String, newLine: Bool, transforms: [String] = []) -> ParserState {
|
||||
var newValue = self
|
||||
newValue.sectionName = name
|
||||
newValue.sectionTransforms = transforms
|
||||
newValue.flags.remove(.isPartialDefinitionTopLevel)
|
||||
if !newLine {
|
||||
newValue.flags.remove(.newLine)
|
||||
}
|
||||
return newValue
|
||||
}
|
||||
|
||||
func withInheritancePartial(_ name: String) -> ParserState {
|
||||
var newValue = self
|
||||
newValue.sectionName = name
|
||||
newValue.flags.insert([.newLine, .isPartialDefinition, .isPartialDefinitionTopLevel])
|
||||
return newValue
|
||||
}
|
||||
|
||||
@@ -66,6 +101,7 @@ extension MustacheTemplate {
|
||||
var newValue = self
|
||||
newValue.startDelimiter = start
|
||||
newValue.endDelimiter = end
|
||||
newValue.flags.remove(.isPartialDefinitionTopLevel)
|
||||
return newValue
|
||||
}
|
||||
}
|
||||
@@ -88,7 +124,19 @@ extension MustacheTemplate {
|
||||
while !parser.reachedEnd() {
|
||||
// if new line read whitespace
|
||||
if state.newLine {
|
||||
whiteSpaceBefore = parser.read(while: Set(" \t"))
|
||||
let whiteSpace = parser.read(while: Set(" \t"))
|
||||
// If inside a partial block definition
|
||||
if state.flags.contains(.isPartialDefinition), !state.flags.contains(.isPartialDefinitionTopLevel) {
|
||||
// if definition indent has been set then remove it from current whitespace otherwise set the
|
||||
// indent as this is the first line of the partial definition
|
||||
if let partialDefinitionIndent = state.partialDefinitionIndent {
|
||||
whiteSpaceBefore = whiteSpace.dropFirst(partialDefinitionIndent.count)
|
||||
} else {
|
||||
state.partialDefinitionIndent = whiteSpace
|
||||
}
|
||||
} else {
|
||||
whiteSpaceBefore = whiteSpace
|
||||
}
|
||||
}
|
||||
let text = try readUntilDelimiterOrNewline(&parser, state: state)
|
||||
// if we hit a newline add text
|
||||
@@ -121,7 +169,7 @@ extension MustacheTemplate {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transforms: transforms))
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
||||
tokens.append(.section(name: name, transforms: transforms, template: MustacheTemplate(sectionTokens)))
|
||||
|
||||
case "^":
|
||||
@@ -134,24 +182,9 @@ extension MustacheTemplate {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transforms: transforms))
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine, transforms: transforms))
|
||||
tokens.append(.invertedSection(name: name, transforms: transforms, template: MustacheTemplate(sectionTokens)))
|
||||
|
||||
case "$":
|
||||
// inherited section
|
||||
parser.unsafeAdvance()
|
||||
let (name, transforms) = try parseName(&parser, state: state)
|
||||
// ERROR: can't have transform applied to inherited sections
|
||||
guard transforms.isEmpty else { throw Error.transformAppliedToInheritanceSection }
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
} else if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, transforms: transforms))
|
||||
tokens.append(.inheritedSection(name: name, template: MustacheTemplate(sectionTokens)))
|
||||
|
||||
case "/":
|
||||
// end of section
|
||||
parser.unsafeAdvance()
|
||||
@@ -215,20 +248,18 @@ extension MustacheTemplate {
|
||||
// partial with inheritance
|
||||
parser.unsafeAdvance()
|
||||
let name = try parsePartialName(&parser, state: state)
|
||||
var indent: String?
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
}
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
} else if whiteSpaceBefore.count > 0 {
|
||||
indent = String(whiteSpaceBefore)
|
||||
tokens.append(.text(indent!))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name))
|
||||
let sectionTokens = try parse(&parser, state: state.withInheritancePartial(name))
|
||||
var inherit: [String: MustacheTemplate] = [:]
|
||||
// parse tokens in section to extract inherited sections
|
||||
for token in sectionTokens {
|
||||
switch token {
|
||||
case .inheritedSection(let name, let template):
|
||||
case .blockDefinition(let name, let template):
|
||||
inherit[name] = template
|
||||
case .text:
|
||||
break
|
||||
@@ -236,7 +267,34 @@ extension MustacheTemplate {
|
||||
throw Error.illegalTokenInsideInheritSection
|
||||
}
|
||||
}
|
||||
tokens.append(.partial(name, indentation: indent, inherits: inherit))
|
||||
tokens.append(.partial(name, indentation: String(whiteSpaceBefore), inherits: inherit))
|
||||
whiteSpaceBefore = ""
|
||||
|
||||
case "$":
|
||||
// inherited section
|
||||
parser.unsafeAdvance()
|
||||
let (name, transforms) = try parseName(&parser, state: state)
|
||||
// ERROR: can't have transforms applied to inherited sections
|
||||
guard transforms.isEmpty else { throw Error.transformAppliedToInheritanceSection }
|
||||
if state.flags.contains(.isPartialDefinitionTopLevel) {
|
||||
let standAlone = self.isStandalone(&parser, state: state)
|
||||
if standAlone {
|
||||
setNewLine = true
|
||||
}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
||||
tokens.append(.blockDefinition(name: name, template: MustacheTemplate(sectionTokens)))
|
||||
|
||||
} else {
|
||||
if whiteSpaceBefore.count > 0 {
|
||||
tokens.append(.text(String(whiteSpaceBefore)))
|
||||
}
|
||||
if self.isStandalone(&parser, state: state) {
|
||||
setNewLine = true
|
||||
} else if whiteSpaceBefore.count > 0 {}
|
||||
let sectionTokens = try parse(&parser, state: state.withSectionName(name, newLine: setNewLine))
|
||||
tokens.append(.blockExpansion(name: name, default: MustacheTemplate(sectionTokens), indentation: String(whiteSpaceBefore)))
|
||||
whiteSpaceBefore = ""
|
||||
}
|
||||
|
||||
case "=":
|
||||
// set delimiter
|
||||
|
||||
@@ -28,7 +28,8 @@ extension MustacheTemplate {
|
||||
if let indentation = context.indentation, indentation != "" {
|
||||
for token in tokens {
|
||||
let renderedString = self.renderToken(token, context: &context)
|
||||
if renderedString != "", string.last == "\n" {
|
||||
// if rendered string is not empty and we are on a new line
|
||||
if renderedString.count > 0, string.last == "\n" {
|
||||
string += indentation
|
||||
}
|
||||
string += renderedString
|
||||
@@ -75,11 +76,11 @@ extension MustacheTemplate {
|
||||
let child = self.getChild(named: variable, transforms: transforms, context: context)
|
||||
return self.renderInvertedSection(child, with: template, context: context)
|
||||
|
||||
case .inheritedSection(let name, let template):
|
||||
case .blockExpansion(let name, let defaultTemplate, let indented):
|
||||
if let override = context.inherited?[name] {
|
||||
return override.render(context: context)
|
||||
return override.render(context: context.withBlockExpansion(indented: indented))
|
||||
} else {
|
||||
return template.render(context: context)
|
||||
return defaultTemplate.render(context: context.withBlockExpansion(indented: indented))
|
||||
}
|
||||
|
||||
case .partial(let name, let indentation, let overrides):
|
||||
@@ -89,6 +90,9 @@ extension MustacheTemplate {
|
||||
|
||||
case .contentType(let contentType):
|
||||
context = context.withContentType(contentType)
|
||||
|
||||
case .blockDefinition:
|
||||
fatalError("Should not be rendering block definitions")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ public struct MustacheTemplate: Sendable {
|
||||
case unescapedVariable(name: String, transforms: [String] = [])
|
||||
case section(name: String, transforms: [String] = [], template: MustacheTemplate)
|
||||
case invertedSection(name: String, transforms: [String] = [], template: MustacheTemplate)
|
||||
case inheritedSection(name: String, template: MustacheTemplate)
|
||||
case blockDefinition(name: String, template: MustacheTemplate)
|
||||
case blockExpansion(name: String, default: MustacheTemplate, indentation: String?)
|
||||
case partial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
|
||||
case contentType(MustacheContentType)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ final class PartialTests: XCTestCase {
|
||||
let template = try MustacheTemplate(string: """
|
||||
<h2>Names</h2>
|
||||
{{#names}}
|
||||
{{> user}}
|
||||
{{> user}}
|
||||
{{/names}}
|
||||
""")
|
||||
let template2 = try MustacheTemplate(string: """
|
||||
@@ -33,9 +33,9 @@ final class PartialTests: XCTestCase {
|
||||
let object: [String: Any] = ["names": ["john", "adam", "claire"]]
|
||||
XCTAssertEqual(library.render(object, withTemplate: "base"), """
|
||||
<h2>Names</h2>
|
||||
<strong>john</strong>
|
||||
<strong>adam</strong>
|
||||
<strong>claire</strong>
|
||||
<strong>john</strong>
|
||||
<strong>adam</strong>
|
||||
<strong>claire</strong>
|
||||
|
||||
""")
|
||||
}
|
||||
@@ -62,8 +62,7 @@ final class PartialTests: XCTestCase {
|
||||
""")
|
||||
var library = MustacheLibrary()
|
||||
library.register(template, named: "base")
|
||||
library.register(template2, named: "user") // , withTemplate: String)// = MustacheLibrary(templates: ["base": template, "user": template2])
|
||||
|
||||
library.register(template2, named: "user")
|
||||
let object: [String: Any] = ["names": ["john", "adam", "claire"]]
|
||||
XCTAssertEqual(library.render(object, withTemplate: "base"), """
|
||||
<h2>Names</h2>
|
||||
@@ -75,6 +74,37 @@ final class PartialTests: XCTestCase {
|
||||
""")
|
||||
}
|
||||
|
||||
func testTrailingNewLines() throws {
|
||||
let template1 = try MustacheTemplate(string: """
|
||||
{{> withNewLine }}
|
||||
>> {{> withNewLine }}
|
||||
[ {{> withNewLine }} ]
|
||||
""")
|
||||
let template2 = try MustacheTemplate(string: """
|
||||
{{> withoutNewLine }}
|
||||
>> {{> withoutNewLine }}
|
||||
[ {{> withoutNewLine }} ]
|
||||
""")
|
||||
let withNewLine = try MustacheTemplate(string: """
|
||||
{{#things}}{{.}}, {{/things}}
|
||||
|
||||
""")
|
||||
let withoutNewLine = try MustacheTemplate(string: "{{#things}}{{.}}, {{/things}}")
|
||||
let library = MustacheLibrary(templates: ["base1": template1, "base2": template2, "withNewLine": withNewLine, "withoutNewLine": withoutNewLine])
|
||||
let object = ["things": [1, 2, 3, 4, 5]]
|
||||
XCTAssertEqual(library.render(object, withTemplate: "base1"), """
|
||||
1, 2, 3, 4, 5,
|
||||
>> 1, 2, 3, 4, 5,
|
||||
|
||||
[ 1, 2, 3, 4, 5,
|
||||
]
|
||||
""")
|
||||
XCTAssertEqual(library.render(object, withTemplate: "base2"), """
|
||||
1, 2, 3, 4, 5, >> 1, 2, 3, 4, 5,
|
||||
[ 1, 2, 3, 4, 5, ]
|
||||
""")
|
||||
}
|
||||
|
||||
/// Testing dynamic partials
|
||||
func testDynamicPartials() throws {
|
||||
let template = try MustacheTemplate(string: """
|
||||
@@ -106,7 +136,6 @@ final class PartialTests: XCTestCase {
|
||||
<head>
|
||||
<title>{{$title}}Default title{{/title}}</title>
|
||||
</head>
|
||||
|
||||
""",
|
||||
named: "header"
|
||||
)
|
||||
@@ -144,4 +173,32 @@ final class PartialTests: XCTestCase {
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
func testInheritanceIndentation() throws {
|
||||
var library = MustacheLibrary()
|
||||
try library.register(
|
||||
"""
|
||||
Hi,
|
||||
{{$block}}{{/block}}
|
||||
""",
|
||||
named: "parent"
|
||||
)
|
||||
try library.register(
|
||||
"""
|
||||
{{<parent}}
|
||||
{{$block}}
|
||||
one
|
||||
two
|
||||
{{/block}}
|
||||
{{/parent}}
|
||||
""",
|
||||
named: "template"
|
||||
)
|
||||
XCTAssertEqual(library.render({}, withTemplate: "template"), """
|
||||
Hi,
|
||||
one
|
||||
two
|
||||
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,19 @@ final class MustacheSpecTests: XCTestCase {
|
||||
|
||||
func XCTAssertSpecEqual(_ result: String?, _ test: Spec.Test) {
|
||||
if result != test.expected {
|
||||
XCTFail("\n\(test.desc)result:\n\(result ?? "nil")\nexpected:\n\(test.expected)")
|
||||
XCTFail("""
|
||||
\(test.name)
|
||||
\(test.desc)
|
||||
template:
|
||||
\(test.template)
|
||||
data:
|
||||
\(test.data.value)
|
||||
\(test.partials.map { "partials:\n\($0)" } ?? "")
|
||||
result:
|
||||
\(result ?? "nil")
|
||||
expected:
|
||||
\(test.expected)
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +116,6 @@ final class MustacheSpecTests: XCTestCase {
|
||||
let data = try Data(contentsOf: url)
|
||||
let spec = try JSONDecoder().decode(Spec.self, from: data)
|
||||
|
||||
print(spec.overview)
|
||||
let date = Date()
|
||||
for test in spec.tests {
|
||||
guard !ignoring.contains(test.name) else { continue }
|
||||
@@ -113,6 +124,23 @@ final class MustacheSpecTests: XCTestCase {
|
||||
print(-date.timeIntervalSinceNow)
|
||||
}
|
||||
|
||||
func testSpec(name: String, only: [String]) throws {
|
||||
let url = URL(string: "https://raw.githubusercontent.com/mustache/spec/master/specs/\(name).json")!
|
||||
try testSpec(url: url, only: only)
|
||||
}
|
||||
|
||||
func testSpec(url: URL, only: [String]) throws {
|
||||
let data = try Data(contentsOf: url)
|
||||
let spec = try JSONDecoder().decode(Spec.self, from: data)
|
||||
|
||||
let date = Date()
|
||||
for test in spec.tests {
|
||||
guard only.contains(test.name) else { continue }
|
||||
XCTAssertNoThrow(try test.run())
|
||||
}
|
||||
print(-date.timeIntervalSinceNow)
|
||||
}
|
||||
|
||||
func testCommentsSpec() throws {
|
||||
try self.testSpec(name: "comments")
|
||||
}
|
||||
@@ -138,7 +166,12 @@ final class MustacheSpecTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testInheritanceSpec() throws {
|
||||
try XCTSkipIf(true) // inheritance spec has been updated and has added requirements, we don't yet support
|
||||
try self.testSpec(name: "~inheritance")
|
||||
try self.testSpec(
|
||||
name: "~inheritance",
|
||||
ignoring: [
|
||||
"Intrinsic indentation",
|
||||
"Nested block reindentation",
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user