use crate::data::StepData;
use crate::rendering::flowchart::{
FLOW_CASE, FLOW_ELSE, FLOW_ELSEIF, FLOW_END, FLOW_IF, FLOW_LOOPS, FLOW_SELECT,
};
use crate::rendering::markdown::sanitize;
const INDENT: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
Plain,
Opens,
Continues,
Closes,
}
fn shape_of(step: &StepData) -> Shape {
let step_type = step.step_type.as_str();
if step_type == FLOW_END {
return Shape::Closes;
}
if step_type == FLOW_ELSE || step_type == FLOW_ELSEIF || step_type == FLOW_CASE {
return Shape::Continues;
}
if step_type == FLOW_IF || step_type == FLOW_SELECT || FLOW_LOOPS.contains(&step_type) {
return Shape::Opens;
}
Shape::Plain
}
fn kind_of(step: &StepData) -> Option<String> {
let step_type = step.step_type.as_str();
if step_type.starts_with("NI_Flow_") || step_type.is_empty() {
return None;
}
Some(step_type.to_owned())
}
pub fn render<Detail>(steps: &[StepData], mut detail: Detail) -> Vec<String>
where
Detail: FnMut(&StepData) -> Vec<String>,
{
let mut out = Vec::new();
let mut level: usize = 0;
let mut counters: Vec<usize> = vec![0];
for step in steps {
let shape = shape_of(step);
if matches!(shape, Shape::Closes | Shape::Continues) {
level = level.saturating_sub(1);
counters.truncate(level + 1);
}
if shape == Shape::Closes {
continue;
}
while counters.len() <= level {
counters.push(0);
}
let ordinal = counters.get_mut(level).map_or(1, |count| {
*count += 1;
*count
});
let pad = " ".repeat(level * INDENT);
let name = sanitize(&step.name);
let mut heading = if step.skipped {
format!("{pad}{ordinal}. ~~**{name}**~~")
} else {
format!("{pad}{ordinal}. **{name}**")
};
if let Some(kind) = kind_of(step) {
heading = format!("{heading} ({})", sanitize(&kind));
}
out.push(heading);
let inner = " ".repeat((level + 1) * INDENT);
for entry in detail(step) {
for line in entry.lines() {
if line.trim().is_empty() {
out.push(String::new());
} else {
out.push(format!("{inner}{line}"));
}
}
}
if matches!(shape, Shape::Opens | Shape::Continues) {
level += 1;
while counters.len() <= level {
counters.push(0);
}
if let Some(count) = counters.get_mut(level) {
*count = 0;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::render;
use crate::data::StepData;
fn step(name: &str, step_type: &str) -> StepData {
StepData {
name: name.to_owned(),
step_type: step_type.to_owned(),
..StepData::default()
}
}
#[test]
fn a_loop_body_nests_under_the_loop() {
let steps = vec![
step("Prepare", "Action"),
step("For", "NI_Flow_For"),
step("Measure", "NumericLimitTest"),
step("End", "NI_Flow_End"),
step("Report", "Action"),
];
let lines = render(&steps, |_| Vec::new());
assert_eq!(
lines,
vec![
"1. **Prepare** (Action)".to_owned(),
"2. **For**".to_owned(),
" 1. **Measure** (NumericLimitTest)".to_owned(),
"3. **Report** (Action)".to_owned(),
]
);
}
#[test]
fn end_is_structure_and_not_a_step() {
let steps = vec![step("If", "NI_Flow_If"), step("End", "NI_Flow_End")];
let lines = render(&steps, |_| Vec::new());
assert_eq!(lines, vec!["1. **If**".to_owned()]);
}
#[test]
fn else_returns_to_the_level_of_its_if() {
let steps = vec![
step("If", "NI_Flow_If"),
step("Option A", "MessagePopup"),
step("Else", "NI_Flow_Else"),
step("Option B", "MessagePopup"),
step("End", "NI_Flow_End"),
];
let lines = render(&steps, |_| Vec::new());
assert_eq!(
lines,
vec![
"1. **If**".to_owned(),
" 1. **Option A** (MessagePopup)".to_owned(),
"2. **Else**".to_owned(),
" 1. **Option B** (MessagePopup)".to_owned(),
]
);
}
#[test]
fn detail_is_indented_under_its_step() {
let steps = vec![step("Measure", "NumericLimitTest")];
let lines = render(&steps, |_| vec!["- Limits: 1 to 5".to_owned()]);
assert_eq!(
lines,
vec![
"1. **Measure** (NumericLimitTest)".to_owned(),
" - Limits: 1 to 5".to_owned(),
]
);
}
#[test]
fn a_nested_loop_restarts_its_numbering() {
let steps = vec![
step("Outer", "NI_Flow_For"),
step("Inner", "NI_Flow_While"),
step("Work", "Action"),
step("End", "NI_Flow_End"),
step("After", "Action"),
step("End", "NI_Flow_End"),
];
let lines = render(&steps, |_| Vec::new());
assert_eq!(
lines,
vec![
"1. **Outer**".to_owned(),
" 1. **Inner**".to_owned(),
" 1. **Work** (Action)".to_owned(),
" 2. **After** (Action)".to_owned(),
]
);
}
}