fix(parsing): keep a leading percent in a block scalar literal

A zero-indent literal or folded block scalar sits at column 0, where the
directive scan runs, so a content line beginning with `%` (`%!PS-Adobe-2.0`)
was misread as a directive and rejected as unsupported.

The directive scan now runs only outside a block scalar, so a leading `%` in
block scalar content is literal text. A real `%YAML` or `%TAG` directive still
parses, and a stray `%` in other content is still rejected rather than silently
accepted, so the should-error behaviour is unchanged.

This is groundwork for yaml-test-suite W4TN, which additionally needs a block
scalar to be terminated by `...` and per-document directives across a multi-
document stream, so conformance counts are unchanged this step.
This commit is contained in:
Mihaela M 2026-06-29 05:04:32 +02:00
parent 5ea6fa668b
commit 942a7ff2bc
3 changed files with 43 additions and 1 deletions

View File

@ -84,6 +84,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- A line beginning with `%` inside a block scalar is now kept as literal content rather than being
misread as a directive. A zero-indent literal or folded scalar sits at column 0, where the
directive scan runs, so a content line such as `%!PS-Adobe-2.0` was rejected as an unsupported
directive. The directive scan now runs only outside a block scalar; a real `%YAML`/`%TAG`
directive and the rejection of a stray `%` in other content are unchanged. This is groundwork for
yaml-test-suite W4TN, which additionally needs block-scalar termination by `...` and per-document
directives, so conformance counts are unchanged.
- A block sequence value carrying an inline property (`sequence: !!seq` then `- entry` at the key's
column) is now correctly terminated by a sibling mapping key. Such a zero-indent sequence has its
entries at the mapping key's own indentation, so a following key ends it. The untagged form already

View File

@ -3,7 +3,11 @@ extension PureYAML.Parsing.Scanner {
guard state.indentation.last == 0 else {
return false
}
if state.reader.peek() == "%" {
// A `%` opens a directive only outside a block scalar. Inside one (a zero-indent literal or
// folded scalar sits at column 0, where this directive check runs) a leading `%` is literal
// content, so let the main scanner read the line. Elsewhere the directive path still runs, so
// a stray `%` in other content is rejected as before, not silently accepted.
if state.reader.peek() == "%", state.blockScalarIndentation == nil {
try scanDirective(&state)
return true
}

View File

@ -0,0 +1,30 @@
@testable import PureYAML
import Testing
@Suite("A percent inside a block scalar is literal content, not a directive")
struct BlockScalarPercentContentTests {
/// A zero-indent block scalar sits at column 0, where the directive scan runs, so a content line
/// beginning with `%` was misread as a directive and rejected. Inside a block scalar a leading
/// `%` is literal text (groundwork for yaml-test-suite W4TN, which also needs block-scalar
/// termination by `...` and multi-document directives).
@Test("A root literal block scalar keeps a leading percent as content")
func test_rootBlockScalarPercentContent() throws {
#expect(try PureYAML.parse("--- |\n%foo\n") == .string("%foo\n"))
}
/// Every content line of the block scalar keeps its leading percent, not only the first.
@Test("Each block scalar line keeps its leading percent")
func test_multilineBlockScalarPercentContent() throws {
#expect(try PureYAML.parse("--- |\n%one\n%two\n") == .string("%one\n%two\n"))
}
/// The change is scoped to block scalars: a real `%YAML` directive still works, and a stray `%`
/// in other content is still rejected rather than silently accepted.
@Test("A directive still parses and a stray percent in content still errors")
func test_directiveStillWorksAndStrayPercentRejected() throws {
#expect(try PureYAML.parse("%YAML 1.2\n---\nx\n") == .string("x"))
#expect(throws: PureYAML.Parsing.ParseError.self) {
try PureYAML.parse("{ a,\n%x }\n")
}
}
}