A mapping key is a full node: it may carry &anchor and !tag properties and may itself be an *alias. Fixes: (1) anchor/alias name scanning now stops at the first non-name character per yaml_parser_scan_anchor, so *a: reads as alias 'a' plus a mapping indicator instead of name 'a:'; (2) the block-mapping parser enters on a leading node property or alias, attaches same-line key properties to the key node, and emits alias keys directly; (3) an anchor or tag at value position with no same-line node composes an empty (null) value carrying the property, so 'a: &anchor' registers the anchor; (4) the composer registers an anchor's resolved model value (an empty anchored scalar registers null); (5) implicit keys with trailing spaces before the colon are recognized. Flips 10 official YAML test suite cases to passing (280/402), including G9HC now correctly rejected. 2SXE no longer false-rejects but still value-mismatches on compact-mapping interpretation (a separate gap), so it is reclassified in the manifest rather than removed. Updates the manifest, fixtures, counts, and roadmap.
345 lines
12 KiB
Swift
345 lines
12 KiB
Swift
extension PureYAML.Parsing.TokenEventParser {
|
|
mutating func parseMappingPair(
|
|
keyProperties: PureYAML.Parsing.NodeProperties = .none,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
try parseMappingPairKey(preconsumedProperties: keyProperties)
|
|
guard cursor.current?.kind.isMappingValue == true else {
|
|
let mark = cursor.current?.mark ?? cursor.previous?.endMark ?? .start
|
|
try emitEmptyScalar(properties: .none, mark: mark)
|
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
|
|
}
|
|
let valueIndicator = try expect("mapping value") { kind in
|
|
if case .mappingValue = kind {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
return try parseMappingPairValue(valueIndicator: valueIndicator)
|
|
}
|
|
|
|
mutating func parseMappingPairValue(
|
|
valueIndicator: PureYAML.Parsing.Token,
|
|
) throws -> PureYAML.Parsing.NodeResult {
|
|
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,
|
|
)
|
|
}
|
|
// An anchor or tag at value position with no node following on the same
|
|
// line (the next token opens a sibling key) marks an empty value that
|
|
// still carries the properties, so `a: &anchor` registers `anchor` as
|
|
// null rather than swallowing the next key into a nested mapping.
|
|
if properties != .none, !hasNodeOnSameLine(after: valueIndicator) {
|
|
let mark = properties.mark ?? valueIndicator.endMark
|
|
try emitEmptyScalar(properties: properties, mark: mark)
|
|
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
|
|
}
|
|
if properties != .none || hasNodeOnSameLine(after: valueIndicator) {
|
|
return try parseNode(properties: properties)
|
|
}
|
|
if cursor.current?.kind.isBlockEntry == true {
|
|
return try parseBlockSequence(
|
|
properties: properties,
|
|
allowMappingKeyTerminator: true,
|
|
)
|
|
}
|
|
|
|
let mark = valueIndicator.endMark
|
|
try emitEmptyScalar(properties: .none, 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 {
|
|
// A pair starts at a `mappingKey`/`mappingValue` indicator, or at the
|
|
// node properties (anchor/tag) or alias that introduce a key node whose
|
|
// own indicator the scanner emits after them.
|
|
guard let current = cursor.current,
|
|
current.kind.isMappingKey
|
|
|| current.kind.isMappingValue
|
|
|| current.kind.isNodeProperty
|
|
|| current.kind.isAlias
|
|
else {
|
|
return false
|
|
}
|
|
guard let minimumColumn else {
|
|
return true
|
|
}
|
|
return current.mark.column >= minimumColumn
|
|
}
|
|
|
|
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,
|
|
)
|
|
}
|
|
}
|