use crate::data::StepData;
use crate::rendering::markdown::{code_block, sanitize};
fn action_label(code: &str) -> &str {
match code.as_bytes() {
b"Next" | b"1" => "Continue",
b"JumpToStep" | b"GotoStep" | b"Goto" | b"2" => "Jump to Step",
b"ReturnFromSequence" | b"3" => "Return from Sequence",
b"TerminateExecution" | b"4" => "Terminate Execution",
b"TerminateExecutionWithError" | b"5" => "Terminate Execution with Error",
b"Break" | b"6" => "Break",
b"ContinueLoop" | b"7" => "Continue Loop",
b"JumpToSequence" | b"8" => "Jump to Sequence",
_ => code,
}
}
fn expression_label(step_type: &str) -> &'static str {
match step_type {
"NI_Flow_If" | "NI_Flow_ElseIf" | "NI_Flow_While" | "NI_Flow_DoWhile" => "Condition",
"NI_Flow_Select" => "Switches on",
"NI_Flow_Case" => "Matches",
"NI_Flow_ForEach" => "Iterates",
_ => "Expression",
}
}
fn code_span(s: &str) -> String {
format!("`{}`", sanitize(s))
}
fn target_suffix(target: &str, names: &StepNames<'_>) -> String {
let target = target.trim().trim_matches('"');
if target.is_empty() {
return String::new();
}
match names.get(target) {
Some(name) => format!(" -> {}", code_span(name)),
None if target.starts_with("ID#") => String::new(),
None => format!(" -> {}", code_span(target)),
}
}
fn code_block_admonition(label: &str, code: &str) -> Vec<String> {
let mut lines = Vec::new();
lines.push(format!("> - **{label}**:"));
lines.push(">".to_owned());
lines.push(code_block(code, "> "));
let spelled = crate::rendering::expression::humanize(code);
if spelled != code.trim() {
lines.push(">".to_owned());
let flowed = spelled.split_whitespace().collect::<Vec<_>>().join(" ");
lines.push(format!("> *Reads as:* {flowed}"));
}
lines.push(">".to_owned());
lines
}
fn format_custom_condition(step: &StepData, out: &mut Vec<String>, names: &StepNames<'_>) {
if let Some(cond) = step.expressions.get("custom_condition")
&& !cond.is_empty()
{
out.extend(code_block_admonition("Custom condition", cond));
if let Some(act) = step.expressions.get("custom_true_action") {
let suffix = target_suffix(
step.expressions
.get("custom_true_target")
.map_or("", String::as_str),
names,
);
out.push(format!("> - **If true**: {}{suffix}", action_label(act)));
}
if let Some(act) = step.expressions.get("custom_false_action") {
let suffix = target_suffix(
step.expressions
.get("custom_false_target")
.map_or("", String::as_str),
names,
);
out.push(format!("> - **If false**: {}{suffix}", action_label(act)));
}
}
}
fn format_pass_fail_actions(step: &StepData, out: &mut Vec<String>, names: &StepNames<'_>) {
if let Some(pass_act) = step.expressions.get("pass_action") {
let arrow = target_suffix(
step.expressions
.get("pass_action_target")
.map_or("", String::as_str),
names,
);
out.push(format!(
"> - **On pass**: {}{arrow}",
action_label(pass_act)
));
}
if let Some(fail_act) = step.expressions.get("fail_action") {
let arrow = target_suffix(
step.expressions
.get("fail_action_target")
.map_or("", String::as_str),
names,
);
out.push(format!(
"> - **On fail**: {}{arrow}",
action_label(fail_act)
));
}
}
fn format_loop_expressions(step: &StepData, out: &mut Vec<String>) {
if let Some(loop_type) = step.expressions.get("loop_type") {
out.push(format!("> - **Loop Type**: {loop_type}"));
}
if let Some(while_cond) = step.expressions.get("while_condition")
&& !while_cond.is_empty()
{
out.extend(code_block_admonition("While condition", while_cond));
}
if let Some(init) = step.expressions.get("for_init")
&& !init.is_empty()
{
out.extend(code_block_admonition("For loop init", init));
}
if let Some(cond) = step.expressions.get("for_condition")
&& !cond.is_empty()
{
out.extend(code_block_admonition("For loop condition", cond));
}
if let Some(inc) = step.expressions.get("for_increment")
&& !inc.is_empty()
{
out.extend(code_block_admonition("For loop increment", inc));
}
}
fn format_message_popup_details(step: &StepData, out: &mut Vec<String>) {
if step.step_type != "MessagePopup" {
return;
}
if let Some(title) = step.expressions.get("title") {
out.push(format!("> - **Title**: {}", code_span(title)));
}
if let Some(msg) = step.expressions.get("message") {
out.extend(code_block_admonition("Message", msg));
}
let mut buttons = Vec::new();
for i in 1..=6 {
if let Some(btn) = step.expressions.get(&format!("button{i}")) {
let text = btn.trim().trim_matches('"').trim();
if !text.is_empty() {
buttons.push(format!("Button {i}: {}", code_span(btn)));
}
}
}
if !buttons.is_empty() {
out.push(format!("> - **Buttons**: {}", buttons.join(", ")));
}
if let Some(def_btn) = step.expressions.get("default_button")
&& def_btn != "0"
&& !def_btn.is_empty()
{
out.push(format!("> - **Default Button**: Button {def_btn}"));
}
if let Some(tim_btn) = step.expressions.get("timer_button")
&& tim_btn != "0"
&& !tim_btn.is_empty()
{
out.push(format!("> - **Timer Button**: Button {tim_btn}"));
}
if let Some(wait) = step.expressions.get("time_to_wait")
&& wait != "0"
&& wait != "0.0"
&& !wait.is_empty()
{
out.push(format!("> - **Time to Wait**: {wait}s"));
}
if let Some(resp) = step.expressions.get("response")
&& !resp.is_empty()
{
out.push(format!("> - **Response Store**: {}", code_span(resp)));
}
}
fn format_step_measurements(step: &StepData, out: &mut Vec<String>) {
if step.measurements.is_empty() {
return;
}
out.push("> - **Measurements**:".to_owned());
for meas in &step.measurements {
let u = if meas.limits.unit.is_empty() {
String::new()
} else {
format!(" {}", meas.limits.unit)
};
let mut l_str = String::new();
if !meas.limits.target.is_empty() {
l_str = format!("== {}{u}", meas.limits.target);
} else if !meas.limits.low.is_empty() && !meas.limits.high.is_empty() {
l_str = format!("{} to {}{u}", meas.limits.low, meas.limits.high);
} else if !meas.limits.low.is_empty() {
l_str = format!(">= {}{u}", meas.limits.low);
} else if !meas.limits.high.is_empty() {
l_str = format!("<= {}{u}", meas.limits.high);
}
out.push(format!(
"> - `{}`: {}",
sanitize(&meas.name),
sanitize(&l_str)
));
}
}
pub type StepNames<'a> = std::collections::BTreeMap<&'a str, &'a str>;
pub fn append_step_extras(
md: &mut Vec<String>,
step: &StepData,
_indent: &str,
names: &StepNames<'_>,
) {
let mut out: Vec<String> = Vec::new();
format_message_popup_details(step, &mut out);
if let Some(expr) = step.expressions.get("expression")
&& !expr.is_empty()
&& step.step_type != "MessagePopup"
{
out.extend(code_block_admonition(
expression_label(&step.step_type),
expr,
));
}
if let Some(pre) = step.expressions.get("pre_expr")
&& !pre.is_empty()
{
out.extend(code_block_admonition("Pre-expression", pre));
}
if let Some(post) = step.expressions.get("post_expr")
&& !post.is_empty()
{
out.extend(code_block_admonition("Post-expression", post));
}
format_custom_condition(step, &mut out, names);
format_loop_expressions(step, &mut out);
format_pass_fail_actions(step, &mut out, names);
format_step_measurements(step, &mut out);
for (k, v) in &step.step_settings {
if k != "IconName" && !v.is_empty() {
out.push(format!("> - **{k}**: {}", code_span(v)));
}
}
if let Some(mutex) = step.expressions.get("mutex")
&& !mutex.is_empty()
{
out.push(format!("> - **Mutex Lock**: {}", code_span(mutex)));
}
if !step.requirements.is_empty() {
let req_list = step
.requirements
.iter()
.map(|r| code_span(r))
.collect::<Vec<_>>()
.join(", ");
out.push(format!("> - **Requirements**: {req_list}"));
}
if out.is_empty() {
return;
}
md.push(String::new());
md.push(format!("> **Configuration: {}**", sanitize(&step.name)));
md.push(">".to_owned());
md.extend(out);
md.push(String::new());
}
#[must_use]
pub fn step_detail(step: &StepData, names: &StepNames<'_>) -> Vec<String> {
let mut lines = Vec::new();
if !step.target_sequence.is_empty() {
lines.push(format!("- **Calls**: {}", code_span(&step.target_sequence)));
} else if !step.module_path.is_empty() {
let file = crate::rendering::markdown::display_name(&step.module_path);
let called = match step
.module_info
.as_ref()
.and_then(|module| module.entry_point.as_ref())
{
Some(entry_point) if !entry_point.is_empty() => format!("{file} -> {entry_point}"),
_ => file,
};
lines.push(format!("- **Calls**: {}", code_span(&called)));
}
if !step.comment.trim().is_empty() {
lines.push(format!("- *{}*", sanitize(step.comment.trim())));
}
let mut quoted = Vec::new();
append_step_extras(&mut quoted, step, "", names);
quoted
.into_iter()
.skip_while(|line| !line.starts_with("> - "))
.map(|entry| {
entry
.lines()
.map(|line| {
line.strip_prefix("> ")
.or_else(|| line.strip_prefix(">"))
.unwrap_or(line)
})
.collect::<Vec<_>>()
.join("\n")
})
.filter(|entry| !entry.trim().is_empty())
.for_each(|entry| lines.push(entry));
lines
}