dmos 0.7.0

Djot HTML renderer with advanced features
Documentation
use std::cmp::Ordering;

use jotdown::{Container, Event, Render};

use crate::TocOpts;

/// Implementation of [`jotdown::Render`] that renders a document's table of contents as HTML.
#[derive(Default)]
pub struct TocRenderer {
    opts: TocOpts,

    current_level: u16,
    nesting: u16,
    in_heading: bool,
    title_skipped: bool,
}

impl TocRenderer {
    /// Create a new renderer with the given options.
    ///
    /// See the [`TocOpts`] for a detailed description of the available
    /// settings.
    pub fn new(opts: TocOpts) -> Self {
        TocRenderer {
            opts,
            current_level: 0,
            nesting: 0,
            in_heading: false,
            title_skipped: false,
        }
    }
}

impl<'s> Render<'s> for TocRenderer {
    fn push_event<W>(&mut self, event: Event<'_>, out: W) -> std::fmt::Result
    where
        W: std::fmt::Write,
    {
        self.render_event(&event, out)
    }
}

impl TocRenderer {
    fn render_event<W>(&mut self, e: &Event<'_>, mut out: W) -> std::fmt::Result
    where
        W: std::fmt::Write,
    {
        match e {
            Event::Start(Container::Document, _) => self.render_prologue(out)?,
            Event::End(Container::Document) => self.render_epilogue(out)?,
            Event::Start(
                Container::Heading {
                    level,
                    has_section: _,
                    id,
                },
                _attrs,
            ) => {
                if !self.opts.skip_title || self.title_skipped {
                    self.in_heading = true;
                    match self.current_level.cmp(level) {
                        Ordering::Less => {
                            self.current_level = *level;
                            self.nesting += 1;
                            out.write_str(if self.nesting > 1 {
                                "\n<ul>\n"
                            } else {
                                "<ol>\n"
                            })?;
                        }
                        Ordering::Greater => {
                            while self.current_level > *level {
                                if self.nesting > 1 {
                                    self.nesting -= 1;
                                    out.write_str("</li>\n</ul>\n</li>\n")?;
                                }
                                self.current_level -= 1;
                            }
                        }
                        Ordering::Equal => {
                            out.write_str("</li>\n")?;
                        }
                    }

                    out.write_str("<li><a href=\"#")?;
                    out.write_str(id)?;
                    out.write_str("\">")?;
                }
            }
            Event::End(Container::Heading {
                level,
                has_section: _,
                id: _,
            }) => {
                if !self.opts.skip_title || self.in_heading {
                    out.write_str("</a>")?;
                }
                self.in_heading = false;
                if *level == 1 && self.opts.skip_title {
                    self.title_skipped = true;
                }
            }
            Event::Str(s) => {
                if self.in_heading {
                    out.write_str(s)?;
                }
            }
            _ => (),
        }
        Ok(())
    }

    fn render_prologue<W>(&mut self, mut out: W) -> std::fmt::Result
    where
        W: std::fmt::Write,
    {
        out.write_str(&self.opts.prologue)?;
        out.write_str("\n")
    }

    fn render_epilogue<W>(&mut self, mut out: W) -> std::fmt::Result
    where
        W: std::fmt::Write,
    {
        while self.nesting > 1 {
            out.write_str("\n</ul>\n")?;
            self.nesting -= 1;
        }
        if self.nesting > 0 {
            out.write_str("</li>\n</ol>\n")?;
        }
        out.write_str(&self.opts.epilogue)?;
        Ok(())
    }
}