feat(emitting): broaden literal block fixtures
This commit is contained in:
parent
84a2de9ea1
commit
92b0d7c92d
@ -72,6 +72,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add executable unsupported-gap fixtures for multi-document streams, complex
|
||||
mapping keys, unsupported directives, unsupported built-in tags, merge-key
|
||||
fallback behavior, and direct value-tree validation.
|
||||
- Add fixture-backed literal block emission coverage for newly supported
|
||||
multiline content and exact quoted fallbacks.
|
||||
|
||||
### Changed
|
||||
|
||||
@ -86,3 +88,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
`super` mapping key instead of the current mapping.
|
||||
- Resolve Yams-compatible plain scalar spellings for `yes`/`no` booleans,
|
||||
radix-prefixed integers, numeric separators, and `.inf`/`.nan` floats.
|
||||
- Broaden opt-in literal block emission while preserving exact parser
|
||||
round-trips for every emitted block scalar.
|
||||
|
||||
@ -75,7 +75,7 @@ flowchart TB
|
||||
Scalar24["#24 Scalar and Tag Corpus"]:::done
|
||||
Collection25["#25 Collection and Anchor Corpus"]:::done
|
||||
Gaps27["#27 Unsupported YAML Gaps"]:::done
|
||||
Literal19["#19 Literal Block Emission"]:::todo
|
||||
Literal19["#19 Literal Block Emission"]:::done
|
||||
Downstream26["#26 Downstream Document Fixtures"]:::todo
|
||||
Epic11 --> Scalar24
|
||||
Scalar24 --> Collection25
|
||||
@ -170,7 +170,9 @@ let compactYAML = PureYAML.dump(document, options: compact)
|
||||
|
||||
Literal block emission is intentionally conservative: multiline strings whose
|
||||
lines would not round-trip through the current parser are emitted as quoted
|
||||
strings instead. Flow collections always use inline scalar output.
|
||||
strings instead. The policy allows more plain-safe block content, such as
|
||||
hashes without comment boundaries and indicator-like text that the parser keeps
|
||||
as scalar content. Flow collections always use inline scalar output.
|
||||
|
||||
## Validation
|
||||
|
||||
|
||||
@ -227,13 +227,68 @@ extension PureYAML.Emitting.Dumper {
|
||||
guard !line.isEmpty, line.first?.isWhitespace == false, line.last?.isWhitespace == false else {
|
||||
return false
|
||||
}
|
||||
guard !line.contains("#"), !containsColonSpace(String(line)) else {
|
||||
let value = String(line)
|
||||
guard !containsCommentStart(value), !containsMappingSeparator(value) else {
|
||||
return false
|
||||
}
|
||||
guard let first = line.first else {
|
||||
return false
|
||||
}
|
||||
return !isPlainScalarIndicator(first)
|
||||
return canRenderLiteralBlockLineStarting(with: first, line: line)
|
||||
}
|
||||
|
||||
func canRenderLiteralBlockLineStarting(
|
||||
with first: Character,
|
||||
line: Substring,
|
||||
) -> Bool {
|
||||
switch first {
|
||||
case "#", ",", "[", "]", "{", "}", "&", "*", "!", "|", ">", "'", "\"", "`":
|
||||
false
|
||||
case "-":
|
||||
!hasOnlyIndicator(first, line: line) && line.dropFirst().first != " "
|
||||
case "?":
|
||||
!hasOnlyIndicator(first, line: line) && line.dropFirst().first != " "
|
||||
case ":":
|
||||
!hasOnlyIndicator(first, line: line) && line.dropFirst().first != " "
|
||||
default:
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
func hasOnlyIndicator(
|
||||
_ indicator: Character,
|
||||
line: Substring,
|
||||
) -> Bool {
|
||||
line.count == 1 && line.first == indicator
|
||||
}
|
||||
|
||||
func containsCommentStart(_ value: String) -> Bool {
|
||||
var previousWasWhitespace = true
|
||||
for character in value {
|
||||
if character == "#", previousWasWhitespace {
|
||||
return true
|
||||
}
|
||||
previousWasWhitespace = character.isWhitespace
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsMappingSeparator(_ value: String) -> Bool {
|
||||
var previousWasColon = false
|
||||
for character in value {
|
||||
if previousWasColon, isMappingSeparatorBoundary(character) {
|
||||
return true
|
||||
}
|
||||
previousWasColon = character == ":"
|
||||
}
|
||||
return previousWasColon
|
||||
}
|
||||
|
||||
func isMappingSeparatorBoundary(_ character: Character) -> Bool {
|
||||
character.isWhitespace
|
||||
|| character == ","
|
||||
|| character == "]"
|
||||
|| character == "}"
|
||||
}
|
||||
|
||||
func canRenderPlainString(_ value: String) -> Bool {
|
||||
|
||||
@ -119,6 +119,42 @@ struct EmitterCorpusTests {
|
||||
#expect(try PureYAML.parse(yaml) == testCase.value)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Emits broader literal block fixture YAML and round trips",
|
||||
arguments: LiteralBlockEmissionFixtures.supported,
|
||||
)
|
||||
func test_literalBlockEmissionFixtures(testCase: LiteralBlockEmissionFixtures.Case) throws {
|
||||
let options = PureYAML.Emitting.Options(scalarStyle: .literalBlockWhenMultiline)
|
||||
let yaml = PureYAML.dump(testCase.value, options: options)
|
||||
|
||||
#expect(yaml == testCase.expected)
|
||||
for required in testCase.required {
|
||||
#expect(yaml.contains(required), "expected \(testCase.name) to contain \(required)")
|
||||
}
|
||||
for forbidden in testCase.forbidden {
|
||||
#expect(!yaml.contains(forbidden), "expected \(testCase.name) not to contain \(forbidden)")
|
||||
}
|
||||
#expect(try PureYAML.parse(yaml) == testCase.value)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Keeps unsupported literal block fixture values quoted",
|
||||
arguments: LiteralBlockEmissionFixtures.fallbacks,
|
||||
)
|
||||
func test_literalBlockEmissionFallbackFixtures(testCase: LiteralBlockEmissionFixtures.Case) throws {
|
||||
let options = PureYAML.Emitting.Options(scalarStyle: .literalBlockWhenMultiline)
|
||||
let yaml = PureYAML.dump(testCase.value, options: options)
|
||||
|
||||
#expect(yaml == testCase.expected)
|
||||
for required in testCase.required {
|
||||
#expect(yaml.contains(required), "expected \(testCase.name) to contain \(required)")
|
||||
}
|
||||
for forbidden in testCase.forbidden {
|
||||
#expect(!yaml.contains(forbidden), "expected \(testCase.name) not to contain \(forbidden)")
|
||||
}
|
||||
#expect(try PureYAML.parse(yaml) == testCase.value)
|
||||
}
|
||||
|
||||
@Test("Emitter corpus keeps unsupported literal block lines quoted")
|
||||
func test_unsupportedLiteralBlockLinesStayQuoted() throws {
|
||||
let value = PureYAML.Model.Value.mapping(.init([
|
||||
|
||||
124
Tests/LiteralBlockEmissionFixtures.swift
Normal file
124
Tests/LiteralBlockEmissionFixtures.swift
Normal file
@ -0,0 +1,124 @@
|
||||
@testable import PureYAML
|
||||
|
||||
enum LiteralBlockEmissionFixtures {
|
||||
struct Case: CustomStringConvertible {
|
||||
var name: String
|
||||
var value: PureYAML.Model.Value
|
||||
var expected: String
|
||||
var required: [String]
|
||||
var forbidden: [String]
|
||||
|
||||
var description: String {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
static let supported: [Case] = [
|
||||
Case(
|
||||
name: "hash without comment boundary",
|
||||
value: .mapping(.init([
|
||||
.init(key: "body", value: .string("swift#wasm\nnext")),
|
||||
])),
|
||||
expected: """
|
||||
body: |-
|
||||
swift#wasm
|
||||
next
|
||||
|
||||
""",
|
||||
required: ["body: |-", "swift#wasm", "next"],
|
||||
forbidden: ["swift#wasm\\nnext", "\"swift#wasm"],
|
||||
),
|
||||
Case(
|
||||
name: "plain-safe indicator-like content",
|
||||
value: .mapping(.init([
|
||||
.init(key: "body", value: .string("-dash\n?question\n:colon\n%percent\n@at")),
|
||||
])),
|
||||
expected: """
|
||||
body: |-
|
||||
-dash
|
||||
?question
|
||||
:colon
|
||||
%percent
|
||||
@at
|
||||
|
||||
""",
|
||||
required: ["-dash", "?question", ":colon", "%percent", "@at"],
|
||||
forbidden: ["\"-dash", "\\n?question"],
|
||||
),
|
||||
]
|
||||
|
||||
static let fallbacks: [Case] = [
|
||||
Case(
|
||||
name: "comment boundary stays quoted",
|
||||
value: .mapping(.init([
|
||||
.init(key: "body", value: .string("swift # wasm\nnext")),
|
||||
])),
|
||||
expected: """
|
||||
body: "swift # wasm\\nnext"
|
||||
|
||||
""",
|
||||
required: ["\"swift # wasm\\nnext\""],
|
||||
forbidden: ["body: |", "swift # wasm\n"],
|
||||
),
|
||||
Case(
|
||||
name: "mapping separator at end stays quoted",
|
||||
value: .mapping(.init([
|
||||
.init(key: "body", value: .string("key:\nnext")),
|
||||
])),
|
||||
expected: """
|
||||
body: "key:\\nnext"
|
||||
|
||||
""",
|
||||
required: ["\"key:\\nnext\""],
|
||||
forbidden: ["body: |", " key:"],
|
||||
),
|
||||
Case(
|
||||
name: "mapping separator before comma stays quoted",
|
||||
value: .mapping(.init([
|
||||
.init(key: "body", value: .string("key:,\nnext")),
|
||||
])),
|
||||
expected: """
|
||||
body: "key:,\\nnext"
|
||||
|
||||
""",
|
||||
required: ["\"key:,\\nnext\""],
|
||||
forbidden: ["body: |", " key:,"],
|
||||
),
|
||||
Case(
|
||||
name: "structural indicator line stays quoted",
|
||||
value: .mapping(.init([
|
||||
.init(key: "body", value: .string("- item\nnext")),
|
||||
])),
|
||||
expected: """
|
||||
body: "- item\\nnext"
|
||||
|
||||
""",
|
||||
required: ["\"- item\\nnext\""],
|
||||
forbidden: ["body: |", " - item"],
|
||||
),
|
||||
Case(
|
||||
name: "blank content line stays quoted",
|
||||
value: .mapping(.init([
|
||||
.init(key: "body", value: .string("one\n\ntwo")),
|
||||
])),
|
||||
expected: """
|
||||
body: "one\\n\\ntwo"
|
||||
|
||||
""",
|
||||
required: ["\"one\\n\\ntwo\""],
|
||||
forbidden: ["body: |", " one\n\n two"],
|
||||
),
|
||||
Case(
|
||||
name: "quoted-looking content stays quoted",
|
||||
value: .mapping(.init([
|
||||
.init(key: "body", value: .string("\"quoted\"\nnext")),
|
||||
])),
|
||||
expected: """
|
||||
body: "\\"quoted\\"\\nnext"
|
||||
|
||||
""",
|
||||
required: ["\"\\\"quoted\\\"\\nnext\""],
|
||||
forbidden: ["body: |", " \"quoted\""],
|
||||
),
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user