diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a62034..765117c 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 +- Plain scalar line folding now strips tabs, not only spaces, around continuation lines. A tab is not + indentation, so a continuation line's leading tab (`\t3rd`) is folding white space and folds away + rather than entering the value, and a line of only white space that holds a tab (`␣␣\t`) is an empty + line that folds to a line break, not an extra space. Raises yaml-test-suite value-level conformance + to 183/194 (94.3%), with the parse and should-error rates unchanged. + - A tab in block scalar content past the block's indentation is now kept verbatim instead of being re-expanded to spaces. A block scalar is indented with spaces only, so a line like `␣\ttext` under a one-space indent strips the space and keeps the tab, yielding `\ttext`; the scanner measured the diff --git a/Sources/Parsing/Scanner.Helpers.swift b/Sources/Parsing/Scanner.Helpers.swift index db60423..7ccdb6b 100644 --- a/Sources/Parsing/Scanner.Helpers.swift +++ b/Sources/Parsing/Scanner.Helpers.swift @@ -190,8 +190,10 @@ extension PureYAML.Parsing.Scanner { var count = 0 while true { var blankProbe = probe + // A line of only white space is empty for folding, including one that holds a tab: a tab + // is not indentation, but a whitespace-only line is still blank, so consume it here. _ = blankProbe.consume { character in - character == " " + character == " " || character == "\t" } guard isLineBreak(blankProbe.peek()) else { break @@ -223,6 +225,12 @@ extension PureYAML.Parsing.Scanner { } func scanPlainScalarContinuationLine(_ state: inout State) -> String { + // Leading white space beyond the line's indentation folds away in a plain scalar. A tab is + // not indentation, so the indentation scan leaves it here; skip it (and any spaces after it) + // so a continuation line like `\t3rd` joins as `3rd`, not `\t3rd`. + while let character = state.reader.peek(), character == " " || character == "\t" { + state.reader.advance() + } var value = "" while let character = state.reader.peek(), !isLineBreak(character) { if character == " ", state.reader.peek(offset: 1) == "#" { diff --git a/Tests/PlainScalarTabFoldingTests.swift b/Tests/PlainScalarTabFoldingTests.swift new file mode 100644 index 0000000..40187fe --- /dev/null +++ b/Tests/PlainScalarTabFoldingTests.swift @@ -0,0 +1,28 @@ +@testable import PureYAML +import Testing + +/// Plain scalar line folding strips white space, including tabs, around continuation lines: a tab is +/// not indentation, so the leading tab of a continuation line folds away, and a line of only white +/// space (even one holding a tab) is an empty line that folds to a line break, not a space. +@Suite("Plain scalar tab folding") +struct PlainScalarTabFoldingTests { + @Test("A leading tab on a continuation line folds away") + func test_leadingTabFoldsToSpace() throws { + #expect(try PureYAML.parse("1st non-empty\n\n 2nd non-empty \n\t3rd non-empty\n") + == .string("1st non-empty\n2nd non-empty 3rd non-empty")) + } + + @Test("A whitespace-only line holding a tab is an empty line, folding to a line break") + func test_whitespaceOnlyTabLineIsBlank() throws { + #expect(try PureYAML.parse("key:\n value\n with\n \t\n tabs\n") == .mapping(.init([ + .init(key: "key", value: .string("value with\ntabs")), + ]))) + } + + @Test("A tab after the indentation of a continuation line folds away") + func test_tabAfterIndentFolds() throws { + #expect(try PureYAML.parse("x:\n - x\n \tx\n") == .mapping(.init([ + .init(key: "x", value: .sequence([.string("x x")])), + ]))) + } +}