From ceec6cca842c4add8997eb9e612d2b04a4e0e401 Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Sat, 27 Jun 2026 01:19:28 +0200 Subject: [PATCH] fix(parsing): allow a space before the colon of a quoted mapping key A quoted mapping key followed by a space and then ':' ('"key" : value') failed to parse: the scanner synthesizes the implicit mapping-key token by checking the character immediately after the key, but a quoted scalar stops at its closing quote, leaving the separating space, whereas a plain scalar consumes that space into itself and ends at the colon. The quoted path now looks past spaces for the colon, which the spec allows between an implicit key and its value indicator. Quoted keys with no space, quoted values, and plain keys (which already worked with a space) are unaffected. Measured against the official YAML test-suite, parse-level conformance rises from 238/333 (71.5%) to 240/333 (72.1%), with no regression. --- CHANGELOG.md | 6 ++++++ Sources/Parsing/Scanner.Helpers.swift | 12 ++++++++++++ Sources/Parsing/Scanner.swift | 7 ++++++- Tests/QuotedKeyTests.swift | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 Tests/QuotedKeyTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 74a7834..2c58b41 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 quoted mapping key may now have a space before its `:` (`"key" : value`). A plain key + consumes the space before the colon into the scalar, but a quoted key stops at its closing + quote, leaving the space, and the implicit-key check looked only at the immediate next + character. It now looks past spaces for the colon, as the spec allows. Quoted keys with no + space, quoted values, and plain keys are unaffected. Lifts official test-suite parse-level + conformance from 238/333 (71.5%) to 240/333 (72.1%). - 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 diff --git a/Sources/Parsing/Scanner.Helpers.swift b/Sources/Parsing/Scanner.Helpers.swift index 518a2ae..d8ee0e0 100644 --- a/Sources/Parsing/Scanner.Helpers.swift +++ b/Sources/Parsing/Scanner.Helpers.swift @@ -291,6 +291,18 @@ extension PureYAML.Parsing.Scanner { state.reader.peek() == ":" && shouldEndPlainScalarAtMappingValue(state: state) } + /// Whether a mapping-value indicator (`: ` or `:` at line end) follows, allowing spaces before + /// the colon. Used after a quoted scalar, where the reader stops at the closing quote and any + /// separating spaces remain, unlike a plain scalar that ends at the colon itself. + func isAtMappingValueIndicatorAfterSpaces(state: State) -> Bool { + var offset = 0 + while state.reader.peek(offset: offset) == " " { + offset += 1 + } + return state.reader.peek(offset: offset) == ":" + && isBlockMappingValueBoundary(state.reader.peek(offset: offset + 1)) + } + @discardableResult func appendMappingKeyIfNeeded( start: PureYAML.Parsing.Mark, diff --git a/Sources/Parsing/Scanner.swift b/Sources/Parsing/Scanner.swift index b30c6a3..9881984 100644 --- a/Sources/Parsing/Scanner.swift +++ b/Sources/Parsing/Scanner.swift @@ -288,7 +288,12 @@ extension PureYAML.Parsing.Scanner { throw PureYAML.Parsing.ParseError.unterminatedQuotedString(line: start.line) } state.reader.advance() - appendMappingKeyIfNeeded(start: start, state: &state) + // Unlike a plain scalar (which consumes the space before a `:` into itself), a quoted scalar + // stops at its closing quote, so the reader sits at any spaces before a `:`. A space between + // an implicit key and its mapping-value indicator is allowed, so look past them. + if isAtMappingValueIndicatorAfterSpaces(state: state) { + state.append(.mappingKey, mark: start, endMark: start) + } state.append(.scalar(value: value, style: style), mark: start, endMark: state.reader.mark) } diff --git a/Tests/QuotedKeyTests.swift b/Tests/QuotedKeyTests.swift new file mode 100644 index 0000000..4a3ec83 --- /dev/null +++ b/Tests/QuotedKeyTests.swift @@ -0,0 +1,19 @@ +@testable import PureYAML +import Testing + +@Suite("Quoted mapping keys") +struct QuotedKeyTests { + @Test("A quoted key may have a space before its colon") + func test_quotedKeyWithSpaceBeforeColon() throws { + let expected = PureYAML.Model.Value.mapping(.init([.init(key: "key", value: .string("value"))])) + #expect(try PureYAML.parse("\"key\" : value\n") == expected) + #expect(try PureYAML.parse("'key' : value\n") == expected) + // The no-space form keeps working. + #expect(try PureYAML.parse("\"key\": value\n") == expected) + } + + @Test("A quoted value, with or without a preceding space, is unaffected") + func test_quotedValueUnaffected() throws { + #expect(try PureYAML.parse("k: \"quoted value\"\n") == .mapping(.init([.init(key: "k", value: .string("quoted value"))]))) + } +}