summaryrefslogtreecommitdiff
path: root/src/md/typeset.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/md/typeset.rs')
-rw-r--r--src/md/typeset.rs229
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)
+}