PureYAML/Sources/Emitting/Dumper.Mapping.swift
Mihaela Mihaljevic ec3642bacc feat(emitting): add indentWidth and sortKeys emitter options
Closes two of the four emitter gaps a feature-parity audit against Yams
found (PureYAMLYamsComparison's FEATURE-PARITY.md): no configurable
indentation and no sorted-key output.

indentWidth replaces every hardcoded indent + 2 in Dumper.swift/
Dumper.Mapping.swift, verified with round-trip tests since indentation is
purely a rendering choice and must never change what a document means.

sortKeys sorts a mapping's pairs by their string key in both block and flow
style, but only when every key in that mapping is a plain string; a mapping
with any complex (mapping/sequence) key stays in insertion order, since
there is no well-defined ordering across mixed key kinds. Off by default,
since insertion order is what makes a duplicate-key-preserving Mapping
round-trip meaningfully in the first place.

width-based line wrapping and canonical mode are each a correctness surface
of their own and are deliberately left for later, not folded into this.
2026-07-10 17:49:44 +02:00

97 lines
3.2 KiB
Swift

extension PureYAML.Emitting.Dumper {
func renderMapping(
_ mapping: PureYAML.Model.Mapping,
indent: Int,
) -> [String] {
let prefix = String(repeating: " ", count: indent)
guard !mapping.pairs.isEmpty else {
return [prefix + "{}"]
}
return orderedPairs(mapping).flatMap { pair in
renderMappingPair(pair, indent: indent)
}
}
/// `mapping.pairs` in insertion order, or sorted by their rendered
/// string-keyed form when `options.sortKeys` is set. Only string keys
/// are sortable this way; a mapping with any non-string key is left in
/// insertion order regardless of the option, since there is no
/// well-defined lexicographic ordering across mixed key kinds
/// (`Model.Key.mapping`/`.sequence`) to sort by.
func orderedPairs(_ mapping: PureYAML.Model.Mapping) -> [PureYAML.Model.Pair] {
guard options.sortKeys, mapping.pairs.allSatisfy({ pair in
if case .string = pair.keyNode {
true
} else {
false
}
}) else {
return mapping.pairs
}
return mapping.pairs.sorted { $0.key < $1.key }
}
func renderMappingPair(
_ pair: PureYAML.Model.Pair,
indent: Int,
) -> [String] {
switch pair.keyNode {
case .mapping, .sequence:
renderComplexMappingPair(pair, indent: indent)
case .string:
renderStringKeyedMappingPair(pair, indent: indent)
}
}
func renderStringKeyedMappingPair(
_ pair: PureYAML.Model.Pair,
indent: Int,
) -> [String] {
let prefix = String(repeating: " ", count: indent)
if let emptyCollection = renderEmptyCollectionLiteral(pair.value) {
return [prefix + escapeKey(pair.key) + ": " + emptyCollection]
}
switch pair.value {
case .mapping, .sequence:
return [prefix + escapeKey(pair.key) + ":"] + render(pair.value, indent: indent + options.indentWidth)
default:
return renderMappingScalarPair(pair, indent: indent)
}
}
func renderComplexMappingPair(
_ pair: PureYAML.Model.Pair,
indent: Int,
) -> [String] {
let prefix = String(repeating: " ", count: indent)
if let emptyCollection = renderEmptyCollectionLiteral(pair.value) {
return [prefix + "? " + pair.keyNode.flowDescription]
+ [prefix + ": " + emptyCollection]
}
return [prefix + "? " + pair.keyNode.flowDescription]
+ [prefix + ":"]
+ render(pair.value, indent: indent + options.indentWidth)
}
func renderFlowMapping(_ mapping: PureYAML.Model.Mapping) -> String {
let pairs = orderedPairs(mapping)
.map { pair in
"\(renderFlowKey(pair.keyNode)): \(renderFlow(pair.value))"
}
.joined(separator: ", ")
return "{\(pairs)}"
}
func renderFlowKey(_ key: PureYAML.Model.Key) -> String {
switch key {
case .mapping, .sequence:
"? \(renderFlow(key.value))"
case let .string(value):
quote(value)
}
}
}