1use std::borrow::Cow;
4use std::collections::{HashMap, HashSet};
5
6use rich::cells::{cell_len, char_cell_width};
7use rich::measure::Measurement;
8use rich::{Console, ConsoleOptions, Justify, Overflow, Renderable, Segment, Style, Table, Text};
9
10use super::table::{TableOptions, TableView, cell_text};
11use super::{Node, Path, Value, XmlKind, quote_str, scalar_text, style, summary};
12use crate::event::flatten as join_lines;
13use crate::table::StyleRules;
14
15const SPACE: &str = " ";
17const CONTINUE: &str = "│ ";
18const FORK: &str = "├── ";
19const END: &str = "└── ";
20
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum View {
24 #[default]
26 Tree,
27 Table,
30}
31
32#[derive(Clone, Debug)]
50#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
51pub struct Explorer<'a> {
52 node: Cow<'a, Node>,
53 max_depth: Option<usize>,
54 max_length: Option<usize>,
55 max_string: Option<usize>,
56 show_paths: bool,
57 show_types: bool,
58 folded: HashSet<Path>,
59 highlighted: HashMap<Path, Style>,
60 root_label: Option<String>,
61 view: View,
62 rules: StyleRules,
63}
64
65enum Key<'n> {
66 Root,
67 Name(&'n str),
68 Index(usize),
69}
70
71enum Item<'n> {
72 Node {
73 key: Key<'n>,
74 node: &'n Node,
75 path: Path,
76 depth: usize,
77 },
78 More(usize),
79}
80
81struct Line {
83 prefix: String,
84 label: Text,
85 minimum: usize,
86}
87
88impl<'a> Explorer<'a> {
89 pub fn new(node: impl Into<Cow<'a, Node>>) -> Self {
91 Explorer {
92 node: node.into(),
93 max_depth: None,
94 max_length: None,
95 max_string: None,
96 show_paths: false,
97 show_types: false,
98 folded: HashSet::new(),
99 highlighted: HashMap::new(),
100 root_label: None,
101 view: View::Tree,
102 rules: StyleRules::new(),
103 }
104 }
105
106 pub fn max_depth(mut self, depth: usize) -> Self {
109 self.max_depth = Some(depth);
110 self
111 }
112
113 pub fn max_length(mut self, length: usize) -> Self {
115 self.max_length = Some(length);
116 self
117 }
118
119 pub fn max_string(mut self, length: usize) -> Self {
122 self.max_string = Some(length);
123 self
124 }
125
126 pub fn show_paths(mut self, show: bool) -> Self {
128 self.show_paths = show;
129 self
130 }
131
132 pub fn show_types(mut self, show: bool) -> Self {
134 self.show_types = show;
135 self
136 }
137
138 pub fn fold(mut self, path: Path) -> Self {
140 self.folded.insert(path);
141 self
142 }
143
144 pub fn highlight(mut self, path: Path, style: Style) -> Self {
147 self.highlighted.insert(path, style);
148 self
149 }
150
151 pub fn root_label(mut self, label: impl Into<String>) -> Self {
153 self.root_label = Some(label.into());
154 self
155 }
156
157 pub fn view(mut self, view: View) -> Self {
159 self.view = view;
160 self
161 }
162
163 pub fn style_rules(mut self, rules: StyleRules) -> Self {
166 self.rules = rules;
167 self
168 }
169
170 pub fn node(&self) -> &Node {
172 &self.node
173 }
174
175 fn is_folded(&self, path: &Path, depth: usize) -> bool {
176 self.max_depth.is_some_and(|max| depth >= max) || self.folded.contains(path)
177 }
178
179 fn expanded(&self, node: &Node, path: &Path, depth: usize) -> bool {
180 node.is_container() && !node.is_empty() && !self.is_folded(path, depth)
181 }
182
183 fn label(
186 &self,
187 console: &Console,
188 key: &Key<'_>,
189 node: &Node,
190 path: &Path,
191 expanded: bool,
192 available: usize,
193 ) -> (Text, usize) {
194 let mut head = Text::new("");
195 match key {
196 Key::Root => {
197 if let Some(label) = &self.root_label {
198 head.append(&super::escape_controls(label), None);
199 }
200 }
201 Key::Name(name) => {
202 let key_style = match node.meta.xml {
203 Some(XmlKind::Attribute) => "data.attribute",
204 Some(XmlKind::Text) => "data.comment",
205 _ => "json.key",
206 };
207 head.append(
208 &super::escape_controls(name),
209 Some(style(console, key_style).into()),
210 );
211 }
212 Key::Index(index) => {
213 head.append(
214 &format!("[{index}]"),
215 Some(style(console, "data.index").into()),
216 );
217 }
218 }
219
220 let root_summary = matches!(key, Key::Root) && self.root_label.is_none();
223 let value: Option<(String, &'static str, bool)> = if node.is_container() {
224 (!expanded || root_summary).then(|| (summary(node), "data.summary", false))
225 } else {
226 let is_string = matches!(node.value, Value::String(_));
227 let text = match &node.value {
228 Value::String(s) => fit_quoted(s, self.max_string, None),
229 other => scalar_text(other, true).0,
230 };
231 Some((text, scalar_text(&node.value, true).1, is_string))
232 };
233 let separator = if value.is_some() && !head.is_empty() {
234 ": "
235 } else {
236 ""
237 };
238
239 let mut badges = Text::new("");
240 if let Some(anchor) = &node.meta.anchor {
241 badges.append(
242 &format!(" &{}", super::escape_controls(anchor)),
243 Some(style(console, "data.anchor").into()),
244 );
245 }
246 if let Some(alias) = &node.meta.alias {
247 badges.append(
248 &format!(" *{}", super::escape_controls(alias)),
249 Some(style(console, "data.alias").into()),
250 );
251 }
252 if self.show_types {
253 badges.append(
254 &format!(" ({})", node.type_name()),
255 Some(style(console, "data.type").into()),
256 );
257 }
258
259 let mut extras = Text::new("");
260 if let Some(comment) = &node.meta.comment {
261 let comment = super::escape_controls(&comment.replace('\n', " "));
262 extras.append(
263 &format!(" # {comment}"),
264 Some(style(console, "data.comment").into()),
265 );
266 }
267 if self.show_paths && !expanded && !path.is_root() {
268 extras.append(
269 &format!(" {path}"),
270 Some(style(console, "data.path").into()),
271 );
272 }
273
274 let fixed = head.cell_len() + cell_len(separator) + badges.cell_len();
275 let minimum = head.cell_len() + 1;
276 let mut text = head;
277 text.append(separator, None);
278 if let Some((mut value, value_style, is_string)) = value {
279 if is_string
280 && fixed + cell_len(&value) > available
281 && let (Some(budget), Value::String(s)) =
282 (available.checked_sub(fixed), &node.value)
283 {
284 value = fit_quoted(s, self.max_string, Some(budget));
285 }
286 text.append(&value, Some(style(console, value_style).into()));
287 }
288 let text = text.append_text(&badges).append_text(&extras);
289 let mut text = text;
290 text.truncate(available, Some(Overflow::Ellipsis), false);
291 if let Some(style) = self.highlighted.get(path) {
292 let end = text.plain().len();
293 text.stylize(style.clone(), 0, end);
294 }
295 (text, minimum)
296 }
297
298 fn tree_lines(&self, console: &Console, width: usize) -> Vec<Line> {
299 let mut lines = Vec::new();
300 let mut stack: Vec<(Item<'_>, String, String)> = vec![(
301 Item::Node {
302 key: Key::Root,
303 node: &self.node,
304 path: Path::root(),
305 depth: 0,
306 },
307 String::new(),
308 String::new(),
309 )];
310 while let Some((item, first, rest)) = stack.pop() {
311 let available = width.saturating_sub(cell_len(&first));
312 let (key, node, path, depth) = match item {
313 Item::More(count) => {
314 let mut label =
315 Text::styled(format!("… {count} more"), style(console, "data.summary"));
316 label.truncate(available, Some(Overflow::Ellipsis), false);
317 lines.push(Line {
318 prefix: first,
319 label,
320 minimum: 1,
321 });
322 continue;
323 }
324 Item::Node {
325 key,
326 node,
327 path,
328 depth,
329 } => (key, node, path, depth),
330 };
331 let expanded = self.expanded(node, &path, depth);
332 let (label, minimum) = self.label(console, &key, node, &path, expanded, available);
333 lines.push(Line {
334 prefix: first,
335 label,
336 minimum,
337 });
338 if !expanded {
339 continue;
340 }
341 let limit = self.max_length.unwrap_or(usize::MAX);
342 let mut children: Vec<Item<'_>> = match &node.value {
343 Value::Seq(items) => items
344 .iter()
345 .enumerate()
346 .take(limit)
347 .map(|(i, child)| Item::Node {
348 key: Key::Index(i),
349 node: child,
350 path: path.child_index(i),
351 depth: depth + 1,
352 })
353 .collect(),
354 Value::Map(entries) => entries
355 .iter()
356 .take(limit)
357 .map(|(k, child)| Item::Node {
358 key: Key::Name(k),
359 node: child,
360 path: path.child_key(k),
361 depth: depth + 1,
362 })
363 .collect(),
364 _ => Vec::new(),
365 };
366 if node.len() > limit {
367 children.push(Item::More(node.len() - limit));
368 }
369 let last = children.len().saturating_sub(1);
370 for (index, child) in children.into_iter().enumerate().rev() {
371 let (fork, space) = if index == last {
372 (END, SPACE)
373 } else {
374 (FORK, CONTINUE)
375 };
376 stack.push((child, format!("{rest}{fork}"), format!("{rest}{space}")));
377 }
378 }
379 lines
380 }
381
382 fn table_view(&self, console: &Console) -> Table {
383 let records = matches!(&self.node.value, Value::Seq(items)
384 if !items.is_empty() && items.iter().all(|i| matches!(i.value, Value::Map(_))));
385 if records {
386 let options = TableOptions {
387 max_rows: self.max_length,
388 max_string: self.max_string,
389 ..TableOptions::default()
390 };
391 return TableView::new(&*self.node)
392 .options(options)
393 .style_rules(self.rules.clone())
394 .to_table(console);
395 }
396 let mut table = Table::new();
397 table.add_column_text(Text::new("path"), Justify::Left);
398 table.add_column_text(Text::new("value"), Justify::Left);
399 if self.show_types {
400 table.add_column_text(Text::new("type"), Justify::Left);
401 }
402 let dim = |text: String| Text::styled(text, style(console, "data.path"));
403 let mut stack: Vec<(&Node, Path, usize)> = vec![(&self.node, Path::root(), 0)];
404 while let Some((node, path, depth)) = stack.pop() {
405 if depth != usize::MAX && self.expanded(node, &path, depth) {
406 let limit = self.max_length.unwrap_or(usize::MAX);
407 if node.len() > limit {
408 stack.push((&self.node, path.clone(), usize::MAX));
410 }
411 match &node.value {
412 Value::Seq(items) => {
413 for (i, item) in items.iter().enumerate().take(limit).rev() {
414 stack.push((item, path.child_index(i), depth + 1));
415 }
416 }
417 Value::Map(entries) => {
418 for (k, v) in entries.iter().take(limit).rev() {
419 stack.push((v, path.child_key(k), depth + 1));
420 }
421 }
422 _ => {}
423 }
424 continue;
425 }
426 let shown_path = if path.is_root() {
427 "(root)".to_string()
428 } else {
429 path.to_string()
430 };
431 let mut row = vec![Text::new(super::escape_controls(&shown_path))];
432 if depth == usize::MAX {
433 let container = self.node.at(&path).map_or(0, Node::len);
435 let limit = self.max_length.unwrap_or(usize::MAX);
436 row.push(dim(format!("… {} more", container.saturating_sub(limit))));
437 } else if node.is_container() {
438 row.push(Text::styled(summary(node), style(console, "data.summary")));
439 } else {
440 row.push(cell_text(console, node, self.max_string));
441 }
442 if self.show_types {
443 row.push(dim(if depth == usize::MAX {
444 String::new()
445 } else {
446 node.type_name().to_string()
447 }));
448 }
449 table.add_row_text(row);
450 }
451 table
452 }
453}
454
455pub(crate) fn fit_quoted(raw: &str, max_chars: Option<usize>, budget: Option<usize>) -> String {
458 let (content, cut) = match max_chars.and_then(|n| raw.char_indices().nth(n)) {
459 Some((i, _)) => (&raw[..i], true),
460 None => (raw, false),
461 };
462 let quoted = quote_str(content);
463 let escaped = "ed[1..quoted.len() - 1];
464 let whole = cell_len(escaped) + 2 + usize::from(cut);
465 match budget {
466 Some(budget) if whole > budget => {
467 let allowed = budget.saturating_sub(3);
468 let mut kept = String::new();
469 let mut used = 0;
470 for c in escaped.chars() {
471 let w = char_cell_width(c);
472 if used + w > allowed {
473 break;
474 }
475 used += w;
476 kept.push(c);
477 }
478 let trailing = kept.chars().rev().take_while(|c| *c == '\\').count();
480 if trailing % 2 == 1 {
481 kept.pop();
482 }
483 format!("\"{kept}…\"")
484 }
485 _ if cut => format!("\"{escaped}…\""),
486 _ => quoted,
487 }
488}
489
490impl Renderable for Explorer<'_> {
491 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
492 if options.max_width == 0 {
493 return Vec::new();
494 }
495 if self.view == View::Table {
496 return self.table_view(console).rich_render(console, options);
497 }
498 let guide = style(console, "tree.line");
499 let rows = self
500 .tree_lines(console, options.max_width)
501 .into_iter()
502 .map(|line| {
503 let mut row = Vec::new();
504 if !line.prefix.is_empty() {
505 row.push(Segment::new(line.prefix, Some(guide.clone())));
506 }
507 row.extend(line.label.render(console.theme(), &Style::new()));
508 row
509 })
510 .collect();
511 join_lines(rows)
512 }
513
514 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
515 if self.view == View::Table {
516 return Measurement::new(options.max_width, options.max_width);
517 }
518 let lines = self.tree_lines(console, usize::MAX / 2);
519 let (mut minimum, mut maximum) = (0, 0);
520 for line in &lines {
521 let prefix = cell_len(&line.prefix);
522 minimum = minimum.max(prefix + line.minimum);
523 maximum = maximum.max(prefix + line.label.cell_len());
524 }
525 Measurement::new(minimum.min(maximum), maximum).with_maximum(options.max_width)
526 }
527}