From 738408d4584e2444bc34124897fb5741e419bdae Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Sun, 28 Jun 2026 20:30:24 +0200 Subject: [PATCH] fix(parsing): accept a tab after the colon value indicator A tab is separation white space in YAML, so it follows the `:` mapping-value indicator exactly as a space does. isMappingValueBoundary (flow) and isBlockMappingValueBoundary (block) accepted only a space, line break, comma, or closing bracket, so `foo:\tbar` folded into the plain scalar `foo:\tbar` instead of the mapping `{foo: bar}`. Accept a tab in both. A `:` with no following white space (`foo:bar`) is still plain scalar content, and the symmetric counterpart for a tab after a leading indicator landed in the previous change. Raises yaml-test-suite parse-level to 300/333 (90.1%) with the should-error rejection rate unchanged. 433 tests pass. --- CHANGELOG.md | 6 +++++ Sources/Parsing/Scanner.Helpers.swift | 4 ++- Tests/MappingValueTabBoundaryTests.swift | 34 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 Tests/MappingValueTabBoundaryTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index e30109f..9964333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A tab now follows the `:` mapping-value indicator the way a space does, in both block and flow + context, since a tab is separation white space in YAML. `foo:\tbar` is the mapping `{foo: bar}` + rather than the plain scalar `foo:\tbar`. A `:` with no following white space at all (`foo:bar`) + stays plain scalar content. Raises yaml-test-suite parse-level conformance to 300/333 (90.1%), with + the should-error rejection rate unchanged. + - A tab now ends a leading indicator the way a space does, since a tab is separation white space in YAML (only indentation forbids a tab). A document marker, block entry, or explicit key followed by a tab (`---\tscalar`, `-\titem`, `?\tkey`) is recognized as the indicator rather than folded into a diff --git a/Sources/Parsing/Scanner.Helpers.swift b/Sources/Parsing/Scanner.Helpers.swift index 33df47b..db60423 100644 --- a/Sources/Parsing/Scanner.Helpers.swift +++ b/Sources/Parsing/Scanner.Helpers.swift @@ -53,7 +53,9 @@ extension PureYAML.Parsing.Scanner { guard let character else { return true } + // A tab is separation white space, so it follows a `:` value indicator as a space does. return character == " " + || character == "\t" || isLineBreak(character) || character == "," || character == "]" @@ -64,7 +66,7 @@ extension PureYAML.Parsing.Scanner { guard let character else { return true } - return character == " " || isLineBreak(character) + return character == " " || character == "\t" || isLineBreak(character) } func isTokenTerminator(_ character: Character) -> Bool { diff --git a/Tests/MappingValueTabBoundaryTests.swift b/Tests/MappingValueTabBoundaryTests.swift new file mode 100644 index 0000000..e5c6db2 --- /dev/null +++ b/Tests/MappingValueTabBoundaryTests.swift @@ -0,0 +1,34 @@ +@testable import PureYAML +import Testing + +/// A tab is separation white space in YAML, so it follows the `:` mapping-value indicator as a space +/// does, in both block and flow context. A `:` with no following white space at all stays plain +/// scalar content (`foo:bar`), unchanged. +@Suite("Mapping value tab boundary") +struct MappingValueTabBoundaryTests { + @Test("A block mapping value may follow the colon after a tab") + func test_blockValueTab() throws { + #expect(try PureYAML.parse("foo:\tbar\n") == .mapping(.init([ + .init(key: "foo", value: .string("bar")), + ]))) + } + + @Test("A block sequence item mapping accepts a tab after the colon") + func test_blockSequenceItemTab() throws { + #expect(try PureYAML.parse("- foo:\tbar\n") == .sequence([ + .mapping(.init([.init(key: "foo", value: .string("bar"))])), + ])) + } + + @Test("A flow mapping value may follow the colon after a tab") + func test_flowValueTab() throws { + #expect(try PureYAML.parse("{a:\tb}\n") == .mapping(.init([ + .init(key: "a", value: .string("b")), + ]))) + } + + @Test("A colon with no following white space stays plain scalar content") + func test_colonWithoutWhitespaceUnchanged() throws { + #expect(try PureYAML.parse("foo:bar\n") == .string("foo:bar")) + } +}