subplot 0.4.0

tools for specifying, documenting, and implementing automated acceptance tests for systems and software
Documentation
use crate::parser::parse_scenario_snippet;
use crate::Bindings;
use crate::PartialStep;
use crate::ScenarioStep;
use crate::StepKind;
use crate::SubplotError;
use crate::{DotMarkup, GraphMarkup, PikchrMarkup, PlantumlMarkup};
use crate::{Warning, Warnings};

use pandoc_ast::Attr;
use pandoc_ast::Block;
use pandoc_ast::Inline;
use pandoc_ast::Target;

/// Typeset an error as a Pandoc AST Block element.
pub fn error(err: SubplotError) -> Block {
    let msg = format!("ERROR: {}", err);
    Block::Para(error_msg(&msg))
}

/// Typeset an error message a vector of inlines.
pub fn error_msg(msg: &str) -> Vec<Inline> {
    vec![Inline::Strong(vec![inlinestr(msg)])]
}

/// Typeset a string as an inline element.
pub fn inlinestr(s: &str) -> Inline {
    Inline::Str(String::from(s))
}

/// Typeset a code block tagged as a file.
pub fn file_block(attr: &Attr, text: &str) -> Block {
    let filename = inlinestr(&attr.0);
    let filename = Inline::Strong(vec![filename]);
    let intro = Block::Para(vec![inlinestr("File:"), space(), filename]);
    let mut cbattrs = attr.clone();
    if cbattrs.1.iter().any(|s| s == "noNumberLines") {
        // If the block says "noNumberLines" we remove that class
        cbattrs.1.retain(|s| s != "noNumberLines");
    } else if cbattrs.1.iter().all(|s| s != "numberLines") {
        // Otherwise if it doesn't say numberLines we add that in.
        cbattrs.1.push("numberLines".to_string());
    }
    let codeblock = Block::CodeBlock(cbattrs, text.to_string());
    let noattr = ("".to_string(), vec![], vec![]);
    Block::Div(noattr, vec![intro, codeblock])
}

/// Typeset a scenario snippet as a Pandoc AST Block.
///
/// Typesetting here means producing the Pandoc abstract syntax tree
/// nodes that result in the desired output, when Pandoc processes
/// them.
///
/// The snippet is given as a text string, which is parsed. It need
/// not be a complete scenario, but it should consist of complete steps.
pub fn scenario_snippet(bindings: &Bindings, snippet: &str, warnings: &mut Warnings) -> Block {
    let lines = parse_scenario_snippet(snippet);
    let mut steps = vec![];
    let mut prevkind: Option<StepKind> = None;

    for line in lines {
        let (this, thiskind) = step(bindings, line, prevkind, warnings);
        steps.push(this);
        prevkind = thiskind;
    }
    Block::LineBlock(steps)
}

// Typeset a single scenario step as a sequence of Pandoc AST Inlines.
fn step(
    bindings: &Bindings,
    text: &str,
    prevkind: Option<StepKind>,
    warnings: &mut Warnings,
) -> (Vec<Inline>, Option<StepKind>) {
    let step = ScenarioStep::new_from_str(text, prevkind);
    if step.is_err() {
        return (
            error_msg(&format!("Could not parse step: {}", text)),
            prevkind,
        );
    }
    let step = step.unwrap();

    let m = match bindings.find("", &step) {
        Ok(m) => m,
        Err(e) => {
            let w = Warning::UnknownBinding(format!("{}", e));
            warnings.push(w.clone());
            return (error_msg(&format!("{}", w)), prevkind);
        }
    };

    let mut inlines = vec![keyword(&step, prevkind), space()];

    for part in m.parts() {
        match part {
            PartialStep::UncapturedText(s) => inlines.push(uncaptured(s.text())),
            PartialStep::CapturedText { text, .. } => inlines.push(captured(text)),
        }
    }

    (inlines, Some(step.kind()))
}

