use super::roles::{self, EntryKind, OperatorShape, Role};
use super::style::STYLE;
use crate::definitions::json::{Member, Node, Spanned};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
AlwaysInline,
InlineIfFits,
AlwaysBreak,
}
enum Doc {
Scalar(String),
Container {
open: char,
close: char,
items: Vec<(Option<String>, Doc)>,
mode: Mode,
flat_len: usize,
can_inline: bool,
},
}
impl Doc {
fn flat_len(&self) -> usize {
match self {
Doc::Scalar(s) => s.len(),
Doc::Container { flat_len, .. } => *flat_len,
}
}
fn can_inline(&self) -> bool {
match self {
Doc::Scalar(_) => true,
Doc::Container { can_inline, .. } => *can_inline,
}
}
}
pub fn print(root: &Node) -> String {
let doc = layout(root, roles::root_role(root), 0);
let mut out = String::new();
emit(&doc, 0, 0, 0, &mut out);
out.push('\n');
out
}
fn layout(node: &Node, role: Role, depth: usize) -> Doc {
match node {
Node::Array(items) => layout_array(items, role, depth),
Node::Object(members) => layout_object(
members,
role,
depth,
roles::key_order(role).map(<[_]>::to_vec),
),
scalar => Doc::Scalar(render_scalar(scalar)),
}
}
fn layout_array(items: &[Spanned<Node>], role: Role, depth: usize) -> Doc {
let children: Vec<(Option<String>, Doc)> = items
.iter()
.map(|item| {
let child_role = roles::child_role(role, None, &item.node, depth + 1);
(None, layout(&item.node, child_role, depth + 1))
})
.collect();
let mode = match role {
Role::TaskList => Mode::AlwaysBreak,
Role::EntryList(EntryKind::Mapping | EntryKind::ValidationRule) if items.len() > 1 => {
Mode::AlwaysBreak
}
Role::ScalarArray if items.len() > STYLE.max_scalar_inline => Mode::AlwaysBreak,
Role::OperatorArgs(OperatorShape::Compound) => Mode::AlwaysBreak,
Role::OperatorArgs(OperatorShape::Unary) => Mode::AlwaysInline,
_ => Mode::InlineIfFits,
};
container('[', ']', children, mode)
}
fn layout_object(
members: &[Member],
role: Role,
depth: usize,
order: Option<Vec<&'static str>>,
) -> Doc {
if let Role::Operator(shape) = role {
let member = &members[0];
let arg_role = match &member.value.node {
Node::Array(_) => Role::OperatorArgs(shape),
other => roles::child_role(role, Some(&member.key.node), other, depth + 1),
};
let child = layout(&member.value.node, arg_role, depth + 1);
let mode = match shape {
OperatorShape::Unary => Mode::AlwaysInline,
OperatorShape::Leaf => Mode::InlineIfFits,
OperatorShape::Compound => Mode::AlwaysBreak,
};
return container(
'{',
'}',
vec![(Some(render_string(&member.key.node)), child)],
mode,
);
}
let function_name = match role {
Role::FunctionHeader => members
.iter()
.find(|m| m.key.node == "name")
.and_then(|m| m.value.node.as_str()),
_ => None,
};
let ordered = roles::order_members(members, order.as_deref());
let children: Vec<(Option<String>, Doc)> = ordered
.into_iter()
.map(|m| {
let key = m.key.node.as_str();
let value = &m.value.node;
let child = match (role, key, value) {
(Role::FunctionHeader, "input", Node::Object(input)) => layout_object(
input,
roles::input_role(function_name),
depth + 1,
roles::input_key_order(function_name),
),
_ => layout(
value,
roles::child_role(role, Some(key), value, depth + 1),
depth + 1,
),
};
(Some(render_string(key)), child)
})
.collect();
let mode = match role {
Role::Workflow
| Role::Channel
| Role::Connector
| Role::SharedDoc
| Role::NamedValues
| Role::FragmentMap
| Role::Fragment
| Role::CaseFile
| Role::Artifact
| Role::ArtifactMeta
| Role::Task
| Role::Group
| Role::UseStep
| Role::PathMap => Mode::AlwaysBreak,
_ => Mode::InlineIfFits,
};
container('{', '}', children, mode)
}
fn container(open: char, close: char, items: Vec<(Option<String>, Doc)>, mode: Mode) -> Doc {
let flat_len = if items.is_empty() {
2
} else {
let pad = if open == '{' { 2 } else { 0 };
let separators = 2 * (items.len() - 1);
let body: usize = items
.iter()
.map(|(key, value)| key.as_ref().map_or(0, |k| k.len() + 2) + value.flat_len())
.sum();
2 + pad + separators + body
};
let can_inline = match mode {
Mode::AlwaysBreak => items.is_empty(),
_ => items.iter().all(|(_, v)| v.can_inline()),
};
Doc::Container {
open,
close,
items,
mode,
flat_len,
can_inline,
}
}
fn emit(doc: &Doc, indent: usize, column: usize, suffix: usize, out: &mut String) {
match doc {
Doc::Scalar(s) => out.push_str(s),
Doc::Container {
open,
close,
items,
mode,
flat_len,
can_inline,
} => {
let fits = column + flat_len + suffix <= STYLE.width;
if items.is_empty() || (*can_inline && (*mode == Mode::AlwaysInline || fits)) {
emit_flat(doc, out);
return;
}
out.push(*open);
let inner = indent + STYLE.indent;
let last = items.len() - 1;
for (i, (key, value)) in items.iter().enumerate() {
out.push('\n');
push_spaces(out, inner);
let mut col = inner;
if let Some(key) = key {
out.push_str(key);
out.push_str(": ");
col += key.len() + 2;
}
let comma = usize::from(i != last);
emit(value, inner, col, comma, out);
if comma == 1 {
out.push(',');
}
}
out.push('\n');
push_spaces(out, indent);
out.push(*close);
}
}
}
fn emit_flat(doc: &Doc, out: &mut String) {
match doc {
Doc::Scalar(s) => out.push_str(s),
Doc::Container {
open, close, items, ..
} => {
out.push(*open);
if !items.is_empty() {
let pad = *open == '{';
if pad {
out.push(' ');
}
for (i, (key, value)) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
if let Some(key) = key {
out.push_str(key);
out.push_str(": ");
}
emit_flat(value, out);
}
if pad {
out.push(' ');
}
}
out.push(*close);
}
}
}
fn push_spaces(out: &mut String, n: usize) {
out.extend(std::iter::repeat_n(' ', n));
}
fn render_scalar(node: &Node) -> String {
match node {
Node::Null => "null".to_string(),
Node::Bool(true) => "true".to_string(),
Node::Bool(false) => "false".to_string(),
Node::Number(lexeme) => lexeme.to_string(),
Node::String(s) => render_string(s),
Node::Array(_) | Node::Object(_) => unreachable!("containers are laid out, not rendered"),
}
}
pub fn render_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
use std::fmt::Write;
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strings_escape_like_serde_json() {
for s in [
"plain",
"quote\"back\\slash/",
"tab\tnewline\ncr\r",
"\u{1}\u{8}\u{c}\u{1f}",
"é😀 raw",
"\u{7f}",
] {
assert_eq!(
render_string(s),
serde_json::to_string(s).expect("test input is valid"),
"{s:?}"
);
}
}
}