mdbook-plotly 0.1.8-alpha

An mdbook preprocessor that renders plot code blocks (e.g., ```plot) into interactive or static charts during book build.
Documentation
pub mod code_handler;
#[cfg(feature = "plotly-html-handler")]
pub mod plotly_html_handler;
#[cfg(feature = "plotly-svg-handler")]
pub mod plotly_svg_handler;

use crate::preprocessor::config::{PlotlyOutputType, PreprocessorConfig};
use anyhow::Result;
use log::{error, warn};
use mdbook_preprocessor::book::Chapter;
use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
use std::path::Path;

const PULLDOWN_CMARK_OPTIONS: pulldown_cmark::Options = pulldown_cmark::Options::all();

pub fn handle(chapter: &mut Chapter, config: &PreprocessorConfig, book_path: &Path) {
    let events = Parser::new_ext(&chapter.content, PULLDOWN_CMARK_OPTIONS);

    let mut code = String::with_capacity(10);
    let mut in_target_code = false;
    let mut new_events = Vec::with_capacity(10);
    let mut code_sequence = 0;

    if config.output_type == PlotlyOutputType::PlotlyHtml {
        new_events.push(plotly_html_handler::inject_header(
            config.offline_js_sources,
        ));
        new_events.push(Event::HardBreak);
    }

    for event in events {
        match event {
            Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref lang))) => {
                if matches!(lang.as_ref(), "plotly" | "plot") {
                    in_target_code = true;
                    code.clear();
                } else {
                    new_events.push(event);
                }
                code_sequence += 1;
            }
            Event::Text(ref text) => {
                if in_target_code {
                    code.push_str(text);
                } else {
                    new_events.push(event);
                }
            }
            Event::End(TagEnd::CodeBlock) => {
                if !in_target_code {
                    new_events.push(event);
                    continue;
                }
                in_target_code = false;
                let ready_code = std::mem::take(&mut code);
                match handle_plotly(ready_code, config, book_path) {
                    Ok(event) => {
                        new_events.push(Event::HardBreak);
                        new_events.push(event);
                        new_events.push(Event::HardBreak);
                    }
                    Err(message) => warn!(
                        "An error occurred during processing in {} at sequence {}:\n{}",
                        chapter.name, code_sequence, message
                    ),
                }
            }
            _ => new_events.push(event),
        }
    }
    let mut new_content = String::with_capacity(chapter.content.len());
    if let Err(e) = pulldown_cmark_to_cmark::cmark_with_options(
        new_events.into_iter(),
        &mut new_content,
        pulldown_cmark_to_cmark::Options::default(),
    ) {
        error!(
            "Processing failed during cmark. {} Chapter will use the original content. \nInterError: {:#?}",
            chapter.name, e
        );
    } else {
        chapter.content = new_content;
    }
}

#[allow(unused)]
pub fn handle_plotly<'a>(
    code: String,
    config: &'a PreprocessorConfig,
    book_path: &'a Path,
) -> Result<Event<'static>> {
    let plot = code_handler::handle(code, &config.input_type)?;
    let result = match config.output_type {
        #[cfg(feature = "plotly-html-handler")]
        PlotlyOutputType::PlotlyHtml => plotly_html_handler::handle(plot),
        #[cfg(feature = "plotly-svg-handler")]
        PlotlyOutputType::PlotlySvg => plotly_svg_handler::handle(plot, book_path),
    };
    Ok(result)
}