cargo-insert-docs 1.6.0

Inserts feature docs into crate docs, and crate docs into README.
use core::ops::Range;
use std::fmt::{self, Write as _};

use crate::{
    markdown::Tree,
    markdown_rs::event::{Event, Kind},
};

impl fmt::Debug for Tree<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use crate::tests::TreeFormatterStack;

        fn range(events: &[Event]) -> Range<usize> {
            let start = events[0].point.index;
            let mut depth = 0usize;

            for event in &events[1..] {
                match event.kind {
                    Kind::Enter => depth += 1,
                    Kind::Exit => match depth.checked_sub(1) {
                        Some(new_depth) => depth = new_depth,
                        None => return start..event.point.index,
                    },
                }
            }

            start..start
        }

        fn children(events: &[Event]) -> usize {
            let mut depth = 0usize;
            let mut count = 0usize;

            for event in events {
                match event.kind {
                    Kind::Enter => {
                        if depth == 0 {
                            count += 1;
                        }

                        depth += 1;
                    }
                    Kind::Exit => match depth.checked_sub(1) {
                        Some(new_depth) => depth = new_depth,
                        None => return count,
                    },
                }
            }

            count
        }

        let mut fmt = TreeFormatterStack::new();

        fmt.push();
        fmt.label("Document");
        fmt.child_len(children(&self.events));

        for (i, event) in self.events.iter().enumerate() {
            match event.kind {
                Kind::Enter => {
                    fmt.push();
                    let name = &event.name;
                    let range = range(&self.events[i..]);
                    let text = &self.markdown[range];
                    let link = event
                        .link
                        .as_ref()
                        .map(|link| format!(" {{ link: {link:?} }}"))
                        .unwrap_or_default();
                    fmt.label(format!("{name:?} {text:?}{link}"));
                    fmt.child_len(children(&self.events[i + 1..]));
                }
                Kind::Exit => fmt.pop(),
            }
        }

        f.write_str(&fmt.finish())
    }
}

pub struct TreeFormatterStack {
    vec: Vec<TreeFormatter>,
}

impl TreeFormatterStack {
    pub fn new() -> Self {
        Self { vec: vec![] }
    }

    pub fn push(&mut self) {
        self.vec.push(TreeFormatter::default())
    }

    pub fn pop(&mut self) {
        let str = self.vec.pop().unwrap().finish();
        assert!(!self.vec.is_empty(), "the last pop must be a finish");
        self.child(&str);
    }

    pub fn label(&mut self, label: impl fmt::Display) {
        self.vec.last_mut().unwrap().label(label);
    }

    pub fn child_len(&mut self, len: usize) {
        self.vec.last_mut().unwrap().child_len(len);
    }

    pub fn child(&mut self, str: &str) {
        self.vec.last_mut().unwrap().child(str);
    }

    pub fn finish(mut self) -> String {
        self.vec.pop().unwrap().finish()
    }
}

#[derive(Default)]
pub struct TreeFormatter {
    out: String,
    child_len: usize,
    child_i: usize,
}

impl TreeFormatter {
    pub fn label(&mut self, label: impl fmt::Display) {
        writeln!(self.out, "{label}").unwrap()
    }

    pub fn children(
        &mut self,
        iter: impl IntoIterator<Item = impl AsRef<str>, IntoIter: ExactSizeIterator>,
    ) {
        let iter = iter.into_iter();
        self.child_len(iter.len());
        iter.for_each(|child| self.child(child.as_ref()));
    }

    pub fn child_len(&mut self, len: usize) {
        self.child_len = len;
    }

    pub fn child(&mut self, string: &str) {
        let child_i = self.child_i;
        let is_last = child_i == self.child_len.wrapping_sub(1);

        for (i, line) in string.lines().enumerate() {
            #[expect(clippy::collapsible_else_if)]
            let indent = if i == 0 {
                if is_last { "└── " } else { "├── " }
            } else {
                if is_last { "    " } else { "" }
            };

            self.out.push_str(indent);
            self.out.push_str(line);
            self.out.push('\n');
        }

        self.child_i += 1;
    }

    pub fn finish(self) -> String {
        self.out
    }
}