mdbook-tabs 1.0.2

mdBook plugin for rendering content in tabs.
Documentation
use std::ops::Range;

use anyhow::{Result, bail};
use log::debug;
use mdbook_plugin_utils::markdown::parse_blocks;
use mdbook_preprocessor::book::Chapter;
use pulldown_cmark::Event;

use crate::config::{TabConfig, TabsConfig};

fn is_tabs_start(event: &Event) -> bool {
    match event {
        Event::Text(text) => {
            (text.to_string() == "{{#tabs}}" || text.starts_with("{{#tabs"))
                && !text.contains("{{#endtabs")
        }
        _ => false,
    }
}

fn is_tabs_end(event: &Event) -> bool {
    match event {
        Event::Text(text) => {
            (text.to_string() == "{{#endtabs}}" || text.starts_with("{{#endtabs "))
                && !text.contains("{{#tabs ")
        }
        _ => false,
    }
}

fn is_tab_start(event: &Event) -> bool {
    match event {
        Event::Text(text) => text.to_string() == "{{#tab}}" || text.starts_with("{{#tab "),
        _ => false,
    }
}

fn is_tab_end(event: &Event) -> bool {
    match event {
        Event::Text(text) => text.to_string() == "{{#endtab}}" || text.starts_with("{{#endtab "),
        _ => false,
    }
}

type SpanAndTabs = (Range<usize>, TabsConfig);

pub fn parse_tabs(chapter: &Chapter) -> Result<(Vec<SpanAndTabs>, bool)> {
    let mut configs: Vec<(Range<usize>, TabsConfig)> = vec![];

    let blocks = parse_blocks(&chapter.content, is_tabs_start, is_tabs_end, true)?;
    debug!("{blocks:?}");

    for block in &blocks {
        let start_text = match &block.events[0].0 {
            Event::Text(text) => text.to_string(),
            _ => bail!("First event should be text."),
        };

        let mut tabs = TabsConfig::parse(
            start_text
                .trim_start_matches("{{#tabs")
                .trim_start()
                .trim_end_matches("}}")
                .trim_end(),
        )?;

        let subblocks = parse_blocks(
            &chapter.content[block.inner_span.clone()],
            is_tab_start,
            is_tab_end,
            true,
        )?;
        debug!("{subblocks:?}");

        for subblock in subblocks {
            let start_text = match &subblock.events[0].0 {
                Event::Text(text) => text.to_string(),
                _ => bail!("First event should be text."),
            };

            tabs.tabs.push((
                TabConfig::parse(
                    start_text
                        .trim_start_matches("{{#tab")
                        .trim_start()
                        .trim_end_matches("}}")
                        .trim_end(),
                )?,
                chapter.content[(block.inner_span.start + subblock.inner_span.start)
                    ..(block.inner_span.start + subblock.inner_span.end)]
                    .to_string(),
            ));
        }

        configs.push((block.span.clone(), tabs));
    }

    debug!("{configs:?}");

    Ok((configs, blocks.iter().any(|block| block.has_nested)))
}