html-outliner 0.1.6

Outline HTML documents for better SEO.
Documentation
use std::fmt::{self, Display, Formatter, Write};

use crate::OutlineStructure;

const EXTRA_INDENT_WIDTH: usize = 1;

#[derive(Debug, Clone, Default)]
pub struct Outline {
    pub text:         Option<String>,
    pub sub_outlines: Vec<Outline>,
}

impl Outline {
    #[inline]
    pub fn parse_html<S: AsRef<str>>(html: S, max_depth: usize) -> Outline {
        OutlineStructure::parse_html(html, max_depth).into()
    }
}

impl From<OutlineStructure> for Outline {
    #[inline]
    fn from(os: OutlineStructure) -> Self {
        if os.sectioning_type.is_heading() {
            Outline {
                text:         os.heading.map(|heading| heading.into()),
                sub_outlines: Vec::new(),
            }
        } else {
            let mut sub_outlines = Vec::new();

            let mut stack = vec![];
            let mut levels: Vec<u8> = vec![];

            for sub_os in os.sub_outline_structures.into_iter().rev() {
                if sub_os.sectioning_type.is_heading() {
                    let heading = sub_os.heading.unwrap();
                    let heading_level = heading.get_start_level();

                    let mut outline = Outline {
                        text:         Some(heading.into()),
                        sub_outlines: Vec::new(),
                    };

                    while let Some(level) = levels.pop() {
                        if level > heading_level {
                            outline.sub_outlines.push(stack.pop().unwrap());
                        } else {
                            levels.push(level);

                            break;
                        }
                    }

                    levels.push(heading_level);
                    stack.push(outline);
                } else {
                    stack.push(sub_os.into());
                }
            }

            let text = if let Some(heading) = os.heading {
                let heading_level = heading.get_start_level();

                let need_flatten = {
                    let mut b = false;

                    for level in levels.iter().copied().rev() {
                        if level >= heading_level {
                            b = true;
                        }
                    }

                    b
                };

                if need_flatten {
                    let mut outline = Outline {
                        text:         Some(heading.into()),
                        sub_outlines: Vec::new(),
                    };

                    while let Some(level) = levels.pop() {
                        if level > heading_level {
                            outline.sub_outlines.push(stack.pop().unwrap());
                        } else {
                            levels.push(level);

                            break;
                        }
                    }

                    levels.push(heading_level);
                    stack.push(outline);

                    None
                } else {
                    Some(heading.into())
                }
            } else if os.sectioning_type.is_sectioning_content_type() {
                Some(format!("Untitled {}", os.sectioning_type.as_str()))
            } else {
                None
            };

            while let Some(sub_os) = stack.pop() {
                sub_outlines.push(sub_os);
            }

            Outline {
                text,
                sub_outlines,
            }
        }
    }
}

impl Display for Outline {
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
        format(f, self, 1, 0)
    }
}

fn format(
    f: &mut Formatter<'_>,
    outline: &Outline,
    number: usize,
    indent: usize,
) -> Result<(), fmt::Error> {
    let new_ident = if let Some(text) = outline.text.as_ref() {
        if indent > 0 {
            f.write_char('\n')?;

            for _ in 0..indent {
                f.write_char(' ')?;
            }
        } else if number > 1 {
            f.write_char('\n')?;
        }

        f.write_fmt(format_args!("{}. ", number))?;
        f.write_str(text.as_str())?;

        indent + count_digit(outline.sub_outlines.len()) + 2 + EXTRA_INDENT_WIDTH
    } else {
        indent
    };

    for (i, sub_outline) in outline.sub_outlines.iter().enumerate() {
        format(f, sub_outline, i + 1, new_ident)?;
    }

    Ok(())
}

#[inline]
fn count_digit(n: usize) -> usize {
    (n as f64).log10().floor() as usize + 1
}