diff --git a/CHANGELOG.md b/CHANGELOG.md index b4803e9..74a7834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A block mapping pair after the first may now carry an anchor or tag on its key + (`k: 1` / `&b key2: 2`). The first pair's key properties were consumed when the mapping was + entered, but later pairs were not, so an anchored key past the first failed with + "expected mapping key". The pair parser now consumes leading key properties for every pair, + and the mapping continues across an anchor or tag that begins a key. Lifts official test-suite + parse-level conformance from 235/333 (70.6%) to 238/333 (71.5%). - An explicit mapping key (`? key`) with no following `:` now maps to null instead of failing to parse, so several `? key` entries in a row (a set, for instance) each map to null. The parser required a `:` after every explicit key; it now treats a missing one as an implicit null value. diff --git a/Sources/Parsing/TokenEventParser.Helpers.swift b/Sources/Parsing/TokenEventParser.Helpers.swift index 702b940..60abae4 100644 --- a/Sources/Parsing/TokenEventParser.Helpers.swift +++ b/Sources/Parsing/TokenEventParser.Helpers.swift @@ -1,7 +1,11 @@ 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() + 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 { @@ -246,7 +250,7 @@ extension PureYAML.Parsing.TokenEventParser { } func canContinueBlockMapping(minimumColumn: Int?) -> Bool { - guard let current = cursor.current, current.kind.isMappingKey else { + guard let current = cursor.current, current.kind.isMappingKey || startsNodeProperties(current.kind) else { return false } guard let minimumColumn else { @@ -255,6 +259,17 @@ extension PureYAML.Parsing.TokenEventParser { 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, diff --git a/Tests/AnchoredMappingKeyTests.swift b/Tests/AnchoredMappingKeyTests.swift new file mode 100644 index 0000000..3d2b803 --- /dev/null +++ b/Tests/AnchoredMappingKeyTests.swift @@ -0,0 +1,19 @@ +@testable import PureYAML +import Testing + +@Suite("Anchored mapping keys") +struct AnchoredMappingKeyTests { + @Test("A subsequent pair's key may carry an anchor") + func test_anchoredKeyOnSubsequentPair() throws { + let value = try PureYAML.parse("k: 1\n&b key2: 2\n") + #expect(value == .mapping(.init([ + .init(key: "k", value: .int(1)), + .init(key: "key2", value: .int(2)), + ]))) + } + + @Test("An anchored key on the first pair still parses") + func test_anchoredKeyOnFirstPair() throws { + #expect(try PureYAML.parse("&a c: 3\n") == .mapping(.init([.init(key: "c", value: .int(3))]))) + } +}