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:
Adam Fowler
2024-07-15 09:36:15 +01:00
committed by GitHub
parent cc0eaffa06
commit 7689de0a42
6 changed files with 215 additions and 45 deletions

View File

@@ -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 /// return context with sequence info and sequence element added to stack
func withSequence(_ object: Any, sequenceContext: MustacheSequenceContext) -> MustacheContext { func withSequence(_ object: Any, sequenceContext: MustacheSequenceContext) -> MustacheContext {
var stack = self.stack var stack = self.stack

View File

@@ -42,23 +42,58 @@ extension MustacheTemplate {
} }
struct ParserState { 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 sectionName: String?
var sectionTransforms: [String] = [] var sectionTransforms: [String] = []
var newLine: Bool var flags: Flags
var startDelimiter: String var startDelimiter: String
var endDelimiter: 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() { init() {
self.sectionName = nil self.sectionName = nil
self.newLine = true self.flags = .newLine
self.startDelimiter = "{{" self.startDelimiter = "{{"
self.endDelimiter = "}}" self.endDelimiter = "}}"
} }
func withSectionName(_ name: String, transforms: [String] = []) -> ParserState { func withSectionName(_ name: String, newLine: Bool, transforms: [String] = []) -> ParserState {
var newValue = self var newValue = self
newValue.sectionName = name newValue.sectionName = name
newValue.sectionTransforms = transforms 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 return newValue
} }
@@ -66,6 +101,7 @@ extension MustacheTemplate {
var newValue = self var newValue = self
newValue.startDelimiter = start newValue.startDelimiter = start
newValue.endDelimiter = end newValue.endDelimiter = end
newValue.flags.remove(.isPartialDefinitionTopLevel)
return newValue return newValue
} }
} }
@@ -88,7 +124,19 @@ extension MustacheTemplate {
while !parser.reachedEnd() { while !parser.reachedEnd() {
// if new line read whitespace // if new line read whitespace
if state.newLine { 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) let text = try readUntilDelimiterOrNewline(&parser, state: state)
// if we hit a newline add text // if we hit a newline add text
@@ -121,7 +169,7 @@ extension MustacheTemplate {
tokens.append(.text(String(whiteSpaceBefore))) tokens.append(.text(String(whiteSpaceBefore)))
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))) tokens.append(.section(name: name, transforms: transforms, template: MustacheTemplate(sectionTokens)))
case "^": case "^":
@@ -134,24 +182,9 @@ extension MustacheTemplate {
tokens.append(.text(String(whiteSpaceBefore))) tokens.append(.text(String(whiteSpaceBefore)))
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))) 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 "/": case "/":
// end of section // end of section
parser.unsafeAdvance() parser.unsafeAdvance()
@@ -215,20 +248,18 @@ extension MustacheTemplate {
// partial with inheritance // partial with inheritance
parser.unsafeAdvance() parser.unsafeAdvance()
let name = try parsePartialName(&parser, state: state) 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) { if self.isStandalone(&parser, state: state) {
setNewLine = true 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] = [:] var inherit: [String: MustacheTemplate] = [:]
// parse tokens in section to extract inherited sections // parse tokens in section to extract inherited sections
for token in sectionTokens { for token in sectionTokens {
switch token { switch token {
case .inheritedSection(let name, let template): case .blockDefinition(let name, let template):
inherit[name] = template inherit[name] = template
case .text: case .text:
break break
@@ -236,7 +267,34 @@ extension MustacheTemplate {
throw Error.illegalTokenInsideInheritSection 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 "=": case "=":
// set delimiter // set delimiter

View File

@@ -28,7 +28,8 @@ extension MustacheTemplate {
if let indentation = context.indentation, indentation != "" { if let indentation = context.indentation, indentation != "" {
for token in tokens { for token in tokens {
let renderedString = self.renderToken(token, context: &context) 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 += indentation
} }
string += renderedString string += renderedString
@@ -75,11 +76,11 @@ extension MustacheTemplate {
let child = self.getChild(named: variable, transforms: transforms, context: context) let child = self.getChild(named: variable, transforms: transforms, context: context)
return self.renderInvertedSection(child, with: template, 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] { if let override = context.inherited?[name] {
return override.render(context: context) return override.render(context: context.withBlockExpansion(indented: indented))
} else { } else {
return template.render(context: context) return defaultTemplate.render(context: context.withBlockExpansion(indented: indented))
} }
case .partial(let name, let indentation, let overrides): case .partial(let name, let indentation, let overrides):
@@ -89,6 +90,9 @@ extension MustacheTemplate {
case .contentType(let contentType): case .contentType(let contentType):
context = context.withContentType(contentType) context = context.withContentType(contentType)
case .blockDefinition:
fatalError("Should not be rendering block definitions")
} }
return "" return ""
} }

View File

@@ -38,7 +38,8 @@ public struct MustacheTemplate: Sendable {
case unescapedVariable(name: String, transforms: [String] = []) case unescapedVariable(name: String, transforms: [String] = [])
case section(name: String, transforms: [String] = [], template: MustacheTemplate) case section(name: String, transforms: [String] = [], template: MustacheTemplate)
case invertedSection(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 partial(String, indentation: String?, inherits: [String: MustacheTemplate]?)
case contentType(MustacheContentType) case contentType(MustacheContentType)
} }

View File

@@ -62,8 +62,7 @@ final class PartialTests: XCTestCase {
""") """)
var library = MustacheLibrary() var library = MustacheLibrary()
library.register(template, named: "base") 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"]] let object: [String: Any] = ["names": ["john", "adam", "claire"]]
XCTAssertEqual(library.render(object, withTemplate: "base"), """ XCTAssertEqual(library.render(object, withTemplate: "base"), """
<h2>Names</h2> <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 /// Testing dynamic partials
func testDynamicPartials() throws { func testDynamicPartials() throws {
let template = try MustacheTemplate(string: """ let template = try MustacheTemplate(string: """
@@ -106,7 +136,6 @@ final class PartialTests: XCTestCase {
<head> <head>
<title>{{$title}}Default title{{/title}}</title> <title>{{$title}}Default title{{/title}}</title>
</head> </head>
""", """,
named: "header" 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
""")
}
} }

View File

@@ -86,7 +86,19 @@ final class MustacheSpecTests: XCTestCase {
func XCTAssertSpecEqual(_ result: String?, _ test: Spec.Test) { func XCTAssertSpecEqual(_ result: String?, _ test: Spec.Test) {
if result != test.expected { 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 data = try Data(contentsOf: url)
let spec = try JSONDecoder().decode(Spec.self, from: data) let spec = try JSONDecoder().decode(Spec.self, from: data)
print(spec.overview)
let date = Date() let date = Date()
for test in spec.tests { for test in spec.tests {
guard !ignoring.contains(test.name) else { continue } guard !ignoring.contains(test.name) else { continue }
@@ -113,6 +124,23 @@ final class MustacheSpecTests: XCTestCase {
print(-date.timeIntervalSinceNow) 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 { func testCommentsSpec() throws {
try self.testSpec(name: "comments") try self.testSpec(name: "comments")
} }
@@ -138,7 +166,12 @@ final class MustacheSpecTests: XCTestCase {
} }
func testInheritanceSpec() throws { func testInheritanceSpec() throws {
try XCTSkipIf(true) // inheritance spec has been updated and has added requirements, we don't yet support try self.testSpec(
try self.testSpec(name: "~inheritance") name: "~inheritance",
ignoring: [
"Intrinsic indentation",
"Nested block reindentation",
]
)
} }
} }