A block mapping pair after the first could not carry an anchor or tag on its key (k: 1 then &b key2: 2): the first pair's key properties are consumed when the mapping is entered, but subsequent pairs were not, so an anchored key past the first failed with 'expected mapping key, found anchor'. The pair parser now consumes leading key properties for every pair, attaching them to the key node, and a block mapping continues across an anchor or tag token that begins a key. Normal mappings, anchored values, sequences, and explicit keys are unaffected. Measured against the official YAML test-suite, parse-level conformance rises from 235/333 (70.6%) to 238/333 (71.5%), with no regression.
342 lines
12 KiB
Swift
342 lines
12 KiB
Swift
extension PureYAML.Parsing.TokenEventParser {
|
|
mutating func parseMappingPair() throws -> PureYAML.Parsing.NodeResult {
|
|
// A subsequent pair's key may carry an anchor or tag (`&a key: …`). The first pair's
|
|
// properties are consumed when the mapping is entered; consuming here covers the rest, and
|
|
// is a no-op (`.none`) when there are none.
|
|
let keyProperties = try consumeProperties()
|
|
_ = try expect("mapping key") { $0.isMappingKey }
|
|
let key = try parseNode(properties: keyProperties)
|
|
// An explicit key (`? key`) with no following `:` has an implicit null value, so several
|
|
// `? key` entries in a row (a set, for instance) each map to null rather than being an error.
|
|
guard case .mappingValue = cursor.current?.kind else {
|
|
return try emitNullMappingValue(at: key.endMark)
|
|
}
|
|
let valueIndicator = try expect("mapping value") { kind in
|
|
if case .mappingValue = kind {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
let properties = try consumeProperties()
|
|
if cursor.current?.kind.isIndent == true {
|
|
_ = try expect("indent") { $0.isIndent }
|
|
let result: PureYAML.Parsing.NodeResult = if cursor.current?.kind.isBlockEntry == true {
|
|
try parseBlockSequence(
|
|
properties: properties,
|
|
allowMappingKeyTerminator: true,
|
|
)
|
|
} else {
|
|
try parseNode(properties: properties)
|
|
}
|
|
if cursor.current?.kind.isDedent == true {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
} else if cursor.current?.kind.isMappingKey != true {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
return result
|
|
}
|
|
if properties == .none, try canParsePlainScalarWithContinuation(after: valueIndicator) {
|
|
return try parsePlainScalarWithContinuation(properties: properties)
|
|
}
|
|
if cursor.current?.kind.isMappingKey == true, hasNodeOnSameLine(after: valueIndicator) {
|
|
return try parseBlockMapping(
|
|
properties: properties,
|
|
includeIndentedContinuation: true,
|
|
minimumColumn: cursor.current?.mark.column,
|
|
)
|
|
}
|
|
if properties != .none || hasNodeOnSameLine(after: valueIndicator) {
|
|
return try parseNode(properties: properties)
|
|
}
|
|
if cursor.current?.kind.isBlockEntry == true {
|
|
return try parseBlockSequence(
|
|
properties: properties,
|
|
allowMappingKeyTerminator: true,
|
|
)
|
|
}
|
|
|
|
return try emitNullMappingValue(at: valueIndicator.endMark)
|
|
}
|
|
|
|
/// Emit an empty (null) scalar as a mapping value and return its node result, used where a
|
|
/// mapping entry has no value: an explicit key with no `:`, or a `key:` with nothing after it.
|
|
mutating func emitNullMappingValue(at mark: PureYAML.Parsing.Mark) throws -> PureYAML.Parsing.NodeResult {
|
|
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark))
|
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
|
|
}
|
|
|
|
mutating func parsePlainScalarWithContinuation(
|
|
properties: PureYAML.Parsing.NodeProperties,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
let first = try expect("plain scalar") { kind in
|
|
if case .scalar(_, .plain) = kind {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
var value = try decodedScalar(from: first)
|
|
var endMark = first.endMark
|
|
|
|
_ = try expect("indent") { $0.isIndent }
|
|
while isScalar(cursor.current) {
|
|
let token = try cursor.advance() ?? first
|
|
let continuation = try plainContinuationText(from: token)
|
|
value += " " + continuation
|
|
endMark = token.endMark
|
|
}
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
|
|
let mark = properties.mark ?? first.mark
|
|
try emit(.scalar(
|
|
value: value,
|
|
anchor: properties.anchor,
|
|
tag: properties.tag,
|
|
style: .plain,
|
|
mark: mark,
|
|
))
|
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: endMark)
|
|
}
|
|
|
|
mutating func consumeProperties() throws -> PureYAML.Parsing.NodeProperties {
|
|
var properties = PureYAML.Parsing.NodeProperties.none
|
|
while let token = cursor.current {
|
|
switch token.kind {
|
|
case let .anchor(anchor):
|
|
properties.anchor = anchor
|
|
properties.mark = properties.mark ?? token.mark
|
|
_ = try cursor.advance()
|
|
case let .tag(tag):
|
|
properties.tag = tag
|
|
properties.mark = properties.mark ?? token.mark
|
|
_ = try cursor.advance()
|
|
default:
|
|
return properties
|
|
}
|
|
}
|
|
return properties
|
|
}
|
|
|
|
func decodeScalar(
|
|
_ value: String,
|
|
style: PureYAML.Parsing.ScalarStyle,
|
|
line: Int,
|
|
) throws -> String {
|
|
switch style {
|
|
case .doubleQuoted:
|
|
try scalarParser.parseDoubleQuoted("\"\(value)\"", line: line)
|
|
case .folded, .literal:
|
|
value
|
|
case .plain:
|
|
value
|
|
case .singleQuoted:
|
|
try scalarParser.parseSingleQuoted("'\(value)'", line: line)
|
|
}
|
|
}
|
|
|
|
func decodedScalar(from token: PureYAML.Parsing.Token) throws -> String {
|
|
guard case let .scalar(value, style) = token.kind else {
|
|
throw unexpectedToken(expected: "scalar")
|
|
}
|
|
return try decodeScalar(value, style: style, line: token.mark.line)
|
|
}
|
|
|
|
func plainContinuationText(from token: PureYAML.Parsing.Token) throws -> String {
|
|
guard case let .scalar(value, style) = token.kind else {
|
|
throw unexpectedToken(expected: "scalar")
|
|
}
|
|
switch style {
|
|
case .doubleQuoted:
|
|
return "\"\(value)\""
|
|
case .singleQuoted:
|
|
return "'\(value)'"
|
|
case .folded, .literal, .plain:
|
|
return try decodeScalar(value, style: style, line: token.mark.line)
|
|
}
|
|
}
|
|
|
|
func hasNodeOnSameLine(after token: PureYAML.Parsing.Token) -> Bool {
|
|
guard let current = cursor.current, !current.kind.isTerminator else {
|
|
return false
|
|
}
|
|
return current.mark.line == token.mark.line
|
|
}
|
|
|
|
mutating func canParsePlainScalarWithContinuation(after token: PureYAML.Parsing.Token) throws -> Bool {
|
|
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
|
return false
|
|
}
|
|
guard cursor.current?.mark.line == token.mark.line else {
|
|
return false
|
|
}
|
|
guard try cursor.peek()?.kind.isIndent == true else {
|
|
return false
|
|
}
|
|
guard try isScalar(cursor.peek(offset: 2)) else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
mutating func canParsePlainScalarContinuation() throws -> Bool {
|
|
guard case .scalar(_, .plain) = cursor.current?.kind else {
|
|
return false
|
|
}
|
|
guard try cursor.peek()?.kind.isIndent == true else {
|
|
return false
|
|
}
|
|
guard try isScalar(cursor.peek(offset: 2)) else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isScalar(_ token: PureYAML.Parsing.Token?) -> Bool {
|
|
guard case .scalar = token?.kind else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
mutating func consumeDedents(atOrAbove minimumColumn: Int) throws {
|
|
while let width = dedentWidth(cursor.current), width >= minimumColumn {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
}
|
|
|
|
mutating func consumeMappingSiblingBoundaryDedents(minimumColumn: Int) throws {
|
|
while try canConsumeMappingSiblingBoundaryDedent(minimumColumn: minimumColumn) {
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
}
|
|
}
|
|
|
|
mutating func canConsumeMappingSiblingBoundaryDedent(minimumColumn: Int) throws -> Bool {
|
|
guard let width = dedentWidth(cursor.current),
|
|
let next = try cursor.peek(),
|
|
next.kind.isMappingKey,
|
|
next.mark.column >= minimumColumn
|
|
else {
|
|
return false
|
|
}
|
|
return width == next.mark.column - 1
|
|
}
|
|
|
|
mutating func consumeMappingSiblingBoundaryReindent(minimumColumn: Int) throws {
|
|
guard let dedent = dedentWidth(cursor.current),
|
|
dedent < minimumColumn,
|
|
try indentWidth(cursor.peek()) == minimumColumn,
|
|
let mappingKey = try cursor.peek(offset: 2),
|
|
mappingKey.kind.isMappingKey,
|
|
mappingKey.mark.column == minimumColumn + 1
|
|
else {
|
|
return
|
|
}
|
|
_ = try expect("dedent") { $0.isDedent }
|
|
_ = try expect("indent") { $0.isIndent }
|
|
}
|
|
|
|
func dedentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
|
|
guard case let .dedent(width) = token?.kind else {
|
|
return nil
|
|
}
|
|
return width
|
|
}
|
|
|
|
func indentWidth(_ token: PureYAML.Parsing.Token?) -> Int? {
|
|
guard case let .indent(width) = token?.kind else {
|
|
return nil
|
|
}
|
|
return width
|
|
}
|
|
|
|
func canContinueBlockMapping(minimumColumn: Int?) -> Bool {
|
|
guard let current = cursor.current, current.kind.isMappingKey || startsNodeProperties(current.kind) else {
|
|
return false
|
|
}
|
|
guard let minimumColumn else {
|
|
return true
|
|
}
|
|
return current.mark.column >= minimumColumn
|
|
}
|
|
|
|
/// Whether a token kind begins a node's properties (an anchor or a tag), which a mapping key may
|
|
/// carry, so the next pair's key is `&a key: …` rather than a bare key.
|
|
func startsNodeProperties(_ kind: PureYAML.Parsing.TokenKind) -> Bool {
|
|
switch kind {
|
|
case .anchor, .tag:
|
|
true
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
func canEndBlockMapping(
|
|
at token: PureYAML.Parsing.Token?,
|
|
minimumColumn: Int? = nil,
|
|
) -> Bool {
|
|
guard let token else {
|
|
return true
|
|
}
|
|
return token.kind.isBlockEntry
|
|
|| token.kind.isDedent
|
|
|| token.kind.isDocumentEnd
|
|
|| token.kind.isDocumentStart
|
|
|| token.kind.isFlowTerminator
|
|
|| isLowerColumnMappingKey(token, minimumColumn: minimumColumn)
|
|
|| token.kind.isStreamEnd
|
|
}
|
|
|
|
func isLowerColumnMappingKey(
|
|
_ token: PureYAML.Parsing.Token?,
|
|
minimumColumn: Int?,
|
|
) -> Bool {
|
|
guard let token, token.kind.isMappingKey, let minimumColumn else {
|
|
return false
|
|
}
|
|
return token.mark.column < minimumColumn
|
|
}
|
|
|
|
mutating func expect(
|
|
_ expected: String,
|
|
matching: (PureYAML.Parsing.TokenKind) -> Bool,
|
|
) throws -> PureYAML.Parsing.Token {
|
|
guard let token = cursor.current else {
|
|
let mark = cursor.previous?.endMark ?? .start
|
|
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: expected,
|
|
actual: "end of tokens",
|
|
line: mark.line,
|
|
column: mark.column,
|
|
)
|
|
}
|
|
guard matching(token.kind) else {
|
|
throw PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: expected,
|
|
actual: token.kind.description,
|
|
line: token.mark.line,
|
|
column: token.mark.column,
|
|
)
|
|
}
|
|
_ = try cursor.advance()
|
|
return token
|
|
}
|
|
|
|
func unexpectedToken(expected: String) -> PureYAML.Parsing.ParseError {
|
|
guard let token = cursor.current else {
|
|
let mark = cursor.previous?.endMark ?? .start
|
|
return PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: expected,
|
|
actual: "end of tokens",
|
|
line: mark.line,
|
|
column: mark.column,
|
|
)
|
|
}
|
|
return PureYAML.Parsing.ParseError.unexpectedToken(
|
|
expected: expected,
|
|
actual: token.kind.description,
|
|
line: token.mark.line,
|
|
column: token.mark.column,
|
|
)
|
|
}
|
|
}
|