Skip to main content

rich_ext/data/
explorer.rs

1//! A width-aware tree (or table) over a [`Node`], with folding and limits.
2
3use 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::{cell_text, TableOptions, TableView};
11use super::{quote_str, scalar_text, style, summary, Node, Path, Value, XmlKind};
12use crate::event::flatten as join_lines;
13
14// The thin guides of core's `Tree` (upstream `TREE_GUIDES[0]`).
15const SPACE: &str = "    ";
16const CONTINUE: &str = "│   ";
17const FORK: &str = "├── ";
18const END: &str = "└── ";
19
20/// How an [`Explorer`] lays the document out.
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub enum View {
23    /// A tree with guide lines.
24    #[default]
25    Tree,
26    /// A table: records (a sequence of maps) as rows, anything else as
27    /// `path | value` rows.
28    Table,
29}
30
31/// A renderable view of a document tree.
32///
33/// Scalars reuse core's JSON styles, keys `json.key`, XML attributes
34/// `data.attribute`; YAML anchors and aliases show as dim `&name` / `*name`
35/// badges and INI/dotenv comments as a dim `# comment`. Every line is cut to
36/// the available width: long strings shrink first (keeping their quotes),
37/// then the line ends in `…`, so nothing wraps.
38///
39/// ```
40/// use rich::Console;
41/// use rich_ext::data::{parse, Explorer, Format};
42///
43/// let node = parse(Format::Json, r#"{"a": {"b": 1, "c": 2}, "d": [1, 2, 3]}"#).unwrap();
44/// let explorer = Explorer::new(&node).max_depth(1).root_label("doc");
45/// let out = Console::builder().width(40).build().render_export(&explorer);
46/// assert_eq!(out, "doc\n├── a: {…} 2 keys\n└── d: […] 3 items\n");
47/// ```
48#[derive(Clone, Debug)]
49pub struct Explorer<'a> {
50    node: Cow<'a, Node>,
51    max_depth: Option<usize>,
52    max_length: Option<usize>,
53    max_string: Option<usize>,
54    show_paths: bool,
55    show_types: bool,
56    folded: HashSet<Path>,
57    highlighted: HashMap<Path, Style>,
58    root_label: Option<String>,
59    view: View,
60}
61
62enum Key<'n> {
63    Root,
64    Name(&'n str),
65    Index(usize),
66}
67
68enum Item<'n> {
69    Node {
70        key: Key<'n>,
71        node: &'n Node,
72        path: Path,
73        depth: usize,
74    },
75    More(usize),
76}
77
78/// One rendered tree line: guide prefix, label, and the label's minimum.
79struct Line {
80    prefix: String,
81    label: Text,
82    minimum: usize,
83}
84
85impl<'a> Explorer<'a> {
86    /// Explore an owned or borrowed node.
87    pub fn new(node: impl Into<Cow<'a, Node>>) -> Self {
88        Explorer {
89            node: node.into(),
90            max_depth: None,
91            max_length: None,
92            max_string: None,
93            show_paths: false,
94            show_types: false,
95            folded: HashSet::new(),
96            highlighted: HashMap::new(),
97            root_label: None,
98            view: View::Tree,
99        }
100    }
101
102    /// Fold containers this deep (the root is depth 0) to a summary like
103    /// `{…} 3 keys`.
104    pub fn max_depth(mut self, depth: usize) -> Self {
105        self.max_depth = Some(depth);
106        self
107    }
108
109    /// Show at most this many children per container, then `… N more`.
110    pub fn max_length(mut self, length: usize) -> Self {
111        self.max_length = Some(length);
112        self
113    }
114
115    /// Cut strings to this many characters (with `…`). Strings are also cut
116    /// to the available width regardless.
117    pub fn max_string(mut self, length: usize) -> Self {
118        self.max_string = Some(length);
119        self
120    }
121
122    /// Append each leaf's path, dim.
123    pub fn show_paths(mut self, show: bool) -> Self {
124        self.show_paths = show;
125        self
126    }
127
128    /// Append each node's type (`str`, `int`, `map`, …), dim.
129    pub fn show_types(mut self, show: bool) -> Self {
130        self.show_types = show;
131        self
132    }
133
134    /// Fold the container at `path`.
135    pub fn fold(mut self, path: Path) -> Self {
136        self.folded.insert(path);
137        self
138    }
139
140    /// Style the tree line of the node at `path`, such as the selection of a
141    /// `data::transform::Highlight`. The table view ignores it.
142    pub fn highlight(mut self, path: Path, style: Style) -> Self {
143        self.highlighted.insert(path, style);
144        self
145    }
146
147    /// The root line's label (default: the root's summary).
148    pub fn root_label(mut self, label: impl Into<String>) -> Self {
149        self.root_label = Some(label.into());
150        self
151    }
152
153    /// Tree or table.
154    pub fn view(mut self, view: View) -> Self {
155        self.view = view;
156        self
157    }
158
159    /// The node being explored.
160    pub fn node(&self) -> &Node {
161        &self.node
162    }
163
164    fn is_folded(&self, path: &Path, depth: usize) -> bool {
165        self.max_depth.is_some_and(|max| depth >= max) || self.folded.contains(path)
166    }
167
168    fn expanded(&self, node: &Node, path: &Path, depth: usize) -> bool {
169        node.is_container() && !node.is_empty() && !self.is_folded(path, depth)
170    }
171
172    /// The label of one tree line, cut to `available` cells. Returns the
173    /// label and its minimum useful width (its head plus an ellipsis).
174    fn label(
175        &self,
176        console: &Console,
177        key: &Key<'_>,
178        node: &Node,
179        path: &Path,
180        expanded: bool,
181        available: usize,
182    ) -> (Text, usize) {
183        let mut head = Text::new("");
184        match key {
185            Key::Root => {
186                if let Some(label) = &self.root_label {
187                    head.append(&super::escape_controls(label), None);
188                }
189            }
190            Key::Name(name) => {
191                let key_style = match node.meta.xml {
192                    Some(XmlKind::Attribute) => "data.attribute",
193                    Some(XmlKind::Text) => "data.comment",
194                    _ => "json.key",
195                };
196                head.append(
197                    &super::escape_controls(name),
198                    Some(style(console, key_style).into()),
199                );
200            }
201            Key::Index(index) => {
202                head.append(
203                    &format!("[{index}]"),
204                    Some(style(console, "data.index").into()),
205                );
206            }
207        }
208
209        // The value: a scalar, or a summary for a folded, empty or unlabelled
210        // root container.
211        let root_summary = matches!(key, Key::Root) && self.root_label.is_none();
212        let value: Option<(String, &'static str, bool)> = if node.is_container() {
213            (!expanded || root_summary).then(|| (summary(node), "data.summary", false))
214        } else {
215            let is_string = matches!(node.value, Value::String(_));
216            let text = match &node.value {
217                Value::String(s) => fit_quoted(s, self.max_string, None),
218                other => scalar_text(other, true).0,
219            };
220            Some((text, scalar_text(&node.value, true).1, is_string))
221        };
222        let separator = if value.is_some() && !head.is_empty() {
223            ": "
224        } else {
225            ""
226        };
227
228        let mut badges = Text::new("");
229        if let Some(anchor) = &node.meta.anchor {
230            badges.append(
231                &format!(" &{}", super::escape_controls(anchor)),
232                Some(style(console, "data.anchor").into()),
233            );
234        }
235        if let Some(alias) = &node.meta.alias {
236            badges.append(
237                &format!(" *{}", super::escape_controls(alias)),
238                Some(style(console, "data.alias").into()),
239            );
240        }
241        if self.show_types {
242            badges.append(
243                &format!(" ({})", node.type_name()),
244                Some(style(console, "data.type").into()),
245            );
246        }
247
248        let mut extras = Text::new("");
249        if let Some(comment) = &node.meta.comment {
250            let comment = super::escape_controls(&comment.replace('\n', " "));
251            extras.append(
252                &format!("  # {comment}"),
253                Some(style(console, "data.comment").into()),
254            );
255        }
256        if self.show_paths && !expanded && !path.is_root() {
257            extras.append(
258                &format!("  {path}"),
259                Some(style(console, "data.path").into()),
260            );
261        }
262
263        let fixed = head.cell_len() + cell_len(separator) + badges.cell_len();
264        let minimum = head.cell_len() + 1;
265        let mut text = head;
266        text.append(separator, None);
267        if let Some((mut value, value_style, is_string)) = value {
268            if is_string && fixed + cell_len(&value) > available {
269                if let (Some(budget), Value::String(s)) =
270                    (available.checked_sub(fixed), &node.value)
271                {
272                    value = fit_quoted(s, self.max_string, Some(budget));
273                }
274            }
275            text.append(&value, Some(style(console, value_style).into()));
276        }
277        let text = text.append_text(&badges).append_text(&extras);
278        let mut text = text;
279        text.truncate(available, Some(Overflow::Ellipsis), false);
280        if let Some(style) = self.highlighted.get(path) {
281            let end = text.plain().len();
282            text.stylize(style.clone(), 0, end);
283        }
284        (text, minimum)
285    }
286
287    fn tree_lines(&self, console: &Console, width: usize) -> Vec<Line> {
288        let mut lines = Vec::new();
289        let mut stack: Vec<(Item<'_>, String, String)> = vec![(
290            Item::Node {
291                key: Key::Root,
292                node: &self.node,
293                path: Path::root(),
294                depth: 0,
295            },
296            String::new(),
297            String::new(),
298        )];
299        while let Some((item, first, rest)) = stack.pop() {
300            let available = width.saturating_sub(cell_len(&first));
301            let (key, node, path, depth) = match item {
302                Item::More(count) => {
303                    let mut label =
304                        Text::styled(format!("… {count} more"), style(console, "data.summary"));
305                    label.truncate(available, Some(Overflow::Ellipsis), false);
306                    lines.push(Line {
307                        prefix: first,
308                        label,
309                        minimum: 1,
310                    });
311                    continue;
312                }
313                Item::Node {
314                    key,
315                    node,
316                    path,
317                    depth,
318                } => (key, node, path, depth),
319            };
320            let expanded = self.expanded(node, &path, depth);
321            let (label, minimum) = self.label(console, &key, node, &path, expanded, available);
322            lines.push(Line {
323                prefix: first,
324                label,
325                minimum,
326            });
327            if !expanded {
328                continue;
329            }
330            let limit = self.max_length.unwrap_or(usize::MAX);
331            let mut children: Vec<Item<'_>> = match &node.value {
332                Value::Seq(items) => items
333                    .iter()
334                    .enumerate()
335                    .take(limit)
336                    .map(|(i, child)| Item::Node {
337                        key: Key::Index(i),
338                        node: child,
339                        path: path.child_index(i),
340                        depth: depth + 1,
341                    })
342                    .collect(),
343                Value::Map(entries) => entries
344                    .iter()
345                    .take(limit)
346                    .map(|(k, child)| Item::Node {
347                        key: Key::Name(k),
348                        node: child,
349                        path: path.child_key(k),
350                        depth: depth + 1,
351                    })
352                    .collect(),
353                _ => Vec::new(),
354            };
355            if node.len() > limit {
356                children.push(Item::More(node.len() - limit));
357            }
358            let last = children.len().saturating_sub(1);
359            for (index, child) in children.into_iter().enumerate().rev() {
360                let (fork, space) = if index == last {
361                    (END, SPACE)
362                } else {
363                    (FORK, CONTINUE)
364                };
365                stack.push((child, format!("{rest}{fork}"), format!("{rest}{space}")));
366            }
367        }
368        lines
369    }
370
371    fn table_view(&self, console: &Console) -> Table {
372        let records = matches!(&self.node.value, Value::Seq(items)
373            if !items.is_empty() && items.iter().all(|i| matches!(i.value, Value::Map(_))));
374        if records {
375            let options = TableOptions {
376                max_rows: self.max_length,
377                max_string: self.max_string,
378                ..TableOptions::default()
379            };
380            return TableView::new(&*self.node)
381                .options(options)
382                .to_table(console);
383        }
384        let mut table = Table::new();
385        table.add_column_text(Text::new("path"), Justify::Left);
386        table.add_column_text(Text::new("value"), Justify::Left);
387        if self.show_types {
388            table.add_column_text(Text::new("type"), Justify::Left);
389        }
390        let dim = |text: String| Text::styled(text, style(console, "data.path"));
391        let mut stack: Vec<(&Node, Path, usize)> = vec![(&self.node, Path::root(), 0)];
392        while let Some((node, path, depth)) = stack.pop() {
393            if depth != usize::MAX && self.expanded(node, &path, depth) {
394                let limit = self.max_length.unwrap_or(usize::MAX);
395                if node.len() > limit {
396                    // Pushed first, so it pops after the shown children.
397                    stack.push((&self.node, path.clone(), usize::MAX));
398                }
399                match &node.value {
400                    Value::Seq(items) => {
401                        for (i, item) in items.iter().enumerate().take(limit).rev() {
402                            stack.push((item, path.child_index(i), depth + 1));
403                        }
404                    }
405                    Value::Map(entries) => {
406                        for (k, v) in entries.iter().take(limit).rev() {
407                            stack.push((v, path.child_key(k), depth + 1));
408                        }
409                    }
410                    _ => {}
411                }
412                continue;
413            }
414            let shown_path = if path.is_root() {
415                "(root)".to_string()
416            } else {
417                path.to_string()
418            };
419            let mut row = vec![Text::new(super::escape_controls(&shown_path))];
420            if depth == usize::MAX {
421                // The `… N more` marker for `path`'s container.
422                let container = self.node.at(&path).map_or(0, Node::len);
423                let limit = self.max_length.unwrap_or(usize::MAX);
424                row.push(dim(format!("… {} more", container.saturating_sub(limit))));
425            } else if node.is_container() {
426                row.push(Text::styled(summary(node), style(console, "data.summary")));
427            } else {
428                row.push(cell_text(console, node, self.max_string));
429            }
430            if self.show_types {
431                row.push(dim(if depth == usize::MAX {
432                    String::new()
433                } else {
434                    node.type_name().to_string()
435                }));
436            }
437            table.add_row_text(row);
438        }
439        table
440    }
441}
442
443/// `raw` as a quoted string, cut to `max_chars` characters and then to
444/// `budget` cells, ending `…"` when cut.
445pub(crate) fn fit_quoted(raw: &str, max_chars: Option<usize>, budget: Option<usize>) -> String {
446    let (content, cut) = match max_chars.and_then(|n| raw.char_indices().nth(n)) {
447        Some((i, _)) => (&raw[..i], true),
448        None => (raw, false),
449    };
450    let quoted = quote_str(content);
451    let escaped = &quoted[1..quoted.len() - 1];
452    let whole = cell_len(escaped) + 2 + usize::from(cut);
453    match budget {
454        Some(budget) if whole > budget => {
455            let allowed = budget.saturating_sub(3);
456            let mut kept = String::new();
457            let mut used = 0;
458            for c in escaped.chars() {
459                let w = char_cell_width(c);
460                if used + w > allowed {
461                    break;
462                }
463                used += w;
464                kept.push(c);
465            }
466            // Never leave half an escape sequence.
467            let trailing = kept.chars().rev().take_while(|c| *c == '\\').count();
468            if trailing % 2 == 1 {
469                kept.pop();
470            }
471            format!("\"{kept}…\"")
472        }
473        _ if cut => format!("\"{escaped}…\""),
474        _ => quoted,
475    }
476}
477
478impl Renderable for Explorer<'_> {
479    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
480        if options.max_width == 0 {
481            return Vec::new();
482        }
483        if self.view == View::Table {
484            return self.table_view(console).rich_render(console, options);
485        }
486        let guide = style(console, "tree.line");
487        let rows = self
488            .tree_lines(console, options.max_width)
489            .into_iter()
490            .map(|line| {
491                let mut row = Vec::new();
492                if !line.prefix.is_empty() {
493                    row.push(Segment::new(line.prefix, Some(guide.clone())));
494                }
495                row.extend(line.label.render(console.theme(), &Style::new()));
496                row
497            })
498            .collect();
499        join_lines(rows)
500    }
501
502    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
503        if self.view == View::Table {
504            return Measurement::new(options.max_width, options.max_width);
505        }
506        let lines = self.tree_lines(console, usize::MAX / 2);
507        let (mut minimum, mut maximum) = (0, 0);
508        for line in &lines {
509            let prefix = cell_len(&line.prefix);
510            minimum = minimum.max(prefix + line.minimum);
511            maximum = maximum.max(prefix + line.label.cell_len());
512        }
513        Measurement::new(minimum.min(maximum), maximum).with_maximum(options.max_width)
514    }
515}