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