From 7689de0a424a983db56c8baa4457824346fbf7ba Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Mon, 15 Jul 2024 09:36:15 +0100 Subject: [PATCH] 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 --- Sources/Mustache/Context.swift | 17 ++++ Sources/Mustache/Template+Parser.swift | 116 ++++++++++++++++++------- Sources/Mustache/Template+Render.swift | 12 ++- Sources/Mustache/Template.swift | 3 +- Tests/MustacheTests/PartialTests.swift | 71 +++++++++++++-- Tests/MustacheTests/SpecTests.swift | 41 ++++++++- 6 files changed, 215 insertions(+), 45 deletions(-) diff --git a/Sources/Mustache/Context.swift b/Sources/Mustache/Context.swift index 9aaae62..ab48e3e 100644 --- a/Sources/Mustache/Context.swift +++ b/Sources/Mustache/Context.swift @@ -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 diff --git a/Sources/Mustache/Template+Parser.swift b/Sources/Mustache/Template+Parser.swift index 566de74..0e61ea2 100644 --- a/Sources/Mustache/Template+Parser.swift +++ b/Sources/Mustache/Template+Parser.swift @@ -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 diff --git a/Sources/Mustache/Template+Render.swift b/Sources/Mustache/Template+Render.swift index 04014ef..6be73eb 100644 --- a/Sources/Mustache/Template+Render.swift +++ b/Sources/Mustache/Template+Render.swift @@ -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 "" } diff --git a/Sources/Mustache/Template.swift b/Sources/Mustache/Template.swift index e62e0ee..fe152ac 100644 --- a/Sources/Mustache/Template.swift +++ b/Sources/Mustache/Template.swift @@ -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) } diff --git a/Tests/MustacheTests/PartialTests.swift b/Tests/MustacheTests/PartialTests.swift index fda39c1..c8496c6 100644 --- a/Tests/MustacheTests/PartialTests.swift +++ b/Tests/MustacheTests/PartialTests.swift @@ -21,7 +21,7 @@ final class PartialTests: XCTestCase { let template = try MustacheTemplate(string: """

Names

{{#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"), """

Names

- john - adam - claire + john + adam + claire """) } @@ -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"), """

Names

@@ -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 { {{$title}}Default title{{/title}} - """, 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( + """ + {{