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.
This commit is contained in:
Mihaela M 2026-06-27 01:19:28 +02:00
parent 0ae88855da
commit ceec6cca84
4 changed files with 43 additions and 1 deletions

View File

@ -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

View File

@ -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,

View File

@ -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)
}

View File

@ -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"))])))
}
}