snakedown 0.3.0

This is a snakedown. Hand over your docs, nice and clean, and nobody gets confused.
Documentation
use std::path::PathBuf;

use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use color_eyre::Result;
use jupyter_protocol::Media;
use jupyter_protocol::MediaType;
use nbformat::v4::{Cell, Output};

use crate::render::formats::Renderer;

pub struct RenderedNotebook {
    pub text: String,
    pub images: Vec<DecodedDisplayData>,
}

#[derive(Clone)]
pub enum DecodedOutput {
    Text(String),
    Image(DecodedDisplayData),
}
#[derive(Debug, Clone)]
pub struct DecodedDisplayData {
    pub name: PathBuf,
    pub data: Vec<u8>,
}

pub fn rank_media_types(media: &MediaType) -> usize {
    match media {
        MediaType::Svg(_) => 10,
        MediaType::Png(_) => 9,
        MediaType::Jpeg(_) => 8,
        MediaType::Gif(_) => 7,
        MediaType::Html(_) => 6,
        MediaType::Json(_) => 5,
        MediaType::GeoJson(_) => 4,
        MediaType::Latex(_) => 3,
        MediaType::Markdown(_) => 2,
        MediaType::Plain(_) => 1,
        MediaType::DataTable(_)
        | MediaType::Javascript(_)
        | MediaType::Plotly(_)
        | MediaType::WidgetView(_)
        | MediaType::WidgetState(_)
        | MediaType::VegaLiteV2(_)
        | MediaType::VegaLiteV3(_)
        | MediaType::VegaLiteV4(_)
        | MediaType::VegaLiteV5(_)
        | MediaType::VegaLiteV6(_)
        | MediaType::VegaV3(_)
        | MediaType::VegaV4(_)
        | MediaType::VegaV5(_)
        | MediaType::Vdom(_)
        | MediaType::Other(_) => 0,
    }
}

pub fn render_jupyter_display_data(cell_nr: usize, data: Media) -> Result<Option<DecodedOutput>> {
    let richest = data.richest(rank_media_types).map(|m| match m {
        MediaType::Svg(svg) => Ok(DecodedOutput::Image(DecodedDisplayData {
            name: PathBuf::from(cell_nr.to_string()).with_extension("svg"),
            data: svg.as_bytes().to_vec(),
        })),
        MediaType::Png(png) => {
            let decoded_data = BASE64_STANDARD.decode(png)?;
            Ok(DecodedOutput::Image(DecodedDisplayData {
                name: PathBuf::from(cell_nr.to_string()).with_extension("png"),
                data: decoded_data,
            }))
        }
        MediaType::Jpeg(jpg) => {
            let decoded_data = BASE64_STANDARD.decode(jpg)?;
            Ok(DecodedOutput::Image(DecodedDisplayData {
                name: PathBuf::from(cell_nr.to_string()).with_extension("jpg"),
                data: decoded_data,
            }))
        }
        MediaType::Gif(gif) => {
            let decoded_data = BASE64_STANDARD.decode(gif)?;
            Ok(DecodedOutput::Image(DecodedDisplayData {
                name: PathBuf::from(cell_nr.to_string()).with_extension("gif"),
                data: decoded_data,
            }))
        }
        MediaType::Html(html) => Ok(DecodedOutput::Text(html.clone())),
        MediaType::Json(json) => Ok(DecodedOutput::Text(json.clone().to_string())),
        MediaType::GeoJson(geojson) => Ok(DecodedOutput::Text(geojson.clone().to_string())),
        MediaType::Latex(latex) => Ok(DecodedOutput::Text(latex.clone())),
        MediaType::Markdown(md) => Ok(DecodedOutput::Text(md.clone())),
        MediaType::Plain(plain) => Ok(DecodedOutput::Text(plain.clone())),
        MediaType::DataTable(_)
        | MediaType::Javascript(_)
        | MediaType::Plotly(_)
        | MediaType::WidgetView(_)
        | MediaType::WidgetState(_)
        | MediaType::VegaLiteV2(_)
        | MediaType::VegaLiteV3(_)
        | MediaType::VegaLiteV4(_)
        | MediaType::VegaLiteV5(_)
        | MediaType::VegaLiteV6(_)
        | MediaType::VegaV3(_)
        | MediaType::VegaV4(_)
        | MediaType::VegaV5(_)
        | MediaType::Vdom(_)
        | MediaType::Other(_) => unreachable!(),
    });
    richest.transpose()
}

pub fn render_notebook<R: Renderer>(
    name: Option<&str>,
    notebook: &[Cell],
    renderer: &R,
) -> Result<RenderedNotebook> {
    let front_matter = renderer.render_front_matter(name);
    let mut rendered_cells = vec![front_matter];
    let mut rendered_notebook = RenderedNotebook {
        text: String::new(),
        images: Vec::new(),
    };
    for (count, cell) in notebook.iter().enumerate() {
        match cell {
            Cell::Markdown {
                id: _,
                metadata: _,
                source,
                attachments: _,
            } => rendered_cells.push(source.join("")),
            Cell::Code {
                id: _,
                metadata: _,
                execution_count: _,
                source,
                outputs,
            } => {
                rendered_cells.push(format!("```python\n{}\n```", source.join("")));
                for output in outputs {
                    match output {
                        Output::Stream { name: _, text } => {
                            rendered_cells.push(format!("```\n{}\n```", text.0.clone()));
                        }
                        Output::DisplayData(display_data) => {
                            if let Some(r) =
                                render_jupyter_display_data(count, display_data.data.clone())?
                            {
                                match r {
                                    DecodedOutput::Text(t) => rendered_cells.push(t),
                                    DecodedOutput::Image(img) => {
                                        rendered_cells
                                            .push(format!("![image]({})", img.name.display()));
                                        rendered_notebook.images.push(img);
                                    }
                                };
                            }
                        }
                        Output::ExecuteResult(exec_result) => {
                            if let Some(r) =
                                render_jupyter_display_data(count, exec_result.data.clone())?
                            {
                                match r {
                                    DecodedOutput::Text(t) => rendered_cells.push(t),
                                    DecodedOutput::Image(img) => {
                                        rendered_cells
                                            .push(format!("![image]({})", img.name.display()));
                                        rendered_notebook.images.push(img);
                                    }
                                };
                            }
                        }
                        Output::Error(_) => (),
                    };
                }
            }
            Cell::Raw {
                id: _,
                metadata: _,
                source,
            } => rendered_cells.push(format!("```\n{}\n```", source.join(""))),
        }
    }

    rendered_notebook.text = rendered_cells.join("\n\n");

    Ok(rendered_notebook)
}
#[cfg(test)]
mod test {}