From 6c322fc5ed192df03f4c0cebac953ae91e8f952b Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Sun, 28 Jun 2026 21:24:59 +0200 Subject: [PATCH] fix(parsing): end a plain scalar at a tab before a comment A comment opens after white space, a tab as well as a space, but the plain scalar terminator checked only for a space before `#`. So `c: d\t#X` kept the tab and hash as content, giving the value `d\t#X` rather than `d` with a trailing comment. Treat a tab before `#` as a space there, in both the lookahead from a white space character and the check on the accumulated value's last character. A tab inside a plain scalar with no `#` after it stays content, and a `#` glued to content with no white space is still content. Raises yaml-test-suite value-level conformance to 187/194 (96.4%) with the parse-level and should-error rejection rates unchanged. 448 tests pass. --- CHANGELOG.md | 6 +++++ Sources/Parsing/Scanner.Helpers.swift | 6 +++-- Tests/PlainScalarTabCommentTests.swift | 36 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 Tests/PlainScalarTabCommentTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 084b6ee..c073da6 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 before a `#` now ends a plain scalar and begins a trailing comment, as a space already did, + since a comment opens after any white space. `c: d\t#X` is the value `d` with a comment, not the + literal `d\t#X`. A tab inside a plain scalar with no `#` after it stays part of the value, and a `#` + glued to content with no white space is still content. Raises yaml-test-suite value-level + conformance to 187/194 (96.4%), with the parse and should-error rates unchanged. + - A blank line next to a more-indented line in a folded block scalar (`>`) now keeps both line breaks. Between two non-more-indented lines a blank run of N folds to N breaks (the fold is absorbed into the first), but next to a more-indented line nothing folds, so the absorbed break diff --git a/Sources/Parsing/Scanner.Helpers.swift b/Sources/Parsing/Scanner.Helpers.swift index 7ccdb6b..29c1c0f 100644 --- a/Sources/Parsing/Scanner.Helpers.swift +++ b/Sources/Parsing/Scanner.Helpers.swift @@ -279,10 +279,12 @@ extension PureYAML.Parsing.Scanner { if isLineBreak(character) { return true } - if character == " ", state.reader.peek(offset: 1) == "#" { + // A comment opens after white space, a tab as well as a space, so a `#` preceded by either + // ends the plain scalar: `d\t#X` is the value `d` with a trailing comment. + if character == " " || character == "\t", state.reader.peek(offset: 1) == "#" { return true } - if character == "#", accumulated.isEmpty || accumulated.last == " " { + if character == "#", accumulated.isEmpty || accumulated.last == " " || accumulated.last == "\t" { return true } if character == ":", shouldEndPlainScalarAtMappingValue(state: state) { diff --git a/Tests/PlainScalarTabCommentTests.swift b/Tests/PlainScalarTabCommentTests.swift new file mode 100644 index 0000000..86efef2 --- /dev/null +++ b/Tests/PlainScalarTabCommentTests.swift @@ -0,0 +1,36 @@ +@testable import PureYAML +import Testing + +/// A comment opens after white space, a tab as well as a space, so a `#` preceded by a tab ends a +/// plain scalar and begins a trailing comment. A tab inside a plain scalar, with no `#` after it, +/// stays part of the value, and a `#` glued directly to content (no white space) is content. +@Suite("Plain scalar tab comment") +struct PlainScalarTabCommentTests { + @Test("A tab before a hash ends the scalar and starts a comment") + func test_tabBeforeComment() throws { + #expect(try PureYAML.parse("c: d\t#X\n") == .mapping(.init([ + .init(key: "c", value: .string("d")), + ]))) + } + + @Test("A tab inside a plain scalar value is kept") + func test_internalTabKept() throws { + #expect(try PureYAML.parse("x: a\tb\n") == .mapping(.init([ + .init(key: "x", value: .string("a\tb")), + ]))) + } + + @Test("A hash glued to content with no white space stays content") + func test_hashWithoutWhitespaceIsContent() throws { + #expect(try PureYAML.parse("c: d#X\n") == .mapping(.init([ + .init(key: "c", value: .string("d#X")), + ]))) + } + + @Test("A space before a hash still ends the scalar") + func test_spaceBeforeCommentUnchanged() throws { + #expect(try PureYAML.parse("c: d #X\n") == .mapping(.init([ + .init(key: "c", value: .string("d")), + ]))) + } +}