use crate::options::{BareStyle, FoldStyle, IndentGlyphMarkerStyle, IndentGlyphMode, MultilineStyle, StringArrayStyle, TableUnindentStyle, RenderOptions, MIN_FOLD_CONTINUATION, indent_glyph_mode};
use crate::value::{BareString, Entry, StrMeta, TableBareString, Value};
use crate::util::*;
use crate::parse::MultilineLocalEol;
#[derive(Clone, Copy)]
pub(crate) enum BasicValue<'a> {
Null,
Bool(bool),
Number(&'a crate::number::Number),
String(&'a str),
EmptyArray,
EmptyObject,
}
impl<'a> BasicValue<'a> {
#[allow(dead_code)]
pub(crate) fn new(value: &'a Value) -> Option<Self> {
match value {
Value::Null => Some(BasicValue::Null),
Value::Bool(b) => Some(BasicValue::Bool(*b)),
Value::Number(n) => Some(BasicValue::Number(n)),
Value::String(s) => Some(BasicValue::String(s.as_str())),
Value::Array(a) if a.is_empty() => Some(BasicValue::EmptyArray),
Value::Object(o) if o.is_empty() => Some(BasicValue::EmptyObject),
Value::Array(_) | Value::Object(_) => None,
}
}
}
fn effective_inline_objects(options: &RenderOptions) -> bool {
options.inline_objects
}
fn effective_inline_arrays(options: &RenderOptions) -> bool {
options.inline_arrays
}
fn effective_force_markers(options: &RenderOptions) -> bool {
options.force_markers
}
fn effective_tables(options: &RenderOptions) -> bool {
options.tables
}
fn table_unindent_target(pair_indent: usize, natural_lines: &[String], options: &RenderOptions) -> Option<usize> {
let n = pair_indent;
let max_natural = natural_lines.iter().map(|l| l.len()).max().unwrap_or(0);
let data_width = max_natural.saturating_sub(n + 2);
match options.table_unindent_style {
TableUnindentStyle::None => None,
TableUnindentStyle::Left => {
if n == 0 { None } else {
let fits = options.wrap_width.map(|w| data_width <= w).unwrap_or(true);
if fits { Some(0) } else { None }
}
}
TableUnindentStyle::Auto => {
let w = options.wrap_width?;
let overflows_natural = max_natural > w;
let fits_at_zero = data_width <= w;
if overflows_natural && fits_at_zero { Some(0) } else { None }
}
TableUnindentStyle::Floating => {
let w = options.wrap_width?;
if max_natural <= w {
return None; }
if data_width + 2 <= w {
let target = w.saturating_sub(data_width + 2);
if target < n { Some(target) } else { None }
} else {
None }
}
}
}
fn subtree_line_count(value: &Value) -> usize {
match value {
Value::Array(v) if !v.is_empty() => v.iter().map(subtree_line_count).sum::<usize>() + 1,
Value::Object(e) if !e.is_empty() => {
e.iter().map(|entry| subtree_line_count(&entry.value) + 1).sum()
}
_ => 1,
}
}
fn subtree_byte_count(value: &Value) -> usize {
match value {
Value::String(s) => s.len(),
Value::Number(n) => n.to_string().len(),
Value::Bool(b) => if *b { 4 } else { 5 },
Value::Null => 4,
Value::Array(v) => v.iter().map(subtree_byte_count).sum(),
Value::Object(e) => e.iter().map(|entry| entry.key.len() + subtree_byte_count(&entry.value)).sum(),
}
}
fn subtree_max_depth(value: &Value) -> usize {
match value {
Value::Array(v) if !v.is_empty() => {
1 + v.iter().map(subtree_max_depth).max().unwrap_or(0)
}
Value::Object(e) if !e.is_empty() => {
1 + e.iter().map(|entry| subtree_max_depth(&entry.value)).max().unwrap_or(0)
}
_ => 0,
}
}
fn should_use_indent_glyph(value: &Value, pair_indent: usize, options: &RenderOptions) -> bool {
let Some(w) = options.wrap_width else { return false; };
let fold_floor = || {
let max_depth = subtree_max_depth(value);
pair_indent + max_depth * 2 >= w.saturating_sub(MIN_FOLD_CONTINUATION + 2)
};
match indent_glyph_mode(options) {
IndentGlyphMode::None => false,
IndentGlyphMode::Fixed => pair_indent >= w / 2,
IndentGlyphMode::IndentWeighted(threshold) => {
if fold_floor() { return true; }
let line_count = subtree_line_count(value);
(pair_indent * line_count) as f64 >= threshold * (w * w) as f64
}
IndentGlyphMode::ByteWeighted(threshold) => {
if fold_floor() { return true; }
let byte_count = subtree_byte_count(value);
(pair_indent * byte_count) as f64 >= threshold * (w * w) as f64
}
}
}
fn indent_glyph_open_lines(key_line: &str, pair_indent: usize, options: &RenderOptions) -> Vec<String> {
match options.indent_glyph_marker_style {
IndentGlyphMarkerStyle::Compact => vec![format!("{}: /<", key_line)],
IndentGlyphMarkerStyle::Separate => vec![
format!("{}:", key_line),
format!("{} /<", spaces(pair_indent)),
],
}
}
fn fits_wrap(options: &RenderOptions, line: &str) -> bool {
match options.wrap_width {
Some(0) | None => true,
Some(width) => line.chars().count() <= width,
}
}
fn pick_preferred_string_array_layout(
preferred: Option<Vec<String>>,
fallback: Option<Vec<String>>,
options: &RenderOptions,
) -> Option<Vec<String>> {
match (preferred, fallback) {
(Some(preferred), Some(fallback))
if string_array_layout_score(&fallback, options)
< string_array_layout_score(&preferred, options) =>
{
Some(fallback)
}
(Some(preferred), _) => Some(preferred),
(None, fallback) => fallback,
}
}
struct StringArrayLayoutScore {
overflow: usize,
line_count: usize,
max_width: usize,
}
impl PartialOrd for StringArrayLayoutScore {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for StringArrayLayoutScore {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.overflow, self.line_count, self.max_width)
.cmp(&(other.overflow, other.line_count, other.max_width))
}
}
impl PartialEq for StringArrayLayoutScore {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == std::cmp::Ordering::Equal
}
}
impl Eq for StringArrayLayoutScore {}
fn string_array_layout_score(lines: &[String], options: &RenderOptions) -> StringArrayLayoutScore {
let overflow = match options.wrap_width {
Some(0) | None => 0,
Some(width) => lines
.iter()
.map(|line| line.chars().count().saturating_sub(width))
.sum(),
};
let max_width = lines
.iter()
.map(|line| line.chars().count())
.max()
.unwrap_or(0);
StringArrayLayoutScore { overflow, line_count: lines.len(), max_width }
}
pub(crate) fn render_key(key: &str, options: &RenderOptions) -> String {
if options.bare_keys == BareStyle::Prefer
&& parse_bare_key_prefix(key).is_some_and(|end| end == key.len())
{
key.to_owned()
} else {
render_json_string(key)
}
}
pub(crate) fn needs_explicit_array_marker(value: &Value) -> bool {
matches!(value, Value::Array(values) if !values.is_empty())
|| matches!(value, Value::Object(entries) if !entries.is_empty())
}
fn split_multiline_fold(text: &str, avail: usize, style: FoldStyle) -> Vec<&str> {
if text.len() <= avail || avail == 0 {
return vec![text];
}
let mut segments = Vec::new();
let mut rest = text;
loop {
if rest.len() <= avail {
segments.push(rest);
break;
}
let split_at = match style {
FoldStyle::Auto => {
let candidate = &rest[..avail.min(rest.len())];
if let Some(pos) = candidate.rfind(' ') {
if pos > 0 { pos } else { avail.min(rest.len()) }
} else {
avail.min(rest.len())
}
}
FoldStyle::Fixed | FoldStyle::None => avail.min(rest.len()),
};
let safe = safe_json_split(rest, split_at);
segments.push(&rest[..safe]);
rest = &rest[safe..];
if rest.is_empty() {
break;
}
}
segments
}
fn fold_bare_string(
value: &str,
indent: usize,
first_line_extra: usize,
style: FoldStyle,
wrap_width: Option<usize>,
) -> Option<Vec<String>> {
let w = wrap_width?;
let first_avail = w.saturating_sub(indent + 1 + first_line_extra);
if value.len() <= first_avail {
return None; }
let cont_avail = w.saturating_sub(indent + 2);
if cont_avail < MIN_FOLD_CONTINUATION {
return None; }
let mut lines = Vec::new();
let mut rest = value;
let mut first = true;
let avail = if first { first_avail } else { cont_avail };
let _ = avail;
let mut current_avail = first_avail;
loop {
if rest.is_empty() {
break;
}
if rest.len() <= current_avail {
if first {
lines.push(format!("{} {}", spaces(indent), rest));
} else {
lines.push(format!("{}/ {}", spaces(indent), rest));
}
break;
}
let split_at = match style {
FoldStyle::Auto => {
let candidate = &rest[..current_avail.min(rest.len())];
let lookahead = rest[candidate.len()..].chars().next();
find_bare_fold_point(candidate, lookahead)
}
FoldStyle::Fixed | FoldStyle::None => current_avail.min(rest.len()),
};
let split_at = if split_at == 0 && !first && matches!(style, FoldStyle::Auto) {
current_avail.min(rest.len())
} else if split_at == 0 {
if first {
lines.push(format!("{} {}", spaces(indent), rest));
} else {
lines.push(format!("{}/ {}", spaces(indent), rest));
}
break;
} else {
split_at
};
let segment = &rest[..split_at];
if first {
lines.push(format!("{} {}", spaces(indent), segment));
first = false;
} else {
lines.push(format!("{}/ {}", spaces(indent), segment));
}
rest = &rest[split_at..];
current_avail = cont_avail;
}
if lines.len() <= 1 {
None } else {
Some(lines)
}
}
fn fold_bare_key(
key: &str,
pair_indent: usize,
style: FoldStyle,
wrap_width: Option<usize>,
) -> Option<Vec<String>> {
let w = wrap_width?;
if matches!(style, FoldStyle::None) { return None; }
if key.len() < w.saturating_sub(pair_indent) { return None; }
let first_avail = w.saturating_sub(pair_indent);
let cont_avail = w.saturating_sub(pair_indent + 2); if cont_avail < MIN_FOLD_CONTINUATION { return None; }
let ind = spaces(pair_indent);
let mut lines: Vec<String> = Vec::new();
let mut rest = key;
let mut first = true;
let mut current_avail = first_avail;
loop {
if rest.is_empty() { break; }
if rest.len() <= current_avail {
lines.push(if first { format!("{}{}", ind, rest) } else { format!("{}/ {}", ind, rest) });
break;
}
let split_at = match style {
FoldStyle::Auto => {
let candidate = &rest[..current_avail.min(rest.len())];
let lookahead = rest[candidate.len()..].chars().next();
find_bare_fold_point(candidate, lookahead)
}
FoldStyle::Fixed | FoldStyle::None => current_avail.min(rest.len()),
};
if split_at == 0 {
lines.push(if first { format!("{}{}", ind, rest) } else { format!("{}/ {}", ind, rest) });
break;
}
lines.push(if first { format!("{}{}", ind, &rest[..split_at]) } else { format!("{}/ {}", ind, &rest[..split_at]) });
rest = &rest[split_at..];
first = false;
current_avail = cont_avail;
}
if lines.len() <= 1 { None } else { Some(lines) }
}
fn fold_number(
value: &str,
indent: usize,
first_line_extra: usize,
style: FoldStyle,
wrap_width: Option<usize>,
) -> Option<Vec<String>> {
if matches!(style, FoldStyle::None) {
return None;
}
let w = wrap_width?;
let first_avail = w.saturating_sub(indent + first_line_extra);
if value.len() <= first_avail {
return None; }
let cont_avail = w.saturating_sub(indent + 2);
if cont_avail < MIN_FOLD_CONTINUATION {
return None;
}
let auto_mode = matches!(style, FoldStyle::Auto);
let mut lines: Vec<String> = Vec::new();
let mut rest = value;
let mut current_avail = first_avail;
let ind = spaces(indent);
loop {
if rest.len() <= current_avail {
lines.push(format!("{}{}", ind, rest));
break;
}
let split_at = find_number_fold_point(rest, current_avail, auto_mode);
if split_at == 0 {
lines.push(format!("{}{}", ind, rest));
break;
}
lines.push(format!("{}{}", ind, &rest[..split_at]));
rest = &rest[split_at..];
current_avail = cont_avail;
let last = lines.last_mut().unwrap();
let _ = last; }
lines.clear();
let mut rest = value;
let mut first = true;
let mut current_avail = first_avail;
loop {
if rest.len() <= current_avail {
if first {
lines.push(format!("{}{}", ind, rest));
} else {
lines.push(format!("{}/ {}", ind, rest));
}
break;
}
let split_at = find_number_fold_point(rest, current_avail, auto_mode);
if split_at == 0 {
if first {
lines.push(format!("{}{}", ind, rest));
} else {
lines.push(format!("{}/ {}", ind, rest));
}
break;
}
if first {
lines.push(format!("{}{}", ind, &rest[..split_at]));
first = false;
} else {
lines.push(format!("{}/ {}", ind, &rest[..split_at]));
}
rest = &rest[split_at..];
current_avail = cont_avail;
}
Some(lines)
}
fn fold_json_string(
value: &str,
indent: usize,
first_line_extra: usize,
style: FoldStyle,
wrap_width: Option<usize>,
) -> Option<Vec<String>> {
let w = wrap_width?;
let encoded = render_json_string(value);
let first_avail = w.saturating_sub(indent + first_line_extra);
if encoded.len() <= first_avail {
return None; }
let cont_avail = w.saturating_sub(indent + 2);
if cont_avail < MIN_FOLD_CONTINUATION {
return None; }
let inner = &encoded[1..encoded.len() - 1]; let mut lines: Vec<String> = Vec::new();
let mut rest = inner;
let mut first = true;
let mut current_avail = first_avail.saturating_sub(1); loop {
if rest.is_empty() {
if let Some(last) = lines.last_mut() {
last.push('"');
}
break;
}
let segment_avail = if rest.len() <= current_avail {
current_avail.saturating_sub(1)
} else {
current_avail
};
if rest.len() <= segment_avail {
let segment = rest;
if first {
lines.push(format!("{}\"{}\"", spaces(indent), segment));
} else {
lines.push(format!("{}/ {}\"", spaces(indent), segment));
}
break;
}
let split_at = match style {
FoldStyle::Auto => {
let candidate = &rest[..segment_avail.min(rest.len())];
find_json_fold_point(candidate)
}
FoldStyle::Fixed | FoldStyle::None => {
safe_json_split(rest, segment_avail.min(rest.len()))
}
};
if split_at == 0 {
if first {
lines.push(format!("{}\"{}\"", spaces(indent), rest));
} else {
lines.push(format!("{}/ {}\"", spaces(indent), rest));
}
break;
}
let segment = &rest[..split_at];
if first {
lines.push(format!("{}\"{}\"", spaces(indent), segment));
let last = lines.last_mut().unwrap();
last.pop(); first = false;
} else {
lines.push(format!("{}/ {}", spaces(indent), segment));
}
rest = &rest[split_at..];
current_avail = cont_avail;
}
if lines.len() <= 1 {
None
} else {
Some(lines)
}
}
fn render_folding_quotes(value: &str, indent: usize, options: &RenderOptions) -> Vec<String> {
let ind = spaces(indent);
let pieces: Vec<&str> = value.split('\n').collect();
let mut lines: Vec<String> = Vec::new();
for (i, piece) in pieces.iter().enumerate() {
let is_last = i == pieces.len() - 1;
let encoded = render_json_string(piece);
let inner = &encoded[1..encoded.len() - 1]; let nl = if is_last { "" } else { "\\n" };
if i == 0 {
lines.push(format!("{}\"{}{}", ind, inner, nl));
if !is_last {
} else {
lines.last_mut().unwrap().push('"');
}
} else if is_last {
lines.push(format!("{}/ {}\"", ind, inner));
} else {
lines.push(format!("{}/ {}{}", ind, inner, nl));
}
if !matches!(options.string_multiline_fold_style, FoldStyle::None)
&& let Some(w) = options.wrap_width {
let last = lines.last().unwrap();
if last.len() > w {
}
}
}
lines
}
#[derive(Clone, Copy)]
enum PackedToken<'a> {
Inline(BasicValue<'a>),
Block(&'a Value),
}
pub(crate) struct Renderer;
impl Renderer {
pub(crate) fn render(value: &Value, options: &RenderOptions) -> String {
let lines = Self::render_root(value, options, options.start_indent);
lines.join("\n")
}
fn render_root(
value: &Value,
options: &RenderOptions,
start_indent: usize,
) -> Vec<String> {
match value {
Value::Null => Self::render_scalar_lines(BasicValue::Null, start_indent, options),
Value::Bool(b) => Self::render_scalar_lines(BasicValue::Bool(*b), start_indent, options),
Value::Number(n) => Self::render_scalar_lines(BasicValue::Number(n), start_indent, options),
Value::String(s) => Self::render_scalar_lines(BasicValue::String(s.as_str()), start_indent, options),
Value::Array(values) if values.is_empty() => {
Self::render_scalar_lines(BasicValue::EmptyArray, start_indent, options)
}
Value::Object(entries) if entries.is_empty() => {
Self::render_scalar_lines(BasicValue::EmptyObject, start_indent, options)
}
Value::Array(values) if effective_force_markers(options) => {
Self::render_explicit_array(values, start_indent, options)
}
Value::Array(values) => Self::render_implicit_array(values, start_indent, options),
Value::Object(entries) if effective_force_markers(options) => {
Self::render_explicit_object(entries, start_indent, options)
}
Value::Object(entries) => {
Self::render_implicit_object(entries, start_indent, options)
}
}
}
fn render_implicit_object(
entries: &[Entry],
parent_indent: usize,
options: &RenderOptions,
) -> Vec<String> {
let pair_indent = parent_indent + 2;
let mut lines = Vec::new();
let mut packed_line = String::new();
for Entry { key, value } in entries {
if effective_inline_objects(options)
&& let Some(token) = Self::render_inline_object_token(key, value, options) {
let candidate = if packed_line.is_empty() {
format!("{}{}", spaces(pair_indent), token)
} else {
format!("{packed_line}{}{token}", spaces(options.kv_pack_multiple * 2))
};
if fits_wrap(options, &candidate) {
packed_line = candidate;
continue;
}
if !packed_line.is_empty() {
lines.push(std::mem::take(&mut packed_line));
}
}
if !packed_line.is_empty() {
lines.push(std::mem::take(&mut packed_line));
}
lines.extend(Self::render_object_entry(key, value, pair_indent, options));
}
if !packed_line.is_empty() {
lines.push(packed_line);
}
lines
}
fn render_object_entry(
key: &str,
value: &Value,
pair_indent: usize,
options: &RenderOptions,
) -> Vec<String> {
let is_bare = options.bare_keys == BareStyle::Prefer
&& parse_bare_key_prefix(key).is_some_and(|end| end == key.len());
let key_text = render_key(key, options);
let key_fold_enabled = if is_bare {
options.string_bare_fold_style != FoldStyle::None
} else {
options.string_quoted_fold_style != FoldStyle::None
};
let key_fold: Option<Vec<String>> =
if is_bare && options.string_bare_fold_style != FoldStyle::None {
fold_bare_key(&key_text, pair_indent, options.string_bare_fold_style, options.wrap_width)
} else if !is_bare && options.string_quoted_fold_style != FoldStyle::None {
fold_json_string(key, pair_indent, 0, options.string_quoted_fold_style, options.wrap_width)
} else {
None
};
if let Some(mut fold_lines) = key_fold {
let last_fold_line = fold_lines.last().unwrap();
let after_colon_avail = options.wrap_width
.map(|w| w.saturating_sub(last_fold_line.len() + 1))
.unwrap_or(usize::MAX);
let normal = Self::render_object_entry_body(&key_text, value, pair_indent, key_fold_enabled, options);
let key_prefix = format!("{}{}:", spaces(pair_indent), key_text);
let suffix = normal[0].strip_prefix(&key_prefix).unwrap_or("").to_owned();
if suffix.is_empty() || after_colon_avail >= suffix.len() {
let last = fold_lines.pop().unwrap();
fold_lines.push(format!("{}:{}", last, suffix));
fold_lines.extend(normal.into_iter().skip(1));
} else {
let Some(bv) = BasicValue::new(value) else {
unreachable!("non-empty arrays/objects always render with empty suffix so suffix.is_empty() is true for them and this branch is unreachable")
};
let cont_lines = Self::render_scalar_value_continuation_lines(bv, pair_indent, options);
let last = fold_lines.pop().unwrap();
fold_lines.push(format!("{}:", last));
let first_cont = &cont_lines[0][pair_indent..];
fold_lines.push(format!("{}/ {}", spaces(pair_indent), first_cont));
fold_lines.extend(cont_lines.into_iter().skip(1));
}
return fold_lines;
}
Self::render_object_entry_body(&key_text, value, pair_indent, key_fold_enabled, options)
}
fn render_scalar_value_continuation_lines(
value: BasicValue<'_>,
pair_indent: usize,
options: &RenderOptions,
) -> Vec<String> {
match value {
BasicValue::String(s) => Self::render_string_lines(s, pair_indent, 2, options),
BasicValue::Number(n) => {
let ns = n.to_string();
fold_number(&ns, pair_indent, 2, options.number_fold_style, options.wrap_width)
.unwrap_or_else(|| vec![format!("{}{}", spaces(pair_indent), ns)])
}
BasicValue::Null => vec![format!("{}null", spaces(pair_indent))],
BasicValue::Bool(b) => vec![format!("{}{}", spaces(pair_indent), if b { "true" } else { "false" })],
BasicValue::EmptyArray => vec![format!("{}[]", spaces(pair_indent))],
BasicValue::EmptyObject => vec![format!("{}{{}}", spaces(pair_indent))],
}
}
fn render_object_entry_body(
key_text: &str,
value: &Value,
pair_indent: usize,
key_fold_enabled: bool,
options: &RenderOptions,
) -> Vec<String> {
match value {
Value::Array(values) if !values.is_empty() => {
if effective_tables(options)
&& let Some(table_lines) = Self::render_table(values, pair_indent, options) {
if let Some(target_indent) = table_unindent_target(pair_indent, &table_lines, options) {
let Some(offset_lines) = Self::render_table(values, target_indent, options) else {
unreachable!("table re-render at offset indent always succeeds");
};
let key_line = format!("{}{}", spaces(pair_indent), key_text);
let mut lines = indent_glyph_open_lines(&key_line, pair_indent, options);
if effective_force_markers(options) {
let elem_indent = target_indent + 2;
let first = offset_lines.first()
.expect("render_table always returns at least a header line");
let stripped = first.get(elem_indent..)
.expect("table line starts at elem_indent");
lines.push(format!("{}[ {}", spaces(target_indent), stripped));
lines.extend(offset_lines.into_iter().skip(1));
} else {
lines.extend(offset_lines);
}
lines.push(format!("{} />", spaces(pair_indent)));
return lines;
}
let mut lines = vec![format!("{}{}:", spaces(pair_indent), key_text)];
if effective_force_markers(options) {
let elem_indent = pair_indent + 2;
let first = table_lines.first()
.expect("render_table always returns at least a header line");
let stripped = first.get(elem_indent..)
.expect("table line starts at elem_indent");
lines.push(format!("{}[ {}", spaces(pair_indent), stripped));
lines.extend(table_lines.into_iter().skip(1));
} else {
lines.extend(table_lines);
}
return lines;
}
if should_use_indent_glyph(value, pair_indent, options) {
let key_line = format!("{}{}", spaces(pair_indent), key_text);
let mut lines = indent_glyph_open_lines(&key_line, pair_indent, options);
if values.first().is_some_and(needs_explicit_array_marker) {
lines.extend(Self::render_explicit_array(values, 2, options));
} else {
lines.extend(Self::render_array_children(values, 2, options));
}
lines.push(format!("{} />", spaces(pair_indent)));
return lines;
}
if effective_inline_arrays(options) {
let all_simple = values.iter().all(|v| match v {
Value::Array(a) => a.is_empty(),
Value::Object(o) => o.is_empty(),
_ => true,
});
if all_simple
&& let Some(lines) = Self::render_packed_array_lines(
values,
format!("{}{}: ", spaces(pair_indent), key_text),
pair_indent + 2,
options,
) {
return lines;
}
}
let mut lines = vec![format!("{}{}:", spaces(pair_indent), key_text)];
if values.first().is_some_and(needs_explicit_array_marker) || effective_force_markers(options) {
lines.extend(Self::render_explicit_array(
values,
pair_indent,
options,
));
} else {
lines.extend(Self::render_array_children(
values,
pair_indent + 2,
options,
));
}
lines
}
Value::Object(entries) if !entries.is_empty() => {
if should_use_indent_glyph(value, pair_indent, options) {
let key_line = format!("{}{}", spaces(pair_indent), key_text);
let mut lines = indent_glyph_open_lines(&key_line, pair_indent, options);
lines.extend(Self::render_implicit_object(entries, 0, options));
lines.push(format!("{} />", spaces(pair_indent)));
return lines;
}
let mut lines = vec![format!("{}{}:", spaces(pair_indent), key_text)];
if effective_force_markers(options) {
lines.extend(Self::render_explicit_object(entries, pair_indent, options));
} else {
lines.extend(Self::render_implicit_object(entries, pair_indent, options));
}
lines
}
_ => {
let bv = match value {
Value::Null => BasicValue::Null,
Value::Bool(b) => BasicValue::Bool(*b),
Value::Number(n) => BasicValue::Number(n),
Value::String(s) => BasicValue::String(s.as_str()),
Value::Array(_) => BasicValue::EmptyArray,
Value::Object(_) => BasicValue::EmptyObject,
};
let scalar_lines = match bv {
BasicValue::String(s) => Self::render_string_lines(s, pair_indent, key_text.len() + 1, options),
_ => Self::render_scalar_lines(bv, pair_indent, options),
};
let first = scalar_lines[0].clone();
let value_suffix = &first[pair_indent..];
let assembled_len = pair_indent + key_text.len() + 1 + value_suffix.len();
if key_fold_enabled
&& let Some(w) = options.wrap_width
&& assembled_len > w {
let cont_lines = Self::render_scalar_value_continuation_lines(bv, pair_indent, options);
let key_line = format!("{}{}:", spaces(pair_indent), key_text);
let first_cont = &cont_lines[0][pair_indent..];
let mut lines = vec![key_line, format!("{}/ {}", spaces(pair_indent), first_cont)];
lines.extend(cont_lines.into_iter().skip(1));
return lines;
}
let mut lines = vec![format!(
"{}{}:{}",
spaces(pair_indent),
key_text,
value_suffix
)];
lines.extend(scalar_lines.into_iter().skip(1));
lines
}
}
}
fn render_implicit_array(
values: &[Value],
parent_indent: usize,
options: &RenderOptions,
) -> Vec<String> {
if effective_tables(options)
&& let Some(lines) = Self::render_table(values, parent_indent, options) {
return lines;
}
if effective_inline_arrays(options) && !values.first().is_some_and(needs_explicit_array_marker)
&& let Some(lines) = Self::render_packed_array_lines(
values,
spaces(parent_indent + 2),
parent_indent + 2,
options,
) {
return lines;
}
let elem_indent = parent_indent + 2;
let element_lines: Vec<Vec<String>> = values
.iter()
.map(|value| Self::render_array_element(value, elem_indent, options))
.collect();
if values.first().is_some_and(needs_explicit_array_marker) {
let mut lines = Vec::new();
let first = &element_lines[0];
let first_line = first.first()
.expect("render_array_element always returns at least one line");
let stripped = first_line.get(elem_indent..)
.expect("array element line is indented at elem_indent");
lines.push(format!("{}[ {}", spaces(parent_indent), stripped));
lines.extend(first.iter().skip(1).cloned());
for extra in element_lines.iter().skip(1) {
lines.extend(extra.clone());
}
lines
} else {
element_lines.into_iter().flatten().collect()
}
}
fn render_array_children(
values: &[Value],
elem_indent: usize,
options: &RenderOptions,
) -> Vec<String> {
let mut lines = Vec::new();
let table_row_prefix = format!("{}|", spaces(elem_indent));
for value in values {
let prev_was_table = lines.last().map(|l: &String| l.starts_with(&table_row_prefix)).unwrap_or(false);
let elem_lines = Self::render_array_element(value, elem_indent, options);
let curr_is_table = elem_lines.first().map(|l| l.starts_with(&table_row_prefix)).unwrap_or(false);
if prev_was_table && curr_is_table {
let first = elem_lines.first().unwrap();
let stripped = &first[elem_indent..]; lines.push(format!("{}[ {}", spaces(elem_indent.saturating_sub(2)), stripped));
lines.extend(elem_lines.into_iter().skip(1));
} else {
lines.extend(elem_lines);
}
}
lines
}
fn render_explicit_array(
values: &[Value],
marker_indent: usize,
options: &RenderOptions,
) -> Vec<String> {
if effective_tables(options)
&& let Some(lines) = Self::render_table(values, marker_indent, options) {
let elem_indent = marker_indent + 2;
let first = lines.first()
.expect("render_table always returns at least a header line");
let stripped = first.get(elem_indent..)
.expect("table line starts at elem_indent");
let mut out = vec![format!("{}[ {}", spaces(marker_indent), stripped)];
out.extend(lines.into_iter().skip(1));
return out;
}
if effective_inline_arrays(options)
&& let Some(lines) = Self::render_packed_array_lines(
values,
format!("{}[ ", spaces(marker_indent)),
marker_indent + 2,
options,
) {
return lines;
}
let elem_indent = marker_indent + 2;
let element_lines: Vec<Vec<String>> = values
.iter()
.map(|value| Self::render_array_element(value, elem_indent, options))
.collect();
let first = element_lines.first()
.unwrap_or_else(|| unreachable!("render_explicit_array called with empty values"));
let first_line = first.first()
.expect("render_array_element always returns at least one line");
let stripped = first_line.get(elem_indent..)
.expect("array element line is indented at elem_indent");
let mut lines = vec![format!("{}[ {}", spaces(marker_indent), stripped)];
lines.extend(first.iter().skip(1).cloned());
for extra in element_lines.iter().skip(1) {
lines.extend(extra.clone());
}
lines
}
fn render_explicit_object(
entries: &[Entry],
marker_indent: usize,
options: &RenderOptions,
) -> Vec<String> {
let pair_indent = marker_indent + 2;
let implicit_lines = Self::render_implicit_object(entries, marker_indent, options);
let first_line = implicit_lines.first()
.expect("render_implicit_object with non-empty entries returns at least one line");
let stripped = first_line.get(pair_indent..)
.expect("implicit object line is indented at pair_indent");
let mut lines = vec![format!("{}{{ {}", spaces(marker_indent), stripped)];
lines.extend(implicit_lines.into_iter().skip(1));
lines
}
fn render_array_element(
value: &Value,
elem_indent: usize,
options: &RenderOptions,
) -> Vec<String> {
match value {
Value::Array(values) if !values.is_empty() => {
if should_use_indent_glyph(value, elem_indent, options) {
let mut lines = vec![format!("{} /<", spaces(elem_indent))];
if values.first().is_some_and(needs_explicit_array_marker) {
lines.extend(Self::render_explicit_array(values, 0, options));
} else {
lines.extend(Self::render_array_children(values, 0, options));
}
lines.push(format!("{} />", spaces(elem_indent)));
return lines;
}
Self::render_explicit_array(values, elem_indent, options)
}
Value::Object(entries) if !entries.is_empty() => {
Self::render_explicit_object(entries, elem_indent, options)
}
Value::Null => Self::render_scalar_lines(BasicValue::Null, elem_indent, options),
Value::Bool(b) => Self::render_scalar_lines(BasicValue::Bool(*b), elem_indent, options),
Value::Number(n) => Self::render_scalar_lines(BasicValue::Number(n), elem_indent, options),
Value::String(s) => Self::render_scalar_lines(BasicValue::String(s.as_str()), elem_indent, options),
Value::Array(_) => Self::render_scalar_lines(BasicValue::EmptyArray, elem_indent, options),
Value::Object(_) => Self::render_scalar_lines(BasicValue::EmptyObject, elem_indent, options),
}
}
fn render_scalar_lines(
value: BasicValue<'_>,
indent: usize,
options: &RenderOptions,
) -> Vec<String> {
match value {
BasicValue::Null => vec![format!("{}null", spaces(indent))],
BasicValue::Bool(b) => vec![format!(
"{}{}",
spaces(indent),
if b { "true" } else { "false" }
)],
BasicValue::Number(n) => {
let s = n.to_string();
if let Some(lines) = fold_number(&s, indent, 0, options.number_fold_style, options.wrap_width) {
return lines;
}
vec![format!("{}{}", spaces(indent), s)]
}
BasicValue::String(s) => Self::render_string_lines(s, indent, 0, options),
BasicValue::EmptyArray => vec![format!("{}[]", spaces(indent))],
BasicValue::EmptyObject => vec![format!("{}{{}}", spaces(indent))],
}
}
fn render_string_lines(
value: &str,
indent: usize,
first_line_extra: usize,
options: &RenderOptions,
) -> Vec<String> {
if value.is_empty() {
return vec![format!("{}\"\"", spaces(indent))];
}
let meta = StrMeta::new(value);
if matches!(options.multiline_style, MultilineStyle::FoldingQuotes) && meta.has_eol && meta.eol_type.is_some() {
return render_folding_quotes(value, indent, options);
}
if options.multiline_strings
&& !meta.has_forbidden_literal
&& meta.has_eol
&& let Some(local_eol) = meta.eol_type
{
let suffix = local_eol.opener_suffix();
let parts: Vec<&str> = match local_eol {
MultilineLocalEol::Lf => value.split('\n').collect(),
MultilineLocalEol::CrLf => value.split("\r\n").collect(),
};
let min_eols = options.multiline_min_lines.max(1);
if parts.len().saturating_sub(1) >= min_eols {
let fold_style = options.string_multiline_fold_style;
let wrap = options.wrap_width;
let pipe_heavy = {
let pipe_count = parts
.iter()
.filter(|p| line_starts_with_ws_then(p, '|'))
.count();
!parts.is_empty() && pipe_count * 10 > parts.len()
};
let backtick_start = parts.iter().any(|p| line_starts_with_ws_then(p, '`'));
let forced_bold = pipe_heavy || backtick_start;
let overflows_at_natural = wrap
.map(|w| parts.iter().any(|p| indent + 2 + p.len() > w))
.unwrap_or(false);
let too_many_lines = options.multiline_max_lines > 0
&& parts.len() > options.multiline_max_lines;
let bold = |body_indent: usize| {
Self::render_multiline_double_backtick(
&parts, indent, body_indent, suffix, fold_style, wrap,
)
};
return match options.multiline_style {
MultilineStyle::Floating => {
if forced_bold || overflows_at_natural || too_many_lines {
bold(0)
} else {
Self::render_multiline_single_backtick(
&parts, indent, suffix, fold_style, wrap,
)
}
}
MultilineStyle::Light => {
if forced_bold {
bold(0)
} else {
Self::render_multiline_single_backtick(
&parts, indent, suffix, fold_style, wrap,
)
}
}
MultilineStyle::Bold => bold(0),
MultilineStyle::BoldFloating => {
let body = if forced_bold || overflows_at_natural { 0 } else { indent };
bold(body)
}
MultilineStyle::Transparent => {
if forced_bold {
bold(0)
} else {
Self::render_multiline_triple_backtick(&parts, indent, suffix)
}
}
MultilineStyle::FoldingQuotes => unreachable!(),
};
}
}
if options.bare_strings == BareStyle::Prefer && meta.is_bare_eligible {
if options.string_bare_fold_style != FoldStyle::None
&& let Some(lines) =
fold_bare_string(value, indent, first_line_extra, options.string_bare_fold_style, options.wrap_width)
{
return lines;
}
return vec![format!("{} {}", spaces(indent), value)];
}
if options.string_quoted_fold_style != FoldStyle::None
&& let Some(lines) =
fold_json_string(value, indent, first_line_extra, options.string_quoted_fold_style, options.wrap_width)
{
return lines;
}
vec![format!("{}{}", spaces(indent), render_json_string(value))]
}
fn render_multiline_single_backtick(
parts: &[&str],
indent: usize,
suffix: &str,
fold_style: FoldStyle,
wrap_width: Option<usize>,
) -> Vec<String> {
let glyph = format!("{} `{}", spaces(indent), suffix);
let body_indent = indent + 2;
let fold_prefix = format!("{}/ ", spaces(indent));
let avail = wrap_width.map(|w| w.saturating_sub(body_indent));
let mut lines = vec![glyph.clone()];
for part in parts {
if fold_style != FoldStyle::None
&& let Some(avail_w) = avail
&& part.len() > avail_w {
let segments = split_multiline_fold(part, avail_w, fold_style);
let mut first = true;
for seg in segments {
if first {
lines.push(format!("{}{}", spaces(body_indent), seg));
first = false;
} else {
lines.push(format!("{}{}", fold_prefix, seg));
}
}
continue;
}
lines.push(format!("{}{}", spaces(body_indent), part));
}
lines.push(glyph);
lines
}
fn render_multiline_double_backtick(
parts: &[&str],
indent: usize,
body_indent: usize,
suffix: &str,
fold_style: FoldStyle,
wrap_width: Option<usize>,
) -> Vec<String> {
let glyph = format!("{} ``{}", spaces(indent), suffix);
let fold_prefix = format!("{}/ ", spaces(body_indent.saturating_sub(2)));
let avail = wrap_width.map(|w| w.saturating_sub(body_indent + 2));
let mut lines = vec![glyph.clone()];
for part in parts {
if fold_style != FoldStyle::None
&& let Some(avail_w) = avail
&& part.len() > avail_w {
let segments = split_multiline_fold(part, avail_w, fold_style);
let mut first = true;
for seg in segments {
if first {
lines.push(format!("{}| {}", spaces(body_indent), seg));
first = false;
} else {
lines.push(format!("{}{}", fold_prefix, seg));
}
}
continue;
}
lines.push(format!("{}| {}", spaces(body_indent), part));
}
lines.push(glyph);
lines
}
#[allow(dead_code)]
fn render_multiline_triple_backtick(parts: &[&str], indent: usize, suffix: &str) -> Vec<String> {
let glyph = format!("{} ```{}", spaces(indent), suffix);
let mut lines = vec![glyph.clone()];
for part in parts {
lines.push((*part).to_owned());
}
lines.push(glyph);
lines
}
fn render_inline_object_token(
key: &str,
value: &Value,
options: &RenderOptions,
) -> Option<String> {
let bv = match value {
Value::String(s) if s.contains('\n') || s.contains('\r') => return None,
_ => BasicValue::new(value)?,
};
Some(format!("{}:{}", render_key(key, options), Self::render_scalar_token(bv, options)))
}
fn render_scalar_token(value: BasicValue<'_>, options: &RenderOptions) -> String {
match value {
BasicValue::Null => "null".to_owned(),
BasicValue::Bool(b) => if b { "true".to_owned() } else { "false".to_owned() },
BasicValue::Number(n) => n.to_string(),
BasicValue::String(s) => {
if options.bare_strings == BareStyle::Prefer && BareString::new(s).is_some() {
format!(" {}", s)
} else {
render_json_string(s)
}
}
BasicValue::EmptyArray => "[]".to_owned(),
BasicValue::EmptyObject => "{}".to_owned(),
}
}
fn render_packed_array_lines(
values: &[Value],
first_prefix: String,
continuation_indent: usize,
options: &RenderOptions,
) -> Option<Vec<String>> {
if values.is_empty() {
return Some(vec![format!("{first_prefix}[]")]);
}
if values
.iter()
.all(|value| matches!(value, Value::String(_)))
{
return Self::render_string_array_lines(
values,
first_prefix,
continuation_indent,
options,
);
}
let tokens = Self::render_packed_array_tokens(values);
Self::render_packed_token_lines(tokens, first_prefix, continuation_indent, false, options)
}
fn render_string_array_lines(
values: &[Value],
first_prefix: String,
continuation_indent: usize,
options: &RenderOptions,
) -> Option<Vec<String>> {
match options.string_array_style {
StringArrayStyle::None => None,
StringArrayStyle::Spaces => {
let tokens = Self::render_packed_array_tokens(values);
Self::render_packed_token_lines(
tokens,
first_prefix,
continuation_indent,
true,
options,
)
}
StringArrayStyle::PreferSpaces => {
let tokens = Self::render_packed_array_tokens(values);
let preferred = Self::render_packed_token_lines(
tokens.clone(),
first_prefix.clone(),
continuation_indent,
true,
options,
);
let fallback = Self::render_packed_token_lines(
tokens,
first_prefix,
continuation_indent,
false,
options,
);
pick_preferred_string_array_layout(preferred, fallback, options)
}
StringArrayStyle::Comma => {
let tokens = Self::render_packed_array_tokens(values);
Self::render_packed_token_lines(
tokens,
first_prefix,
continuation_indent,
false,
options,
)
}
StringArrayStyle::PreferComma => {
let tokens = Self::render_packed_array_tokens(values);
let preferred = Self::render_packed_token_lines(
tokens.clone(),
first_prefix.clone(),
continuation_indent,
false,
options,
);
let fallback = Self::render_packed_token_lines(
tokens,
first_prefix,
continuation_indent,
true,
options,
);
pick_preferred_string_array_layout(preferred, fallback, options)
}
}
}
fn render_packed_array_tokens(values: &[Value]) -> Vec<PackedToken<'_>> {
let mut tokens = Vec::new();
for value in values {
let token = match value {
Value::String(text) if text.contains('\n') || text.contains('\r') => {
PackedToken::Block(value)
}
Value::Array(vals) if !vals.is_empty() => PackedToken::Block(value),
Value::Object(entries) if !entries.is_empty() => PackedToken::Block(value),
Value::Null => PackedToken::Inline(BasicValue::Null),
Value::Bool(b) => PackedToken::Inline(BasicValue::Bool(*b)),
Value::Number(n) => PackedToken::Inline(BasicValue::Number(n)),
Value::String(s) => PackedToken::Inline(BasicValue::String(s.as_str())),
Value::Array(_) => PackedToken::Inline(BasicValue::EmptyArray),
Value::Object(_) => PackedToken::Inline(BasicValue::EmptyObject),
};
tokens.push(token);
}
tokens
}
fn fold_packed_inline(
value: BasicValue<'_>,
continuation_indent: usize,
first_line_extra: usize,
options: &RenderOptions,
) -> Option<Vec<String>> {
match value {
BasicValue::String(s) => {
let lines = Self::render_string_lines(s, continuation_indent, first_line_extra, options);
if lines.len() > 1 { Some(lines) } else { None }
}
BasicValue::Number(n) => {
let ns = n.to_string();
fold_number(
&ns,
continuation_indent,
first_line_extra,
options.number_fold_style,
options.wrap_width,
)
.filter(|l| l.len() > 1)
}
_ => None,
}
}
fn render_packed_token_lines(
tokens: Vec<PackedToken<'_>>,
first_prefix: String,
continuation_indent: usize,
string_spaces_mode: bool,
options: &RenderOptions,
) -> Option<Vec<String>> {
if tokens.is_empty() {
return Some(vec![first_prefix]);
}
if let Some(w) = options.wrap_width
&& first_prefix.len() >= w
{
return None;
}
if string_spaces_mode && tokens.iter().any(|t| matches!(t, PackedToken::Block(_))) {
return None;
}
let separator = if string_spaces_mode { " " } else { ", " };
let continuation_prefix = spaces(continuation_indent);
let mut current = first_prefix.clone();
let mut current_is_fresh = true;
let mut lines: Vec<String> = Vec::new();
for token in tokens {
match token {
PackedToken::Block(value) => {
if !current_is_fresh {
if !string_spaces_mode {
current.push(',');
}
lines.push(current);
}
let block_lines = match value {
Value::String(s) => {
Self::render_string_lines(s, continuation_indent, 0, options)
}
Value::Array(vals) if !vals.is_empty() => {
Self::render_explicit_array(vals, continuation_indent, options)
}
Value::Object(entries) if !entries.is_empty() => {
Self::render_explicit_object(entries, continuation_indent, options)
}
_ => unreachable!("PackedToken::Block must contain a block value"),
};
let current_prefix_str = if lines.is_empty() {
first_prefix.clone()
} else {
continuation_prefix.clone()
};
let first_block_content =
block_lines[0].get(continuation_indent..).unwrap_or("");
lines.push(format!("{}{}", current_prefix_str, first_block_content));
for bl in block_lines.into_iter().skip(1) {
lines.push(bl);
}
current = continuation_prefix.clone();
current_is_fresh = true;
}
PackedToken::Inline(bv) => {
let token_str = match bv {
BasicValue::String(s) if s.chars().any(is_comma_like) => {
render_json_string(s)
}
_ => Self::render_scalar_token(bv, options),
};
if current_is_fresh {
current.push_str(&token_str);
current_is_fresh = false;
if !fits_wrap(options, ¤t) {
let first_line_extra = if lines.is_empty() {
first_prefix.len().saturating_sub(continuation_indent)
} else {
0
};
if let Some(fold_lines) = Self::fold_packed_inline(
bv,
continuation_indent,
first_line_extra,
options,
) {
let actual_prefix = if lines.is_empty() {
first_prefix.clone()
} else {
continuation_prefix.clone()
};
let first_content =
fold_lines[0].get(continuation_indent..).unwrap_or("");
lines.push(format!("{}{}", actual_prefix, first_content));
for fl in fold_lines.into_iter().skip(1) {
lines.push(fl);
}
current = continuation_prefix.clone();
current_is_fresh = true;
}
}
} else {
let candidate = format!("{current}{separator}{token_str}");
if fits_wrap(options, &candidate) {
current = candidate;
} else {
if !string_spaces_mode {
current.push(',');
}
lines.push(current);
current = format!("{}{}", continuation_prefix, token_str);
current_is_fresh = false;
if !fits_wrap(options, ¤t)
&& let Some(fold_lines) = Self::fold_packed_inline(
bv,
continuation_indent,
0,
options,
) {
let first_content =
fold_lines[0].get(continuation_indent..).unwrap_or("");
lines.push(format!(
"{}{}",
continuation_prefix, first_content
));
for fl in fold_lines.into_iter().skip(1) {
lines.push(fl);
}
current = continuation_prefix.clone();
current_is_fresh = true;
}
}
}
}
}
}
if !current_is_fresh {
lines.push(current);
}
Some(lines)
}
fn render_table(
values: &[Value],
parent_indent: usize,
options: &RenderOptions,
) -> Option<Vec<String>> {
if values.len() < options.table_min_rows {
return None;
}
let mut columns = Vec::<String>::new();
let mut present_cells = 0usize;
let mut first_row_keys: Option<Vec<&str>> = None;
for value in values {
let Value::Object(entries) = value else {
return None;
};
present_cells += entries.len();
for Entry { key, value: cell } in entries {
if matches!(cell, Value::Array(inner) if !inner.is_empty())
|| matches!(cell, Value::Object(inner) if !inner.is_empty())
|| matches!(cell, Value::String(text) if text.contains('\n') || text.contains('\r'))
{
return None;
}
if !columns.iter().any(|column| column == key) {
columns.push(key.clone());
}
}
let row_keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
if let Some(ref first) = first_row_keys {
let shared_in_first: Vec<&str> = first.iter().copied().filter(|k| row_keys.contains(k)).collect();
let shared_in_row: Vec<&str> = row_keys.iter().copied().filter(|k| first.contains(k)).collect();
if shared_in_first != shared_in_row {
return None;
}
} else {
first_row_keys = Some(row_keys);
}
}
if columns.len() < options.table_min_columns {
return None;
}
let similarity = present_cells as f32 / (values.len() * columns.len()) as f32;
if similarity < options.table_min_similarity {
return None;
}
let mut header_cells = Vec::new();
let mut rows = Vec::new();
for column in &columns {
header_cells.push(render_key(column, options));
}
for value in values {
let Value::Object(entries) = value else {
return None;
};
let mut row: Vec<String> = Vec::new();
for column in &columns {
let token = if let Some(entry) = entries.iter().find(|e| &e.key == column) {
let value = &entry.value;
Self::render_table_cell_token(value, options)
} else {
None
};
row.push(token.unwrap_or_default());
}
rows.push(row);
}
let mut widths = vec![0usize; columns.len()];
for (index, header) in header_cells.iter().enumerate() {
widths[index] = header.len();
}
for row in &rows {
for (index, cell) in row.iter().enumerate() {
widths[index] = widths[index].max(cell.len());
}
}
if let Some(col_max) = options.table_column_max_width
&& widths.iter().any(|w| *w > col_max) {
return None;
}
for width in &mut widths {
*width += 2;
}
if let Some(w) = options.wrap_width {
let min_row_width = 2 + widths.iter().sum::<usize>() + widths.len() + 1;
if min_row_width > w && !options.table_fold {
return None;
}
}
let indent = spaces(parent_indent + 2);
let mut lines = Vec::new();
lines.push(format!(
"{}{}",
indent,
header_cells
.iter()
.zip(widths.iter())
.map(|(cell, width)| format!("|{cell:<width$}", width = *width))
.collect::<String>()
+ "|"
));
let pair_indent = parent_indent; let fold_prefix = spaces(pair_indent);
for row in rows {
let row_line = format!(
"{}{}",
indent,
row.iter()
.zip(widths.iter())
.map(|(cell, width)| format!("|{cell:<width$}", width = *width))
.collect::<String>()
+ "|"
);
if options.table_fold {
let fold_avail = options
.wrap_width
.unwrap_or(usize::MAX)
.saturating_sub(pair_indent + 2); if row_line.len() > fold_avail + pair_indent + 2 {
if let Some((before, after)) = split_table_row_for_fold(&row_line, fold_avail + pair_indent + 2) {
lines.push(before);
lines.push(format!("{}/ {}", fold_prefix, after));
continue;
}
}
}
lines.push(row_line);
}
Some(lines)
}
fn render_table_cell_token(
value: &Value,
options: &RenderOptions,
) -> Option<String> {
match value {
Value::Null => Some("null".to_owned()),
Value::Bool(value) => Some(if *value {
"true".to_owned()
} else {
"false".to_owned()
}),
Value::Number(value) => Some(value.to_string()),
Value::String(value) => {
if value.contains('\n') || value.contains('\r') {
None
} else if options.bare_strings == BareStyle::Prefer
&& TableBareString::new(value).is_some()
{
Some(format!(" {}", value))
} else {
Some(render_json_string(value))
}
}
Value::Array(values) if values.is_empty() => Some("[]".to_owned()),
Value::Object(entries) if entries.is_empty() => Some("{}".to_owned()),
_ => None,
}
}
}