Completes issue #10. Canonical mode follows libyaml's canonical layout on both the Model and Tagged dump paths (tags explicit, scalars double quoted, flow one entry per line, anchors and aliases preserved on the Tagged path). explicitDocumentStart/End wrap the body in --- and ... markers. lineWidth wraps overflowing flow collections one entry per line and folds plain block scalars at single-space boundaries, vetoing any fold that would manufacture syntax, so values always reparse unchanged. Closes #10
235 lines
7.3 KiB
Swift
235 lines
7.3 KiB
Swift
public extension PureYAML.Emitting {
|
|
/// Block-style YAML serializer for ``PureYAML/Model/Value``.
|
|
struct Dumper: Sendable {
|
|
public var options: Options
|
|
|
|
public init(options: Options = .default) {
|
|
self.options = options
|
|
}
|
|
|
|
public func dump(_ value: PureYAML.Model.Value) -> String {
|
|
if options.canonical {
|
|
var body = canonicalDocument(value)
|
|
if options.explicitDocumentEnd {
|
|
body += "...\n"
|
|
}
|
|
return body
|
|
}
|
|
var body = switch options.collectionStyle {
|
|
case .block:
|
|
render(value, indent: 0).joined(separator: "\n") + "\n"
|
|
case .flow where options.lineWidth > 0:
|
|
renderFlowWrappedDocument(value)
|
|
case .flow:
|
|
renderFlowDocument(value)
|
|
}
|
|
if options.explicitDocumentStart {
|
|
body = "---\n" + body
|
|
}
|
|
if options.explicitDocumentEnd {
|
|
body += "...\n"
|
|
}
|
|
return body
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PureYAML.Emitting.Dumper {
|
|
func render(
|
|
_ value: PureYAML.Model.Value,
|
|
indent: Int,
|
|
) -> [String] {
|
|
switch value {
|
|
case let .mapping(mapping):
|
|
renderMapping(mapping, indent: indent)
|
|
case let .sequence(values):
|
|
renderSequence(values, indent: indent)
|
|
default:
|
|
renderScalarNode(value, indent: indent)
|
|
}
|
|
}
|
|
|
|
func renderSequence(
|
|
_ values: [PureYAML.Model.Value],
|
|
indent: Int,
|
|
) -> [String] {
|
|
let prefix = String(repeating: " ", count: indent)
|
|
guard !values.isEmpty else {
|
|
return [prefix + "[]"]
|
|
}
|
|
|
|
return values.flatMap { value in
|
|
if let emptyCollection = renderEmptyCollectionLiteral(value) {
|
|
return [prefix + "- " + emptyCollection]
|
|
}
|
|
|
|
switch value {
|
|
case .mapping, .sequence:
|
|
return [prefix + "-"] + render(value, indent: indent + options.indentWidth)
|
|
default:
|
|
return renderSequenceScalarItem(value, indent: indent)
|
|
}
|
|
}
|
|
}
|
|
|
|
func renderFlowDocument(_ value: PureYAML.Model.Value) -> String {
|
|
switch value {
|
|
case .mapping, .sequence:
|
|
renderFlow(value) + "\n"
|
|
default:
|
|
renderScalarNode(value, indent: 0).joined(separator: "\n") + "\n"
|
|
}
|
|
}
|
|
|
|
func renderFlow(_ value: PureYAML.Model.Value) -> String {
|
|
switch value {
|
|
case let .mapping(mapping):
|
|
renderFlowMapping(mapping)
|
|
case let .sequence(values):
|
|
renderFlowSequence(values)
|
|
default:
|
|
renderFlowScalar(value)
|
|
}
|
|
}
|
|
|
|
func renderFlowSequence(_ values: [PureYAML.Model.Value]) -> String {
|
|
let items = values
|
|
.map(renderFlow)
|
|
.joined(separator: ", ")
|
|
return "[\(items)]"
|
|
}
|
|
|
|
func renderEmptyCollectionLiteral(_ value: PureYAML.Model.Value) -> String? {
|
|
switch value {
|
|
case let .mapping(mapping) where mapping.pairs.isEmpty:
|
|
"{}"
|
|
case let .sequence(values) where values.isEmpty:
|
|
"[]"
|
|
default:
|
|
nil
|
|
}
|
|
}
|
|
|
|
func renderFlowScalar(_ value: PureYAML.Model.Value) -> String {
|
|
switch value {
|
|
case .null:
|
|
"null"
|
|
case let .bool(value):
|
|
value ? "true" : "false"
|
|
case let .int(value):
|
|
String(value)
|
|
case let .double(value):
|
|
String(value)
|
|
case let .string(value):
|
|
renderFlowString(value)
|
|
case .sequence, .mapping:
|
|
""
|
|
}
|
|
}
|
|
|
|
func renderScalarNode(
|
|
_ value: PureYAML.Model.Value,
|
|
indent: Int,
|
|
) -> [String] {
|
|
let prefix = String(repeating: " ", count: indent)
|
|
if let lines = renderLiteralBlockString(value, contentIndent: indent + options.indentWidth) {
|
|
return [prefix + lines.header] + lines.content
|
|
}
|
|
let rendered = renderScalar(value)
|
|
if let folded = foldedPlainScalarLine(head: prefix, value: value, rendered: rendered, indent: indent) {
|
|
return folded
|
|
}
|
|
return [prefix + rendered]
|
|
}
|
|
|
|
func renderMappingScalarPair(
|
|
_ pair: PureYAML.Model.Pair,
|
|
indent: Int,
|
|
) -> [String] {
|
|
let prefix = String(repeating: " ", count: indent)
|
|
let key = escapeKey(pair.key)
|
|
if let lines = renderLiteralBlockString(pair.value, contentIndent: indent + options.indentWidth) {
|
|
return [prefix + key + ": " + lines.header] + lines.content
|
|
}
|
|
let rendered = renderScalar(pair.value)
|
|
let head = prefix + key + ": "
|
|
if let folded = foldedPlainScalarLine(head: head, value: pair.value, rendered: rendered, indent: indent) {
|
|
return folded
|
|
}
|
|
return [head + rendered]
|
|
}
|
|
|
|
func renderSequenceScalarItem(
|
|
_ value: PureYAML.Model.Value,
|
|
indent: Int,
|
|
) -> [String] {
|
|
let prefix = String(repeating: " ", count: indent)
|
|
if let lines = renderLiteralBlockString(value, contentIndent: indent + options.indentWidth) {
|
|
return [prefix + "- " + lines.header] + lines.content
|
|
}
|
|
let rendered = renderScalar(value)
|
|
if let folded = foldedPlainScalarLine(head: prefix + "- ", value: value, rendered: rendered, indent: indent) {
|
|
return folded
|
|
}
|
|
return [prefix + "- " + rendered]
|
|
}
|
|
|
|
func renderScalar(_ value: PureYAML.Model.Value) -> String {
|
|
switch value {
|
|
case .null:
|
|
"null"
|
|
case let .bool(value):
|
|
value ? "true" : "false"
|
|
case let .int(value):
|
|
String(value)
|
|
case let .double(value):
|
|
String(value)
|
|
case let .string(value):
|
|
renderString(value)
|
|
case .sequence, .mapping:
|
|
""
|
|
}
|
|
}
|
|
|
|
func renderString(_ value: String) -> String {
|
|
switch options.scalarStyle {
|
|
case .quoted:
|
|
quote(value)
|
|
case .plainWhenSafe:
|
|
canRenderPlainString(value) ? value : quote(value)
|
|
case .literalBlockWhenMultiline:
|
|
quote(value)
|
|
}
|
|
}
|
|
|
|
func renderFlowString(_ value: String) -> String {
|
|
switch options.scalarStyle {
|
|
case .plainWhenSafe:
|
|
canRenderFlowPlainString(value) ? value : quote(value)
|
|
case .quoted, .literalBlockWhenMultiline:
|
|
quote(value)
|
|
}
|
|
}
|
|
|
|
func renderLiteralBlockString(
|
|
_ value: PureYAML.Model.Value,
|
|
contentIndent: Int,
|
|
) -> (header: String, content: [String])? {
|
|
guard
|
|
case let .string(string) = value,
|
|
options.scalarStyle == .literalBlockWhenMultiline,
|
|
canRenderLiteralBlockString(string)
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
let keepsTrailingNewline = string.hasSuffix("\n")
|
|
let header = keepsTrailingNewline ? "|" : "|-"
|
|
let contentSource = keepsTrailingNewline ? String(string.dropLast()) : string
|
|
let prefix = String(repeating: " ", count: contentIndent)
|
|
let content = contentSource.split(separator: "\n", omittingEmptySubsequences: false)
|
|
.map { prefix + String($0) }
|
|
return (header, content)
|
|
}
|
|
}
|