diff --git a/CHANGELOG.md b/CHANGELOG.md index 0afed6a..8a785fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/Scanner.Directives.swift b/Sources/Parsing/Scanner.Directives.swift index c752f37..5119ea7 100644 --- a/Sources/Parsing/Scanner.Directives.swift +++ b/Sources/Parsing/Scanner.Directives.swift @@ -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 } diff --git a/Tests/BlockScalarPercentContentTests.swift b/Tests/BlockScalarPercentContentTests.swift new file mode 100644 index 0000000..f24ae6a --- /dev/null +++ b/Tests/BlockScalarPercentContentTests.swift @@ -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") + } + } +}