A property written inline before a block mapping's first key (`&a key: v`) was attached to the whole mapping, so an anchor registered only when the mapping ended, too late for an alias inside one of the mapping's values to resolve. The YAML grammar requires a block collection's own properties to be followed by a line break (`s-l+block-collection`), so a property on the first key's line belongs to the key, not the collection. The mapping now keeps only a property written on its own line above the first key; an inline one is threaded back to the first pair, where the key parses it and an anchor registers with the key. Fixes yaml-test-suite 2SXE (`&a:` and `&a` are distinct anchors, since a name includes a trailing `:`) and HMQ5 (a key carrying both a tag and an anchor). Raises parse-level conformance to 308/333 (92.5%) and value-level to 201/201 (100.0%), with the should-error rate unchanged.
367 lines
16 KiB
Swift
367 lines
16 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)
|
|
// A compact nested sequence (`- - x`) carries no indent/dedent tokens, only block entries at
|
|
// their columns, so the sibling-entry loop must stay at this sequence's own column: a later
|
|
// entry at a smaller column belongs to an enclosing sequence and ends this one.
|
|
let sequenceColumn = start.mark.column
|
|
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, cursor.current?.mark.column == sequenceColumn {
|
|
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
|
|
// A property written inline before the first key (`&a key: v`) binds to that key, not to the
|
|
// mapping: the spec requires a block collection's own properties to be followed by a line
|
|
// break (`s-l+block-collection`), so a property on the key's line is the key's. This matters
|
|
// for anchors, which must register with the key (early enough for an alias inside a value to
|
|
// resolve) rather than with the mapping, which registers only at its end. A property on its
|
|
// own line above the first key still binds to the mapping.
|
|
let firstKeyProperties: PureYAML.Parsing.NodeProperties
|
|
let mappingProperties: PureYAML.Parsing.NodeProperties
|
|
if properties != .none, properties.mark?.line == cursor.current?.mark.line {
|
|
firstKeyProperties = properties
|
|
mappingProperties = .none
|
|
} else {
|
|
firstKeyProperties = .none
|
|
mappingProperties = properties
|
|
}
|
|
try emit(.mappingStart(
|
|
anchor: mappingProperties.anchor,
|
|
tag: mappingProperties.tag,
|
|
style: .block,
|
|
mark: startMark,
|
|
))
|
|
|
|
try parseBlockMappingPairs(
|
|
boundaryColumn: boundaryColumn,
|
|
includeIndentedContinuation: includeIndentedContinuation,
|
|
firstKeyProperties: firstKeyProperties,
|
|
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,
|
|
firstKeyProperties: PureYAML.Parsing.NodeProperties = .none,
|
|
endMark: inout PureYAML.Parsing.Mark,
|
|
) throws {
|
|
var pendingKeyProperties = firstKeyProperties
|
|
while canContinueBlockMapping(minimumColumn: boundaryColumn) {
|
|
endMark = try parseMappingPairAndBoundaries(
|
|
boundaryColumn: boundaryColumn,
|
|
includeIndentedContinuation: includeIndentedContinuation,
|
|
firstKeyProperties: pendingKeyProperties,
|
|
).endMark
|
|
pendingKeyProperties = .none
|
|
}
|
|
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,
|
|
firstKeyProperties: PureYAML.Parsing.NodeProperties = .none,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
let result = try parseMappingPair(firstKeyProperties: firstKeyProperties)
|
|
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
|
|
}
|
|
}
|