///|
/// Render a value as YAML the way the reference implementation prints it.
///
/// `config` prints `yaml.safe_dump()`, so matching its output
/// means matching PyYAML's block emitter: keys sorted, indent 2, width 80,
/// ASCII only, flow style only for empty collections, and PyYAML's own rules
/// for when a scalar may stay unquoted. The code below ports that subset of
/// `yaml/emitter.py` plus the implicit resolvers of `yaml/resolver.py`, because
/// the quoting rules are subtle enough that examples alone get them wrong:
/// `12:30` is an int, `1.5e3` is not a float, `2024-1-1` is not a timestamp.
/// `scripts/check-yaml-dump.py` diffs this against PyYAML.
const YAML_INDENT : Int32 = 2
///|
/// PyYAML's `best_width`: the column past which it looks for a break point.
const YAML_WIDTH : Int32 = 80
///|
/// One document's worth of emitter state, mirroring PyYAML's `Emitter`.
///
/// `indent` is `-1` for PyYAML's `None` (nothing has opened a block yet).
/// `whitespace` and `indention` say whether the next token may be glued to the
/// previous one, which is what decides between `- - 1` and a line break.
priv struct PyYamlEmitter {
out : StringBuilder
mut indent : Int32
mut column : Int32
mut whitespace : Bool
mut indention : Bool
mut root_context : Bool
mut open_ended : Bool
}
///|
fn PyYamlEmitter::new() -> PyYamlEmitter {
{
out: StringBuilder(),
indent: -1,
column: 0,
whitespace: true,
indention: true,
root_context: false,
open_ended: false,
}
}
///|
/// Write text that is not an indicator: the column advances, the other flags
/// are left alone because the caller decides what the text means.
fn PyYamlEmitter::raw(self : PyYamlEmitter, text : String) -> Unit {
self.out.write_string(text)
self.column += text.char_length()
}
///|
fn PyYamlEmitter::raw_char(self : PyYamlEmitter, c : Char) -> Unit {
self.out.write_char(c)
self.column += 1
}
///|
/// Port of `Emitter.write_indicator`: inserts a separating space when the
/// previous token needs one, then records what the indicator implies.
fn PyYamlEmitter::indicator(
self : PyYamlEmitter,
text : String,
need_whitespace : Bool,
whitespace~ : Bool = false,
indention~ : Bool = false,
) -> Unit {
if self.whitespace || !need_whitespace {
self.raw(text)
} else {
self.raw(" ")
self.raw(text)
}
self.whitespace = whitespace
self.indention = self.indention && indention
}
///|
/// Port of `Emitter.write_line_break`.
fn PyYamlEmitter::line_break(self : PyYamlEmitter) -> Unit {
self.whitespace = true
self.indention = true
self.column = 0
self.out.write_char('\n')
}
///|
/// Port of `Emitter.write_indent`: start a fresh line and pad to `indent` when
/// the current position is not already a valid start of a line.
fn PyYamlEmitter::indent_to(self : PyYamlEmitter) -> Unit {
let indent = if self.indent < 0 { 0 } else { self.indent }
if !self.indention ||
self.column > indent ||
(self.column == indent && !self.whitespace) {
self.line_break()
}
if self.column < indent {
self.whitespace = true
while self.column < indent {
self.raw(" ")
}
}
}
///|
/// Port of `Emitter.increase_indent`, returning the indent to restore later.
fn PyYamlEmitter::push_indent(
self : PyYamlEmitter,
flow~ : Bool = false,
indentless~ : Bool = false,
) -> Int32 {
let saved = self.indent
if self.indent < 0 {
self.indent = if flow { YAML_INDENT } else { 0 }
} else if !indentless {
self.indent += YAML_INDENT
}
saved
}
///|
/// The part of PyYAML's `ScalarAnalysis` the block emitter consults.
priv struct ScalarAnalysis {
empty : Bool
multiline : Bool
allow_block_plain : Bool
allow_single_quoted : Bool
}
///|
fn is_yaml_break(c : Char) -> Bool {
match c {
'\n' | '\u{85}' | '\u{2028}' | '\u{2029}' => true
_ => false
}
}
///|
fn is_yaml_blank(c : Char) -> Bool {
match c {
'\u{0}' | ' ' | '\t' | '\r' | '\n' | '\u{85}' | '\u{2028}' | '\u{2029}' =>
true
_ => false
}
}
///|
/// Indicators that make a plain scalar ambiguous when they come first.
fn is_leading_indicator(c : Char) -> Bool {
match c {
'#'
| ','
| '['
| ']'
| '{'
| '}'
| '&'
| '*'
| '!'
| '|'
| '>'
| '\''
| '"'
| '%'
| '@'
| '`' => true
_ => false
}
}
///|
/// Port of `Emitter.analyze_scalar` reduced to the flags block style uses.
fn analyze_scalar(ch : Array[Char]) -> ScalarAnalysis {
let n = ch.length()
if n == 0 {
return {
empty: true,
multiline: false,
allow_block_plain: true,
allow_single_quoted: true,
}
}
let mut block_indicators = false
let mut line_breaks = false
let mut special_characters = false
let mut leading_space = false
let mut leading_break = false
let mut trailing_space = false
let mut trailing_break = false
let mut break_space = false
let mut space_break = false
if (n >= 3 && ch[0] == '-' && ch[1] == '-' && ch[2] == '-') ||
(n >= 3 && ch[0] == '.' && ch[1] == '.' && ch[2] == '.') {
block_indicators = true
}
let mut preceded_by_whitespace = true
let mut followed_by_whitespace = n == 1 || is_yaml_blank(ch[1])
let mut previous_space = false
let mut previous_break = false
let mut index = 0
while index < n {
let c = ch[index]
if index == 0 {
if is_leading_indicator(c) {
block_indicators = true
}
if (c == '?' || c == ':') && followed_by_whitespace {
block_indicators = true
}
if c == '-' && followed_by_whitespace {
block_indicators = true
}
} else {
if c == ':' && followed_by_whitespace {
block_indicators = true
}
if c == '#' && preceded_by_whitespace {
block_indicators = true
}
}
if is_yaml_break(c) {
line_breaks = true
}
// `safe_dump` uses `allow_unicode=False`, so anything outside printable
// ASCII has to be escaped, which rules out single quotes.
if c != '\n' && !(c >= '\u{20}' && c <= '\u{7E}') {
special_characters = true
}
if c == ' ' {
if index == 0 {
leading_space = true
}
if index == n - 1 {
trailing_space = true
}
if previous_break {
break_space = true
}
previous_space = true
previous_break = false
} else if is_yaml_break(c) {
if index == 0 {
leading_break = true
}
if index == n - 1 {
trailing_break = true
}
if previous_space {
space_break = true
}
previous_space = false
previous_break = true
} else {
previous_space = false
previous_break = false
}
index += 1
preceded_by_whitespace = is_yaml_blank(c)
followed_by_whitespace = index + 1 >= n || is_yaml_blank(ch[index + 1])
}
let mut allow_block_plain = true
let mut allow_single_quoted = true
if leading_space || leading_break || trailing_space || trailing_break {
allow_block_plain = false
}
if break_space {
allow_block_plain = false
allow_single_quoted = false
}
if space_break || special_characters {
allow_block_plain = false
allow_single_quoted = false
}
if line_breaks {
allow_block_plain = false
}
if block_indicators {
allow_block_plain = false
}
{
empty: false,
multiline: line_breaks,
allow_block_plain,
allow_single_quoted,
}
}
///|
fn is_ascii_digit(c : Char) -> Bool {
c >= '0' && c <= '9'
}
///|
fn is_hex_digit(c : Char) -> Bool {
is_ascii_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
///|
/// Digits and underscores, as used by PyYAML's int and float patterns.
fn is_int_char(c : Char) -> Bool {
is_ascii_digit(c) || c == '_'
}
///|
/// Match `[-+]?` and return the index after it.
fn skip_sign(ch : Array[Char], from : Int32) -> Int32 {
if from < ch.length() && (ch[from] == '+' || ch[from] == '-') {
from + 1
} else {
from
}
}
///|
/// Match `[0-9]{1,max}` at `from`, returning the index after it.
fn match_digits(ch : Array[Char], from : Int32, max : Int32) -> Int32? {
let mut index = from
while index < ch.length() && index - from < max && is_ascii_digit(ch[index]) {
index += 1
}
if index > from {
Some(index)
} else {
None
}
}
///|
/// Match one `[0-5]?[0-9]` sexagesimal group at `from`.
fn match_sexagesimal(ch : Array[Char], from : Int32) -> Int32? {
let n = ch.length()
if from < n &&
ch[from] >= '0' &&
ch[from] <= '5' &&
from + 1 < n &&
is_ascii_digit(ch[from + 1]) {
Some(from + 2)
} else if from < n && is_ascii_digit(ch[from]) {
Some(from + 1)
} else {
None
}
}
///|
/// Match `(:[0-5]?[0-9])+` at `from`, returning the index after the last group.
fn match_sexagesimal_groups(ch : Array[Char], from : Int32) -> Int32? {
let mut index = from
let mut groups = 0
while index < ch.length() && ch[index] == ':' {
match match_sexagesimal(ch, index + 1) {
Some(next) => {
index = next
groups += 1
}
None => return None
}
}
if groups > 0 {
Some(index)
} else {
None
}
}
///|
/// The `tag:yaml.org,2002:int` resolver.
fn matches_yaml_int(ch : Array[Char]) -> Bool {
let n = ch.length()
let start = skip_sign(ch, 0)
if start >= n {
return false
}
let head = ch[start]
if head == '0' && start + 1 < n && ch[start + 1] == 'b' {
let mut index = start + 2
if index >= n {
return false
}
while index < n &&
(ch[index] == '0' || ch[index] == '1' || ch[index] == '_') {
index += 1
}
return index == n
}
if head == '0' && start + 1 < n && ch[start + 1] == 'x' {
let mut index = start + 2
if index >= n {
return false
}
while index < n && (is_hex_digit(ch[index]) || ch[index] == '_') {
index += 1
}
return index == n
}
if head == '0' {
// `[-+]?0[0-7_]+`, plus the bare `0` from `[-+]?(0|[1-9][0-9_]*)`.
let mut index = start + 1
while index < n &&
((ch[index] >= '0' && ch[index] <= '7') || ch[index] == '_') {
index += 1
}
return index == n
}
if head < '1' || head > '9' {
return false
}
let mut index = start + 1
while index < n && is_int_char(ch[index]) {
index += 1
}
if index == n {
return true
}
match match_sexagesimal_groups(ch, index) {
Some(end) => end == n
None => false
}
}
///|
/// Match `(?:[eE][-+][0-9]+)?` and return the index after it. PyYAML's float
/// pattern demands the sign, which is why `1.5e3` stays a string.
fn match_float_exponent(ch : Array[Char], from : Int32) -> Int32 {
let n = ch.length()
if from >= n || (ch[from] != 'e' && ch[from] != 'E') {
return from
}
if from + 1 >= n || (ch[from + 1] != '-' && ch[from + 1] != '+') {
return from
}
let mut index = from + 2
if index >= n || !is_ascii_digit(ch[index]) {
return from
}
while index < n && is_ascii_digit(ch[index]) {
index += 1
}
index
}
///|
/// Match one of `words` (`.inf`/`.Inf`/`.INF`, `.nan`/`.NaN`/`.NAN`).
fn match_yaml_word(
ch : Array[Char],
from : Int32,
words : Array[String],
) -> Bool {
let n = ch.length()
if from >= n {
return false
}
let word = String::from_array(ch.exact_view(start=from, end=n))
for candidate in words {
if word == candidate {
return true
}
}
false
}
///|
/// The `tag:yaml.org,2002:float` resolver.
fn matches_yaml_float(ch : Array[Char]) -> Bool {
let n = ch.length()
if n == 0 {
return false
}
if ch[0] == '.' {
// `\.(?:nan|NaN|NAN)`, `[-+]?\.(?:inf|Inf|INF)`, or
// `\.[0-9][0-9_]*(?:[eE][-+][0-9]+)?`
if match_yaml_word(ch, 0, [".nan", ".NaN", ".NAN"]) {
return true
}
if match_yaml_word(ch, 0, [".inf", ".Inf", ".INF"]) {
return true
}
let mut index = 1
if index < n && is_ascii_digit(ch[index]) {
index += 1
while index < n && is_int_char(ch[index]) {
index += 1
}
return match_float_exponent(ch, index) == n
}
return false
}
let start = skip_sign(ch, 0)
if start >= n {
return false
}
if ch[start] == '.' {
// `[-+]?\.(?:inf|Inf|INF)`
return match_yaml_word(ch, start, [".inf", ".Inf", ".INF"])
}
if !is_ascii_digit(ch[start]) {
return false
}
let mut index = start
while index < n && is_int_char(ch[index]) {
index += 1
}
if index < n && ch[index] == '.' {
// `[-+]?[0-9][0-9_]*\.[0-9_]*(?:[eE][-+][0-9]+)?`
index += 1
while index < n && is_int_char(ch[index]) {
index += 1
}
return match_float_exponent(ch, index) == n
}
// `[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*`
match match_sexagesimal_groups(ch, index) {
Some(end) =>
if end < n && ch[end] == '.' {
let mut tail = end + 1
while tail < n && is_int_char(ch[tail]) {
tail += 1
}
tail == n
} else {
false
}
None => false
}
}
///|
/// The `tag:yaml.org,2002:bool` resolver.
fn matches_yaml_bool(ch : Array[Char]) -> Bool {
let word = String::from_array(ch[:])
match word {
"yes"
| "Yes"
| "YES"
| "no"
| "No"
| "NO"
| "true"
| "True"
| "TRUE"
| "false"
| "False"
| "FALSE"
| "on"
| "On"
| "ON"
| "off"
| "Off"
| "OFF" => true
_ => false
}
}
///|
/// The `tag:yaml.org,2002:null` resolver (`~`, `null`, `Null`, `NULL`, empty).
fn matches_yaml_null(ch : Array[Char]) -> Bool {
let word = String::from_array(ch[:])
word.length() == 0 ||
word == "~" ||
word == "null" ||
word == "Null" ||
word == "NULL"
}
///|
/// `[0-9][0-9][0-9][0-9]` at the start.
fn match_date_year(ch : Array[Char]) -> Bool {
if ch.length() < 4 {
return false
}
for index in 0..<4 {
if !is_ascii_digit(ch[index]) {
return false
}
}
true
}
///|
/// `[0-9]{4}-[0-9]{2}-[0-9]{2}` starting at `from`.
fn match_date(ch : Array[Char], from : Int32) -> Bool {
if from + 10 > ch.length() {
return false
}
for offset in 0..<10 {
let c = ch[from + offset]
if offset == 4 || offset == 7 {
if c != '-' {
return false
}
} else if !is_ascii_digit(c) {
return false
}
}
true
}
///|
/// The `tag:yaml.org,2002:timestamp` resolver.
fn matches_yaml_timestamp(ch : Array[Char]) -> Bool {
let n = ch.length()
if match_date(ch, 0) && n == 10 {
return true
}
// `[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}([Tt]|[ \t]+)...`
if !match_date_year(ch) {
return false
}
let mut index = 4
if index >= n || ch[index] != '-' {
return false
}
match match_digits(ch, index + 1, 2) {
Some(next) => index = next
None => return false
}
if index >= n || ch[index] != '-' {
return false
}
match match_digits(ch, index + 1, 2) {
Some(next) => index = next
None => return false
}
if index < n && (ch[index] == 'T' || ch[index] == 't') {
index += 1
} else {
let mut blanks = index
while blanks < n && (ch[blanks] == ' ' || ch[blanks] == '\t') {
blanks += 1
}
if blanks == index {
return false
}
index = blanks
}
match match_digits(ch, index, 2) {
Some(next) => index = next
None => return false
}
if index >= n || ch[index] != ':' {
return false
}
match match_digits(ch, index + 1, 2) {
Some(next) => index = next
None => return false
}
if index >= n || ch[index] != ':' {
return false
}
match match_digits(ch, index + 1, 2) {
Some(next) => index = next
None => return false
}
if index < n && ch[index] == '.' {
let mut tail = index + 1
while tail < n && is_ascii_digit(ch[tail]) {
tail += 1
}
index = tail
}
let mut blanks = index
while blanks < n && (ch[blanks] == ' ' || ch[blanks] == '\t') {
blanks += 1
}
if blanks == n {
return true
}
if ch[blanks] == 'Z' {
return blanks + 1 == n
}
if ch[blanks] != '-' && ch[blanks] != '+' {
return false
}
match match_digits(ch, blanks + 1, 2) {
Some(next) =>
if next == n {
true
} else if ch[next] == ':' {
match match_digits(ch, next + 1, 2) {
Some(end) => end == n
None => false
}
} else {
false
}
None => false
}
}
///|
/// Whether a plain scalar would be read back as a string, which is what lets
/// PyYAML leave it unquoted. Mirrors `Resolver.resolve` for the `str` tag.
fn plain_is_string(ch : Array[Char]) -> Bool {
if matches_yaml_null(ch) ||
matches_yaml_bool(ch) ||
matches_yaml_int(ch) ||
matches_yaml_float(ch) ||
matches_yaml_timestamp(ch) {
return false
}
let word = String::from_array(ch[:])
word != "<<" && word != "="
}
///|
fn hex_digit(value : Int32) -> Char {
if value < 10 {
(value + 48).unsafe_to_char()
} else {
(value + 55).unsafe_to_char()
}
}
///|
fn hex_pad(value : Int32, width : Int32) -> String {
let mut out = ""
let mut shift = (width - 1) * 4
while shift >= 0 {
out += hex_digit((value >> shift) & 0xF).to_string()
shift -= 4
}
out
}
///|
/// Escape one character for a double-quoted scalar: PyYAML's
/// `ESCAPE_REPLACEMENTS`, then `\xNN` / `\uNNNN` / `\UNNNNNNNN`.
fn escape_yaml_char(c : Char) -> String {
match c {
'\u{0}' => "\\0"
'\u{7}' => "\\a"
'\u{8}' => "\\b"
'\t' => "\\t"
'\n' => "\\n"
'\u{B}' => "\\v"
'\u{C}' => "\\f"
'\r' => "\\r"
'\u{1B}' => "\\e"
'"' => "\\\""
'\\' => "\\\\"
'\u{85}' => "\\N"
'\u{A0}' => "\\_"
'\u{2028}' => "\\L"
'\u{2029}' => "\\P"
_ => {
let code = c.to_int()
if code <= 0xFF {
"\\x" + hex_pad(code, 2)
} else if code <= 0xFFFF {
"\\u" + hex_pad(code, 4)
} else {
"\\U" + hex_pad(code, 8)
}
}
}
}
///|
fn yaml_slice(ch : Array[Char], start : Int32, end : Int32) -> String {
String::from_array(ch.exact_view(start~, end~))
}
///|
/// Port of `Emitter.write_plain`.
fn PyYamlEmitter::write_plain(
self : PyYamlEmitter,
ch : Array[Char],
split : Bool,
) -> Unit {
let n = ch.length()
if n == 0 {
return
}
if !self.whitespace {
self.raw(" ")
}
self.whitespace = false
self.indention = false
let mut spaces = false
let mut start = 0
let mut end = 0
while end <= n {
let current : Char? = if end < n { Some(ch[end]) } else { None }
if spaces {
if current != Some(' ') {
if start + 1 == end && self.column > YAML_WIDTH && split {
self.indent_to()
self.whitespace = false
self.indention = false
} else {
self.raw(yaml_slice(ch, start, end))
}
start = end
}
} else if current == None || current == Some(' ') {
if start < end {
self.raw(yaml_slice(ch, start, end))
}
start = end
}
if current != None {
spaces = current == Some(' ')
}
end += 1
}
// A plain scalar as a whole document would need `...` to end it.
if self.root_context {
self.open_ended = true
}
}
///|
/// Port of `Emitter.write_single_quoted`, including its habit of writing one
/// line break per input break and then another one for the run itself.
fn PyYamlEmitter::write_single_quoted(
self : PyYamlEmitter,
ch : Array[Char],
split : Bool,
) -> Unit {
let n = ch.length()
self.indicator("'", true)
let mut spaces = false
let mut breaks = false
let mut start = 0
let mut end = 0
while end <= n {
let current : Char? = if end < n { Some(ch[end]) } else { None }
let is_current_break = match current {
Some(c) => is_yaml_break(c)
None => false
}
if spaces {
if current != Some(' ') {
if start + 1 == end &&
self.column > YAML_WIDTH &&
split &&
start != 0 &&
end != n {
self.indent_to()
} else {
self.raw(yaml_slice(ch, start, end))
}
start = end
}
} else if breaks {
if !is_current_break {
if ch[start] == '\n' {
self.line_break()
}
for index in start.. Unit {
let n = ch.length()
self.indicator("\"", true)
let mut start = 0
let mut end = 0
while end <= n {
let current : Char? = if end < n { Some(ch[end]) } else { None }
let must_escape = match current {
None => true
Some(c) =>
c == '"' ||
c == '\\' ||
c == '\u{85}' ||
c == '\u{2028}' ||
c == '\u{2029}' ||
c == '\u{FEFF}' ||
!(c >= '\u{20}' && c <= '\u{7E}')
}
if must_escape {
if start < end {
self.raw(yaml_slice(ch, start, end))
start = end
}
match current {
Some(c) => {
self.raw(escape_yaml_char(c))
start = end + 1
}
None => ()
}
}
if 0 < end &&
end < n - 1 &&
(current == Some(' ') || start >= end) &&
self.column + (end - start) > YAML_WIDTH &&
split {
let data = yaml_slice(ch, start, end) + "\\"
if start < end {
start = end
}
self.raw(data)
self.indent_to()
self.whitespace = false
self.indention = false
if ch[start] == ' ' {
self.raw("\\")
}
}
end += 1
}
self.indicator("\"", false)
}
///|
/// Which quoting PyYAML picks for a scalar with no explicit style.
priv enum ScalarStyle {
Plain
Single
Double
}
///|
/// Port of `Emitter.choose_scalar_style` for `style is None`, plus the indent
/// bump and restore that `expect_scalar` does around writing a value.
fn PyYamlEmitter::write_scalar(
self : PyYamlEmitter,
ch : Array[Char],
plain_ok : Bool,
simple_key_context : Bool,
) -> Unit {
let analysis = analyze_scalar(ch)
let style = if plain_ok &&
!(simple_key_context && (analysis.empty || analysis.multiline)) &&
analysis.allow_block_plain {
Plain
} else if analysis.allow_single_quoted &&
!(simple_key_context && analysis.multiline) {
Single
} else {
Double
}
let split = !simple_key_context
let saved = self.push_indent(flow=true)
match style {
Plain => self.write_plain(ch, split)
Single => self.write_single_quoted(ch, split)
Double => self.write_double_quoted(ch, split)
}
self.indent = saved
}
///|
/// `repr(float)` as CPython writes it, which is what PyYAML emits for floats.
///
/// MoonBit's `Double::to_string` has its own notation thresholds (it prints
/// `1e16` as `10000000000000000`), so the digits are re-rendered the way
/// CPython does: fixed notation while the decimal exponent is in `(-4, 16]`,
/// scientific notation outside that, with a signed two-digit exponent.
fn yaml_float_repr(value : Double) -> String {
if value.is_nan() {
return ".nan"
}
if value.is_pos_inf() {
return ".inf"
}
if value.is_neg_inf() {
return "-.inf"
}
if value == 0.0 {
// MoonBit prints `-0.0` as `0`; Python keeps the sign in `repr`.
return if value.reinterpret_as_uint64() >> 63 == 1 { "-0.0" } else { "0.0" }
}
let negative = value < 0.0
let raw = if negative { (-value).to_string() } else { value.to_string() }
let ch = raw.to_array()
let mut index = 0
let mut int_digits = ""
while index < ch.length() &&
ch[index] != '.' &&
ch[index] != 'e' &&
ch[index] != 'E' {
int_digits += ch[index].to_string()
index += 1
}
let mut frac_digits = ""
if index < ch.length() && ch[index] == '.' {
index += 1
while index < ch.length() && ch[index] != 'e' && ch[index] != 'E' {
frac_digits += ch[index].to_string()
index += 1
}
}
let mut exp10 = 0
if index < ch.length() && (ch[index] == 'e' || ch[index] == 'E') {
index += 1
let mut exp_negative = false
if index < ch.length() && (ch[index] == '+' || ch[index] == '-') {
exp_negative = ch[index] == '-'
index += 1
}
while index < ch.length() {
exp10 = exp10 * 10 + (ch[index].to_int() - 48)
index += 1
}
if exp_negative {
exp10 = -exp10
}
}
// The value is `0. * 10^decpt`.
let mut digits = int_digits + frac_digits
let mut decpt = int_digits.length() + exp10
let mut first = 0
while first < digits.length() && digits[first] == '0' {
first += 1
decpt -= 1
}
let mut last = digits.length()
while last > first && digits[last - 1] == '0' {
last -= 1
}
digits = digits[first:last].to_owned()
if digits.length() == 0 {
return if negative { "-0.0" } else { "0.0" }
}
let body = if decpt > -4 && decpt <= 16 {
if decpt <= 0 {
"0." + String::make(-decpt, '0') + digits
} else if decpt >= digits.length() {
digits + String::make(decpt - digits.length(), '0') + ".0"
} else {
digits[:decpt].to_owned() + "." + digits[decpt:].to_owned()
}
} else {
let mantissa = if digits.length() > 1 {
digits[:1].to_owned() + "." + digits[1:].to_owned()
} else {
digits + ".0"
}
mantissa + format_float_exponent(decpt - 1)
}
if negative {
"-" + body
} else {
body
}
}
///|
/// CPython writes float exponents with a sign and at least two digits.
fn format_float_exponent(exponent : Int32) -> String {
let magnitude = if exponent < 0 { -exponent } else { exponent }
let digits = magnitude.to_string()
let padded = if digits.length() < 2 { "0" + digits } else { digits }
(if exponent < 0 { "e-" } else { "e+" }) + padded
}
///|
/// The text PyYAML writes for a scalar, plus whether the plain form would read
/// back as the same tag (PyYAML's `event.implicit[0]`).
fn scalar_text(v : @yaml.Yaml) -> (Array[Char], Bool) {
match v {
@yaml.Yaml::String(s) => {
let ch = s.to_array()
(ch, plain_is_string(ch))
}
@yaml.Yaml::Integer(i) => {
let ch = i.to_string().to_array()
(ch, !plain_is_string(ch))
}
@yaml.Yaml::Boolean(b) => {
let ch = (if b { "true" } else { "false" }).to_array()
(ch, !plain_is_string(ch))
}
@yaml.Yaml::Real(d, ..) => {
let ch = yaml_float_repr(d).to_array()
(ch, !plain_is_string(ch))
}
@yaml.Yaml::Null | @yaml.Yaml::BadValue => {
let ch = "null".to_array()
(ch, !plain_is_string(ch))
}
_ => {
let ch = "null".to_array()
(ch, !plain_is_string(ch))
}
}
}
///|
fn PyYamlEmitter::dump_scalar(
self : PyYamlEmitter,
v : @yaml.Yaml,
simple_key_context : Bool,
) -> Unit {
let (ch, plain_ok) = scalar_text(v)
self.write_scalar(ch, plain_ok, simple_key_context)
}
///|
/// Port of `Emitter.check_simple_key`: a key needs the explicit `?` form when
/// it is empty, spans lines, or is too long.
///
/// The limit counts the prepared tag, which for a string key is `!!str` — five
/// characters PyYAML never writes, since the tag is implicit. The cutoff is
/// therefore 122 characters of key.
fn is_simple_yaml_key(ch : Array[Char]) -> Bool {
let analysis = analyze_scalar(ch)
!analysis.empty && !analysis.multiline && ch.length() + 5 < 128
}
///|
/// Port of `Emitter.expect_flow_sequence`/`expect_flow_mapping` for the only
/// flow-style nodes PyYAML emits with `default_flow_style=False`: empty ones.
fn PyYamlEmitter::dump_empty_flow(
self : PyYamlEmitter,
open_ : String,
close : String,
) -> Unit {
self.indicator(open_, true, whitespace=true)
self.indicator(close, false)
}
///|
/// Port of `Emitter.expect_block_sequence`.
fn PyYamlEmitter::dump_sequence(
self : PyYamlEmitter,
items : Array[@yaml.Yaml],
mapping_context : Bool,
) -> Unit {
let indentless = mapping_context && !self.indention
let saved = self.push_indent(indentless~)
for item in items {
self.indent_to()
self.indicator("-", true, indention=true)
self.dump_node(item, false)
}
self.indent = saved
}
///|
/// Keys in the order `yaml.safe_dump` writes them. `lexical_compare` compares
/// UTF-16 code units, which matches Python's code point order for the ASCII
/// keys compose files use.
fn sorted_yaml_keys(m : Map[String, @yaml.Yaml]) -> Array[String] {
let keys : Array[String] = []
for key in m.keys() {
keys.push(key)
}
keys.sort_by(fn(a, b) { a.lexical_compare(b) })
keys
}
///|
/// Port of `Emitter.expect_block_mapping`.
fn PyYamlEmitter::dump_mapping(
self : PyYamlEmitter,
m : Map[String, @yaml.Yaml],
) -> Unit {
let saved = self.push_indent()
for key in sorted_yaml_keys(m) {
let key_ch = key.to_array()
// `expect_block_mapping_key` calls `expect_node(simple_key=True)`, which
// clears `root_context`; a key is never the document root.
self.root_context = false
self.indent_to()
if is_simple_yaml_key(key_ch) {
self.write_scalar(key_ch, plain_is_string(key_ch), true)
self.indicator(":", false)
} else {
self.indicator("?", true, indention=true)
// `expect_node(mapping=True)` without `simple_key=True`, so an explicit
// key is written in value context.
self.write_scalar(key_ch, plain_is_string(key_ch), false)
self.indent_to()
self.indicator(":", true, indention=true)
}
match m.get(key) {
Some(value) => self.dump_node(value, true)
None => self.dump_scalar(@yaml.Yaml::Null, false)
}
}
self.indent = saved
}
///|
/// Port of `Emitter.expect_node` for the block styles.
fn PyYamlEmitter::dump_node(
self : PyYamlEmitter,
v : @yaml.Yaml,
mapping_context : Bool,
root~ : Bool = false,
) -> Unit {
self.root_context = root
match v {
@yaml.Yaml::Map(m) =>
if m.length() == 0 {
self.dump_empty_flow("{", "}")
} else {
self.dump_mapping(m)
}
@yaml.Yaml::Array(items) =>
if items.length() == 0 {
self.dump_empty_flow("[", "]")
} else {
self.dump_sequence(items, mapping_context)
}
_ => self.dump_scalar(v, false)
}
}
///|
/// Render a value the way `yaml.safe_dump` does, trailing newline included.
///
/// `config` prints the result with `println`, which adds the second newline
/// the reference's `print(compose.merged_yaml)` also produces.
pub fn y_dump(v : @yaml.Yaml) -> String {
let emitter = PyYamlEmitter::new()
emitter.dump_node(v, false, root=true)
emitter.indent_to()
if emitter.open_ended {
emitter.indicator("...", true)
emitter.indent_to()
}
emitter.out.to_string()
}