indented-blocks 0.6.0

parser for indented blocks
Documentation
#![forbid(unsafe_code, unconditional_recursion)]
#![no_std]
#![recursion_limit = "8"]

extern crate alloc;
extern crate core;

use alloc::{borrow, string::String, vec, vec::Vec};

#[cfg(test)]
mod tests;

pub mod indention;
use indention::{Indented, IntoUnindented};

pub mod view;
pub use view::View;

#[derive(Clone, Debug)]
#[cfg_attr(any(feature = "extra-traits", test), derive(PartialEq))]
pub struct Block<S> {
    pub head: S,
    pub subs: Vec<Block<S>>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "extra-traits", derive(PartialOrd, Ord, Hash))]
pub enum EmptyLinesMode {
    Skip,
    HonorIndent,
    PrevIndent,
}

impl<A> Block<A> {
    pub fn map_ref<B, F>(&self, mut f: F) -> Block<B>
    where
        F: FnMut(&A) -> B,
    {
        fn intern_<A, B, F: FnMut(&A) -> B>(this: &Block<A>, f: &mut F) -> Block<B> {
            let head = f(&this.head);
            Block {
                head,
                subs: this.subs.iter().map(move |i| intern_(i, f)).collect(),
            }
        }
        intern_(self, &mut f)
    }

    pub fn map<B, F>(self, mut f: F) -> Block<B>
    where
        F: FnMut(A) -> B,
    {
        fn intern_<A, B, F: FnMut(A) -> B>(this: Block<A>, f: &mut F) -> Block<B> {
            let Block { head, subs } = this;
            let head = f(head);
            Block {
                head,
                subs: subs.into_iter().map(move |i| intern_(i, f)).collect(),
            }
        }
        intern_(self, &mut f)
    }
}

impl<'a, T: borrow::ToOwned> Block<&'a T> {
    #[inline]
    pub fn to_owned(&self) -> Block<T::Owned> {
        self.map_ref(|&i| i.to_owned())
    }
}

/// This function is a simple wrapper around [`parse_nested_blocks_from_lines`].
pub fn parse_nested_blocks(
    s: &str,
    empty_lines_mode: EmptyLinesMode,
) -> Vec<Block<Indented<&str>>> {
    parse_nested_blocks_from_lines(s.lines(), empty_lines_mode)
}

/// This function does the same as [`parse_nested_blocks`], but expects
/// an [`Iterator`] over lines as an argument, which allows pre-filtering
/// of the passed lines without additional allocations
/// (e.g. to filter comments and such)
///
/// It returns all parsed blocks, with the lines split into indention
/// and main text. (see also: the [`Indention`] type)
pub fn parse_nested_blocks_from_lines<'a, LI>(
    lines: LI,
    empty_lines_mode: EmptyLinesMode,
) -> Vec<Block<Indented<&'a str>>>
where
    LI: Iterator<Item = &'a str>,
{
    let mut base = vec![];
    let mut stack: Vec<Block<Indented<&str>>> = vec![];

    let mut lit = lines.map(|i| Block {
        head: Indented::new(i),
        subs: Vec::new(),
    });

    loop {
        let i = lit.next();

        let i_indent = if let Some(j) = &i {
            match empty_lines_mode {
                _ if !j.head.data.is_empty() => Some(j),
                EmptyLinesMode::HonorIndent => Some(j),
                EmptyLinesMode::PrevIndent => stack.last(),
                EmptyLinesMode::Skip => continue,
            }
            .map(|x| x.head.indent)
        } else {
            None
        }
        .unwrap_or("");

        // reduce scope if necessary
        while let Some(old_top) = stack.pop() {
            let old_top_indent = old_top.head.indent;
            if i_indent.starts_with(old_top_indent) && i_indent.len() != old_top_indent.len() {
                stack.push(old_top);
                break;
            } else {
                // merge block with parent
                stack
                    .last_mut()
                    .map(|top2| &mut top2.subs)
                    .unwrap_or(&mut base)
                    .push(old_top);
            }
        }

        assert!(i_indent.starts_with(stack.last().map(|top| top.head.indent).unwrap_or("")));
        match i {
            Some(j) => stack.push(j),
            None => break,
        }
    }

    assert!(stack.is_empty());
    base
}

pub fn blocks_to_string<'ast, BIter, B, S, F>(blks: BIter, single_indent: &str) -> String
where
    BIter: core::iter::IntoIterator<Item = B>,
    B: IntoUnindented<Output = view::View<'ast, S, F>> + 'ast,
    S: 'ast,
    F: Clone + view::ViewFn<&'ast S>,
    F::Output: alloc::string::ToString,
{
    blks.into_iter()
        .map(IntoUnindented::into_unindented)
        .fold(String::new(), |mut acc, x| {
            x.append_to_string(&mut acc, single_indent, 0);
            acc
        })
}