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
|
/// 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
""")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user