pub const MIN_STRINGS: usize = 2;
pub const MAX_STRINGS: usize = 12;
pub const DEFAULT_FRETS_SHOWN: usize = 5;
pub const MAX_BASE_FRET: u32 = 24;
pub const MIN_FRETS_SHOWN: usize = 1;
pub const MAX_FRETS_SHOWN: usize = 24;
#[derive(Debug, Clone)]
pub struct DiagramData {
pub name: String,
pub display_name: Option<String>,
pub strings: usize,
pub frets_shown: usize,
pub base_fret: u32,
pub frets: Vec<i32>,
pub fingers: Vec<u8>,
}
impl DiagramData {
#[must_use]
pub fn title(&self) -> &str {
self.display_name.as_deref().unwrap_or(&self.name)
}
}
impl DiagramData {
#[must_use]
pub fn from_raw_infer(name: &str, raw: &str) -> Option<Self> {
Self::from_raw(name, raw, 0)
}
#[must_use]
pub fn from_raw_infer_frets(name: &str, raw: &str, frets_shown: usize) -> Option<Self> {
Self::from_raw_frets(name, raw, 0, frets_shown)
}
#[must_use]
pub fn from_raw(name: &str, raw: &str, num_strings: usize) -> Option<Self> {
Self::from_raw_frets(name, raw, num_strings, DEFAULT_FRETS_SHOWN)
}
#[must_use]
pub fn from_raw_frets(
name: &str,
raw: &str,
num_strings: usize,
frets_shown: usize,
) -> Option<Self> {
let mut base_fret: u32 = 1;
let mut frets: Vec<i32> = Vec::new();
let mut fingers: Vec<u8> = Vec::new();
let tokens: Vec<&str> = raw.split_whitespace().collect();
let mut i = 0;
while i < tokens.len() {
let tok_lower = tokens[i].to_ascii_lowercase();
match tok_lower.as_str() {
"base-fret" => {
if i + 1 < tokens.len() {
base_fret = tokens[i + 1].parse().unwrap_or(1).clamp(1, MAX_BASE_FRET);
i += 2;
} else {
i += 1;
}
}
"frets" => {
i += 1;
while i < tokens.len() {
let low = tokens[i].to_ascii_lowercase();
if matches!(
low.as_str(),
"frets" | "fingers" | "base-fret" | "display" | "format"
) {
break;
}
let val = match low.as_str() {
"x" | "n" => -1,
s => {
let v = s.parse::<i32>().unwrap_or(-1);
if v < -1 { -1 } else { v }
}
};
frets.push(val);
i += 1;
}
}
"fingers" => {
i += 1;
while i < tokens.len() {
let low = tokens[i].to_ascii_lowercase();
if matches!(
low.as_str(),
"frets" | "fingers" | "base-fret" | "display" | "format"
) {
break;
}
fingers.push(tokens[i].parse().unwrap_or(0));
i += 1;
}
}
_ => {
i += 1;
}
}
}
if frets.is_empty() {
return None;
}
let strings = num_strings.max(frets.len());
if !(MIN_STRINGS..=MAX_STRINGS).contains(&strings) {
return None;
}
let frets_shown = frets_shown.clamp(MIN_FRETS_SHOWN, MAX_FRETS_SHOWN);
let frets: Vec<i32> = frets
.into_iter()
.map(|f| {
if f > frets_shown as i32 {
frets_shown as i32
} else {
f
}
})
.collect();
Some(Self {
name: name.to_string(),
display_name: None,
strings,
frets_shown,
base_fret,
frets,
fingers,
})
}
}
const CELL_W: f32 = 16.0;
const CELL_H: f32 = 20.0;
const TOP_MARGIN: f32 = 30.0;
const LEFT_MARGIN: f32 = 20.0;
const DOT_RADIUS: f32 = 5.0;
const OPEN_RADIUS: f32 = 4.0;
#[must_use]
pub fn render_svg(data: &DiagramData) -> String {
if data.strings < MIN_STRINGS
|| data.strings > MAX_STRINGS
|| data.frets_shown < MIN_FRETS_SHOWN
|| data.frets_shown > MAX_FRETS_SHOWN
{
return String::new();
}
let num_strings = data.strings;
let num_frets = data.frets_shown;
let grid_w = (num_strings - 1) as f32 * CELL_W;
let grid_h = num_frets as f32 * CELL_H;
let total_w = grid_w + LEFT_MARGIN * 2.0;
let total_h = grid_h + TOP_MARGIN + 30.0;
let mut svg = format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{total_w}\" height=\"{total_h}\" \
viewBox=\"0 0 {total_w} {total_h}\" class=\"chord-diagram\">\n"
);
let name_x = LEFT_MARGIN + grid_w / 2.0;
svg.push_str(&format!(
"<text x=\"{name_x}\" y=\"15\" text-anchor=\"middle\" \
font-family=\"sans-serif\" font-size=\"14\" font-weight=\"bold\">{}</text>\n",
crate::escape::escape_xml(data.title())
));
let nut_y = TOP_MARGIN;
if data.base_fret == 1 {
svg.push_str(&format!(
"<line x1=\"{LEFT_MARGIN}\" y1=\"{nut_y}\" x2=\"{}\" y2=\"{nut_y}\" \
stroke=\"black\" stroke-width=\"3\"/>\n",
LEFT_MARGIN + grid_w
));
} else {
svg.push_str(&format!(
"<text x=\"{}\" y=\"{}\" text-anchor=\"end\" \
font-family=\"sans-serif\" font-size=\"10\">{}fr</text>\n",
LEFT_MARGIN - 4.0,
nut_y + CELL_H / 2.0 + 3.0,
data.base_fret
));
}
for i in 0..num_strings {
let x = LEFT_MARGIN + i as f32 * CELL_W;
svg.push_str(&format!(
"<line x1=\"{x}\" y1=\"{nut_y}\" x2=\"{x}\" y2=\"{}\" \
stroke=\"black\" stroke-width=\"1\"/>\n",
nut_y + grid_h
));
}
for j in 0..=num_frets {
let y = nut_y + j as f32 * CELL_H;
svg.push_str(&format!(
"<line x1=\"{LEFT_MARGIN}\" y1=\"{y}\" x2=\"{}\" y2=\"{y}\" \
stroke=\"black\" stroke-width=\"1\"/>\n",
LEFT_MARGIN + grid_w
));
}
for (i, &fret) in data.frets.iter().enumerate() {
if i >= num_strings {
break;
}
let x = LEFT_MARGIN + i as f32 * CELL_W;
if fret == -1 {
let y = nut_y - 10.0;
svg.push_str(&format!(
"<text x=\"{x}\" y=\"{y}\" text-anchor=\"middle\" \
font-family=\"sans-serif\" font-size=\"10\">X</text>\n"
));
} else if fret == 0 {
let y = nut_y - 10.0;
svg.push_str(&format!(
"<circle cx=\"{x}\" cy=\"{y}\" r=\"{OPEN_RADIUS}\" \
fill=\"none\" stroke=\"black\" stroke-width=\"1\"/>\n"
));
} else {
let y = nut_y + (fret as f32 - 0.5) * CELL_H;
svg.push_str(&format!(
"<circle cx=\"{x}\" cy=\"{y}\" r=\"{DOT_RADIUS}\" fill=\"black\"/>\n"
));
if let Some(&finger) = data.fingers.get(i) {
if finger > 0 {
svg.push_str(&format!(
"<text x=\"{x}\" y=\"{}\" text-anchor=\"middle\" \
font-family=\"sans-serif\" font-size=\"8\" \
fill=\"white\">{finger}</text>\n",
y + 3.0
));
}
}
}
}
svg.push_str("</svg>");
svg
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_svg_basic() {
let data = DiagramData {
name: "Am".to_string(),
display_name: None,
strings: 6,
frets_shown: 5,
base_fret: 1,
frets: vec![-1, 0, 2, 2, 1, 0],
fingers: vec![],
};
let svg = render_svg(&data);
assert!(svg.contains("<svg"));
assert!(svg.contains("</svg>"));
assert!(svg.contains("Am"));
assert!(svg.contains("<circle"));
assert!(svg.contains(">X<"));
}
#[test]
fn test_render_svg_barre_chord() {
let data = DiagramData {
name: "F".to_string(),
display_name: None,
strings: 6,
frets_shown: 5,
base_fret: 1,
frets: vec![1, 1, 2, 3, 3, 1],
fingers: vec![],
};
let svg = render_svg(&data);
assert!(svg.contains(">F<"));
}
#[test]
fn test_render_svg_high_position() {
let data = DiagramData {
name: "Bm".to_string(),
display_name: None,
strings: 6,
frets_shown: 5,
base_fret: 7,
frets: vec![-1, 1, 3, 3, 2, 1],
fingers: vec![],
};
let svg = render_svg(&data);
assert!(svg.contains("7fr"));
}
#[test]
fn test_from_raw_basic() {
let data = DiagramData::from_raw("Am", "base-fret 1 frets x 0 2 2 1 0", 6).unwrap();
assert_eq!(data.name, "Am");
assert_eq!(data.base_fret, 1);
assert_eq!(data.frets, vec![-1, 0, 2, 2, 1, 0]);
}
#[test]
fn test_from_raw_with_fingers() {
let data =
DiagramData::from_raw("C", "base-fret 1 frets x 3 2 0 1 0 fingers 0 3 2 0 1 0", 6)
.unwrap();
assert_eq!(data.frets, vec![-1, 3, 2, 0, 1, 0]);
assert_eq!(data.fingers, vec![0, 3, 2, 0, 1, 0]);
}
#[test]
fn test_from_raw_no_frets() {
assert!(DiagramData::from_raw("X", "base-fret 1", 6).is_none());
}
#[test]
fn test_from_raw_ukulele() {
let data = DiagramData::from_raw("C", "frets 0 0 0 3", 4).unwrap();
assert_eq!(data.strings, 4);
assert_eq!(data.frets, vec![0, 0, 0, 3]);
}
#[test]
fn test_from_raw_infer_guitar() {
let data = DiagramData::from_raw_infer("Am", "base-fret 1 frets x 0 2 2 1 0").unwrap();
assert_eq!(data.strings, 6);
assert_eq!(data.frets, vec![-1, 0, 2, 2, 1, 0]);
}
#[test]
fn test_from_raw_infer_ukulele() {
let data = DiagramData::from_raw_infer("C", "frets 0 0 0 3").unwrap();
assert_eq!(data.strings, 4);
assert_eq!(data.frets, vec![0, 0, 0, 3]);
}
#[test]
fn test_from_raw_infer_banjo() {
let data = DiagramData::from_raw_infer("G", "frets 0 0 0 0 0").unwrap();
assert_eq!(data.strings, 5);
assert_eq!(data.frets, vec![0, 0, 0, 0, 0]);
}
#[test]
fn test_title_returns_display_name_when_set() {
let data = DiagramData {
name: "Am".to_string(),
display_name: Some("A minor".to_string()),
strings: 6,
frets_shown: 5,
base_fret: 1,
frets: vec![-1, 0, 2, 2, 1, 0],
fingers: vec![],
};
assert_eq!(data.title(), "A minor");
}
#[test]
fn test_title_falls_back_to_name() {
let data = DiagramData {
name: "Am".to_string(),
display_name: None,
strings: 6,
frets_shown: 5,
base_fret: 1,
frets: vec![-1, 0, 2, 2, 1, 0],
fingers: vec![],
};
assert_eq!(data.title(), "Am");
}
#[test]
fn test_render_svg_with_display_name() {
let data = DiagramData {
name: "Am".to_string(),
display_name: Some("A minor".to_string()),
strings: 6,
frets_shown: 5,
base_fret: 1,
frets: vec![-1, 0, 2, 2, 1, 0],
fingers: vec![],
};
let svg = render_svg(&data);
assert!(svg.contains("A minor"));
assert!(!svg.contains(">Am<"));
}
#[test]
fn test_from_raw_display_name_is_none() {
let data = DiagramData::from_raw_infer("Am", "base-fret 1 frets x 0 2 2 1 0").unwrap();
assert!(data.display_name.is_none());
}
#[test]
fn test_render_svg_with_finger_numbers() {
let data = DiagramData {
name: "C".to_string(),
display_name: None,
strings: 6,
frets_shown: 5,
base_fret: 1,
frets: vec![-1, 3, 2, 0, 1, 0],
fingers: vec![0, 3, 2, 0, 1, 0],
};
let svg = render_svg(&data);
assert!(svg.contains("fill=\"white\">3</text>"));
assert!(svg.contains("fill=\"white\">2</text>"));
assert!(svg.contains("fill=\"white\">1</text>"));
}
#[test]
fn test_render_svg_zero_finger_not_shown() {
let data = DiagramData {
name: "Am".to_string(),
display_name: None,
strings: 6,
frets_shown: 5,
base_fret: 1,
frets: vec![-1, 0, 2, 2, 1, 0],
fingers: vec![0, 0, 2, 3, 1, 0],
};
let svg = render_svg(&data);
assert!(!svg.contains("fill=\"white\">0</text>"));
assert!(svg.contains("fill=\"white\">2</text>"));
assert!(svg.contains("fill=\"white\">3</text>"));
assert!(svg.contains("fill=\"white\">1</text>"));
}
#[test]
fn test_render_svg_no_fingers_no_crash() {
let data = DiagramData {
name: "G".to_string(),
display_name: None,
strings: 6,
frets_shown: 5,
base_fret: 1,
frets: vec![3, 2, 0, 0, 0, 3],
fingers: vec![],
};
let svg = render_svg(&data);
assert!(svg.contains("<svg"));
assert!(!svg.contains("fill=\"white\""));
}
#[test]
fn test_render_svg_fewer_fingers_than_frets() {
let data = DiagramData {
name: "Am".to_string(),
display_name: None,
strings: 6,
frets_shown: 5,
base_fret: 1,
frets: vec![-1, 0, 2, 2, 1, 0],
fingers: vec![0, 0, 2],
};
let svg = render_svg(&data);
assert!(svg.contains("fill=\"white\">2</text>"));
assert!(svg.contains("<svg"));
}
#[test]
fn test_from_raw_zero_strings_rejected() {
assert!(DiagramData::from_raw("X", "frets 0", 0).is_none());
}
#[test]
fn test_from_raw_one_string_rejected() {
assert!(DiagramData::from_raw("X", "frets 0", 1).is_none());
}
#[test]
fn test_from_raw_two_strings_accepted() {
let data = DiagramData::from_raw("X", "frets 0 0", 0).unwrap();
assert_eq!(data.strings, 2);
}
#[test]
fn test_from_raw_twelve_strings_accepted() {
let data = DiagramData::from_raw("X", "frets 0 0 0 0 0 0 0 0 0 0 0 0", 0).unwrap();
assert_eq!(data.strings, 12);
}
#[test]
fn test_from_raw_thirteen_strings_rejected() {
assert!(DiagramData::from_raw("X", "frets 0 0 0 0 0 0 0 0 0 0 0 0 0", 0,).is_none());
}
#[test]
fn test_from_raw_num_strings_forces_too_many() {
assert!(DiagramData::from_raw("X", "frets 0 0 0 0 0 0", 13).is_none());
}
#[test]
fn test_fret_exceeding_range_clamped() {
let data = DiagramData::from_raw("X", "base-fret 1 frets 0 12 0 0 0 0", 6).unwrap();
assert_eq!(data.frets[1], 5);
}
#[test]
fn test_fret_at_boundary_not_clamped() {
let data = DiagramData::from_raw("X", "base-fret 1 frets 0 5 0 0 0 0", 6).unwrap();
assert_eq!(data.frets[1], 5);
}
#[test]
fn test_fret_within_range_unchanged() {
let data = DiagramData::from_raw("X", "base-fret 1 frets 0 3 0 0 0 0", 6).unwrap();
assert_eq!(data.frets[1], 3);
}
#[test]
fn test_muted_and_open_not_clamped() {
let data = DiagramData::from_raw("X", "frets x 0 1 2 3 4", 6).unwrap();
assert_eq!(data.frets[0], -1); assert_eq!(data.frets[1], 0); }
#[test]
fn test_extreme_fret_value_clamped() {
let data = DiagramData::from_raw("X", "frets 1000 0 0 0 0 0", 6).unwrap();
assert_eq!(data.frets[0], 5);
}
#[test]
fn test_seven_string_instrument() {
let data = DiagramData::from_raw("X", "frets 0 0 0 0 0 0 0", 7).unwrap();
assert_eq!(data.strings, 7);
let svg = render_svg(&data);
assert!(svg.contains("<svg"));
}
#[test]
fn test_eight_string_instrument() {
let data = DiagramData::from_raw("X", "frets 0 0 0 0 0 0 0 0", 8).unwrap();
assert_eq!(data.strings, 8);
let svg = render_svg(&data);
assert!(svg.contains("<svg"));
}
#[test]
fn test_twelve_string_renders_without_panic() {
let data = DiagramData::from_raw("G12", "frets 0 0 0 0 0 0 0 0 0 0 0 0", 12).unwrap();
let svg = render_svg(&data);
assert!(svg.contains("G12"));
}
#[test]
fn test_fewer_fingers_than_frets() {
let data = DiagramData::from_raw("Am", "frets x 0 2 2 1 0 fingers 0 0 2", 6).unwrap();
assert_eq!(data.frets.len(), 6);
assert_eq!(data.fingers.len(), 3);
let svg = render_svg(&data);
assert!(svg.contains("<svg"));
}
#[test]
fn test_empty_frets_rejected() {
assert!(DiagramData::from_raw("X", "base-fret 1", 6).is_none());
}
#[test]
fn test_non_numeric_fret_treated_as_muted() {
let data = DiagramData::from_raw("X", "frets abc 0 0 0 0 0", 6).unwrap();
assert_eq!(data.frets[0], -1);
}
#[test]
fn test_extreme_base_fret() {
let data = DiagramData::from_raw("X", "base-fret 1000 frets 1 2 3 4 5 6", 6).unwrap();
assert_eq!(data.base_fret, 24);
let svg = render_svg(&data);
assert!(svg.contains("24fr"));
}
#[test]
fn test_all_muted_strings() {
let data = DiagramData::from_raw("X", "frets x x x x x x", 6).unwrap();
assert!(data.frets.iter().all(|&f| f == -1));
let svg = render_svg(&data);
assert!(svg.contains("<svg"));
}
#[test]
fn test_all_open_strings() {
let data = DiagramData::from_raw("Open", "frets 0 0 0 0 0 0", 6).unwrap();
assert!(data.frets.iter().all(|&f| f == 0));
let svg = render_svg(&data);
assert!(svg.contains("<svg"));
}
#[test]
fn test_format_stops_fret_parsing() {
let data = DiagramData::from_raw("Am", "base-fret 1 frets x 0 2 2 1 0 format", 6).unwrap();
assert_eq!(data.frets, vec![-1, 0, 2, 2, 1, 0]);
}
#[test]
fn test_base_fret_after_frets_is_stop_word() {
let data = DiagramData::from_raw("Am", "frets x 0 2 2 1 0 base-fret 3", 6).unwrap();
assert_eq!(data.frets, vec![-1, 0, 2, 2, 1, 0]);
assert_eq!(data.base_fret, 3);
}
#[test]
fn test_base_fret_after_fingers_is_stop_word() {
let data =
DiagramData::from_raw("C", "frets x 3 2 0 1 0 fingers 0 3 2 0 1 0 base-fret 2", 6)
.unwrap();
assert_eq!(data.fingers, vec![0, 3, 2, 0, 1, 0]);
assert_eq!(data.base_fret, 2);
}
#[test]
fn test_format_stops_finger_parsing() {
let data =
DiagramData::from_raw("C", "frets x 3 2 0 1 0 fingers 0 3 2 0 1 0 format", 6).unwrap();
assert_eq!(data.fingers, vec![0, 3, 2, 0, 1, 0]);
}
#[test]
fn test_base_fret_zero_clamped_to_one() {
let data = DiagramData::from_raw("Am", "base-fret 0 frets x 0 2 2 1 0", 6).unwrap();
assert_eq!(data.base_fret, 1);
}
#[test]
fn test_base_fret_negative_defaults_to_one() {
let data = DiagramData::from_raw("Am", "base-fret -1 frets x 0 2 2 1 0", 6).unwrap();
assert_eq!(data.base_fret, 1);
}
#[test]
fn test_base_fret_large_value_clamped() {
let data = DiagramData::from_raw("Am", "base-fret 100 frets x 0 2 2 1 0", 6).unwrap();
assert_eq!(data.base_fret, 24);
}
#[test]
fn test_render_svg_zero_strings_returns_empty() {
let data = DiagramData {
name: "X".to_string(),
display_name: None,
strings: 0,
frets_shown: 5,
base_fret: 1,
frets: vec![],
fingers: vec![],
};
assert!(render_svg(&data).is_empty());
}
#[test]
fn test_render_svg_one_string_returns_empty() {
let data = DiagramData {
name: "X".to_string(),
display_name: None,
strings: 1,
frets_shown: 5,
base_fret: 1,
frets: vec![0],
fingers: vec![],
};
assert!(render_svg(&data).is_empty());
}
#[test]
fn test_frets_stops_finger_parsing() {
let data =
DiagramData::from_raw("Am", "frets x 0 2 2 1 0 fingers 0 0 2 frets x 0 2 2 1 0", 6)
.unwrap();
assert_eq!(data.fingers, vec![0, 0, 2]);
assert_eq!(data.frets.len(), 12);
assert_eq!(data.strings, 12);
}
#[test]
fn test_repeated_frets_keyword_not_consumed_as_value() {
let data = DiagramData::from_raw("Am", "frets 1 2 3 frets 4 5 6", 0).unwrap();
assert_eq!(data.frets.len(), 6);
assert!(!data.frets.contains(&-1));
}
#[test]
fn test_from_raw_frets_custom_frets_shown() {
let data =
DiagramData::from_raw_frets("Am", "base-fret 1 frets x 0 2 2 1 0", 6, 4).unwrap();
assert_eq!(data.frets_shown, 4);
}
#[test]
fn test_from_raw_infer_frets_custom_frets_shown() {
let data =
DiagramData::from_raw_infer_frets("Am", "base-fret 1 frets x 0 2 2 1 0", 3).unwrap();
assert_eq!(data.frets_shown, 3);
}
#[test]
fn test_from_raw_frets_clamps_fret_values() {
let data =
DiagramData::from_raw_frets("Am", "base-fret 1 frets 0 5 0 0 0 0", 6, 3).unwrap();
assert_eq!(data.frets[1], 3);
}
#[test]
fn test_render_svg_zero_frets_shown_returns_empty() {
let data = DiagramData {
name: "X".to_string(),
display_name: None,
strings: 6,
frets_shown: 0,
base_fret: 1,
frets: vec![0, 0, 0, 0, 0, 0],
fingers: vec![],
};
assert!(render_svg(&data).is_empty());
}
#[test]
fn test_render_svg_exceeding_max_strings_returns_empty() {
let data = DiagramData {
name: "X".to_string(),
display_name: None,
strings: 13,
frets_shown: 5,
base_fret: 1,
frets: vec![0; 13],
fingers: vec![],
};
assert!(render_svg(&data).is_empty());
}
#[test]
fn test_render_svg_at_max_strings_ok() {
let data = DiagramData {
name: "X".to_string(),
display_name: None,
strings: MAX_STRINGS,
frets_shown: 5,
base_fret: 1,
frets: vec![0; MAX_STRINGS],
fingers: vec![],
};
assert!(!render_svg(&data).is_empty());
}
#[test]
fn test_render_svg_exceeding_max_frets_shown_returns_empty() {
let data = DiagramData {
name: "X".to_string(),
display_name: None,
strings: 6,
frets_shown: MAX_FRETS_SHOWN + 1,
base_fret: 1,
frets: vec![0; 6],
fingers: vec![],
};
assert!(render_svg(&data).is_empty());
}
#[test]
fn test_render_svg_at_max_frets_shown_ok() {
let data = DiagramData {
name: "X".to_string(),
display_name: None,
strings: 6,
frets_shown: MAX_FRETS_SHOWN,
base_fret: 1,
frets: vec![0; 6],
fingers: vec![],
};
assert!(!render_svg(&data).is_empty());
}
#[test]
fn test_from_raw_frets_clamps_excessive_frets_shown() {
let data = DiagramData::from_raw_frets("Am", "frets x 0 2 2 1 0", 6, 100).unwrap();
assert_eq!(data.frets_shown, MAX_FRETS_SHOWN);
}
#[test]
fn test_from_raw_frets_clamps_zero_frets_shown() {
let data = DiagramData::from_raw_frets("Am", "frets x 0 2 2 1 0", 6, 0).unwrap();
assert_eq!(data.frets_shown, MIN_FRETS_SHOWN);
}
#[test]
fn test_duplicate_fingers_keyword_is_stop_word() {
let data = DiagramData::from_raw("Am", "frets x 0 2 2 1 0 fingers 0 0 2 fingers 0 0 2", 6)
.unwrap();
assert_eq!(data.fingers.len(), 6);
assert_eq!(data.fingers, vec![0, 0, 2, 0, 0, 2]);
}
#[test]
fn test_finger_overflow_beyond_u8_max_becomes_zero() {
let data =
DiagramData::from_raw("Am", "frets x 0 2 2 1 0 fingers 256 1 2 3 1 0", 6).unwrap();
assert_eq!(data.fingers[0], 0, "256 should overflow u8 and become 0");
assert_eq!(data.fingers[1], 1);
}
#[test]
fn test_negative_fret_below_minus_one_clamped() {
let data = DiagramData::from_raw("X", "frets -5 0 2 2 1 0", 6).unwrap();
assert_eq!(data.frets[0], -1);
}
#[test]
fn test_minus_one_fret_unchanged() {
let data = DiagramData::from_raw("X", "frets -1 0 2 2 1 0", 6).unwrap();
assert_eq!(data.frets[0], -1);
}
#[test]
fn test_large_negative_fret_clamped() {
let data = DiagramData::from_raw("X", "frets -100 0 0 0 0 0", 6).unwrap();
assert_eq!(data.frets[0], -1);
}
#[test]
fn test_mixed_case_keywords() {
let data =
DiagramData::from_raw("Am", "Base-Fret 3 Frets x 0 2 2 1 0 Fingers 0 0 2 3 1 0", 6)
.unwrap();
assert_eq!(data.base_fret, 3);
assert_eq!(data.frets, vec![-1, 0, 2, 2, 1, 0]);
assert_eq!(data.fingers, vec![0, 0, 2, 3, 1, 0]);
}
#[test]
fn test_uppercase_keywords() {
let data =
DiagramData::from_raw("Am", "BASE-FRET 2 FRETS X 0 2 2 1 0 FINGERS 0 0 2 3 1 0", 6)
.unwrap();
assert_eq!(data.base_fret, 2);
assert_eq!(data.frets, vec![-1, 0, 2, 2, 1, 0]);
assert_eq!(data.fingers, vec![0, 0, 2, 3, 1, 0]);
}
#[test]
fn test_mixed_case_display_stop_word() {
let data = DiagramData::from_raw("Am", "frets x 0 2 2 1 0 Display", 6).unwrap();
assert_eq!(data.frets, vec![-1, 0, 2, 2, 1, 0]);
}
#[test]
fn test_mixed_case_format_stop_word() {
let data = DiagramData::from_raw("Am", "frets x 0 2 2 1 0 Format", 6).unwrap();
assert_eq!(data.frets, vec![-1, 0, 2, 2, 1, 0]);
}
#[test]
fn test_fewer_fret_values_than_num_strings() {
let data = DiagramData::from_raw("X", "frets 1 2 3", 6).unwrap();
assert_eq!(data.strings, 6);
assert_eq!(data.frets.len(), 3);
let svg = render_svg(&data);
assert!(svg.contains("<svg"));
}
#[test]
fn test_fewer_fret_values_inferred() {
let data = DiagramData::from_raw_infer("X", "frets 1 2 3").unwrap();
assert_eq!(data.strings, 3);
assert_eq!(data.frets.len(), 3);
}
}