///|
/// The spellings of a scalar, in order of preference: plain, double quotes,
/// single quotes. The native profile removes one pair of outer quotes and
/// has no escapes.
fn spellings(text : String) -> Array[String] {
["\{text}", "\"\{text}\"", "'\{text}'"]
}
///|
/// A flow item inside quotes, with backslash escapes for the quote and the
/// backslash, as the native flow splitter reads them.
fn flow_quoted(text : String, quote : Char) -> String {
let out = StringBuilder()
out.write_char(quote)
for c in text {
if c == quote || c == '\\' {
out.write_char('\\')
}
out.write_char(c)
}
out.write_char(quote)
out.to_string()
}
///|
fn flow_plain_safe(text : String) -> Bool {
!text.is_empty() &&
java_trim(text) == text &&
!text.contains(",") &&
!text.contains("\\") &&
!is_quote(text.code_unit_at(0).to_int())
}
///|
/// The flow list spellings: plain items where that is safe, then all items
/// in double quotes, then all items in single quotes.
fn flow_spellings(items : ReadOnlyArray[String]) -> Array[String] {
let mixed = items
.iter()
.map(item => {
if flow_plain_safe(item) {
item
} else {
flow_quoted(item, '"')
}
})
let double = items.iter().map(item => flow_quoted(item, '"'))
let single = items.iter().map(item => flow_quoted(item, '\''))
[
"[" + mixed.to_array().join(", ") + "]",
"[" + double.to_array().join(", ") + "]",
"[" + single.to_array().join(", ") + "]",
]
}
///|
/// True when the shared envelope keeps the line as one YAML body line, even
/// at the start of a body, where statements such as `title` are read.
fn body_line(text : String) -> Bool {
let scan = @envelope.scan_body(Yaml, text)
scan.diagnostics.is_empty() &&
scan.lines.length() == 1 &&
scan.lines[0].role is Body
}
///|
/// True when `text` reads back as one line with this indentation and content.
/// Block content lines are checked separately.
fn reads_as(text : String, indent : Int32, content : Content) -> Bool {
if !body_line(text) {
return false
}
let (_, _, class) = classify_line(text)
guard class.content() is Some(actual) else { return false }
let expected = match content {
Pair(item, key, Block(_)) => Pair(item, key, Block([]))
other => other
}
class.count == indent && content_logical_equal(actual, expected)
}
///|
/// The source text of one line, with block content lines after a `|` line.
fn spell_line(line : Line) -> Result[String, String] {
if line.indent < 0 {
return Err("A line cannot have a negative indentation")
}
let lead = String::make(line.indent, ' ')
let candidates : Array[String] = match line.content {
Dash => [lead + "-"]
Item(value) => spellings(value.text).map(v => lead + "- " + v)
Text(value) => spellings(value.text).map(v => lead + v)
Pair(item, key, entry) => {
let head = lead + (if item { "- " } else { "" })
let tails = match entry {
Empty => [":"]
Value(value) => spellings(value.text).map(v => ": " + v)
Flow(items) => flow_spellings(items).map(v => ": " + v)
Block(_) => [": |"]
Folded => [": >"]
}
let out = []
for k in spellings(key.text) {
for tail in tails {
out.push(head + k + tail)
}
}
out
}
}
guard candidates
.iter()
.find_first(text => reads_as(text, line.indent, line.content))
is Some(text) else {
return Err(
"The value has no spelling that the native YAML profile reads back",
)
}
guard line.content is Pair(_, _, Block(contents)) else { return Ok(text) }
let out = StringBuilder()
out.write_string(text)
let native = line.native_indent()
let pad = String::make(native + 2, ' ')
for content in contents {
let spelled = pad + content
let (_, _, class) = classify_line(spelled)
if content.is_empty() ||
java_trim(content) != content ||
content.contains("\n") ||
content.contains("\r") ||
!body_line(spelled) ||
(
class.ty != TextOnly &&
class.ty != Empty &&
class.native_indent() <= native
) {
return Err(
"A block content line must be non-empty, have no outer spaces and be a body line",
)
}
out.write_char('\n')
out.write_string(spelled)
}
Ok(out.to_string())
}
///|
/// Write the canonical body of a YAML block from the typed lines: one
/// `#highlight` line for each selector, then each line with its indentation,
/// the first scalar spelling that reads back to the same value, and no
/// comments or blank lines. Every line ends with a line break. The text has
/// no block markers and no shared statements. A value that the native
/// profile cannot express gives an error; there is no partial output and
/// no source fallback.
pub fn print_body(
document : Document,
limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> Result[String, ReadOnlyArray[@diagrams.Diagnostic]] {
if !limits.valid() {
return Err([
@diagrams.Diagnostic::error(
"yaml/invalid-limits", "YAML limits must be positive",
),
])
}
let out = StringBuilder()
let mut units = 0
fn emit(text : String) -> Bool {
if text.length() + 1 > limits.output - units {
return false
}
units += text.length() + 1
out.write_string(text)
out.write_char('\n')
true
}
let output_failure : ReadOnlyArray[@diagrams.Diagnostic] = [
@diagrams.Diagnostic::error(
"yaml/output-limit", "Canonical YAML exceeds the output limit",
),
]
for selector in document.highlights {
let line = selector.to_line()
match @data.parse_selector(line) {
Ok(parsed) if parsed.logical_equal(selector) => ()
_ =>
return Err([
@diagrams.Diagnostic::error(
"yaml/unrepresentable-value", "A highlight selector cannot be written in the native profile",
),
])
}
if !emit(line) {
return Err(output_failure)
}
}
if document.lines.length() > limits.nodes {
return Err([
@diagrams.Diagnostic::error(
"yaml/node-limit", "YAML data exceeds the node limit",
),
])
}
for line in document.lines {
match spell_line(line) {
Ok(text) => if !emit(text) { return Err(output_failure) }
Err(message) =>
return Err([
@diagrams.Diagnostic::error(
"yaml/unrepresentable-value",
message,
span?=line.span,
),
])
}
}
let body = out.to_string()
// Read the whole body back. Each line passed its own check; this also
// proves that lines do not change the meaning of their neighbours.
let reread = read(@envelope.scan_body(Yaml, body), limits~)
let same = match reread.document {
Some(back) => {
let mut equal = back.lines.length() == document.lines.length() &&
back.highlights.length() == document.highlights.length()
if equal {
for i in 0.. false
}
if !same {
return Err([
@diagrams.Diagnostic::error(
"yaml/unrepresentable-document", "The lines do not read back to the same document in the native YAML profile",
),
])
}
Ok(body)
}