diff options
Diffstat (limited to 'src/md/typeset.rs')
-rw-r--r-- | src/md/typeset.rs | 229 |
1 files changed, 229 insertions, 0 deletions
diff --git a/src/md/typeset.rs b/src/md/typeset.rs new file mode 100644 index 0000000..f63206a --- /dev/null +++ b/src/md/typeset.rs @@ -0,0 +1,229 @@ +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<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()); + } + // 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<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 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) +} |