diff --git a/CHANGELOG.md b/CHANGELOG.md index 84999d8..ea0908c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Apply the YAML 1.2 flow `:` rule instead of libyaml's "always a value + indicator in flow". In a flow collection a `:` only ends a node when it is + followed by whitespace, the end of input, or a flow indicator, or when it is + adjacent to a JSON key (a quoted scalar or a flow collection used as the key). + Otherwise the `:` is plain-scalar content. So `[:x]` and `{x: :x}` keep the + scalar `:x`, `http://foo.com` and `::vector` stay single scalars, `"key":value` + and `{JSON: like}:adjacent` bind the value across the adjacent `:`, and + `omitted value:,` is a key with an empty value rather than a scan error. The + pending simple key now records whether it is a JSON node, and the plain scalar + scanner ends (rather than rejecting) at a `:` before a flow indicator. Surpasses + libyaml on 4ABK, 58MP, 5T43, DBG4, FRK4, and HM87/00, with 9MMW (mixed + adjacent/separate keys) still correct. - Treat `?` in a flow collection as plain-scalar content unless it is followed by whitespace, the end of input, or a flow indicator. libyaml makes every `?` in flow context a mapping-key indicator, dropping the `?` from `?foo`, but diff --git a/Sources/Parsing/LibYAML.Scanner.Fetch.swift b/Sources/Parsing/LibYAML.Scanner.Fetch.swift index 58f3567..bcb6c1c 100644 --- a/Sources/Parsing/LibYAML.Scanner.Fetch.swift +++ b/Sources/Parsing/LibYAML.Scanner.Fetch.swift @@ -111,7 +111,12 @@ extension PureYAML.Parsing.LibYAML.Scanner { // `ns-plain-first` a `?` followed by a flow-safe character (anything but // whitespace, end, or a flow indicator) begins a plain scalar (`?foo`). fetchKey() - case ":" where flowLevel > 0 || Classify.isBlankZ(reader.peek(1)): + case ":" where Classify.isBlankZ(reader.peek(1)) + || (flowLevel > 0 && (Classify.isFlowIndicator(reader.peek(1)) || flowKeyIsJSON)): + // In flow context libyaml makes every `:` a value indicator. Per the + // spec a `:` only ends a node when followed by whitespace, end of + // input, or a flow indicator, except an adjacent `:` after a JSON + // (quoted) key. Otherwise the `:` is plain-scalar content (`:x`). try fetchValue() default: // A plain scalar may begin with any non-blank character; a blank (a @@ -128,6 +133,13 @@ extension PureYAML.Parsing.LibYAML.Scanner { } } + /// Whether this flow level's pending simple key is a JSON (quoted) node, which + /// lets an adjacent `:` open its value without a separating space. + private var flowKeyIsJSON: Bool { + let key = simpleKeys[simpleKeys.count - 1] + return key.possible && key.isJSON + } + /// Fetch a flow-collection indicator (`[` `]` `{` `}` `,`); returns false /// otherwise. private mutating func fetchFlowToken(_ character: Unicode.Scalar) throws -> Bool { @@ -178,7 +190,13 @@ extension PureYAML.Parsing.LibYAML.Scanner { /// Port of `yaml_parser_fetch_flow_scalar`. private mutating func fetchFlowScalar(single: Bool) throws { mark = reader.mark + let saved = simpleKeyAllowed try saveSimpleKey() + // A quoted scalar that became this level's simple key is a JSON node, so a + // later adjacent `:` (no space) still opens its value. + if saved { + simpleKeys[simpleKeys.count - 1].isJSON = true + } simpleKeyAllowed = false let token = try scanFlowScalar(single: single) indentEngine.enqueue(token) @@ -196,7 +214,13 @@ extension PureYAML.Parsing.LibYAML.Scanner { /// Port of `yaml_parser_fetch_flow_collection_start`. private mutating func fetchFlowCollectionStart(_ type: PureYAML.Parsing.LibYAML.TokenType) { // saveSimpleKey cannot throw in flow: a required key needs block context. + let saved = simpleKeyAllowed try? saveSimpleKey() + // A flow collection is a JSON node, so when it is this level's simple key + // an adjacent `:` after its closing `]`/`}` still opens its value. + if saved { + simpleKeys[simpleKeys.count - 1].isJSON = true + } increaseFlowLevel() simpleKeyAllowed = true let startMark = reader.mark diff --git a/Sources/Parsing/LibYAML.Scanner.PlainScalar.swift b/Sources/Parsing/LibYAML.Scanner.PlainScalar.swift index d6be040..908e28d 100644 --- a/Sources/Parsing/LibYAML.Scanner.PlainScalar.swift +++ b/Sources/Parsing/LibYAML.Scanner.PlainScalar.swift @@ -52,13 +52,16 @@ extension PureYAML.Parsing.LibYAML.Scanner { /// Consume the run of non-blank characters, folding any pending breaks or /// whitespace ahead of each one. - private mutating func consumePlainContent(_ state: inout PlainState, startMark: PureYAML.Parsing.Mark) throws { + private mutating func consumePlainContent(_ state: inout PlainState, startMark _: PureYAML.Parsing.Mark) throws { while let character = reader.peek(), !Classify.isBlankZ(character) { if character == ":", Classify.isBlankZ(reader.peek(1)) { break } if flowLevel > 0, character == ":", Classify.isFlowIndicator(reader.peek(1)) { - throw scannerError("found unexpected ':'", at: startMark) + // A `:` before a flow indicator ends the key; the value is empty. + // libyaml rejects this as an unexpected ':', but it is a valid + // value indicator in flow context. + break } if flowLevel > 0, Classify.isFlowIndicator(character) { break diff --git a/Sources/Parsing/LibYAML.SimpleKey.swift b/Sources/Parsing/LibYAML.SimpleKey.swift index 503c6e0..c6b6c4a 100644 --- a/Sources/Parsing/LibYAML.SimpleKey.swift +++ b/Sources/Parsing/LibYAML.SimpleKey.swift @@ -11,6 +11,10 @@ extension PureYAML.Parsing.LibYAML { var tokenNumber: Int /// The position the potential key started at. var mark: PureYAML.Parsing.Mark + /// Whether this key is a JSON-style node (a quoted flow scalar). A `:` + /// adjacent to such a key is a value indicator even without a following + /// space, per the YAML flow "adjacent value" rule. + var isJSON = false /// The empty entry pushed for a new flow level (`{ 0, 0, 0, {0,0,0} }`). static var empty: Self { diff --git a/Tests/CanonicalEventConformanceTests.swift b/Tests/CanonicalEventConformanceTests.swift index 059bfdb..3fb51a4 100644 --- a/Tests/CanonicalEventConformanceTests.swift +++ b/Tests/CanonicalEventConformanceTests.swift @@ -12,7 +12,7 @@ struct CanonicalEventConformanceTests { /// Valid suite cases whose event stream PureYAML does NOT yet emit exactly. /// This is the authoritative gap set: a fix removes an id (and the test fails /// until it is removed), a regression adds one (and the test fails). Of these, - /// 14 currently throw on valid input and 2 emit a wrong event stream. + /// 8 currently throw on valid input and 2 emit a wrong event stream. static let knownEventGaps: Set = [ // Gap set for the ported libyaml engine. Verified against the libyaml // reference dumper: on all but four of these the port matches libyaml @@ -21,9 +21,9 @@ struct CanonicalEventConformanceTests { // itself handles. // // false-rejects (parseEvents throws on valid input, as libyaml does) - "4ABK", "58MP", "5T43", "6BCT", "6CA3", - "A2M4", "CFD4", "DBG4", "DK95/00", - "DK95/04", "FRK4", "HM87/00", + "6BCT", "6CA3", + "A2M4", "CFD4", "DK95/00", + "DK95/04", "Q5MG", "Y79Y/010", // wrong event stream: the port matches libyaml but both deviate from the