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
148 lines
5.8 KiB
Swift
148 lines
5.8 KiB
Swift
extension PureYAML.Emitting.Dumper {
|
|
/// Width-aware flow rendering: when a flow collection's single-line
|
|
/// form (with its indent) overflows `lineWidth`, it re-renders with one
|
|
/// entry per line, recursively, which is equally valid flow YAML. With
|
|
/// wrapping disabled or content that fits, the single-line form is
|
|
/// unchanged.
|
|
func renderFlowWrappedDocument(_ value: PureYAML.Model.Value) -> String {
|
|
renderFlowWrappedLines(value, indent: 0).joined(separator: "\n") + "\n"
|
|
}
|
|
|
|
func renderFlowWrappedLines(
|
|
_ value: PureYAML.Model.Value,
|
|
indent: Int,
|
|
) -> [String] {
|
|
let prefix = String(repeating: " ", count: indent)
|
|
let single = renderFlow(value)
|
|
if indent + single.count <= options.lineWidth {
|
|
return [prefix + single]
|
|
}
|
|
switch value {
|
|
case let .sequence(values):
|
|
guard !values.isEmpty else {
|
|
return [prefix + "[]"]
|
|
}
|
|
var lines = [prefix + "["]
|
|
for item in values {
|
|
var entry = renderFlowWrappedLines(item, indent: indent + options.indentWidth)
|
|
entry[entry.count - 1] += ","
|
|
lines.append(contentsOf: entry)
|
|
}
|
|
lines.append(prefix + "]")
|
|
return lines
|
|
case let .mapping(mapping):
|
|
guard !mapping.pairs.isEmpty else {
|
|
return [prefix + "{}"]
|
|
}
|
|
let inner = indent + options.indentWidth
|
|
let innerPrefix = String(repeating: " ", count: inner)
|
|
var lines = [prefix + "{"]
|
|
for pair in orderedPairs(mapping) {
|
|
let key = renderFlowKey(pair.keyNode)
|
|
var entry = renderFlowWrappedLines(pair.value, indent: inner)
|
|
entry[0] = innerPrefix + key + ": " + String(entry[0].dropFirst(innerPrefix.count))
|
|
entry[entry.count - 1] += ","
|
|
lines.append(contentsOf: entry)
|
|
}
|
|
lines.append(prefix + "}")
|
|
return lines
|
|
default:
|
|
// A scalar has no flow break points; an overlong one stays on
|
|
// its single line.
|
|
return [prefix + single]
|
|
}
|
|
}
|
|
|
|
/// Folds an overlong plain scalar line at single-space boundaries onto
|
|
/// more-indented continuation lines. Parsing folds each break back to
|
|
/// exactly one space, so the value is unchanged. Returns `nil` when
|
|
/// wrapping is off, the line fits, the scalar did not render plain, or
|
|
/// no safe break point exists.
|
|
func foldedPlainScalarLine(
|
|
head: String,
|
|
value: PureYAML.Model.Value,
|
|
rendered: String,
|
|
indent: Int,
|
|
) -> [String]? {
|
|
guard options.lineWidth > 0, head.count + rendered.count > options.lineWidth else {
|
|
return nil
|
|
}
|
|
guard case let .string(string) = value, rendered == string else {
|
|
return nil
|
|
}
|
|
let continuationIndent = indent + options.indentWidth
|
|
let continuationPrefix = String(repeating: " ", count: continuationIndent)
|
|
let segments = foldSegments(
|
|
string,
|
|
firstBudget: options.lineWidth - head.count,
|
|
continuationBudget: options.lineWidth - continuationIndent,
|
|
)
|
|
guard segments.count > 1, foldSegmentsAreSafe(segments) else {
|
|
return nil
|
|
}
|
|
var lines = [head + segments[0]]
|
|
for segment in segments.dropFirst() {
|
|
lines.append(continuationPrefix + segment)
|
|
}
|
|
return lines
|
|
}
|
|
|
|
/// A fold boundary must not manufacture YAML syntax: a segment ending
|
|
/// in `:` would put a colon before a line break (a value indicator), a
|
|
/// continuation line starting with `#` would open a comment (the fold
|
|
/// break is whitespace), and one starting with `-`, `?`, or `:` before
|
|
/// a space (or alone) would read as a block indicator. Any unsafe
|
|
/// boundary vetoes the whole fold; the line stays long.
|
|
func foldSegmentsAreSafe(_ segments: [String]) -> Bool {
|
|
for (index, segment) in segments.enumerated() {
|
|
if index < segments.count - 1, segment.hasSuffix(":") {
|
|
return false
|
|
}
|
|
guard index > 0 else {
|
|
continue
|
|
}
|
|
if segment.hasPrefix("#") {
|
|
return false
|
|
}
|
|
if let first = segment.first, "-?:".contains(first) {
|
|
let second = segment.dropFirst().first
|
|
if second == nil || second == " " {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/// Splits at spaces whose neighbors are non-space, so no segment gains
|
|
/// a leading or trailing space and every fold restores to exactly one
|
|
/// space on reparse. A word longer than the budget stays whole.
|
|
func foldSegments(
|
|
_ string: String,
|
|
firstBudget: Int,
|
|
continuationBudget: Int,
|
|
) -> [String] {
|
|
var segments: [String] = []
|
|
var current = ""
|
|
var budget = firstBudget
|
|
for word in string.split(separator: " ", omittingEmptySubsequences: false) {
|
|
if current.isEmpty {
|
|
current = String(word)
|
|
} else if current.count + 1 + word.count <= budget || word.isEmpty || current.hasSuffix(" ") {
|
|
// Empty words mark runs of consecutive spaces: those spaces
|
|
// must stay literal (a fold would swallow them), so they
|
|
// glue onto the current segment and forbid breaking here.
|
|
current += " " + word
|
|
} else {
|
|
segments.append(current)
|
|
current = String(word)
|
|
budget = continuationBudget
|
|
}
|
|
}
|
|
if !current.isEmpty {
|
|
segments.append(current)
|
|
}
|
|
return segments
|
|
}
|
|
}
|