use crate::parser::parse_scenario_snippet; use crate::Bindings; use crate::PartialStep; use crate::ScenarioStep; use crate::StepKind; use crate::SubplotError; use crate::{DiagramMarkup, DotMarkup, PikchrMarkup, PlantumlMarkup, Svg}; 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 { 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()); } // If this was an `example`, convert that class to `file` if cbattrs.1.iter().any(|s| s == "example") { cbattrs.1.retain(|s| s != "example"); cbattrs.1.push("file".into()); } 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 = 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, warnings: &mut Warnings, ) -> (Vec, Option) { 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) -> 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, 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 diagram, render it as SVG, and return an AST block element. /// /// The `Block` will contain the SVG data. This allows the diagram 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 diagram, render it as SVG, and return an AST Block // element. The Block will contain the SVG data. This allows the // diagram 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 diagram, render it as SVG, and return an AST Block // element. The Block will contain the SVG data. This allows the // diagram 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: Svg) -> 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: Svg) -> String { let svg = base64::encode(svg.data()); format!("data:image/svg+xml;base64,{}", svg) }