// Typeset first word, which is assumed to be a keyword, of a scenario
// step.
fn keyword(step: &ScenarioStep, prevkind: Option<StepKind>) -> Inline {
    let actual = inlinestr(&format!("{}", step.kind()));
    let and = inlinestr("and");
    let keyword = if let Some(prevkind) = prevkind {
        if prevkind == step.kind() {
            and
        } else {
            actual
        }
    } else {
        actual
    };
    Inline::Emph(vec![keyword])
}

// Typeset a space between words.
fn space() -> Inline {
    Inline::Space
}

// Typeset an uncaptured part of a step.
fn uncaptured(s: &str) -> Inline {
    inlinestr(s)
}

// Typeset a captured part of a step.
fn captured(s: &str) -> Inline {
    Inline::Strong(vec![inlinestr(s)])
}

/// Typeset a link as a note.
pub fn link_as_note(attr: Attr, text: Vec<Inline>, target: Target) -> Inline {
    let (url, _) = target.clone();
    let url = Inline::Code(attr.clone(), url);
    let link = Inline::Link(attr.clone(), vec![url], target);
    let note = Inline::Note(vec![Block::Para(vec![link])]);
    let mut text = text;
    text.push(note);
    Inline::Span(attr, text)
}

/// Take a pikchr graph, render it as SVG, and return an AST block element.
///
/// The `Block` will contain the SVG data.  This allows the graph to be
/// rendered without referencing external entities.
///
/// If the code block which contained the pikchr contains other classes, they
/// can be added to the SVG for use in later typesetting etc.
pub fn pikchr_to_block(pikchr: &str, class: Option<&str>, warnings: &mut Warnings) -> Block {
    match PikchrMarkup::new(pikchr, class).as_svg() {
        Ok(svg) => typeset_svg(svg),
        Err(err) => {
            warnings.push(Warning::Pikchr(format!("{}", err)));
            error(err)
        }
    }
}

// Take a dot graph, render it as SVG, and return an AST Block
// element. The Block will contain the SVG data. This allows the graph
// to be rendered without referending external entities.
pub fn dot_to_block(dot: &str, warnings: &mut Warnings) -> Block {
    match DotMarkup::new(dot).as_svg() {
        Ok(svg) => typeset_svg(svg),
        Err(err) => {
            warnings.push(Warning::Dot(format!("{}", err)));
            error(err)
        }
    }
}

// Take a PlantUML graph, render it as SVG, and return an AST Block
// element. The Block will contain the SVG data. This allows the graph
// to be rendered without referending external entities.
pub fn plantuml_to_block(markup: &str, warnings: &mut Warnings) -> Block {
    match PlantumlMarkup::new(markup).as_svg() {
        Ok(svg) => typeset_svg(svg),
        Err(err) => {
            warnings.push(Warning::Plantuml(format!("{}", err)));
            error(err)
        }
    }
}

/// Typeset a project roadmap expressed as textual YAML, and render it
/// as an SVG image.
pub fn roadmap_to_block(yaml: &str, warnings: &mut Warnings) -> Block {
    match roadmap::from_yaml(yaml) {
        Ok(ref mut roadmap) => {
            roadmap.set_missing_statuses();
            let width = 50;
            match roadmap.format_as_dot(width) {
                Ok(dot) => dot_to_block(&dot, warnings),
                Err(e) => Block::Para(vec![inlinestr(&e.to_string())]),
            }
        }
        Err(e) => Block::Para(vec![inlinestr(&e.to_string())]),
    }
}

// Typeset an SVG, represented as a byte vector, as a Pandoc AST Block
// element.
fn typeset_svg(svg: Vec<u8>) -> Block {
    let url = svg_as_data_url(svg);
    let attr = ("".to_string(), vec![], vec![]);
    let img = Inline::Image(attr, vec![], (url, "".to_string()));
    Block::Para(vec![img])
}

// Convert an SVG, represented as a byte vector, into a data: URL,
// which can be inlined so the image can be rendered without
// referencing external files.
fn svg_as_data_url(svg: Vec<u8>) -> String {
    let svg = base64::encode(&svg);
    format!("data:image/svg+xml;base64,{}", svg)
}