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: """