use super::constants::*;
use super::parser::JourneyDiagram;
use super::templates::{self, esc};
use crate::theme::Theme;
pub fn render(diag: &JourneyDiagram, theme: Theme) -> String {
let vars = theme.resolve();
let ff = vars.font_family;
let line_color = vars.line_color;
let id = "mermaid-journey";
if diag.tasks.is_empty() {
return templates::empty_svg(id);
}
let task_fills_owned: Vec<String> = match theme {
crate::theme::Theme::Dark => vec![
vars.primary_color.to_string(),
vars.cluster_bg.to_string(),
"hsl(244, 1.5873015873%, 12.3529411765%)".to_string(),
"hsl(244, 1.5873015873%, 28.3529411765%)".to_string(),
"hsl(116, 1.5873015873%, 12.3529411765%)".to_string(),
"hsl(116, 1.5873015873%, 28.3529411765%)".to_string(),
"hsl(308, 1.5873015873%, 12.3529411765%)".to_string(),
"hsl(308, 1.5873015873%, 28.3529411765%)".to_string(),
],
crate::theme::Theme::Forest => vec![
vars.primary_color.to_string(),
vars.secondary_color.to_string(),
"hsl(142.1578947368, 58.4615384615%, 74.5098039216%)".to_string(),
"hsl(162.961038961, 100%, 84.9019607843%)".to_string(),
"hsl(14.1578947368, 58.4615384615%, 74.5098039216%)".to_string(),
"hsl(34.961038961, 100%, 84.9019607843%)".to_string(),
"hsl(206.1578947368, 58.4615384615%, 74.5098039216%)".to_string(),
"hsl(226.961038961, 100%, 84.9019607843%)".to_string(),
],
crate::theme::Theme::Neutral => vec![
vars.primary_color.to_string(),
"hsl(0, 0%, 98.9215686275%)".to_string(),
"hsl(64, 0%, 93.3333333333%)".to_string(),
"hsl(64, 0%, 98.9215686275%)".to_string(),
"hsl(-64, 0%, 93.3333333333%)".to_string(),
"hsl(-64, 0%, 98.9215686275%)".to_string(),
"hsl(128, 0%, 93.3333333333%)".to_string(),
"hsl(128, 0%, 98.9215686275%)".to_string(),
],
_ => {
let mut v: Vec<String> = TASK_FILLS.iter().map(|s| s.to_string()).collect();
v[0] = vars.primary_color.to_string();
v[1] = vars.secondary_color.to_string();
v
}
};
let task_fills: Vec<&str> = task_fills_owned.iter().map(String::as_str).collect();
struct ActorInfo {
name: String,
color: &'static str,
position: usize,
}
let actor_infos: Vec<ActorInfo> = diag
.actors
.iter()
.enumerate()
.map(|(i, name)| ActorInfo {
name: name.clone(),
color: ACTOR_COLOURS[i % ACTOR_COLOURS.len()],
position: i,
})
.collect();
let num_tasks = diag.tasks.len();
let task_step = TASK_MARGIN + TASK_WIDTH; let total_width = (num_tasks as f64) * task_step + 2.0 * LEFT_MARGIN;
let mut out = String::new();
out.push_str(&templates::svg_root(
id,
total_width as i64,
total_width as i64,
VIEW_HEIGHT as i64,
(VIEW_HEIGHT + 25.0) as i64,
));
out.push_str("<g></g>");
out.push_str(&templates::arrowhead_marker(id));
let mut legend_y = ACTOR_LEGEND_START_Y;
for actor in &actor_infos {
out.push_str(&templates::actor_circle(
legend_y as i64,
actor.position,
actor.color,
));
out.push_str(&templates::actor_label(
(legend_y + 7.0) as i64,
&esc(&actor.name),
line_color,
));
legend_y += ACTOR_LEGEND_STEP;
}
{
let mut last_section: Option<(&str, usize, usize)> = None;
let mut task_i = 0usize;
let flush_section =
|out: &mut String, name: &str, sec_idx: usize, task_start: usize, task_end: usize| {
let fill = task_fills[sec_idx % task_fills.len()];
let count = task_end - task_start;
let sec_x = (task_start as f64) * task_step + LEFT_MARGIN;
let sec_w = (count as f64) * task_step - TASK_MARGIN;
let tx = (sec_x + sec_w / 2.0) as i64;
let ty = (50.0 + SECTION_HEIGHT / 2.0) as i64; let si = sec_idx % task_fills.len();
out.push_str(&templates::section_rect(
sec_x as i64,
fill,
sec_w as i64,
SECTION_HEIGHT as i64,
si,
));
out.push_str(&templates::section_label(
sec_x as i64,
sec_w as i64,
SECTION_HEIGHT as i64,
si,
tx,
ty,
&esc(name),
ff,
vars.text_color,
));
};
for task in &diag.tasks {
let same = last_section
.as_ref()
.map(|(n, _, _)| *n == task.section.as_str())
.unwrap_or(false);
if same {
task_i += 1;
} else {
if let Some((n, si, start)) = last_section.take() {
flush_section(&mut out, n, si, start, task_i);
}
last_section = Some((task.section.as_str(), task.section_index, task_i));
task_i += 1;
}
}
if let Some((n, si, start)) = last_section {
flush_section(&mut out, n, si, start, task_i);
}
}
for (i, task) in diag.tasks.iter().enumerate() {
let task_x = (i as f64) * task_step + LEFT_MARGIN;
let task_cx = task_x + TASK_WIDTH / 2.0; let fill = task_fills[task.section_index % task_fills.len()];
let si = task.section_index % task_fills.len();
let face_cy = TASK_LINE_BOTTOM - (task.score as f64) * 30.0;
out.push_str("<g>");
out.push_str(&templates::task_line(
id,
i,
task_cx as i64,
TASK_LINE_TOP as i64,
TASK_LINE_BOTTOM as i64,
));
out.push_str(&templates::face_circle(task_cx as i64, face_cy as i64));
let eye_left_cx = task_cx - 5.0;
let eye_right_cx = task_cx + 5.0;
let eye_y = face_cy - 5.0;
out.push_str(&templates::face_eyes(
eye_left_cx as i64,
eye_right_cx as i64,
eye_y as i64,
));
if task.score >= 4 {
out.push_str(&templates::mouth_smile(
task_cx as i64,
(face_cy + 2.0) as i64,
));
} else if task.score == 3 {
out.push_str(&templates::mouth_neutral(
(task_cx - 5.0) as i64,
(task_cx + 5.0) as i64,
(face_cy + 7.0) as i64,
));
} else {
out.push_str(&templates::mouth_frown(
task_cx as i64,
(face_cy + 7.0) as i64,
));
}
out.push_str("</g>");
out.push_str(&templates::task_rect(
task_x as i64,
TASK_LINE_TOP as i64,
fill,
TASK_WIDTH as i64,
SECTION_HEIGHT as i64,
si,
));
for (ai, actor_name) in task.people.iter().enumerate() {
if let Some(actor) = actor_infos.iter().find(|a| &a.name == actor_name) {
let dot_x = task_x + 14.0 + (ai as f64) * 10.0;
out.push_str(&templates::actor_dot(
dot_x as i64,
TASK_LINE_TOP as i64,
actor.position,
actor.color,
&esc(&actor.name),
));
}
}
let text_cx = task_cx as i64;
let text_cy = (TASK_LINE_TOP + SECTION_HEIGHT / 2.0) as i64; out.push_str(&templates::task_label(
task_x as i64,
TASK_LINE_TOP as i64,
TASK_WIDTH as i64,
SECTION_HEIGHT as i64,
text_cx,
text_cy,
&esc(&task.task),
ff,
vars.text_color,
));
out.push_str("</g>"); }
if let Some(ref title) = diag.title {
out.push_str(&templates::title_text(
LEFT_MARGIN as i64,
ff,
&esc(title),
vars.text_color,
));
}
let line_x1 = LEFT_MARGIN;
let line_x2 = (num_tasks as f64) * task_step + LEFT_MARGIN - 4.0;
out.push_str(&templates::activity_line(
line_x1 as i64,
ACTIVITY_LINE_Y as i64,
line_x2 as i64,
id,
));
out.push_str("</svg>");
out
}
#[cfg(test)]
mod tests {
use super::super::parser;
use super::*;
#[test]
fn basic_render_produces_svg() {
let input = "journey\n title My working day\n section Go to work\n Make tea: 5: Me\n Go upstairs: 3: Me\n Do work: 1: Me, Cat\n section Go home\n Go downstairs: 5: Me\n Sit down: 3: Me";
let diag = parser::parse(input).diagram;
let svg = render(&diag, crate::theme::Theme::Default);
assert!(svg.contains("<svg"), "no <svg element");
assert!(svg.contains("My working day"), "no title");
assert!(svg.contains("Make tea"), "no task");
assert!(svg.contains("Go to work"), "no section");
}
#[test]
fn renders_no_tasks() {
let input = "journey\n title Empty";
let diag = parser::parse(input).diagram;
let svg = render(&diag, crate::theme::Theme::Default);
assert!(svg.contains("Empty Journey"));
}
#[test]
fn snapshot_default_theme() {
let input = "journey\n title My working day\n section Go to work\n Make tea: 5: Me\n Go upstairs: 3: Me\n Do work: 1: Me, Cat\n section Go home\n Go downstairs: 5: Me\n Sit down: 5: Me";
let diag = parser::parse(input).diagram;
let svg = render(&diag, crate::theme::Theme::Default);
insta::assert_snapshot!(crate::svg::normalize_floats(&svg));
}
}