Blank lines between a block scalar header and its first content line come before the indent token, so the earlier blank-line capture skipped them. Buffer leading blanks while the block indentation is still pending and flush them as empty content once the indent is established, in the shared indentation path used by both scan loops. Also fix folded bodies to emit a break for a leading blank rather than folding it into a leading space. This closes the last block-scalar blank-line edge from #57.
321 lines
13 KiB
Swift
321 lines
13 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 }
|
|
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)
|
|
}
|
|
|
|
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 {
|
|
// A run of N blank lines folds to N line breaks: the first break
|
|
// is absorbed, the rest are literal.
|
|
var blankCount = 0
|
|
while index < lines.count, lines[index].isEmpty {
|
|
blankCount += 1
|
|
index += 1
|
|
}
|
|
result += String(repeating: "\n", count: blankCount)
|
|
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
|
|
}
|
|
}
|