fix(parsing): fold over a tab-only blank line in a plain scalar

A plain scalar may span blank lines, and a blank line that contains a tab
is still blank, so the tab is not indentation. libyaml rejects any tab
below the scalar indentation with 'found a tab character that violates
indentation', which wrongly rejects 'foo: 1' / tab-only line / 'bar: 2'.

Guard the tab check with blankLineAhead so it fires only when the tab
indents real content. Surpasses libyaml on DK95/04. Port score 305 -> 306,
leaving zero false-rejects.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 19:18:05 +02:00
parent b2d2d2f595
commit 825084dbff
4 changed files with 11 additions and 5 deletions

View File

@ -22,6 +22,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fold over a tab-only blank line inside a multi-line plain scalar. A plain
scalar may span blank lines, and a blank line that contains a tab is still
blank, so the tab is not indentation. libyaml rejects any tab below the scalar
indentation; the scanner now rejects it only when it indents real content, not
when the rest of the line is blank. Surpasses libyaml on DK95/04.
- Accept a tab as in-line separation while still rejecting it as indentation. A
tab after a `-`, `?`, or `:` indicator, or before a flow collection, is valid
separation, but libyaml refuses to skip it whenever a simple key is allowed and

View File

@ -365,7 +365,7 @@ extension PureYAML.Parsing.LibYAML.Scanner {
/// Whether the rest of the current line, from the current position, is only
/// spaces and tabs up to a line break or end of stream (a blank line).
private func blankLineAhead() -> Bool {
func blankLineAhead() -> Bool {
var offset = 0
while let character = reader.peek(offset), character == " " || Classify.isTab(character) {
offset += 1

View File

@ -106,7 +106,10 @@ extension PureYAML.Parsing.LibYAML.Scanner {
private mutating func consumePlainBreaks(_ state: inout PlainState, indent: Int, startMark: PureYAML.Parsing.Mark) throws {
while Classify.isBlank(reader.peek()) || Classify.isBreak(reader.peek()) {
if Classify.isBlank(reader.peek()) {
if state.leadingBlanks, reader.column < indent, Classify.isTab(reader.peek()) {
if state.leadingBlanks, reader.column < indent, Classify.isTab(reader.peek()), !blankLineAhead() {
// A tab below the indentation is illegal only when it indents
// real content; on an otherwise-blank line it is just a blank
// line the scalar folds over. libyaml rejects it either way.
throw scannerError("found a tab character that violates indentation", at: startMark)
}
if !state.leadingBlanks, let blank = reader.peek() {

View File

@ -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,
/// 1 currently throws on valid input and 2 emit a wrong event stream.
/// All remaining gaps emit a wrong event stream (none throw on valid input).
static let knownEventGaps: Set<String> = [
// Gap set for the ported libyaml engine. Verified against the libyaml
// reference dumper: on all but four of these the port matches libyaml
@ -20,8 +20,6 @@ struct CanonicalEventConformanceTests {
// bug. Surpassing libyaml means shrinking this set beyond what libyaml
// itself handles.
//
// false-rejects (parseEvents throws on valid input, as libyaml does)
"DK95/04",
// wrong event stream: the port matches libyaml but both deviate from the
// canonical events (libyaml quirks to be surpassed).
"JEF9/02", "W4TN",