In a folded block scalar, a blank run of N lines between two non-more-indented lines folds to N line breaks: the fold absorbs the first break. Next to a more-indented line nothing folds, so that break survives and the run yields N+1 breaks. The folder used N in every case, so paragraph spacing around an indented block collapsed: a blank line before a more-indented block came out as one break instead of two. Track whether the line before the blank run, or the one after it, is more-indented and add the extra break in that case. Folding between plain lines and runs with no blank lines are unchanged. Raises yaml-test-suite value-level conformance to 186/194 (95.9%) with the parse-level and should-error rejection rates unchanged. 444 tests pass.
342 lines
14 KiB
Swift
342 lines
14 KiB
Swift
extension PureYAML.Parsing.TokenEventParser {
|
|
mutating func parseBlockSequence(
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
allowMappingKeyTerminator: Bool = false,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
let start = try expect("block sequence entry") { $0.isBlockEntry }
|
|
try enforceBlockSequenceNotInlineAfterProperty(properties, firstEntry: start)
|
|
let startMark = properties.mark ?? start.mark
|
|
var endMark = start.endMark
|
|
try emit(.sequenceStart(
|
|
anchor: properties.anchor,
|
|
tag: properties.tag,
|
|
style: .block,
|
|
mark: startMark,
|
|
))
|
|
|
|
try parseBlockSequenceItem(
|
|
after: start,
|
|
endMark: &endMark,
|
|
allowMappingKeyTerminator: allowMappingKeyTerminator,
|
|
)
|
|
try consumeSequenceItemBoundaryDedent(sequenceIndentWidth: start.mark.column - 1)
|
|
try consumeSequenceItemBoundaryIndent(sequenceIndentWidth: start.mark.column - 1)
|
|
while cursor.current?.kind.isBlockEntry == true {
|
|
let entry = try expect("block sequence entry") { $0.isBlockEntry }
|
|
try parseBlockSequenceItem(
|
|
after: entry,
|
|
endMark: &endMark,
|
|
allowMappingKeyTerminator: allowMappingKeyTerminator,
|
|
)
|
|
try consumeSequenceItemBoundaryDedent(sequenceIndentWidth: entry.mark.column - 1)
|
|
try consumeSequenceItemBoundaryIndent(sequenceIndentWidth: entry.mark.column - 1)
|
|
}
|
|
if let current = cursor.current, current.kind.isMappingKey, !allowMappingKeyTerminator {
|
|
throw PureYAML.Parsing.ParseError.mixedCollectionStyles(line: current.mark.line)
|
|
}
|
|
|
|
try emit(.sequenceEnd(mark: endMark))
|
|
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
|
|
}
|
|
|
|
/// A block collection must begin on the line after its node properties, so an anchor or tag on
|
|
/// the same line as the sequence's first entry (`&a - x`) is malformed.
|
|
func enforceBlockSequenceNotInlineAfterProperty(
|
|
_ properties: PureYAML.Parsing.NodeProperties,
|
|
firstEntry: PureYAML.Parsing.Token,
|
|
) throws {
|
|
guard properties.anchor != nil || properties.tag != nil,
|
|
let propertyMark = properties.mark, propertyMark.line == firstEntry.mark.line
|
|
else {
|
|
return
|
|
}
|
|
throw PureYAML.Parsing.ParseError.inlineBlockSequenceAfterProperty(
|
|
line: propertyMark.line, column: propertyMark.column,
|
|
)
|
|
}
|
|
|
|
mutating func consumeSequenceItemBoundaryDedent(sequenceIndentWidth: Int) throws {
|
|
guard dedentWidth(cursor.current) == sequenceIndentWidth,
|
|
try cursor.peek()?.kind.isBlockEntry == true
|
|
else {
|
|
return
|
|
}
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
|
|
mutating func consumeSequenceItemBoundaryIndent(sequenceIndentWidth: Int) throws {
|
|
guard indentWidth(cursor.current) == sequenceIndentWidth,
|
|
try cursor.peek()?.kind.isBlockEntry == true
|
|
else {
|
|
return
|
|
}
|
|
_ = try expect("indent") { $0.isIndent }
|
|
}
|
|
|
|
mutating func parseBlockSequenceItem(
|
|
after entry: PureYAML.Parsing.Token,
|
|
endMark: inout PureYAML.Parsing.Mark,
|
|
allowMappingKeyTerminator: Bool,
|
|
) throws {
|
|
if hasNodeOnSameLine(after: entry) {
|
|
let properties = try consumeProperties()
|
|
if cursor.current?.kind.isIndent == true {
|
|
_ = try expect("indent") { $0.isIndent }
|
|
endMark = try parseNode(properties: properties).endMark
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
return
|
|
}
|
|
if cursor.current?.kind.isMappingKey == true {
|
|
endMark = try parseBlockMapping(
|
|
properties: properties,
|
|
includeIndentedContinuation: true,
|
|
minimumColumn: entry.mark.column + 1,
|
|
).endMark
|
|
} else if cursor.current?.kind.isBlockEntry == true {
|
|
endMark = try parseBlockSequence(
|
|
properties: properties,
|
|
allowMappingKeyTerminator: allowMappingKeyTerminator,
|
|
).endMark
|
|
} else {
|
|
endMark = try parseNode(properties: properties).endMark
|
|
}
|
|
} else if cursor.current?.kind.isIndent == true {
|
|
_ = try expect("indent") { $0.isIndent }
|
|
endMark = try parseNode().endMark
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
} else {
|
|
let mark = entry.endMark
|
|
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
|
|
endMark = mark
|
|
}
|
|
}
|
|
|
|
mutating func parseBlockMapping(
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
includeIndentedContinuation: Bool,
|
|
minimumColumn: Int? = nil,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
let startMark = properties.mark ?? cursor.current?.mark ?? .start
|
|
let tokenColumn = cursor.current?.mark.column ?? startMark.column
|
|
let contentColumn = min(startMark.column, tokenColumn)
|
|
let boundaryColumn = minimumColumn ?? contentColumn
|
|
var endMark = startMark
|
|
try emit(.mappingStart(
|
|
anchor: properties.anchor,
|
|
tag: properties.tag,
|
|
style: .block,
|
|
mark: startMark,
|
|
))
|
|
|
|
try parseBlockMappingPairs(
|
|
boundaryColumn: boundaryColumn,
|
|
includeIndentedContinuation: includeIndentedContinuation,
|
|
endMark: &endMark,
|
|
)
|
|
if let current = cursor.current, !canEndBlockMapping(at: current, minimumColumn: boundaryColumn) {
|
|
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: "mapping key",
|
|
actual: current.kind.description,
|
|
line: current.mark.line,
|
|
column: current.mark.column,
|
|
)
|
|
}
|
|
try emit(.mappingEnd(mark: endMark))
|
|
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
|
|
}
|
|
|
|
mutating func parseBlockMappingPairs(
|
|
boundaryColumn: Int,
|
|
includeIndentedContinuation: Bool,
|
|
endMark: inout PureYAML.Parsing.Mark,
|
|
) throws {
|
|
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
|
|
endMark = try parseMappingPairAndBoundaries(
|
|
boundaryColumn: boundaryColumn,
|
|
includeIndentedContinuation: includeIndentedContinuation,
|
|
).endMark
|
|
}
|
|
while includeIndentedContinuation, cursor.current?.kind.isIndent == true {
|
|
endMark = try parseIndentedBlockMappingPairs(boundaryColumn: boundaryColumn).endMark
|
|
}
|
|
}
|
|
|
|
mutating func parseIndentedBlockMappingPairs(
|
|
boundaryColumn: Int,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
_ = try expect("indent") { $0.isIndent }
|
|
guard cursor.current?.kind.isMappingKey == true else {
|
|
throw unexpectedToken(expected: "mapping key")
|
|
}
|
|
var endMark = cursor.current?.mark ?? .start
|
|
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
|
|
endMark = try parseMappingPairAndBoundaries(
|
|
boundaryColumn: boundaryColumn,
|
|
includeIndentedContinuation: true,
|
|
).endMark
|
|
}
|
|
if !canEndBlockMapping(at: cursor.current, minimumColumn: boundaryColumn) {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
|
|
}
|
|
|
|
mutating func parseMappingPairAndBoundaries(
|
|
boundaryColumn: Int,
|
|
includeIndentedContinuation: Bool,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
let result = try parseMappingPair()
|
|
try consumeMappingSiblingBoundaryDedents(minimumColumn: boundaryColumn)
|
|
try consumeMappingSiblingBoundaryReindent(minimumColumn: boundaryColumn)
|
|
if includeIndentedContinuation {
|
|
try consumeDedents(atOrAbove: boundaryColumn)
|
|
try consumeMappingSiblingBoundaryDedents(minimumColumn: boundaryColumn)
|
|
try consumeMappingSiblingBoundaryReindent(minimumColumn: boundaryColumn)
|
|
}
|
|
return result
|
|
}
|
|
|
|
mutating func parseBlockScalar(
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
let header = try expect("block scalar header") { kind in
|
|
if case .blockScalarHeader = kind {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
var lines: [String] = []
|
|
var endMark = header.endMark
|
|
if cursor.current?.kind.isIndent == true {
|
|
_ = try expect("indent") { $0.isIndent }
|
|
while cursor.current != nil, cursor.current?.kind.isDedent != true {
|
|
guard case let .scalar(value, style) = cursor.current?.kind else {
|
|
throw unexpectedToken(expected: "block scalar content")
|
|
}
|
|
let token = try cursor.advance() ?? header
|
|
try lines.append(decodeScalar(value, style: style, line: token.mark.line))
|
|
endMark = token.endMark
|
|
}
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
|
|
let value = blockScalarValue(lines: lines, header: header.kind)
|
|
let mark = properties.mark ?? header.mark
|
|
try emit(.scalar(
|
|
value: value,
|
|
anchor: properties.anchor,
|
|
tag: properties.tag,
|
|
style: scalarStyle(for: header.kind),
|
|
mark: mark,
|
|
))
|
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: endMark)
|
|
}
|
|
|
|
func scalarStyle(for kind: PureYAML.Parsing.TokenKind) -> PureYAML.Parsing.ScalarStyle {
|
|
switch kind {
|
|
case .blockScalarHeader(.folded, _):
|
|
.folded
|
|
case .blockScalarHeader(.literal, _):
|
|
.literal
|
|
default:
|
|
.plain
|
|
}
|
|
}
|
|
|
|
func blockScalarValue(
|
|
lines: [String],
|
|
header: PureYAML.Parsing.TokenKind,
|
|
) -> String {
|
|
let (style, chomping): (PureYAML.Parsing.BlockScalarStyle, PureYAML.Parsing.BlockScalarChomping)
|
|
switch header {
|
|
case let .blockScalarHeader(headerStyle, headerChomping):
|
|
style = headerStyle
|
|
chomping = headerChomping
|
|
default:
|
|
style = .literal
|
|
chomping = .clip
|
|
}
|
|
|
|
// Each content line carries its own line break. The folded style folds a
|
|
// single break between two non-empty lines into a space, while blank
|
|
// lines stay as line breaks. Chomping then trims the trailing breaks.
|
|
// For content with no blank lines (the only shape the scanner captures
|
|
// today) this is identical to the previous join-and-append behavior.
|
|
let body: String = switch style {
|
|
case .literal:
|
|
lines.map { $0 + "\n" }.joined()
|
|
case .folded:
|
|
foldedBlockBody(lines)
|
|
}
|
|
return chompedBlockScalar(body, chomping: chomping)
|
|
}
|
|
|
|
func foldedBlockBody(_ lines: [String]) -> String {
|
|
guard let firstContent = lines.firstIndex(where: { !$0.isEmpty }) else {
|
|
// The block is entirely blank lines: one break each, no folding.
|
|
return String(repeating: "\n", count: lines.count)
|
|
}
|
|
// Leading blank lines each contribute a line break before the content.
|
|
var result = String(repeating: "\n", count: firstContent) + lines[firstContent]
|
|
var index = firstContent + 1
|
|
while index < lines.count {
|
|
if lines[index].isEmpty {
|
|
// Between two non-more-indented lines a blank run of N folds to N line breaks: the
|
|
// first break is the absorbed fold, the rest are literal. Next to a more-indented line
|
|
// nothing folds, so the absorbed break survives too and the run yields N+1 breaks.
|
|
let beforeMoreIndented = isMoreIndentedBlockLine(lines[index - 1])
|
|
var blankCount = 0
|
|
while index < lines.count, lines[index].isEmpty {
|
|
blankCount += 1
|
|
index += 1
|
|
}
|
|
let afterMoreIndented = index < lines.count && isMoreIndentedBlockLine(lines[index])
|
|
let breaks = beforeMoreIndented || afterMoreIndented ? blankCount + 1 : blankCount
|
|
result += String(repeating: "\n", count: breaks)
|
|
if index < lines.count {
|
|
result += lines[index]
|
|
index += 1
|
|
}
|
|
} else if isMoreIndentedBlockLine(lines[index]) || isMoreIndentedBlockLine(lines[index - 1]) {
|
|
// A line break adjacent to a more-indented line is kept literal,
|
|
// so the more-indented line and its breaks survive folding.
|
|
result += "\n" + lines[index]
|
|
index += 1
|
|
} else {
|
|
result += " " + lines[index]
|
|
index += 1
|
|
}
|
|
}
|
|
return result + "\n"
|
|
}
|
|
|
|
func isMoreIndentedBlockLine(_ line: String) -> Bool {
|
|
// In a block scalar the scanner strips content lines to the block's
|
|
// indentation, so only a more-indented line retains leading whitespace.
|
|
line.first == " " || line.first == "\t"
|
|
}
|
|
|
|
func chompedBlockScalar(
|
|
_ body: String,
|
|
chomping: PureYAML.Parsing.BlockScalarChomping,
|
|
) -> String {
|
|
switch chomping {
|
|
case .keep:
|
|
return body
|
|
case .strip:
|
|
return blockScalarTrimmingTrailingNewlines(body)
|
|
case .clip:
|
|
let trimmed = blockScalarTrimmingTrailingNewlines(body)
|
|
return trimmed.isEmpty ? "" : trimmed + "\n"
|
|
}
|
|
}
|
|
|
|
func blockScalarTrimmingTrailingNewlines(_ value: String) -> String {
|
|
var result = value
|
|
while result.hasSuffix("\n") {
|
|
result.removeLast()
|
|
}
|
|
return result
|
|
}
|
|
}
|