use crate::console::{ConsoleOptions, RenderResult, Renderable};
use crate::highlighter::ReprHighlighter;
use crate::segment::Segment;
use crate::text::Text;
#[derive(Debug, Clone)]
pub struct Node {
pub key: Option<String>,
pub value: Option<String>,
pub children: Vec<Node>,
pub is_container: bool,
pub is_iter: bool,
pub is_mapping: bool,
pub is_attrs: bool,
pub open_brace: String,
pub close_brace: String,
pub empty: String,
pub last: bool,
}
impl Node {
pub fn new(key: Option<String>, value: Option<String>) -> Self {
Self {
key,
value,
children: Vec::new(),
is_container: false,
is_iter: false,
is_mapping: false,
is_attrs: false,
open_brace: "(".to_string(),
close_brace: ")".to_string(),
empty: String::new(),
last: true,
}
}
pub fn add_child(&mut self, child: Node) {
self.children.push(child);
}
}
#[derive(Debug, Clone)]
pub struct Pretty {
node: Node,
indent_guides: bool,
max_depth: Option<usize>,
max_string: Option<usize>,
max_length: Option<usize>,
expand_all: bool,
highlighter: ReprHighlighter,
}
impl Pretty {
pub fn new(node: Node) -> Self {
Self {
node,
indent_guides: true,
max_depth: None,
max_string: None,
max_length: None,
expand_all: false,
highlighter: ReprHighlighter::new(),
}
}
pub fn indent_guides(mut self, value: bool) -> Self {
self.indent_guides = value;
self
}
pub fn max_depth(mut self, depth: usize) -> Self {
self.max_depth = Some(depth);
self
}
pub fn max_string(mut self, max: usize) -> Self {
self.max_string = Some(max);
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.max_length = Some(max);
self
}
pub fn expand_all(mut self) -> Self {
self.expand_all = true;
self
}
pub fn from_debug<T: std::fmt::Debug>(value: &T) -> Self {
let debug_str = format!("{:#?}", value);
let node = parse_debug_to_node(&debug_str);
Self::new(node)
}
pub fn from_json(value: &serde_json::Value) -> Self {
let node = json_to_node(value, None);
Self::new(node)
}
}
impl Renderable for Pretty {
fn render(&self, options: &ConsoleOptions) -> RenderResult {
let mut lines: Vec<Vec<Segment>> = Vec::new();
let prefix = String::new();
let depth = 0;
self.render_node(&self.node, &mut lines, &prefix, depth, options);
RenderResult { lines, items: Vec::new() }
}
}
impl Pretty {
fn render_node(
&self,
node: &Node,
lines: &mut Vec<Vec<Segment>>,
prefix: &str,
depth: usize,
options: &ConsoleOptions,
) {
if let Some(max) = self.max_depth {
if depth > max && !self.expand_all {
lines.push(vec![
Segment::new(prefix),
Segment::new("..."),
Segment::line(),
]);
return;
}
}
let indent = " ";
let guide = if self.indent_guides && !options.ascii_only {
"│ "
} else {
" "
};
let mut line_text = String::from(prefix);
if let Some(ref key) = node.key {
let highlighted = self.highlighter.highlight_str(key);
line_text.push_str(&highlighted.plain);
line_text.push_str(": ");
}
if node.children.is_empty() {
if let Some(ref value) = node.value {
let truncated = if let Some(max) = self.max_string {
if value.len() > max {
format!("{}...", &value[..max])
} else {
value.clone()
}
} else {
value.clone()
};
let highlighted = self.highlighter.highlight_str(&truncated);
line_text.push_str(&highlighted.plain);
}
lines.push(vec![Segment::new(&line_text), Segment::line()]);
} else {
line_text.push_str(&node.open_brace);
lines.push(vec![Segment::new(&line_text), Segment::line()]);
let max_len = self.max_length.unwrap_or(usize::MAX);
let count = node.children.len();
let show_ellipsis = count > max_len;
for (i, child) in node.children.iter().enumerate() {
if i >= max_len {
if show_ellipsis {
let child_prefix = format!("{prefix}{indent}");
lines.push(vec![
Segment::new(format!(
"{child_prefix}... ({} more)",
count - max_len
)),
Segment::line(),
]);
}
break;
}
let child_prefix = if self.indent_guides && i < count - 1 {
format!("{prefix}{guide}")
} else {
format!("{prefix}{indent}")
};
self.render_node(child, lines, &child_prefix, depth + 1, options);
}
lines.push(vec![
Segment::new(format!("{prefix}{}", node.close_brace)),
Segment::line(),
]);
}
}
}
pub fn install() {}
pub fn pprint<T: std::fmt::Debug>(value: &T, console: &mut crate::console::Console) {
let pretty = Pretty::from_debug(value);
console.println(&pretty);
}
pub fn pretty_repr<T: std::fmt::Debug>(value: &T) -> Text {
let debug_str = format!("{:#?}", value);
let highlighter = ReprHighlighter::new();
highlighter.highlight_str(&debug_str)
}
pub fn traverse(value: &dyn std::fmt::Debug) -> Node {
let debug_str = format!("{:#?}", value);
parse_debug_to_node(&debug_str)
}
fn parse_debug_to_node(debug: &str) -> Node {
let lines: Vec<&str> = debug.lines().collect();
if lines.is_empty() {
return Node::new(None, Some(String::new()));
}
if lines.len() == 1 {
return Node::new(None, Some(lines[0].trim().to_string()));
}
let trimmed = debug.trim();
let (open_brace, close_brace) = if trimmed.starts_with('{') {
("{", "}")
} else if trimmed.starts_with('[') {
("[", "]")
} else {
("(", ")")
};
let mut node = Node::new(None, None);
node.open_brace = open_brace.to_string();
node.close_brace = close_brace.to_string();
node.is_container = true;
for line in lines.iter().skip(1) {
let trimmed_line = line.trim();
if trimmed_line.is_empty() || trimmed_line == close_brace {
continue;
}
if let Some(idx) = trimmed_line.find(": ") {
let key = trimmed_line[..idx].trim().to_string();
let value = trimmed_line[idx + 2..].trim().to_string();
let child = Node::new(Some(key), Some(value));
node.children.push(child);
} else {
let child = Node::new(None, Some(trimmed_line.to_string()));
node.children.push(child);
}
}
node
}
fn json_to_node(value: &serde_json::Value, key: Option<String>) -> Node {
match value {
serde_json::Value::Null => {
let mut node = Node::new(key, Some("null".to_string()));
node.is_container = false;
node
}
serde_json::Value::Bool(b) => {
let mut node = Node::new(key, Some(b.to_string()));
node.is_container = false;
node
}
serde_json::Value::Number(n) => {
let mut node = Node::new(key, Some(n.to_string()));
node.is_container = false;
node
}
serde_json::Value::String(s) => {
let display = format!("\"{}\"", s);
let mut node = Node::new(key, Some(display));
node.is_container = false;
node
}
serde_json::Value::Array(arr) => {
let mut node = Node::new(key, None);
node.is_container = true;
node.is_iter = true;
node.open_brace = "[".to_string();
node.close_brace = "]".to_string();
node.empty = "[]".to_string();
for item in arr {
node.children.push(json_to_node(item, None));
}
node
}
serde_json::Value::Object(obj) => {
let mut node = Node::new(key, None);
node.is_container = true;
node.is_mapping = true;
node.open_brace = "{".to_string();
node.close_brace = "}".to_string();
node.empty = "{}".to_string();
for (k, v) in obj {
node.children.push(json_to_node(v, Some(k.clone())));
}
node
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::console::ConsoleOptions;
#[test]
fn test_pretty_from_debug() {
let value = vec!["hello", "world"];
let pretty = Pretty::from_debug(&value);
let opts = ConsoleOptions::default();
let result = pretty.render(&opts);
let ansi = result.to_ansi();
assert!(ansi.contains("hello"));
assert!(ansi.contains("world"));
}
#[test]
fn test_pretty_from_json() {
let value = serde_json::json!({"name": "Alice", "age": 30});
let pretty = Pretty::from_json(&value);
let opts = ConsoleOptions::default();
let result = pretty.render(&opts);
let ansi = result.to_ansi();
assert!(ansi.contains("Alice"));
assert!(ansi.contains("30"));
}
#[test]
fn test_pretty_repr() {
let text = pretty_repr(&42);
assert!(!text.plain.is_empty());
}
#[test]
fn test_max_depth() {
let inner = serde_json::json!({"a": {"b": {"c": 1}}});
let pretty = Pretty::from_json(&inner).max_depth(1);
let opts = ConsoleOptions::default();
let result = pretty.render(&opts);
let ansi = result.to_ansi();
assert!(ansi.contains("...") || ansi.contains("1"));
}
#[test]
fn test_json_to_node_empty_object() {
let value = serde_json::Value::Object(serde_json::Map::new());
let node = json_to_node(&value, None);
assert!(node.is_container);
assert!(node.children.is_empty());
}
#[test]
fn test_json_to_node_empty_array() {
let value = serde_json::Value::Array(Vec::new());
let node = json_to_node(&value, None);
assert!(node.is_container);
assert!(node.children.is_empty());
}
#[test]
fn test_json_to_node_scalars() {
let null_node = json_to_node(&serde_json::Value::Null, None);
assert_eq!(null_node.value.as_deref(), Some("null"));
let bool_node = json_to_node(&serde_json::Value::Bool(true), Some("flag".into()));
assert_eq!(bool_node.key.as_deref(), Some("flag"));
assert_eq!(bool_node.value.as_deref(), Some("true"));
let num_node = json_to_node(&serde_json::json!(42), None);
assert_eq!(num_node.value.as_deref(), Some("42"));
let str_node = json_to_node(&serde_json::json!("hello"), None);
assert!(str_node.value.as_deref().unwrap_or("").contains("hello"));
}
#[test]
fn test_install_is_noop() {
install();
}
#[test]
fn test_traverse() {
let node = traverse(&"test");
assert!(node.value.is_some());
}
#[test]
fn test_builder_methods() {
let node = Node::new(None, Some("value".to_string()));
let pretty = Pretty::new(node)
.indent_guides(false)
.max_depth(5)
.max_string(100)
.max_length(10)
.expand_all();
assert!(!pretty.indent_guides);
assert_eq!(pretty.max_depth, Some(5));
assert_eq!(pretty.max_string, Some(100));
assert_eq!(pretty.max_length, Some(10));
assert!(pretty.expand_all);
}
}