The scanner dropped every blank line within and after a block scalar, so a literal block lost internal blank lines, a folded block turned a blank line into a space rather than a newline, and the keep indicator could not preserve trailing blank lines. Capture blank lines as empty content in both the batch scan loop and the streaming TokenCursor loop, which had drifted as duplicate copies. Rework blockScalarValue to apply literal versus folded line semantics and clip/strip/keep chomping over the captured lines; content without blank lines is unchanged. Re-pin the multiline-scalar test whose expectation encoded the old dropped-blank behavior, and add a chomping-and-folding test matrix.
308 lines
12 KiB
Swift
308 lines
12 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 first = lines.first else {
|
|
return ""
|
|
}
|
|
var result = first
|
|
var index = 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 {
|
|
result += " " + lines[index]
|
|
index += 1
|
|
}
|
|
}
|
|
return result + "\n"
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|