1use crate::inline_markup::TextSpan;
27
28#[derive(Debug, Clone, PartialEq)]
48pub struct Song {
49 pub metadata: Metadata,
51 pub lines: Vec<Line>,
53}
54
55impl Song {
56 #[must_use]
58 pub fn new() -> Self {
59 Self {
60 metadata: Metadata::new(),
61 lines: Vec::new(),
62 }
63 }
64
65 #[must_use]
74 pub fn config_overrides(&self) -> Vec<(&str, &str)> {
75 let mut overrides = Vec::new();
76 for line in &self.lines {
77 if let Line::Directive(directive) = line {
78 if let DirectiveKind::ConfigOverride(ref key) = directive.kind {
79 if let Some(ref value) = directive.value {
80 overrides.push((key.as_str(), value.as_str()));
81 }
82 }
83 }
84 }
85 overrides
86 }
87
88 pub fn apply_define_displays(&mut self) {
98 let mut define_map: Vec<(String, Option<String>, Option<String>)> = Vec::new();
100 for line in &self.lines {
101 if let Line::Directive(directive) = line {
102 if directive.kind == DirectiveKind::Define {
103 if let Some(ref value) = directive.value {
104 let def = ChordDefinition::parse_value(value);
105 if def.display.is_some() || def.format.is_some() {
106 if let Some(entry) =
107 define_map.iter_mut().find(|(n, _, _)| *n == def.name)
108 {
109 entry.1 = def.display;
110 entry.2 = def.format;
111 } else {
112 define_map.push((def.name, def.display, def.format));
113 }
114 }
115 }
116 }
117 }
118 }
119
120 if define_map.is_empty() {
121 return;
122 }
123
124 for line in &mut self.lines {
126 if let Line::Lyrics(lyrics_line) = line {
127 for segment in &mut lyrics_line.segments {
128 if let Some(ref mut chord) = segment.chord {
129 if chord.display.is_none() {
130 if let Some((_, display, format)) =
131 define_map.iter().find(|(n, _, _)| *n == chord.name)
132 {
133 if let Some(d) = display {
134 chord.display = Some(d.clone());
136 } else if let Some(f) = format {
137 if let Some(expanded) = chord.expand_format(f) {
139 chord.display = Some(expanded);
140 }
141 }
142 }
143 }
144 }
145 }
146 }
147 }
148 }
149
150 #[must_use]
158 pub fn fretted_defines(&self) -> Vec<(String, String)> {
159 let mut result: Vec<(String, String)> = Vec::new();
160 for line in &self.lines {
161 if let Line::Directive(directive) = line {
162 if directive.kind == DirectiveKind::Define
163 || directive.kind == DirectiveKind::ChordDirective
164 {
165 if let Some(ref value) = directive.value {
166 let def = ChordDefinition::parse_value(value);
167 if let Some(raw) = def.raw {
168 if let Some(pos) = result.iter().position(|(n, _)| *n == def.name) {
169 result[pos].1 = raw;
170 } else {
171 result.push((def.name, raw));
172 }
173 }
174 }
175 }
176 }
177 }
178 result
179 }
180
181 #[must_use]
191 pub fn keyboard_defines(&self) -> Vec<(String, Vec<i32>)> {
192 let mut result: Vec<(String, Vec<i32>)> = Vec::new();
193 for line in &self.lines {
194 if let Line::Directive(directive) = line {
195 if directive.kind == DirectiveKind::Define
196 || directive.kind == DirectiveKind::ChordDirective
197 {
198 if let Some(ref value) = directive.value {
199 let def = ChordDefinition::parse_value(value);
200 if let Some(keys) = def.keys {
201 if let Some(pos) = result.iter().position(|(n, _)| *n == def.name) {
202 result[pos].1 = keys;
203 } else {
204 result.push((def.name, keys));
205 }
206 }
207 }
208 }
209 }
210 }
211 result
212 }
213
214 #[must_use]
221 pub fn used_chord_names(&self) -> Vec<String> {
222 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
223 let mut result: Vec<String> = Vec::new();
224 for line in &self.lines {
225 if let Line::Lyrics(lyrics) = line {
226 for seg in &lyrics.segments {
227 if let Some(ref chord) = seg.chord {
228 if seen.insert(chord.name.clone()) {
229 result.push(chord.name.clone());
230 }
231 }
232 }
233 }
234 }
235 result
236 }
237}
238
239impl Default for Song {
240 fn default() -> Self {
241 Self::new()
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Default)]
262pub struct Metadata {
263 pub title: Option<String>,
265 pub subtitles: Vec<String>,
267 pub artists: Vec<String>,
269 pub composers: Vec<String>,
271 pub lyricists: Vec<String>,
273 pub album: Option<String>,
275 pub year: Option<String>,
277 pub key: Option<String>,
279 pub tempo: Option<String>,
281 pub time: Option<String>,
283 pub capo: Option<String>,
285 pub sort_title: Option<String>,
287 pub sort_artist: Option<String>,
289 pub arrangers: Vec<String>,
291 pub copyright: Option<String>,
293 pub duration: Option<String>,
295 pub tags: Vec<String>,
297 pub custom: Vec<(String, String)>,
304}
305
306impl Metadata {
307 #[must_use]
309 pub fn new() -> Self {
310 Self::default()
311 }
312}
313
314#[derive(Debug, Clone, PartialEq)]
323pub enum Line {
324 Lyrics(LyricsLine),
328
329 Directive(Directive),
331
332 Comment(CommentStyle, String),
336
337 Empty,
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
356pub enum CommentStyle {
357 Normal,
359 Italic,
361 Boxed,
363}
364
365#[derive(Debug, Clone, PartialEq)]
403pub struct LyricsLine {
404 pub segments: Vec<LyricsSegment>,
406}
407
408impl LyricsLine {
409 #[must_use]
411 pub fn new() -> Self {
412 Self {
413 segments: Vec::new(),
414 }
415 }
416
417 #[must_use]
419 pub fn text(&self) -> String {
420 self.segments.iter().map(|s| s.text.as_str()).collect()
421 }
422
423 #[must_use]
425 pub fn has_chords(&self) -> bool {
426 self.segments.iter().any(|s| s.chord.is_some())
427 }
428}
429
430impl Default for LyricsLine {
431 fn default() -> Self {
432 Self::new()
433 }
434}
435
436#[derive(Debug, Clone, PartialEq)]
448pub struct LyricsSegment {
449 pub chord: Option<Chord>,
451 pub text: String,
457 pub spans: Vec<TextSpan>,
464}
465
466impl LyricsSegment {
467 #[must_use]
469 pub fn new(chord: Option<Chord>, text: impl Into<String>) -> Self {
470 Self {
471 chord,
472 text: text.into(),
473 spans: Vec::new(),
474 }
475 }
476
477 #[must_use]
479 pub fn text_only(text: impl Into<String>) -> Self {
480 Self {
481 chord: None,
482 text: text.into(),
483 spans: Vec::new(),
484 }
485 }
486
487 #[must_use]
489 pub fn chord_only(chord: Chord) -> Self {
490 Self {
491 chord: Some(chord),
492 text: String::new(),
493 spans: Vec::new(),
494 }
495 }
496
497 #[must_use]
499 pub fn with_spans(chord: Option<Chord>, text: impl Into<String>, spans: Vec<TextSpan>) -> Self {
500 Self {
501 chord,
502 text: text.into(),
503 spans,
504 }
505 }
506
507 #[must_use]
509 pub fn has_markup(&self) -> bool {
510 !self.spans.is_empty()
511 }
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Hash)]
529pub struct Chord {
530 pub name: String,
532 pub detail: Option<crate::chord::ChordDetail>,
534 pub display: Option<String>,
538}
539
540impl Chord {
541 #[must_use]
547 pub fn new(name: impl Into<String>) -> Self {
548 let name = name.into();
549 let detail = crate::chord::parse_chord(&name);
550 Self {
551 name,
552 detail,
553 display: None,
554 }
555 }
556
557 #[must_use]
562 pub fn display_name(&self) -> &str {
563 self.display.as_deref().unwrap_or(&self.name)
564 }
565
566 #[must_use]
576 pub fn expand_format(&self, pattern: &str) -> Option<String> {
577 let detail = self.detail.as_ref()?;
578
579 let root = {
580 let mut s = detail.root.to_string();
581 if let Some(ref acc) = detail.root_accidental {
582 s.push_str(&acc.to_string());
583 }
584 s
585 };
586 let quality = detail.quality.to_string();
587 let ext = detail.extension.as_deref().unwrap_or("");
588 let bass = detail
589 .bass_note
590 .as_ref()
591 .map_or(String::new(), |(note, acc)| {
592 let mut s = note.to_string();
593 if let Some(a) = acc {
594 s.push_str(&a.to_string());
595 }
596 s
597 });
598
599 let result = pattern
600 .replace("%{root}", &root)
601 .replace("%{quality}", &quality)
602 .replace("%{ext}", ext)
603 .replace("%{bass}", &bass);
604
605 Some(result)
606 }
607}
608
609impl core::fmt::Display for Chord {
610 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
611 f.write_str(self.display_name())
612 }
613}
614
615#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
638pub struct ImageAttributes {
639 pub src: String,
641 pub width: Option<String>,
643 pub height: Option<String>,
645 pub scale: Option<String>,
647 pub title: Option<String>,
649 pub anchor: Option<String>,
651}
652
653impl ImageAttributes {
654 #[must_use]
657 pub fn new(src: impl Into<String>) -> Self {
658 Self {
659 src: src.into(),
660 ..Self::default()
661 }
662 }
663
664 #[must_use]
669 pub fn has_src(&self) -> bool {
670 !self.src.is_empty()
671 }
672}
673
674fn extract_attribute(s: &mut String, key: &str) -> Option<String> {
692 let needle = format!("{key}=");
693 let match_pos = s
694 .match_indices(&needle)
695 .find(|&(pos, _)| pos == 0 || s.as_bytes()[pos - 1].is_ascii_whitespace());
696
697 let (pos, _) = match_pos?;
698 let after = &s[pos + needle.len()..];
699
700 let (val, token_end) = if let Some(stripped) = after.strip_prefix('"') {
701 let close = stripped.find('"').unwrap_or(stripped.len());
703 let v = stripped[..close].to_string();
704 let has_close = close < stripped.len();
706 (
707 Some(v),
708 pos + needle.len() + 1 + close + usize::from(has_close),
709 )
710 } else {
711 match after.split_whitespace().next() {
712 Some(t) if !t.contains('=') => (Some(t.to_string()), pos + needle.len() + t.len()),
713 Some(_) | None => (Some(String::new()), pos + needle.len()),
717 }
718 };
719
720 let before = s[..pos].trim_end();
722 let after_token = s[token_end..].trim_start();
723 *s = if before.is_empty() {
724 after_token.to_string()
725 } else if after_token.is_empty() {
726 before.to_string()
727 } else {
728 format!("{before} {after_token}")
729 };
730
731 val
732}
733
734#[derive(Debug, Clone, PartialEq, Eq)]
751pub struct ChordDefinition {
752 pub name: String,
754 pub keys: Option<Vec<i32>>,
759 pub copy: Option<String>,
761 pub copyall: Option<String>,
763 pub display: Option<String>,
765 pub format: Option<String>,
771 pub raw: Option<String>,
773}
774
775impl ChordDefinition {
776 #[must_use]
781 pub fn parse_value(value: &str) -> Self {
782 let value = value.trim();
783 let mut parts = value.splitn(2, char::is_whitespace);
784 let name = parts
789 .next()
790 .expect("splitn always yields at least one element")
791 .to_string();
792 let rest = parts.next().unwrap_or("").trim();
795
796 let mut def = Self {
797 name,
798 keys: None,
799 copy: None,
800 copyall: None,
801 display: None,
802 format: None,
803 raw: None,
804 };
805
806 if rest.is_empty() {
807 return def;
808 }
809
810 let mut remaining = rest.to_string();
813 def.display = extract_attribute(&mut remaining, "display");
814 def.format = extract_attribute(&mut remaining, "format");
815 let remaining = remaining.trim();
816
817 if remaining.is_empty() {
818 return def;
819 }
820
821 if let Some(keys_str) = remaining.strip_prefix("keys").and_then(|rest| {
826 if rest.is_empty() || rest.starts_with(|c: char| c.is_ascii_whitespace()) {
827 Some(rest)
828 } else {
829 None
830 }
831 }) {
832 let keys: Vec<i32> = keys_str
833 .split_whitespace()
834 .filter_map(|s| s.parse::<i32>().ok())
835 .filter(|&v| (0..=127).contains(&v))
836 .collect();
837 def.keys = if keys.is_empty() { None } else { Some(keys) };
839 return def;
840 }
841
842 if let Some(rest) = remaining.strip_prefix("copyall").and_then(|r| {
846 if r.is_empty() || r.starts_with(|c: char| c.is_ascii_whitespace()) {
847 Some(r)
848 } else {
849 None
850 }
851 }) {
852 let name = rest.split_whitespace().next().unwrap_or("").trim();
853 if !name.is_empty() {
854 def.copyall = Some(name.to_string());
855 }
856 return def;
857 }
858 if let Some(rest) = remaining.strip_prefix("copy").and_then(|r| {
859 if r.is_empty() || r.starts_with(|c: char| c.is_ascii_whitespace()) {
860 Some(r)
861 } else {
862 None
863 }
864 }) {
865 let name = rest.split_whitespace().next().unwrap_or("").trim();
866 if !name.is_empty() {
867 def.copy = Some(name.to_string());
868 }
869 return def;
870 }
871
872 def.raw = if remaining.is_empty() {
873 None
874 } else {
875 Some(remaining.to_string())
876 };
877
878 def
879 }
880}
881
882#[derive(Debug, Clone, PartialEq, Eq, Hash)]
905pub enum DirectiveKind {
906 Title,
909 Subtitle,
911 Artist,
913 Composer,
915 Lyricist,
917 Album,
919 Year,
921 Key,
923 Tempo,
925 Time,
927 Capo,
929 SortTitle,
931 SortArtist,
933 Arranger,
935 Copyright,
937 Duration,
939 Tag,
941
942 Transpose,
945
946 Comment,
949 CommentItalic,
951 CommentBox,
953
954 StartOfChorus,
957 EndOfChorus,
959 StartOfVerse,
961 EndOfVerse,
963 StartOfBridge,
965 EndOfBridge,
967 StartOfTab,
969 EndOfTab,
971 StartOfGrid,
973 EndOfGrid,
975
976 TextFont,
979 TextSize,
981 TextColour,
983 ChordFont,
985 ChordSize,
987 ChordColour,
989 TabFont,
991 TabSize,
993 TabColour,
995
996 Chorus,
1001
1002 NewPage,
1005 NewPhysicalPage,
1007 ColumnBreak,
1009 Columns,
1011
1012 TitleFont,
1015 TitleSize,
1017 TitleColour,
1019 ChorusFont,
1021 ChorusSize,
1023 ChorusColour,
1025 FooterFont,
1027 FooterSize,
1029 FooterColour,
1031 HeaderFont,
1033 HeaderSize,
1035 HeaderColour,
1037 LabelFont,
1039 LabelSize,
1041 LabelColour,
1043 GridFont,
1045 GridSize,
1047 GridColour,
1049 TocFont,
1051 TocSize,
1053 TocColour,
1055
1056 NewSong,
1059
1060 Define,
1063 ChordDirective,
1065
1066 StartOfAbc,
1070 EndOfAbc,
1072 StartOfLy,
1075 EndOfLy,
1077 StartOfSvg,
1080 EndOfSvg,
1082 StartOfTextblock,
1085 EndOfTextblock,
1087 StartOfMusicxml,
1090 EndOfMusicxml,
1092
1093 StartOfSection(String),
1097 EndOfSection(String),
1100
1101 Meta(String),
1108
1109 Diagrams,
1114 NoDiagrams,
1117
1118 Image(ImageAttributes),
1121
1122 ConfigOverride(String),
1129
1130 Unknown(String),
1134}
1135
1136impl DirectiveKind {
1137 #[must_use]
1142 pub fn from_name(name: &str) -> Self {
1143 match name.to_ascii_lowercase().as_str() {
1144 "title" | "t" => Self::Title,
1146 "subtitle" | "st" => Self::Subtitle,
1147 "artist" => Self::Artist,
1148 "composer" => Self::Composer,
1149 "lyricist" => Self::Lyricist,
1150 "album" => Self::Album,
1151 "year" => Self::Year,
1152 "key" => Self::Key,
1153 "tempo" => Self::Tempo,
1154 "time" => Self::Time,
1155 "capo" => Self::Capo,
1156 "sorttitle" => Self::SortTitle,
1157 "sortartist" => Self::SortArtist,
1158 "arranger" => Self::Arranger,
1159 "copyright" => Self::Copyright,
1160 "duration" => Self::Duration,
1161 "tag" => Self::Tag,
1162
1163 "transpose" => Self::Transpose,
1165
1166 "new_song" | "ns" => Self::NewSong,
1168
1169 "comment" | "c" => Self::Comment,
1171 "comment_italic" | "ci" => Self::CommentItalic,
1172 "comment_box" | "cb" => Self::CommentBox,
1173
1174 "start_of_chorus" | "soc" => Self::StartOfChorus,
1176 "end_of_chorus" | "eoc" => Self::EndOfChorus,
1177 "start_of_verse" | "sov" => Self::StartOfVerse,
1178 "end_of_verse" | "eov" => Self::EndOfVerse,
1179 "start_of_bridge" | "sob" => Self::StartOfBridge,
1180 "end_of_bridge" | "eob" => Self::EndOfBridge,
1181 "start_of_tab" | "sot" => Self::StartOfTab,
1182 "end_of_tab" | "eot" => Self::EndOfTab,
1183 "start_of_grid" | "sog" => Self::StartOfGrid,
1184 "end_of_grid" | "eog" => Self::EndOfGrid,
1185
1186 "textfont" | "tf" => Self::TextFont,
1188 "textsize" | "ts" => Self::TextSize,
1189 "textcolour" | "textcolor" | "tc" => Self::TextColour,
1190 "chordfont" | "cf" => Self::ChordFont,
1191 "chordsize" | "cs" => Self::ChordSize,
1192 "chordcolour" | "chordcolor" | "cc" => Self::ChordColour,
1193 "tabfont" => Self::TabFont,
1194 "tabsize" => Self::TabSize,
1195 "tabcolour" | "tabcolor" => Self::TabColour,
1196
1197 "start_of_abc" => Self::StartOfAbc,
1199 "end_of_abc" => Self::EndOfAbc,
1200 "start_of_ly" => Self::StartOfLy,
1201 "end_of_ly" => Self::EndOfLy,
1202 "start_of_svg" => Self::StartOfSvg,
1203 "end_of_svg" => Self::EndOfSvg,
1204 "start_of_textblock" => Self::StartOfTextblock,
1205 "end_of_textblock" => Self::EndOfTextblock,
1206 "start_of_musicxml" => Self::StartOfMusicxml,
1207 "end_of_musicxml" => Self::EndOfMusicxml,
1208
1209 "chorus" => Self::Chorus,
1211
1212 "new_page" | "np" => Self::NewPage,
1214 "new_physical_page" | "npp" => Self::NewPhysicalPage,
1215 "column_break" | "colb" => Self::ColumnBreak,
1216 "columns" | "col" => Self::Columns,
1217
1218 "titlefont" => Self::TitleFont,
1220 "titlesize" => Self::TitleSize,
1221 "titlecolour" | "titlecolor" => Self::TitleColour,
1222 "chorusfont" => Self::ChorusFont,
1223 "chorussize" => Self::ChorusSize,
1224 "choruscolour" | "choruscolor" => Self::ChorusColour,
1225 "footerfont" => Self::FooterFont,
1226 "footersize" => Self::FooterSize,
1227 "footercolour" | "footercolor" => Self::FooterColour,
1228 "headerfont" => Self::HeaderFont,
1229 "headersize" => Self::HeaderSize,
1230 "headercolour" | "headercolor" => Self::HeaderColour,
1231 "labelfont" => Self::LabelFont,
1232 "labelsize" => Self::LabelSize,
1233 "labelcolour" | "labelcolor" => Self::LabelColour,
1234 "gridfont" => Self::GridFont,
1235 "gridsize" => Self::GridSize,
1236 "gridcolour" | "gridcolor" => Self::GridColour,
1237 "tocfont" => Self::TocFont,
1238 "tocsize" => Self::TocSize,
1239 "toccolour" | "toccolor" => Self::TocColour,
1240
1241 "define" => Self::Define,
1243 "chord" => Self::ChordDirective,
1244 "diagrams" => Self::Diagrams,
1245 "no_diagrams" | "nodiagrams" => Self::NoDiagrams,
1246
1247 "meta" => Self::Meta(String::new()),
1249
1250 "image" => Self::Image(ImageAttributes::default()),
1253
1254 other => {
1256 if let Some(key) = other.strip_prefix("+config.") {
1258 if !key.is_empty() {
1259 return Self::ConfigOverride(key.to_string());
1260 }
1261 }
1262 if let Some(section) = other.strip_prefix("start_of_") {
1263 if !section.is_empty() {
1264 return Self::StartOfSection(section.to_string());
1265 }
1266 }
1267 if let Some(section) = other.strip_prefix("end_of_") {
1268 if !section.is_empty() {
1269 return Self::EndOfSection(section.to_string());
1270 }
1271 }
1272 Self::Unknown(other.to_string())
1273 }
1274 }
1275 }
1276
1277 #[must_use]
1299 pub fn resolve_with_selector(name: &str) -> (Self, Option<String>) {
1300 let kind = Self::from_name(name);
1301
1302 let is_known = !matches!(
1305 kind,
1306 Self::Unknown(_) | Self::StartOfSection(_) | Self::EndOfSection(_)
1307 );
1308 if is_known {
1309 return (kind, None);
1310 }
1311
1312 if let Some(last_hyphen) = name.rfind('-') {
1314 let prefix = &name[..last_hyphen];
1315 let suffix = &name[last_hyphen + 1..];
1316
1317 if !prefix.is_empty() && !suffix.is_empty() {
1318 let prefix_kind = Self::from_name(prefix);
1319 if !matches!(prefix_kind, Self::Unknown(_)) {
1320 return (prefix_kind, Some(suffix.to_ascii_lowercase()));
1321 }
1322 }
1323 }
1324
1325 (kind, None)
1328 }
1329
1330 #[must_use]
1334 pub fn canonical_name(&self) -> &str {
1335 match self {
1336 Self::Title => "title",
1337 Self::Subtitle => "subtitle",
1338 Self::Artist => "artist",
1339 Self::Composer => "composer",
1340 Self::Lyricist => "lyricist",
1341 Self::Album => "album",
1342 Self::Year => "year",
1343 Self::Key => "key",
1344 Self::Tempo => "tempo",
1345 Self::Time => "time",
1346 Self::Capo => "capo",
1347 Self::SortTitle => "sorttitle",
1348 Self::SortArtist => "sortartist",
1349 Self::Arranger => "arranger",
1350 Self::Copyright => "copyright",
1351 Self::Duration => "duration",
1352 Self::Tag => "tag",
1353 Self::Transpose => "transpose",
1354 Self::NewSong => "new_song",
1355 Self::Comment => "comment",
1356 Self::CommentItalic => "comment_italic",
1357 Self::CommentBox => "comment_box",
1358 Self::StartOfChorus => "start_of_chorus",
1359 Self::EndOfChorus => "end_of_chorus",
1360 Self::StartOfVerse => "start_of_verse",
1361 Self::EndOfVerse => "end_of_verse",
1362 Self::StartOfBridge => "start_of_bridge",
1363 Self::EndOfBridge => "end_of_bridge",
1364 Self::StartOfTab => "start_of_tab",
1365 Self::EndOfTab => "end_of_tab",
1366 Self::StartOfGrid => "start_of_grid",
1367 Self::EndOfGrid => "end_of_grid",
1368
1369 Self::TextFont => "textfont",
1370 Self::TextSize => "textsize",
1371 Self::TextColour => "textcolour",
1372 Self::ChordFont => "chordfont",
1373 Self::ChordSize => "chordsize",
1374 Self::ChordColour => "chordcolour",
1375 Self::TabFont => "tabfont",
1376 Self::TabSize => "tabsize",
1377 Self::TabColour => "tabcolour",
1378 Self::TitleFont => "titlefont",
1379 Self::TitleSize => "titlesize",
1380 Self::TitleColour => "titlecolour",
1381 Self::ChorusFont => "chorusfont",
1382 Self::ChorusSize => "chorussize",
1383 Self::ChorusColour => "choruscolour",
1384 Self::FooterFont => "footerfont",
1385 Self::FooterSize => "footersize",
1386 Self::FooterColour => "footercolour",
1387 Self::HeaderFont => "headerfont",
1388 Self::HeaderSize => "headersize",
1389 Self::HeaderColour => "headercolour",
1390 Self::LabelFont => "labelfont",
1391 Self::LabelSize => "labelsize",
1392 Self::LabelColour => "labelcolour",
1393 Self::GridFont => "gridfont",
1394 Self::GridSize => "gridsize",
1395 Self::GridColour => "gridcolour",
1396 Self::TocFont => "tocfont",
1397 Self::TocSize => "tocsize",
1398 Self::TocColour => "toccolour",
1399 Self::StartOfAbc => "start_of_abc",
1400 Self::EndOfAbc => "end_of_abc",
1401 Self::StartOfLy => "start_of_ly",
1402 Self::EndOfLy => "end_of_ly",
1403 Self::StartOfSvg => "start_of_svg",
1404 Self::EndOfSvg => "end_of_svg",
1405 Self::StartOfTextblock => "start_of_textblock",
1406 Self::EndOfTextblock => "end_of_textblock",
1407 Self::StartOfMusicxml => "start_of_musicxml",
1408 Self::EndOfMusicxml => "end_of_musicxml",
1409 Self::Chorus => "chorus",
1410 Self::NewPage => "new_page",
1411 Self::NewPhysicalPage => "new_physical_page",
1412 Self::ColumnBreak => "column_break",
1413 Self::Columns => "columns",
1414 Self::Define => "define",
1415 Self::ChordDirective => "chord",
1416 Self::Diagrams => "diagrams",
1417 Self::NoDiagrams => "no_diagrams",
1418 Self::Meta(_) => "meta",
1419
1420 Self::Image(_) => "image",
1421 Self::ConfigOverride(key) => key.as_str(),
1422 Self::StartOfSection(name) | Self::EndOfSection(name) | Self::Unknown(name) => {
1423 name.as_str()
1424 }
1425 }
1426 }
1427
1428 #[must_use]
1434 pub fn full_canonical_name(&self) -> String {
1435 match self {
1436 Self::StartOfSection(name) => format!("start_of_{name}"),
1437 Self::EndOfSection(name) => format!("end_of_{name}"),
1438 Self::ConfigOverride(key) => format!("+config.{key}"),
1439 _ => self.canonical_name().to_string(),
1440 }
1441 }
1442
1443 #[must_use]
1445 pub fn is_metadata(&self) -> bool {
1446 matches!(
1447 self,
1448 Self::Title
1449 | Self::Subtitle
1450 | Self::Artist
1451 | Self::Composer
1452 | Self::Lyricist
1453 | Self::Album
1454 | Self::Year
1455 | Self::Key
1456 | Self::Tempo
1457 | Self::Time
1458 | Self::Capo
1459 | Self::SortTitle
1460 | Self::SortArtist
1461 | Self::Arranger
1462 | Self::Copyright
1463 | Self::Duration
1464 | Self::Tag
1465 | Self::Meta(_)
1466 )
1467 }
1468
1469 #[must_use]
1471 pub fn is_comment(&self) -> bool {
1472 matches!(self, Self::Comment | Self::CommentItalic | Self::CommentBox)
1473 }
1474
1475 #[must_use]
1477 pub fn is_font_size_color(&self) -> bool {
1478 matches!(
1479 self,
1480 Self::TextFont
1481 | Self::TextSize
1482 | Self::TextColour
1483 | Self::ChordFont
1484 | Self::ChordSize
1485 | Self::ChordColour
1486 | Self::TabFont
1487 | Self::TabSize
1488 | Self::TabColour
1489 | Self::TitleFont
1490 | Self::TitleSize
1491 | Self::TitleColour
1492 | Self::ChorusFont
1493 | Self::ChorusSize
1494 | Self::ChorusColour
1495 | Self::FooterFont
1496 | Self::FooterSize
1497 | Self::FooterColour
1498 | Self::HeaderFont
1499 | Self::HeaderSize
1500 | Self::HeaderColour
1501 | Self::LabelFont
1502 | Self::LabelSize
1503 | Self::LabelColour
1504 | Self::GridFont
1505 | Self::GridSize
1506 | Self::GridColour
1507 | Self::TocFont
1508 | Self::TocSize
1509 | Self::TocColour
1510 )
1511 }
1512
1513 #[must_use]
1515 pub fn is_section_start(&self) -> bool {
1516 matches!(
1517 self,
1518 Self::StartOfChorus
1519 | Self::StartOfVerse
1520 | Self::StartOfBridge
1521 | Self::StartOfTab
1522 | Self::StartOfGrid
1523 | Self::StartOfAbc
1524 | Self::StartOfLy
1525 | Self::StartOfSvg
1526 | Self::StartOfTextblock
1527 | Self::StartOfMusicxml
1528 | Self::StartOfSection(_)
1529 )
1530 }
1531
1532 #[must_use]
1534 pub fn is_section_end(&self) -> bool {
1535 matches!(
1536 self,
1537 Self::EndOfChorus
1538 | Self::EndOfVerse
1539 | Self::EndOfBridge
1540 | Self::EndOfTab
1541 | Self::EndOfGrid
1542 | Self::EndOfAbc
1543 | Self::EndOfLy
1544 | Self::EndOfSvg
1545 | Self::EndOfTextblock
1546 | Self::EndOfMusicxml
1547 | Self::EndOfSection(_)
1548 )
1549 }
1550
1551 #[must_use]
1553 pub fn is_environment(&self) -> bool {
1554 self.is_section_start() || self.is_section_end()
1555 }
1556
1557 #[must_use]
1559 pub fn is_image(&self) -> bool {
1560 matches!(self, Self::Image(_))
1561 }
1562
1563 #[must_use]
1565 pub fn is_page_control(&self) -> bool {
1566 matches!(
1567 self,
1568 Self::NewPage | Self::NewPhysicalPage | Self::ColumnBreak | Self::Columns
1569 )
1570 }
1571}
1572
1573#[derive(Debug, Clone, PartialEq)]
1617pub struct Directive {
1618 pub name: String,
1620 pub value: Option<String>,
1622 pub kind: DirectiveKind,
1624 pub selector: Option<String>,
1629}
1630
1631impl Directive {
1632 #[must_use]
1637 pub fn with_value(name: impl Into<String>, value: impl Into<String>) -> Self {
1638 let name_str = name.into();
1639 let kind = DirectiveKind::from_name(&name_str);
1640 let canonical = kind.full_canonical_name();
1641 Self {
1642 name: canonical,
1643 value: Some(value.into()),
1644 kind,
1645 selector: None,
1646 }
1647 }
1648
1649 #[must_use]
1654 pub fn name_only(name: impl Into<String>) -> Self {
1655 let name_str = name.into();
1656 let kind = DirectiveKind::from_name(&name_str);
1657 let canonical = kind.full_canonical_name();
1658 Self {
1659 name: canonical,
1660 value: None,
1661 kind,
1662 selector: None,
1663 }
1664 }
1665
1666 #[must_use]
1671 pub fn with_selector(
1672 name: impl Into<String>,
1673 value: Option<String>,
1674 selector: impl Into<String>,
1675 ) -> Self {
1676 let name_str = name.into();
1677 let kind = DirectiveKind::from_name(&name_str);
1678 let canonical = kind.full_canonical_name();
1679 Self {
1680 name: canonical,
1681 value,
1682 kind,
1683 selector: Some(selector.into().to_ascii_lowercase()),
1684 }
1685 }
1686
1687 #[must_use]
1690 pub fn is_section_start(&self) -> bool {
1691 self.kind.is_section_start()
1692 }
1693
1694 #[must_use]
1697 pub fn is_section_end(&self) -> bool {
1698 self.kind.is_section_end()
1699 }
1700
1701 #[must_use]
1704 pub fn section_name(&self) -> Option<&str> {
1705 if let Some(suffix) = self.name.strip_prefix("start_of_") {
1706 Some(suffix)
1707 } else if let Some(suffix) = self.name.strip_prefix("end_of_") {
1708 Some(suffix)
1709 } else {
1710 None
1711 }
1712 }
1713}
1714
1715#[cfg(test)]
1720mod tests {
1721 use super::*;
1722 use crate::chord::{Accidental, ChordQuality, Note};
1723
1724 #[test]
1727 fn song_new_is_empty() {
1728 let song = Song::new();
1729 assert!(song.lines.is_empty());
1730 assert_eq!(song.metadata.title, None);
1731 }
1732
1733 #[test]
1734 fn song_default_equals_new() {
1735 assert_eq!(Song::default(), Song::new());
1736 }
1737
1738 #[test]
1739 fn used_chord_names_empty() {
1740 let song = crate::parse("{title: Test}").unwrap();
1741 assert!(song.used_chord_names().is_empty());
1742 }
1743
1744 #[test]
1745 fn used_chord_names_order_and_dedup() {
1746 let song = crate::parse("[Am]one [G]two [Am]three [C]four").unwrap();
1747 assert_eq!(song.used_chord_names(), vec!["Am", "G", "C"]);
1748 }
1749
1750 #[test]
1751 fn fretted_defines_empty() {
1752 let song = crate::parse("{title: Test}").unwrap();
1753 assert!(song.fretted_defines().is_empty());
1754 }
1755
1756 #[test]
1757 fn fretted_defines_returns_raw_only() {
1758 let input = "{define: Am base-fret 1 frets x 0 2 2 1 0}\n{define: G keys 0 4 7}";
1759 let song = crate::parse(input).unwrap();
1760 let defs = song.fretted_defines();
1761 assert_eq!(defs.len(), 1);
1762 assert_eq!(defs[0].0, "Am");
1763 }
1764
1765 #[test]
1766 fn fretted_defines_later_overrides_earlier() {
1767 let input = "{define: Am base-fret 1 frets x 0 2 2 1 0}\n{define: Am base-fret 1 frets x 0 2 2 0 0}";
1768 let song = crate::parse(input).unwrap();
1769 let defs = song.fretted_defines();
1770 assert_eq!(defs.len(), 1);
1771 assert!(
1772 defs[0].1.contains("0 0"),
1773 "later define should override earlier"
1774 );
1775 }
1776
1777 #[test]
1778 fn fretted_defines_chord_directive_alias() {
1779 let input_chord = "{chord: Am base-fret 1 frets x 0 2 2 1 0}";
1781 let input_define = "{define: Am base-fret 1 frets x 0 2 2 1 0}";
1782 let defs_chord = crate::parse(input_chord).unwrap().fretted_defines();
1783 let defs_define = crate::parse(input_define).unwrap().fretted_defines();
1784 assert_eq!(
1785 defs_chord.len(),
1786 1,
1787 "{{chord:}} must appear in fretted_defines"
1788 );
1789 assert_eq!(defs_chord[0].0, defs_define[0].0, "chord names must match");
1790 assert_eq!(defs_chord[0].1, defs_define[0].1, "raw values must match");
1791 }
1792
1793 #[test]
1794 fn song_with_lines() {
1795 let mut song = Song::new();
1796 song.metadata.title = Some("My Song".to_string());
1797 song.lines.push(Line::Empty);
1798 song.lines
1799 .push(Line::Comment(CommentStyle::Normal, "A comment".to_string()));
1800 assert_eq!(song.lines.len(), 2);
1801 assert_eq!(song.metadata.title.as_deref(), Some("My Song"));
1802 }
1803
1804 #[test]
1807 fn metadata_default_is_empty() {
1808 let meta = Metadata::new();
1809 assert_eq!(meta.title, None);
1810 assert!(meta.subtitles.is_empty());
1811 assert!(meta.artists.is_empty());
1812 assert!(meta.composers.is_empty());
1813 assert!(meta.lyricists.is_empty());
1814 assert_eq!(meta.album, None);
1815 assert_eq!(meta.year, None);
1816 assert_eq!(meta.key, None);
1817 assert_eq!(meta.tempo, None);
1818 assert_eq!(meta.time, None);
1819 assert_eq!(meta.capo, None);
1820 assert_eq!(meta.sort_title, None);
1821 assert_eq!(meta.sort_artist, None);
1822 assert!(meta.arrangers.is_empty());
1823 assert_eq!(meta.copyright, None);
1824 assert_eq!(meta.duration, None);
1825 assert!(meta.tags.is_empty());
1826 assert!(meta.custom.is_empty());
1827 }
1828
1829 #[test]
1832 fn lyrics_line_text_concatenation() {
1833 let line = LyricsLine {
1834 segments: vec![
1835 LyricsSegment::new(Some(Chord::new("Am")), "Hello "),
1836 LyricsSegment::new(Some(Chord::new("G")), "world"),
1837 ],
1838 };
1839 assert_eq!(line.text(), "Hello world");
1840 }
1841
1842 #[test]
1843 fn lyrics_line_has_chords() {
1844 let with_chords = LyricsLine {
1845 segments: vec![LyricsSegment::new(Some(Chord::new("C")), "text")],
1846 };
1847 assert!(with_chords.has_chords());
1848
1849 let without_chords = LyricsLine {
1850 segments: vec![LyricsSegment::text_only("just text")],
1851 };
1852 assert!(!without_chords.has_chords());
1853 }
1854
1855 #[test]
1856 fn lyrics_line_empty_default() {
1857 let line = LyricsLine::new();
1858 assert!(line.segments.is_empty());
1859 assert_eq!(line.text(), "");
1860 assert!(!line.has_chords());
1861 }
1862
1863 #[test]
1866 fn segment_text_only() {
1867 let seg = LyricsSegment::text_only("hello");
1868 assert_eq!(seg.chord, None);
1869 assert_eq!(seg.text, "hello");
1870 }
1871
1872 #[test]
1873 fn segment_chord_only() {
1874 let seg = LyricsSegment::chord_only(Chord::new("Dm"));
1875 assert_eq!(seg.chord, Some(Chord::new("Dm")));
1876 assert!(seg.text.is_empty());
1877 }
1878
1879 #[test]
1880 fn segment_with_chord_and_text() {
1881 let seg = LyricsSegment::new(Some(Chord::new("E7")), "lyrics");
1882 assert_eq!(seg.chord.as_ref().map(|c| c.name.as_str()), Some("E7"));
1883 assert_eq!(seg.text, "lyrics");
1884 }
1885
1886 #[test]
1889 fn chord_display() {
1890 let chord = Chord::new("F#m7");
1891 assert_eq!(format!("{chord}"), "F#m7");
1892 }
1893
1894 #[test]
1895 fn chord_equality() {
1896 assert_eq!(Chord::new("Am"), Chord::new("Am"));
1897 assert_ne!(Chord::new("Am"), Chord::new("Bm"));
1898 }
1899
1900 #[test]
1901 fn chord_detail_parsed() {
1902 let chord = Chord::new("C#m7");
1903 let detail = chord.detail.as_ref().expect("should have detail");
1904 assert_eq!(detail.root, Note::C);
1905 assert_eq!(detail.root_accidental, Some(Accidental::Sharp));
1906 assert_eq!(detail.quality, ChordQuality::Minor);
1907 assert_eq!(detail.extension.as_deref(), Some("7"));
1908 }
1909
1910 #[test]
1911 fn chord_detail_slash_chord() {
1912 let chord = Chord::new("G/B");
1913 let detail = chord.detail.as_ref().expect("should have detail");
1914 assert_eq!(detail.root, Note::G);
1915 assert_eq!(detail.bass_note, Some((Note::B, None)));
1916 }
1917
1918 #[test]
1919 fn chord_detail_unparseable() {
1920 let chord = Chord::new("");
1921 assert!(chord.detail.is_none());
1922 assert_eq!(chord.name, "");
1923 }
1924
1925 #[test]
1926 fn chord_detail_invalid_notation() {
1927 let chord = Chord::new("xyz");
1928 assert!(chord.detail.is_none());
1929 assert_eq!(chord.name, "xyz");
1930 }
1931
1932 #[test]
1935 fn directive_kind_from_name_metadata() {
1936 assert_eq!(DirectiveKind::from_name("title"), DirectiveKind::Title);
1937 assert_eq!(DirectiveKind::from_name("t"), DirectiveKind::Title);
1938 assert_eq!(DirectiveKind::from_name("TITLE"), DirectiveKind::Title);
1939 assert_eq!(DirectiveKind::from_name("Title"), DirectiveKind::Title);
1940 assert_eq!(
1941 DirectiveKind::from_name("subtitle"),
1942 DirectiveKind::Subtitle
1943 );
1944 assert_eq!(DirectiveKind::from_name("st"), DirectiveKind::Subtitle);
1945 assert_eq!(DirectiveKind::from_name("artist"), DirectiveKind::Artist);
1946 assert_eq!(
1947 DirectiveKind::from_name("composer"),
1948 DirectiveKind::Composer
1949 );
1950 assert_eq!(
1951 DirectiveKind::from_name("lyricist"),
1952 DirectiveKind::Lyricist
1953 );
1954 assert_eq!(DirectiveKind::from_name("album"), DirectiveKind::Album);
1955 assert_eq!(DirectiveKind::from_name("year"), DirectiveKind::Year);
1956 assert_eq!(DirectiveKind::from_name("key"), DirectiveKind::Key);
1957 assert_eq!(DirectiveKind::from_name("tempo"), DirectiveKind::Tempo);
1958 assert_eq!(DirectiveKind::from_name("time"), DirectiveKind::Time);
1959 assert_eq!(DirectiveKind::from_name("capo"), DirectiveKind::Capo);
1960 assert_eq!(
1961 DirectiveKind::from_name("sorttitle"),
1962 DirectiveKind::SortTitle
1963 );
1964 assert_eq!(
1965 DirectiveKind::from_name("SORTTITLE"),
1966 DirectiveKind::SortTitle
1967 );
1968 assert_eq!(
1969 DirectiveKind::from_name("sortartist"),
1970 DirectiveKind::SortArtist
1971 );
1972 assert_eq!(
1973 DirectiveKind::from_name("arranger"),
1974 DirectiveKind::Arranger
1975 );
1976 assert_eq!(
1977 DirectiveKind::from_name("copyright"),
1978 DirectiveKind::Copyright
1979 );
1980 assert_eq!(
1981 DirectiveKind::from_name("duration"),
1982 DirectiveKind::Duration
1983 );
1984 assert_eq!(DirectiveKind::from_name("tag"), DirectiveKind::Tag);
1985 }
1986
1987 #[test]
1988 fn directive_kind_from_name_comment() {
1989 assert_eq!(DirectiveKind::from_name("comment"), DirectiveKind::Comment);
1990 assert_eq!(DirectiveKind::from_name("c"), DirectiveKind::Comment);
1991 assert_eq!(
1992 DirectiveKind::from_name("comment_italic"),
1993 DirectiveKind::CommentItalic
1994 );
1995 assert_eq!(DirectiveKind::from_name("ci"), DirectiveKind::CommentItalic);
1996 assert_eq!(
1997 DirectiveKind::from_name("comment_box"),
1998 DirectiveKind::CommentBox
1999 );
2000 assert_eq!(DirectiveKind::from_name("cb"), DirectiveKind::CommentBox);
2001 }
2002
2003 #[test]
2004 fn directive_kind_from_name_environment() {
2005 assert_eq!(
2006 DirectiveKind::from_name("start_of_chorus"),
2007 DirectiveKind::StartOfChorus
2008 );
2009 assert_eq!(
2010 DirectiveKind::from_name("soc"),
2011 DirectiveKind::StartOfChorus
2012 );
2013 assert_eq!(
2014 DirectiveKind::from_name("end_of_chorus"),
2015 DirectiveKind::EndOfChorus
2016 );
2017 assert_eq!(DirectiveKind::from_name("eoc"), DirectiveKind::EndOfChorus);
2018 assert_eq!(
2019 DirectiveKind::from_name("start_of_verse"),
2020 DirectiveKind::StartOfVerse
2021 );
2022 assert_eq!(DirectiveKind::from_name("sov"), DirectiveKind::StartOfVerse);
2023 assert_eq!(
2024 DirectiveKind::from_name("end_of_verse"),
2025 DirectiveKind::EndOfVerse
2026 );
2027 assert_eq!(DirectiveKind::from_name("eov"), DirectiveKind::EndOfVerse);
2028 assert_eq!(
2029 DirectiveKind::from_name("start_of_bridge"),
2030 DirectiveKind::StartOfBridge
2031 );
2032 assert_eq!(
2033 DirectiveKind::from_name("sob"),
2034 DirectiveKind::StartOfBridge
2035 );
2036 assert_eq!(
2037 DirectiveKind::from_name("end_of_bridge"),
2038 DirectiveKind::EndOfBridge
2039 );
2040 assert_eq!(DirectiveKind::from_name("eob"), DirectiveKind::EndOfBridge);
2041 assert_eq!(
2042 DirectiveKind::from_name("start_of_tab"),
2043 DirectiveKind::StartOfTab
2044 );
2045 assert_eq!(DirectiveKind::from_name("sot"), DirectiveKind::StartOfTab);
2046 assert_eq!(
2047 DirectiveKind::from_name("end_of_tab"),
2048 DirectiveKind::EndOfTab
2049 );
2050 assert_eq!(DirectiveKind::from_name("eot"), DirectiveKind::EndOfTab);
2051 }
2052
2053 #[test]
2054 fn directive_kind_from_name_page_control() {
2055 assert_eq!(DirectiveKind::from_name("new_page"), DirectiveKind::NewPage);
2056 assert_eq!(DirectiveKind::from_name("np"), DirectiveKind::NewPage);
2057 assert_eq!(
2058 DirectiveKind::from_name("new_physical_page"),
2059 DirectiveKind::NewPhysicalPage
2060 );
2061 assert_eq!(
2062 DirectiveKind::from_name("npp"),
2063 DirectiveKind::NewPhysicalPage
2064 );
2065 assert_eq!(
2066 DirectiveKind::from_name("column_break"),
2067 DirectiveKind::ColumnBreak
2068 );
2069 assert_eq!(DirectiveKind::from_name("colb"), DirectiveKind::ColumnBreak);
2070 assert_eq!(DirectiveKind::from_name("columns"), DirectiveKind::Columns);
2071 assert_eq!(DirectiveKind::from_name("col"), DirectiveKind::Columns);
2072 }
2073 #[test]
2074 fn directive_kind_from_name_unknown() {
2075 let kind = DirectiveKind::from_name("custom_thing");
2076 assert_eq!(kind, DirectiveKind::Unknown("custom_thing".to_string()));
2077 }
2078
2079 #[test]
2080 fn directive_kind_case_insensitive() {
2081 assert_eq!(DirectiveKind::from_name("TITLE"), DirectiveKind::Title);
2082 assert_eq!(DirectiveKind::from_name("Title"), DirectiveKind::Title);
2083 assert_eq!(
2084 DirectiveKind::from_name("START_OF_CHORUS"),
2085 DirectiveKind::StartOfChorus
2086 );
2087 assert_eq!(
2088 DirectiveKind::from_name("Comment_Italic"),
2089 DirectiveKind::CommentItalic
2090 );
2091 assert_eq!(DirectiveKind::from_name("NEW_PAGE"), DirectiveKind::NewPage);
2092 assert_eq!(
2093 DirectiveKind::from_name("Column_Break"),
2094 DirectiveKind::ColumnBreak
2095 );
2096 }
2097
2098 #[test]
2099 fn directive_kind_canonical_name() {
2100 assert_eq!(DirectiveKind::Title.canonical_name(), "title");
2101 assert_eq!(
2102 DirectiveKind::StartOfChorus.canonical_name(),
2103 "start_of_chorus"
2104 );
2105 assert_eq!(DirectiveKind::Comment.canonical_name(), "comment");
2106 assert_eq!(
2107 DirectiveKind::Unknown("foo".to_string()).canonical_name(),
2108 "foo"
2109 );
2110 assert_eq!(DirectiveKind::SortTitle.canonical_name(), "sorttitle");
2111 assert_eq!(DirectiveKind::SortArtist.canonical_name(), "sortartist");
2112 assert_eq!(DirectiveKind::Arranger.canonical_name(), "arranger");
2113 assert_eq!(DirectiveKind::Copyright.canonical_name(), "copyright");
2114 assert_eq!(DirectiveKind::Duration.canonical_name(), "duration");
2115 assert_eq!(DirectiveKind::Tag.canonical_name(), "tag");
2116 assert_eq!(DirectiveKind::NewPage.canonical_name(), "new_page");
2117 assert_eq!(
2118 DirectiveKind::NewPhysicalPage.canonical_name(),
2119 "new_physical_page"
2120 );
2121 assert_eq!(DirectiveKind::ColumnBreak.canonical_name(), "column_break");
2122 assert_eq!(DirectiveKind::Columns.canonical_name(), "columns");
2123 }
2124
2125 #[test]
2126 fn directive_kind_category_checks() {
2127 assert!(DirectiveKind::Title.is_metadata());
2128 assert!(!DirectiveKind::Title.is_comment());
2129 assert!(!DirectiveKind::Title.is_environment());
2130
2131 assert!(DirectiveKind::Comment.is_comment());
2132 assert!(!DirectiveKind::Comment.is_metadata());
2133
2134 assert!(DirectiveKind::StartOfChorus.is_section_start());
2135 assert!(DirectiveKind::StartOfChorus.is_environment());
2136 assert!(!DirectiveKind::StartOfChorus.is_section_end());
2137
2138 assert!(DirectiveKind::EndOfChorus.is_section_end());
2139 assert!(DirectiveKind::EndOfChorus.is_environment());
2140 assert!(!DirectiveKind::EndOfChorus.is_section_start());
2141
2142 let unknown = DirectiveKind::Unknown("x".to_string());
2143 assert!(!unknown.is_metadata());
2144 assert!(!unknown.is_comment());
2145 assert!(!unknown.is_environment());
2146
2147 assert!(DirectiveKind::NewPage.is_page_control());
2148 assert!(DirectiveKind::NewPhysicalPage.is_page_control());
2149 assert!(DirectiveKind::ColumnBreak.is_page_control());
2150 assert!(DirectiveKind::Columns.is_page_control());
2151 assert!(!DirectiveKind::NewPage.is_metadata());
2152 assert!(!DirectiveKind::NewPage.is_comment());
2153 assert!(!DirectiveKind::NewPage.is_environment());
2154 assert!(!DirectiveKind::Title.is_page_control());
2155 assert!(!unknown.is_page_control());
2156 }
2157
2158 #[test]
2161 fn directive_with_value() {
2162 let d = Directive::with_value("title", "My Song");
2163 assert_eq!(d.name, "title");
2164 assert_eq!(d.value.as_deref(), Some("My Song"));
2165 assert_eq!(d.kind, DirectiveKind::Title);
2166 }
2167
2168 #[test]
2169 fn directive_name_only() {
2170 let d = Directive::name_only("start_of_chorus");
2171 assert_eq!(d.name, "start_of_chorus");
2172 assert!(d.value.is_none());
2173 assert_eq!(d.kind, DirectiveKind::StartOfChorus);
2174 }
2175
2176 #[test]
2177 fn directive_short_alias_resolution() {
2178 let d = Directive::with_value("t", "My Song");
2179 assert_eq!(d.name, "title");
2180 assert_eq!(d.kind, DirectiveKind::Title);
2181
2182 let d = Directive::name_only("soc");
2183 assert_eq!(d.name, "start_of_chorus");
2184 assert_eq!(d.kind, DirectiveKind::StartOfChorus);
2185
2186 let d = Directive::with_value("st", "Alternate Title");
2187 assert_eq!(d.name, "subtitle");
2188 assert_eq!(d.kind, DirectiveKind::Subtitle);
2189 }
2190
2191 #[test]
2192 fn directive_case_insensitive_resolution() {
2193 let d = Directive::with_value("TITLE", "My Song");
2194 assert_eq!(d.name, "title");
2195 assert_eq!(d.kind, DirectiveKind::Title);
2196
2197 let d = Directive::name_only("SOC");
2198 assert_eq!(d.name, "start_of_chorus");
2199 assert_eq!(d.kind, DirectiveKind::StartOfChorus);
2200 }
2201
2202 #[test]
2203 fn directive_unknown_preserves_name() {
2204 let d = Directive::with_value("my_custom", "value");
2205 assert_eq!(d.name, "my_custom");
2206 assert_eq!(d.kind, DirectiveKind::Unknown("my_custom".to_string()));
2207 }
2208
2209 #[test]
2210 fn directive_section_detection() {
2211 let soc = Directive::name_only("start_of_chorus");
2212 assert!(soc.is_section_start());
2213 assert!(!soc.is_section_end());
2214 assert_eq!(soc.section_name(), Some("chorus"));
2215
2216 let eoc = Directive::name_only("end_of_chorus");
2217 assert!(!eoc.is_section_start());
2218 assert!(eoc.is_section_end());
2219 assert_eq!(eoc.section_name(), Some("chorus"));
2220
2221 let title = Directive::with_value("title", "Test");
2222 assert!(!title.is_section_start());
2223 assert!(!title.is_section_end());
2224 assert_eq!(title.section_name(), None);
2225 }
2226
2227 #[test]
2228 fn directive_section_name_variants() {
2229 let sov = Directive::name_only("start_of_verse");
2230 assert_eq!(sov.section_name(), Some("verse"));
2231
2232 let eob = Directive::name_only("end_of_bridge");
2233 assert_eq!(eob.section_name(), Some("bridge"));
2234 }
2235
2236 #[test]
2237 fn directive_section_detection_via_short_alias() {
2238 let soc = Directive::name_only("soc");
2239 assert!(soc.is_section_start());
2240 assert_eq!(soc.section_name(), Some("chorus"));
2241
2242 let eot = Directive::name_only("eot");
2243 assert!(eot.is_section_end());
2244 assert_eq!(eot.section_name(), Some("tab"));
2245 }
2246
2247 #[test]
2250 fn directive_kind_start_of_custom_section() {
2251 let kind = DirectiveKind::from_name("start_of_intro");
2252 assert_eq!(kind, DirectiveKind::StartOfSection("intro".to_string()));
2253 assert!(kind.is_section_start());
2254 assert!(!kind.is_section_end());
2255 assert!(kind.is_environment());
2256 }
2257
2258 #[test]
2259 fn directive_kind_end_of_custom_section() {
2260 let kind = DirectiveKind::from_name("end_of_intro");
2261 assert_eq!(kind, DirectiveKind::EndOfSection("intro".to_string()));
2262 assert!(kind.is_section_end());
2263 assert!(!kind.is_section_start());
2264 assert!(kind.is_environment());
2265 }
2266
2267 #[test]
2268 fn directive_kind_custom_section_case_insensitive() {
2269 let kind = DirectiveKind::from_name("Start_Of_Intro");
2270 assert_eq!(kind, DirectiveKind::StartOfSection("intro".to_string()));
2271 }
2272
2273 #[test]
2274 fn directive_kind_custom_section_various_names() {
2275 assert_eq!(
2276 DirectiveKind::from_name("start_of_outro"),
2277 DirectiveKind::StartOfSection("outro".to_string())
2278 );
2279 assert_eq!(
2280 DirectiveKind::from_name("start_of_solo"),
2281 DirectiveKind::StartOfSection("solo".to_string())
2282 );
2283 assert_eq!(
2284 DirectiveKind::from_name("end_of_solo"),
2285 DirectiveKind::EndOfSection("solo".to_string())
2286 );
2287 assert_eq!(
2288 DirectiveKind::from_name("start_of_interlude"),
2289 DirectiveKind::StartOfSection("interlude".to_string())
2290 );
2291 }
2292
2293 #[test]
2296 fn directive_kind_from_name_font_size_color() {
2297 assert_eq!(
2299 DirectiveKind::from_name("textfont"),
2300 DirectiveKind::TextFont
2301 );
2302 assert_eq!(DirectiveKind::from_name("tf"), DirectiveKind::TextFont);
2303 assert_eq!(
2304 DirectiveKind::from_name("TEXTFONT"),
2305 DirectiveKind::TextFont
2306 );
2307 assert_eq!(
2308 DirectiveKind::from_name("textsize"),
2309 DirectiveKind::TextSize
2310 );
2311 assert_eq!(DirectiveKind::from_name("ts"), DirectiveKind::TextSize);
2312 assert_eq!(
2313 DirectiveKind::from_name("textcolour"),
2314 DirectiveKind::TextColour
2315 );
2316 assert_eq!(
2317 DirectiveKind::from_name("textcolor"),
2318 DirectiveKind::TextColour
2319 );
2320 assert_eq!(DirectiveKind::from_name("tc"), DirectiveKind::TextColour);
2321
2322 assert_eq!(
2324 DirectiveKind::from_name("chordfont"),
2325 DirectiveKind::ChordFont
2326 );
2327 assert_eq!(DirectiveKind::from_name("cf"), DirectiveKind::ChordFont);
2328 assert_eq!(
2329 DirectiveKind::from_name("chordsize"),
2330 DirectiveKind::ChordSize
2331 );
2332 assert_eq!(DirectiveKind::from_name("cs"), DirectiveKind::ChordSize);
2333 assert_eq!(
2334 DirectiveKind::from_name("chordcolour"),
2335 DirectiveKind::ChordColour
2336 );
2337 assert_eq!(
2338 DirectiveKind::from_name("chordcolor"),
2339 DirectiveKind::ChordColour
2340 );
2341 assert_eq!(DirectiveKind::from_name("cc"), DirectiveKind::ChordColour);
2342
2343 assert_eq!(DirectiveKind::from_name("tabfont"), DirectiveKind::TabFont);
2345 assert_eq!(DirectiveKind::from_name("tabsize"), DirectiveKind::TabSize);
2346 assert_eq!(
2347 DirectiveKind::from_name("tabcolour"),
2348 DirectiveKind::TabColour
2349 );
2350 assert_eq!(
2351 DirectiveKind::from_name("tabcolor"),
2352 DirectiveKind::TabColour
2353 );
2354 }
2355
2356 #[test]
2357 fn directive_custom_section_full_canonical_name() {
2358 let kind = DirectiveKind::StartOfSection("intro".to_string());
2359 assert_eq!(kind.full_canonical_name(), "start_of_intro");
2360
2361 let kind = DirectiveKind::EndOfSection("outro".to_string());
2362 assert_eq!(kind.full_canonical_name(), "end_of_outro");
2363 }
2364
2365 #[test]
2366 fn directive_custom_section_name_only() {
2367 let d = Directive::name_only("start_of_intro");
2368 assert_eq!(d.name, "start_of_intro");
2369 assert!(d.value.is_none());
2370 assert_eq!(d.kind, DirectiveKind::StartOfSection("intro".to_string()));
2371 assert!(d.is_section_start());
2372 assert_eq!(d.section_name(), Some("intro"));
2373 }
2374
2375 #[test]
2376 fn directive_custom_section_with_label() {
2377 let d = Directive::with_value("start_of_intro", "Guitar Intro");
2378 assert_eq!(d.name, "start_of_intro");
2379 assert_eq!(d.value.as_deref(), Some("Guitar Intro"));
2380 assert_eq!(d.kind, DirectiveKind::StartOfSection("intro".to_string()));
2381 }
2382
2383 #[test]
2384 fn directive_end_custom_section() {
2385 let d = Directive::name_only("end_of_intro");
2386 assert_eq!(d.name, "end_of_intro");
2387 assert!(d.is_section_end());
2388 assert_eq!(d.section_name(), Some("intro"));
2389 }
2390
2391 #[test]
2392 fn directive_known_sections_not_custom() {
2393 assert_eq!(
2395 DirectiveKind::from_name("start_of_chorus"),
2396 DirectiveKind::StartOfChorus
2397 );
2398 assert_eq!(
2399 DirectiveKind::from_name("start_of_verse"),
2400 DirectiveKind::StartOfVerse
2401 );
2402 assert_eq!(
2403 DirectiveKind::from_name("start_of_bridge"),
2404 DirectiveKind::StartOfBridge
2405 );
2406 assert_eq!(
2407 DirectiveKind::from_name("start_of_tab"),
2408 DirectiveKind::StartOfTab
2409 );
2410 }
2411
2412 #[test]
2413 fn directive_kind_font_size_color_canonical_names() {
2414 assert_eq!(DirectiveKind::TextFont.canonical_name(), "textfont");
2415 assert_eq!(DirectiveKind::TextSize.canonical_name(), "textsize");
2416 assert_eq!(DirectiveKind::TextColour.canonical_name(), "textcolour");
2417 assert_eq!(DirectiveKind::ChordFont.canonical_name(), "chordfont");
2418 assert_eq!(DirectiveKind::ChordSize.canonical_name(), "chordsize");
2419 assert_eq!(DirectiveKind::ChordColour.canonical_name(), "chordcolour");
2420 assert_eq!(DirectiveKind::TabFont.canonical_name(), "tabfont");
2421 assert_eq!(DirectiveKind::TabSize.canonical_name(), "tabsize");
2422 assert_eq!(DirectiveKind::TabColour.canonical_name(), "tabcolour");
2423 }
2424
2425 #[test]
2426 fn directive_kind_font_size_color_category_checks() {
2427 let font_kinds = [
2428 DirectiveKind::TextFont,
2429 DirectiveKind::TextSize,
2430 DirectiveKind::TextColour,
2431 DirectiveKind::ChordFont,
2432 DirectiveKind::ChordSize,
2433 DirectiveKind::ChordColour,
2434 DirectiveKind::TabFont,
2435 DirectiveKind::TabSize,
2436 DirectiveKind::TabColour,
2437 ];
2438 for kind in &font_kinds {
2439 assert!(
2440 kind.is_font_size_color(),
2441 "{kind:?} should be font_size_color"
2442 );
2443 assert!(!kind.is_metadata(), "{kind:?} should not be metadata");
2444 assert!(!kind.is_comment(), "{kind:?} should not be comment");
2445 assert!(!kind.is_environment(), "{kind:?} should not be environment");
2446 }
2447 }
2448
2449 #[test]
2450 fn directive_font_alias_resolution() {
2451 let d = Directive::with_value("tf", "Times");
2452 assert_eq!(d.name, "textfont");
2453 assert_eq!(d.kind, DirectiveKind::TextFont);
2454 assert_eq!(d.value.as_deref(), Some("Times"));
2455
2456 let d = Directive::with_value("cc", "#FF0000");
2457 assert_eq!(d.name, "chordcolour");
2458 assert_eq!(d.kind, DirectiveKind::ChordColour);
2459
2460 let d = Directive::with_value("textcolor", "blue");
2461 assert_eq!(d.name, "textcolour");
2462 assert_eq!(d.kind, DirectiveKind::TextColour);
2463 }
2464
2465 #[test]
2468 fn comment_style_variants() {
2469 let normal = Line::Comment(CommentStyle::Normal, "text".to_string());
2470 let italic = Line::Comment(CommentStyle::Italic, "text".to_string());
2471 let boxed = Line::Comment(CommentStyle::Boxed, "text".to_string());
2472
2473 assert!(matches!(normal, Line::Comment(CommentStyle::Normal, _)));
2474 assert!(matches!(italic, Line::Comment(CommentStyle::Italic, _)));
2475 assert!(matches!(boxed, Line::Comment(CommentStyle::Boxed, _)));
2476 }
2477
2478 #[test]
2481 fn line_enum_variants() {
2482 let lyrics = Line::Lyrics(LyricsLine::new());
2483 let directive = Line::Directive(Directive::name_only("soc"));
2484 let comment = Line::Comment(CommentStyle::Normal, "test".to_string());
2485 let empty = Line::Empty;
2486
2487 assert!(matches!(lyrics, Line::Lyrics(_)));
2489 assert!(matches!(directive, Line::Directive(_)));
2490 assert!(matches!(comment, Line::Comment(..)));
2491 assert!(matches!(empty, Line::Empty));
2492 }
2493
2494 #[test]
2495 fn line_clone_and_eq() {
2496 let line = Line::Lyrics(LyricsLine {
2497 segments: vec![LyricsSegment::new(Some(Chord::new("C")), "hello")],
2498 });
2499 let cloned = line.clone();
2500 assert_eq!(line, cloned);
2501 }
2502
2503 #[test]
2506 fn directive_with_selector_constructor() {
2507 let d = Directive::with_selector("title", Some("My Song".to_string()), "piano");
2508 assert_eq!(d.name, "title");
2509 assert_eq!(d.value.as_deref(), Some("My Song"));
2510 assert_eq!(d.kind, DirectiveKind::Title);
2511 assert_eq!(d.selector.as_deref(), Some("piano"));
2512 }
2513
2514 #[test]
2515 fn directive_with_value_has_no_selector() {
2516 let d = Directive::with_value("title", "My Song");
2517 assert_eq!(d.selector, None);
2518 }
2519
2520 #[test]
2521 fn directive_name_only_has_no_selector() {
2522 let d = Directive::name_only("start_of_chorus");
2523 assert_eq!(d.selector, None);
2524 }
2525
2526 #[test]
2527 fn resolve_with_selector_plain_directive() {
2528 let (kind, sel) = DirectiveKind::resolve_with_selector("title");
2529 assert_eq!(kind, DirectiveKind::Title);
2530 assert_eq!(sel, None);
2531 }
2532
2533 #[test]
2534 fn resolve_with_selector_with_suffix() {
2535 let (kind, sel) = DirectiveKind::resolve_with_selector("title-piano");
2536 assert_eq!(kind, DirectiveKind::Title);
2537 assert_eq!(sel.as_deref(), Some("piano"));
2538 }
2539
2540 #[test]
2541 fn resolve_with_selector_comment() {
2542 let (kind, sel) = DirectiveKind::resolve_with_selector("comment-bass");
2543 assert_eq!(kind, DirectiveKind::Comment);
2544 assert_eq!(sel.as_deref(), Some("bass"));
2545 }
2546
2547 #[test]
2548 fn resolve_with_selector_comment_italic() {
2549 let (kind, sel) = DirectiveKind::resolve_with_selector("comment_italic-guitar");
2550 assert_eq!(kind, DirectiveKind::CommentItalic);
2551 assert_eq!(sel.as_deref(), Some("guitar"));
2552 }
2553
2554 #[test]
2555 fn resolve_with_selector_environment() {
2556 let (kind, sel) = DirectiveKind::resolve_with_selector("start_of_chorus-piano");
2557 assert_eq!(kind, DirectiveKind::StartOfChorus);
2558 assert_eq!(sel.as_deref(), Some("piano"));
2559 }
2560
2561 #[test]
2562 fn resolve_with_selector_end_of_tab() {
2563 let (kind, sel) = DirectiveKind::resolve_with_selector("end_of_tab-guitar");
2564 assert_eq!(kind, DirectiveKind::EndOfTab);
2565 assert_eq!(sel.as_deref(), Some("guitar"));
2566 }
2567
2568 #[test]
2569 fn resolve_with_selector_custom_section_no_selector() {
2570 let (kind, sel) = DirectiveKind::resolve_with_selector("start_of_intro");
2571 assert_eq!(kind, DirectiveKind::StartOfSection("intro".to_string()));
2572 assert_eq!(sel, None);
2573 }
2574
2575 #[test]
2576 fn resolve_with_selector_custom_section_with_selector() {
2577 let (kind, sel) = DirectiveKind::resolve_with_selector("start_of_intro-piano");
2581 assert_eq!(kind, DirectiveKind::StartOfSection("intro".to_string()));
2582 assert_eq!(sel.as_deref(), Some("piano"));
2583 }
2584
2585 #[test]
2586 fn resolve_with_selector_unknown_no_hyphen() {
2587 let (kind, sel) = DirectiveKind::resolve_with_selector("mything");
2588 assert_eq!(kind, DirectiveKind::Unknown("mything".to_string()));
2589 assert_eq!(sel, None);
2590 }
2591
2592 #[test]
2593 fn resolve_with_selector_unknown_with_hyphen() {
2594 let (kind, sel) = DirectiveKind::resolve_with_selector("my-thing");
2596 assert_eq!(kind, DirectiveKind::Unknown("my-thing".to_string()));
2597 assert_eq!(sel, None);
2598 }
2599
2600 #[test]
2601 fn resolve_with_selector_case_insensitive() {
2602 let (kind, sel) = DirectiveKind::resolve_with_selector("Title-Piano");
2603 assert_eq!(kind, DirectiveKind::Title);
2604 assert_eq!(sel.as_deref(), Some("piano"));
2605 }
2606
2607 #[test]
2608 fn resolve_with_selector_short_alias_with_suffix() {
2609 let (kind, sel) = DirectiveKind::resolve_with_selector("t-guitar");
2610 assert_eq!(kind, DirectiveKind::Title);
2611 assert_eq!(sel.as_deref(), Some("guitar"));
2612 }
2613
2614 #[test]
2617 fn full_song_construction() {
2618 let mut song = Song::new();
2619 song.metadata.title = Some("Amazing Grace".to_string());
2620 song.metadata.key = Some("G".to_string());
2621 song.metadata.artists.push("John Newton".to_string());
2622
2623 song.lines
2625 .push(Line::Directive(Directive::name_only("start_of_verse")));
2626
2627 song.lines.push(Line::Lyrics(LyricsLine {
2629 segments: vec![
2630 LyricsSegment::new(Some(Chord::new("G")), "Amazing "),
2631 LyricsSegment::new(Some(Chord::new("G7")), "grace, how "),
2632 LyricsSegment::new(Some(Chord::new("C")), "sweet the "),
2633 LyricsSegment::new(Some(Chord::new("G")), "sound"),
2634 ],
2635 }));
2636
2637 song.lines
2639 .push(Line::Directive(Directive::name_only("end_of_verse")));
2640
2641 assert_eq!(song.lines.len(), 3);
2642 if let Line::Lyrics(ref lyrics) = song.lines[1] {
2643 assert_eq!(lyrics.text(), "Amazing grace, how sweet the sound");
2644 assert!(lyrics.has_chords());
2645 assert_eq!(lyrics.segments.len(), 4);
2646 } else {
2647 panic!("Expected Line::Lyrics");
2648 }
2649 }
2650
2651 #[test]
2652 fn directive_kind_grid_from_name() {
2653 assert_eq!(
2654 DirectiveKind::from_name("start_of_grid"),
2655 DirectiveKind::StartOfGrid
2656 );
2657 assert_eq!(
2658 DirectiveKind::from_name("end_of_grid"),
2659 DirectiveKind::EndOfGrid
2660 );
2661 assert_eq!(DirectiveKind::from_name("sog"), DirectiveKind::StartOfGrid);
2662 assert_eq!(DirectiveKind::from_name("eog"), DirectiveKind::EndOfGrid);
2663 }
2664
2665 #[test]
2666 fn directive_kind_grid_canonical_name() {
2667 assert_eq!(DirectiveKind::StartOfGrid.canonical_name(), "start_of_grid");
2668 assert_eq!(DirectiveKind::EndOfGrid.canonical_name(), "end_of_grid");
2669 }
2670
2671 #[test]
2672 fn directive_kind_grid_is_section() {
2673 assert!(DirectiveKind::StartOfGrid.is_section_start());
2674 assert!(!DirectiveKind::StartOfGrid.is_section_end());
2675 assert!(DirectiveKind::EndOfGrid.is_section_end());
2676 assert!(!DirectiveKind::EndOfGrid.is_section_start());
2677 assert!(DirectiveKind::StartOfGrid.is_environment());
2678 assert!(DirectiveKind::EndOfGrid.is_environment());
2679 }
2680
2681 #[test]
2682 fn directive_grid_section_name() {
2683 let sog = Directive::name_only("sog");
2684 assert!(sog.is_section_start());
2685 assert_eq!(sog.section_name(), Some("grid"));
2686
2687 let eog = Directive::name_only("eog");
2688 assert!(eog.is_section_end());
2689 assert_eq!(eog.section_name(), Some("grid"));
2690 }
2691
2692 #[test]
2695 fn directive_kind_from_name_title_font_size_color() {
2696 assert_eq!(
2697 DirectiveKind::from_name("titlefont"),
2698 DirectiveKind::TitleFont
2699 );
2700 assert_eq!(
2701 DirectiveKind::from_name("TITLEFONT"),
2702 DirectiveKind::TitleFont
2703 );
2704 assert_eq!(
2705 DirectiveKind::from_name("titlesize"),
2706 DirectiveKind::TitleSize
2707 );
2708 assert_eq!(
2709 DirectiveKind::from_name("titlecolour"),
2710 DirectiveKind::TitleColour
2711 );
2712 assert_eq!(
2713 DirectiveKind::from_name("titlecolor"),
2714 DirectiveKind::TitleColour
2715 );
2716 }
2717
2718 #[test]
2719 fn directive_kind_from_name_chorus_font_size_color() {
2720 assert_eq!(
2721 DirectiveKind::from_name("chorusfont"),
2722 DirectiveKind::ChorusFont
2723 );
2724 assert_eq!(
2725 DirectiveKind::from_name("chorussize"),
2726 DirectiveKind::ChorusSize
2727 );
2728 assert_eq!(
2729 DirectiveKind::from_name("choruscolour"),
2730 DirectiveKind::ChorusColour
2731 );
2732 assert_eq!(
2733 DirectiveKind::from_name("choruscolor"),
2734 DirectiveKind::ChorusColour
2735 );
2736 }
2737
2738 #[test]
2739 fn directive_kind_from_name_footer_header_label() {
2740 assert_eq!(
2741 DirectiveKind::from_name("footerfont"),
2742 DirectiveKind::FooterFont
2743 );
2744 assert_eq!(
2745 DirectiveKind::from_name("footersize"),
2746 DirectiveKind::FooterSize
2747 );
2748 assert_eq!(
2749 DirectiveKind::from_name("footercolour"),
2750 DirectiveKind::FooterColour
2751 );
2752 assert_eq!(
2753 DirectiveKind::from_name("footercolor"),
2754 DirectiveKind::FooterColour
2755 );
2756 assert_eq!(
2757 DirectiveKind::from_name("headerfont"),
2758 DirectiveKind::HeaderFont
2759 );
2760 assert_eq!(
2761 DirectiveKind::from_name("headersize"),
2762 DirectiveKind::HeaderSize
2763 );
2764 assert_eq!(
2765 DirectiveKind::from_name("headercolour"),
2766 DirectiveKind::HeaderColour
2767 );
2768 assert_eq!(
2769 DirectiveKind::from_name("headercolor"),
2770 DirectiveKind::HeaderColour
2771 );
2772 assert_eq!(
2773 DirectiveKind::from_name("labelfont"),
2774 DirectiveKind::LabelFont
2775 );
2776 assert_eq!(
2777 DirectiveKind::from_name("labelsize"),
2778 DirectiveKind::LabelSize
2779 );
2780 assert_eq!(
2781 DirectiveKind::from_name("labelcolour"),
2782 DirectiveKind::LabelColour
2783 );
2784 assert_eq!(
2785 DirectiveKind::from_name("labelcolor"),
2786 DirectiveKind::LabelColour
2787 );
2788 }
2789
2790 #[test]
2791 fn directive_kind_from_name_grid_toc() {
2792 assert_eq!(
2793 DirectiveKind::from_name("gridfont"),
2794 DirectiveKind::GridFont
2795 );
2796 assert_eq!(
2797 DirectiveKind::from_name("gridsize"),
2798 DirectiveKind::GridSize
2799 );
2800 assert_eq!(
2801 DirectiveKind::from_name("gridcolour"),
2802 DirectiveKind::GridColour
2803 );
2804 assert_eq!(
2805 DirectiveKind::from_name("gridcolor"),
2806 DirectiveKind::GridColour
2807 );
2808 assert_eq!(DirectiveKind::from_name("tocfont"), DirectiveKind::TocFont);
2809 assert_eq!(DirectiveKind::from_name("tocsize"), DirectiveKind::TocSize);
2810 assert_eq!(
2811 DirectiveKind::from_name("toccolour"),
2812 DirectiveKind::TocColour
2813 );
2814 assert_eq!(
2815 DirectiveKind::from_name("toccolor"),
2816 DirectiveKind::TocColour
2817 );
2818 }
2819
2820 #[test]
2821 fn directive_kind_extra_font_size_color_canonical_names() {
2822 assert_eq!(DirectiveKind::TitleFont.canonical_name(), "titlefont");
2823 assert_eq!(DirectiveKind::TitleSize.canonical_name(), "titlesize");
2824 assert_eq!(DirectiveKind::TitleColour.canonical_name(), "titlecolour");
2825 assert_eq!(DirectiveKind::ChorusFont.canonical_name(), "chorusfont");
2826 assert_eq!(DirectiveKind::ChorusSize.canonical_name(), "chorussize");
2827 assert_eq!(DirectiveKind::ChorusColour.canonical_name(), "choruscolour");
2828 assert_eq!(DirectiveKind::FooterFont.canonical_name(), "footerfont");
2829 assert_eq!(DirectiveKind::FooterSize.canonical_name(), "footersize");
2830 assert_eq!(DirectiveKind::FooterColour.canonical_name(), "footercolour");
2831 assert_eq!(DirectiveKind::HeaderFont.canonical_name(), "headerfont");
2832 assert_eq!(DirectiveKind::HeaderSize.canonical_name(), "headersize");
2833 assert_eq!(DirectiveKind::HeaderColour.canonical_name(), "headercolour");
2834 assert_eq!(DirectiveKind::LabelFont.canonical_name(), "labelfont");
2835 assert_eq!(DirectiveKind::LabelSize.canonical_name(), "labelsize");
2836 assert_eq!(DirectiveKind::LabelColour.canonical_name(), "labelcolour");
2837 assert_eq!(DirectiveKind::GridFont.canonical_name(), "gridfont");
2838 assert_eq!(DirectiveKind::GridSize.canonical_name(), "gridsize");
2839 assert_eq!(DirectiveKind::GridColour.canonical_name(), "gridcolour");
2840 assert_eq!(DirectiveKind::TocFont.canonical_name(), "tocfont");
2841 assert_eq!(DirectiveKind::TocSize.canonical_name(), "tocsize");
2842 assert_eq!(DirectiveKind::TocColour.canonical_name(), "toccolour");
2843 }
2844
2845 #[test]
2846 fn directive_kind_extra_font_size_color_category_checks() {
2847 let font_kinds = [
2848 DirectiveKind::TitleFont,
2849 DirectiveKind::TitleSize,
2850 DirectiveKind::TitleColour,
2851 DirectiveKind::ChorusFont,
2852 DirectiveKind::ChorusSize,
2853 DirectiveKind::ChorusColour,
2854 DirectiveKind::FooterFont,
2855 DirectiveKind::FooterSize,
2856 DirectiveKind::FooterColour,
2857 DirectiveKind::HeaderFont,
2858 DirectiveKind::HeaderSize,
2859 DirectiveKind::HeaderColour,
2860 DirectiveKind::LabelFont,
2861 DirectiveKind::LabelSize,
2862 DirectiveKind::LabelColour,
2863 DirectiveKind::GridFont,
2864 DirectiveKind::GridSize,
2865 DirectiveKind::GridColour,
2866 DirectiveKind::TocFont,
2867 DirectiveKind::TocSize,
2868 DirectiveKind::TocColour,
2869 ];
2870 for kind in &font_kinds {
2871 assert!(
2872 kind.is_font_size_color(),
2873 "{kind:?} should be font_size_color"
2874 );
2875 assert!(!kind.is_metadata(), "{kind:?} should not be metadata");
2876 assert!(!kind.is_comment(), "{kind:?} should not be comment");
2877 assert!(!kind.is_environment(), "{kind:?} should not be environment");
2878 }
2879 }
2880
2881 #[test]
2882 fn directive_font_size_color_alias_resolution() {
2883 let d = Directive::with_value("titlefont", "Times");
2884 assert_eq!(d.name, "titlefont");
2885 assert_eq!(d.kind, DirectiveKind::TitleFont);
2886 assert_eq!(d.value.as_deref(), Some("Times"));
2887
2888 let d = Directive::with_value("choruscolor", "#FF0000");
2889 assert_eq!(d.name, "choruscolour");
2890 assert_eq!(d.kind, DirectiveKind::ChorusColour);
2891
2892 let d = Directive::with_value("titlecolor", "blue");
2893 assert_eq!(d.name, "titlecolour");
2894 assert_eq!(d.kind, DirectiveKind::TitleColour);
2895
2896 let d = Directive::with_value("gridsize", "12");
2897 assert_eq!(d.name, "gridsize");
2898 assert_eq!(d.kind, DirectiveKind::GridSize);
2899 assert_eq!(d.value.as_deref(), Some("12"));
2900 }
2901}
2902
2903#[cfg(test)]
2904mod delegate_tests {
2905 use super::*;
2906
2907 #[test]
2908 fn directive_kind_from_name_delegate_abc() {
2909 assert_eq!(
2910 DirectiveKind::from_name("start_of_abc"),
2911 DirectiveKind::StartOfAbc
2912 );
2913 assert_eq!(
2914 DirectiveKind::from_name("end_of_abc"),
2915 DirectiveKind::EndOfAbc
2916 );
2917 }
2918
2919 #[test]
2920 fn directive_kind_from_name_delegate_ly() {
2921 assert_eq!(
2922 DirectiveKind::from_name("start_of_ly"),
2923 DirectiveKind::StartOfLy
2924 );
2925 assert_eq!(
2926 DirectiveKind::from_name("end_of_ly"),
2927 DirectiveKind::EndOfLy
2928 );
2929 }
2930
2931 #[test]
2932 fn directive_kind_from_name_delegate_svg() {
2933 assert_eq!(
2934 DirectiveKind::from_name("start_of_svg"),
2935 DirectiveKind::StartOfSvg
2936 );
2937 assert_eq!(
2938 DirectiveKind::from_name("end_of_svg"),
2939 DirectiveKind::EndOfSvg
2940 );
2941 }
2942
2943 #[test]
2944 fn directive_kind_from_name_delegate_textblock() {
2945 assert_eq!(
2946 DirectiveKind::from_name("start_of_textblock"),
2947 DirectiveKind::StartOfTextblock
2948 );
2949 assert_eq!(
2950 DirectiveKind::from_name("end_of_textblock"),
2951 DirectiveKind::EndOfTextblock
2952 );
2953 }
2954
2955 #[test]
2956 fn delegate_environments_case_insensitive() {
2957 assert_eq!(
2958 DirectiveKind::from_name("START_OF_ABC"),
2959 DirectiveKind::StartOfAbc
2960 );
2961 assert_eq!(
2962 DirectiveKind::from_name("End_Of_Ly"),
2963 DirectiveKind::EndOfLy
2964 );
2965 assert_eq!(
2966 DirectiveKind::from_name("START_OF_SVG"),
2967 DirectiveKind::StartOfSvg
2968 );
2969 assert_eq!(
2970 DirectiveKind::from_name("End_Of_Textblock"),
2971 DirectiveKind::EndOfTextblock
2972 );
2973 }
2974
2975 #[test]
2976 fn delegate_environments_are_section_start() {
2977 assert!(DirectiveKind::StartOfAbc.is_section_start());
2978 assert!(DirectiveKind::StartOfLy.is_section_start());
2979 assert!(DirectiveKind::StartOfSvg.is_section_start());
2980 assert!(DirectiveKind::StartOfTextblock.is_section_start());
2981 }
2982
2983 #[test]
2984 fn delegate_environments_are_section_end() {
2985 assert!(DirectiveKind::EndOfAbc.is_section_end());
2986 assert!(DirectiveKind::EndOfLy.is_section_end());
2987 assert!(DirectiveKind::EndOfSvg.is_section_end());
2988 assert!(DirectiveKind::EndOfTextblock.is_section_end());
2989 }
2990
2991 #[test]
2992 fn delegate_environments_are_environments() {
2993 assert!(DirectiveKind::StartOfAbc.is_environment());
2994 assert!(DirectiveKind::EndOfAbc.is_environment());
2995 assert!(DirectiveKind::StartOfLy.is_environment());
2996 assert!(DirectiveKind::EndOfLy.is_environment());
2997 assert!(DirectiveKind::StartOfSvg.is_environment());
2998 assert!(DirectiveKind::EndOfSvg.is_environment());
2999 assert!(DirectiveKind::StartOfTextblock.is_environment());
3000 assert!(DirectiveKind::EndOfTextblock.is_environment());
3001 }
3002
3003 #[test]
3004 fn delegate_environments_canonical_names() {
3005 assert_eq!(DirectiveKind::StartOfAbc.canonical_name(), "start_of_abc");
3006 assert_eq!(DirectiveKind::EndOfAbc.canonical_name(), "end_of_abc");
3007 assert_eq!(DirectiveKind::StartOfLy.canonical_name(), "start_of_ly");
3008 assert_eq!(DirectiveKind::EndOfLy.canonical_name(), "end_of_ly");
3009 assert_eq!(DirectiveKind::StartOfSvg.canonical_name(), "start_of_svg");
3010 assert_eq!(DirectiveKind::EndOfSvg.canonical_name(), "end_of_svg");
3011 assert_eq!(
3012 DirectiveKind::StartOfTextblock.canonical_name(),
3013 "start_of_textblock"
3014 );
3015 assert_eq!(
3016 DirectiveKind::EndOfTextblock.canonical_name(),
3017 "end_of_textblock"
3018 );
3019 }
3020
3021 #[test]
3022 fn delegate_not_metadata() {
3023 assert!(!DirectiveKind::StartOfAbc.is_metadata());
3024 assert!(!DirectiveKind::EndOfLy.is_metadata());
3025 assert!(!DirectiveKind::StartOfSvg.is_metadata());
3026 assert!(!DirectiveKind::EndOfTextblock.is_metadata());
3027 }
3028
3029 #[test]
3030 fn delegate_not_comment() {
3031 assert!(!DirectiveKind::StartOfAbc.is_comment());
3032 assert!(!DirectiveKind::EndOfLy.is_comment());
3033 }
3034
3035 #[test]
3036 fn delegate_directive_section_name() {
3037 let d = Directive::name_only("start_of_abc");
3038 assert_eq!(d.section_name(), Some("abc"));
3039
3040 let d = Directive::name_only("end_of_ly");
3041 assert_eq!(d.section_name(), Some("ly"));
3042
3043 let d = Directive::name_only("start_of_svg");
3044 assert_eq!(d.section_name(), Some("svg"));
3045
3046 let d = Directive::name_only("end_of_textblock");
3047 assert_eq!(d.section_name(), Some("textblock"));
3048 }
3049
3050 #[test]
3051 fn delegate_directive_with_label() {
3052 let d = Directive::with_value("start_of_abc", "Melody");
3053 assert_eq!(d.name, "start_of_abc");
3054 assert_eq!(d.value.as_deref(), Some("Melody"));
3055 assert_eq!(d.kind, DirectiveKind::StartOfAbc);
3056 }
3057
3058 #[test]
3059 fn delegate_sections_not_custom() {
3060 assert!(!matches!(
3062 DirectiveKind::from_name("start_of_abc"),
3063 DirectiveKind::StartOfSection(_)
3064 ));
3065 assert!(!matches!(
3066 DirectiveKind::from_name("start_of_ly"),
3067 DirectiveKind::StartOfSection(_)
3068 ));
3069 assert!(!matches!(
3070 DirectiveKind::from_name("start_of_svg"),
3071 DirectiveKind::StartOfSection(_)
3072 ));
3073 assert!(!matches!(
3074 DirectiveKind::from_name("start_of_textblock"),
3075 DirectiveKind::StartOfSection(_)
3076 ));
3077 }
3078}
3079
3080#[cfg(test)]
3081mod chord_definition_tests {
3082 use super::*;
3083
3084 #[test]
3085 fn test_parse_keyboard_definition() {
3086 let def = ChordDefinition::parse_value("Am keys 0 3 7");
3087 assert_eq!(def.name, "Am");
3088 assert_eq!(def.keys, Some(vec![0, 3, 7]));
3089 assert!(def.copy.is_none());
3090 }
3091
3092 #[test]
3093 fn test_parse_keyboard_empty_keys() {
3094 let def = ChordDefinition::parse_value("Am keys");
3096 assert_eq!(def.name, "Am");
3097 assert_eq!(def.keys, None);
3098 }
3099
3100 #[test]
3101 fn test_parse_keyboard_keys_midi_range() {
3102 let def = ChordDefinition::parse_value("Am keys 0 60 127");
3104 assert_eq!(def.keys, Some(vec![0, 60, 127]));
3105 }
3106
3107 #[test]
3108 fn test_parse_keyboard_keys_out_of_range_dropped() {
3109 let def = ChordDefinition::parse_value("Am keys -1 0 128 60");
3111 assert_eq!(def.keys, Some(vec![0, 60]));
3112 }
3113
3114 #[test]
3115 fn test_parse_keyboard_keys_all_invalid() {
3116 let def = ChordDefinition::parse_value("Am keys abc def");
3118 assert_eq!(def.keys, None);
3119 }
3120
3121 #[test]
3122 fn test_parse_keyboard_keys_non_numeric_dropped() {
3123 let def = ChordDefinition::parse_value("Am keys 0 abc 7 xyz 12");
3125 assert_eq!(def.keys, Some(vec![0, 7, 12]));
3126 }
3127
3128 #[test]
3129 fn test_parse_copy() {
3130 let def = ChordDefinition::parse_value("Am copy Amin");
3131 assert_eq!(def.name, "Am");
3132 assert_eq!(def.copy, Some("Amin".to_string()));
3133 assert!(def.keys.is_none());
3134 }
3135
3136 #[test]
3137 fn test_parse_copyall() {
3138 let def = ChordDefinition::parse_value("Am copyall Amin");
3139 assert_eq!(def.name, "Am");
3140 assert_eq!(def.copyall, Some("Amin".to_string()));
3141 }
3142
3143 #[test]
3144 fn test_parse_copy_first_token_only() {
3145 let def = ChordDefinition::parse_value("Am copy Amin extra stuff");
3147 assert_eq!(def.copy, Some("Amin".to_string()));
3148 }
3149
3150 #[test]
3151 fn test_parse_copyall_first_token_only() {
3152 let def = ChordDefinition::parse_value("Am copyall Amin extra stuff");
3153 assert_eq!(def.copyall, Some("Amin".to_string()));
3154 }
3155
3156 #[test]
3157 fn test_parse_copy_with_display() {
3158 let def = ChordDefinition::parse_value("Am copy Bm display=\"Alt\"");
3160 assert_eq!(def.copy, Some("Bm".to_string()));
3161 assert_eq!(def.display, Some("Alt".to_string()));
3162 }
3163
3164 #[test]
3165 fn test_parse_copyall_with_display() {
3166 let def = ChordDefinition::parse_value("Am copyall Bm display=\"Alt\"");
3167 assert_eq!(def.copyall, Some("Bm".to_string()));
3168 assert_eq!(def.display, Some("Alt".to_string()));
3169 }
3170
3171 #[test]
3172 fn test_parse_copy_with_format() {
3173 let def = ChordDefinition::parse_value("Am copy Bm format=\"%{root}m\"");
3174 assert_eq!(def.copy, Some("Bm".to_string()));
3175 assert_eq!(def.format, Some("%{root}m".to_string()));
3176 }
3177
3178 #[test]
3179 fn test_parse_keys_with_display() {
3180 let def = ChordDefinition::parse_value("Am keys 0 3 7 display=\"A minor\"");
3182 assert_eq!(def.keys, Some(vec![0, 3, 7]));
3183 assert_eq!(def.display, Some("A minor".to_string()));
3184 }
3185
3186 #[test]
3187 fn test_parse_keys_with_format() {
3188 let def = ChordDefinition::parse_value("Am keys 0 3 7 format=\"%{root}m\"");
3189 assert_eq!(def.keys, Some(vec![0, 3, 7]));
3190 assert_eq!(def.format, Some("%{root}m".to_string()));
3191 }
3192
3193 #[test]
3194 fn test_parse_fretted_definition() {
3195 let def = ChordDefinition::parse_value("Am base-fret 1 frets x 0 2 2 1 0");
3196 assert_eq!(def.name, "Am");
3197 assert!(def.raw.is_some());
3198 assert!(def.raw.unwrap().contains("base-fret"));
3199 }
3200
3201 #[test]
3202 fn test_parse_name_only() {
3203 let def = ChordDefinition::parse_value("Am");
3204 assert_eq!(def.name, "Am");
3205 assert!(def.keys.is_none());
3206 assert!(def.copy.is_none());
3207 assert!(def.raw.is_none());
3208 }
3209
3210 #[test]
3211 fn test_parse_display_attribute() {
3212 let def =
3213 ChordDefinition::parse_value("Am base-fret 1 frets x 0 2 2 1 0 display=\"A minor\"");
3214 assert_eq!(def.name, "Am");
3215 assert_eq!(def.display, Some("A minor".to_string()));
3216 let raw = def.raw.unwrap();
3218 assert!(
3219 !raw.contains("display="),
3220 "display= should be stripped from raw, got: {raw}"
3221 );
3222 assert!(raw.contains("base-fret"));
3223 }
3224
3225 #[test]
3226 fn test_parse_display_attribute_at_start() {
3227 let def =
3228 ChordDefinition::parse_value("Am display=\"A minor\" base-fret 1 frets x 0 2 2 1 0");
3229 assert_eq!(def.display, Some("A minor".to_string()));
3230 let raw = def.raw.unwrap();
3231 assert!(!raw.contains("display="));
3232 assert!(raw.contains("base-fret"));
3233 }
3234
3235 #[test]
3236 fn test_parse_display_attribute_middle() {
3237 let def =
3238 ChordDefinition::parse_value("Am base-fret 1 display=\"A minor\" frets x 0 2 2 1 0");
3239 assert_eq!(def.display, Some("A minor".to_string()));
3240 let raw = def.raw.unwrap();
3241 assert!(!raw.contains("display="));
3242 assert!(raw.contains("base-fret"));
3243 assert!(raw.contains("frets"));
3244 }
3245
3246 #[test]
3247 fn test_parse_display_unquoted() {
3248 let def = ChordDefinition::parse_value("Am base-fret 1 frets x 0 2 2 1 0 display=Aminor");
3249 assert_eq!(def.display, Some("Aminor".to_string()));
3250 let raw = def.raw.unwrap();
3251 assert!(!raw.contains("display="));
3252 }
3253
3254 #[test]
3255 fn test_parse_display_no_false_match() {
3256 let def = ChordDefinition::parse_value("Am undisplay=foo base-fret 1 frets x 0 2 2 1 0");
3258 assert_eq!(def.display, None);
3259 let raw = def.raw.unwrap();
3260 assert!(raw.contains("undisplay=foo"));
3261 }
3262
3263 #[test]
3264 fn test_parse_display_only() {
3265 let def = ChordDefinition::parse_value("Am display=\"A minor\"");
3267 assert_eq!(def.display, Some("A minor".to_string()));
3268 assert!(def.raw.is_none());
3269 }
3270
3271 #[test]
3272 fn test_parse_format_attribute() {
3273 let def = ChordDefinition::parse_value(
3274 "Am base-fret 1 frets x 0 2 2 1 0 format=\"%{root}%{quality}\"",
3275 );
3276 assert_eq!(def.format, Some("%{root}%{quality}".to_string()));
3277 let raw = def.raw.unwrap();
3278 assert!(!raw.contains("format="));
3279 assert!(raw.contains("base-fret"));
3280 }
3281
3282 #[test]
3283 fn test_parse_both_display_and_format() {
3284 let def = ChordDefinition::parse_value(
3285 "Am display=\"A minor\" format=\"%{root}%{quality}\" base-fret 1 frets x 0 2 2 1 0",
3286 );
3287 assert_eq!(def.display, Some("A minor".to_string()));
3288 assert_eq!(def.format, Some("%{root}%{quality}".to_string()));
3289 let raw = def.raw.unwrap();
3290 assert!(!raw.contains("display="));
3291 assert!(!raw.contains("format="));
3292 }
3293
3294 #[test]
3295 fn test_parse_format_only() {
3296 let def = ChordDefinition::parse_value("Am format=\"%{root}-%{quality}\"");
3297 assert_eq!(def.format, Some("%{root}-%{quality}".to_string()));
3298 assert!(def.raw.is_none());
3299 }
3300
3301 #[test]
3302 fn test_parse_format_unclosed_quote_no_panic() {
3303 let def = ChordDefinition::parse_value("Am display=\"unclosed");
3305 assert_eq!(def.display, Some("unclosed".to_string()));
3306 assert!(def.raw.is_none());
3307 }
3308
3309 #[test]
3310 fn test_parse_format_unclosed_quote_format_attr() {
3311 let def = ChordDefinition::parse_value("Am format=\"%{root}%{quality}");
3312 assert_eq!(def.format, Some("%{root}%{quality}".to_string()));
3313 }
3314
3315 #[test]
3316 fn test_parse_keyboard_negative_keys_dropped() {
3317 let def = ChordDefinition::parse_value("Cm keys -1 0 3 7");
3319 assert_eq!(def.keys, Some(vec![0, 3, 7]));
3320 }
3321
3322 #[test]
3325 fn test_parse_copy_tab_delimiter() {
3326 let def = ChordDefinition::parse_value("Am copy\tAmin");
3327 assert_eq!(def.copy, Some("Amin".to_string()));
3328 }
3329
3330 #[test]
3331 fn test_parse_copyall_tab_delimiter() {
3332 let def = ChordDefinition::parse_value("Am copyall\tAmin");
3333 assert_eq!(def.copyall, Some("Amin".to_string()));
3334 }
3335
3336 #[test]
3337 fn test_parse_copy_multiple_spaces() {
3338 let def = ChordDefinition::parse_value("Am copy Amin");
3339 assert_eq!(def.copy, Some("Amin".to_string()));
3340 }
3341
3342 #[test]
3343 fn test_parse_copyall_multiple_spaces() {
3344 let def = ChordDefinition::parse_value("Am copyall Amin");
3345 assert_eq!(def.copyall, Some("Amin".to_string()));
3346 }
3347
3348 #[test]
3349 fn test_parse_copy_mixed_whitespace() {
3350 let def = ChordDefinition::parse_value("Am copy \t Amin");
3351 assert_eq!(def.copy, Some("Amin".to_string()));
3352 }
3353
3354 #[test]
3355 fn test_parse_copyall_mixed_whitespace() {
3356 let def = ChordDefinition::parse_value("Am copyall \t Amin");
3357 assert_eq!(def.copyall, Some("Amin".to_string()));
3358 }
3359
3360 #[test]
3363 fn test_parse_trailing_display_equals_no_value() {
3364 let def = ChordDefinition::parse_value("Am base-fret 1 frets x 0 2 2 1 0 display=");
3367 assert_eq!(def.display, Some(String::new()));
3368 assert_eq!(def.raw, Some("base-fret 1 frets x 0 2 2 1 0".to_string()));
3370 }
3371
3372 #[test]
3375 fn test_unquoted_empty_display_with_format() {
3376 let def = ChordDefinition::parse_value("Am display= format=\"test\"");
3379 assert_eq!(def.display, Some(String::new()));
3380 assert_eq!(def.format, Some("test".to_string()));
3381 }
3382
3383 #[test]
3384 fn test_unquoted_empty_format_with_display() {
3385 let def = ChordDefinition::parse_value("Am format= display=\"A minor\"");
3388 assert_eq!(def.format, Some(String::new()));
3389 assert_eq!(def.display, Some("A minor".to_string()));
3390 }
3391
3392 #[test]
3395 fn test_unquoted_value_with_equals_treated_as_empty() {
3396 let def = ChordDefinition::parse_value("Am display=val=ue");
3400 assert_eq!(
3401 def.display,
3402 Some(String::new()),
3403 "unquoted value with '=' should be treated as empty"
3404 );
3405 }
3406
3407 #[test]
3408 fn test_quoted_value_with_equals_preserved() {
3409 let def = ChordDefinition::parse_value("Am display=\"val=ue\"");
3411 assert_eq!(def.display, Some("val=ue".to_string()));
3412 }
3413
3414 #[test]
3417 fn test_define_after_usage_still_applies() {
3418 let mut song = Song::new();
3422 let mut lyrics = LyricsLine::new();
3423 lyrics
3424 .segments
3425 .push(LyricsSegment::new(Some(Chord::new("Am")), "word "));
3426 song.lines.push(Line::Lyrics(lyrics));
3427 song.lines.push(Line::Directive(Directive::with_value(
3428 "define",
3429 "Am display=\"A minor\"",
3430 )));
3431 song.apply_define_displays();
3432
3433 if let Line::Lyrics(ref lyrics) = song.lines[0] {
3434 assert_eq!(
3435 lyrics.segments[0].chord.as_ref().unwrap().display_name(),
3436 "A minor"
3437 );
3438 } else {
3439 panic!("expected lyrics line");
3440 }
3441 }
3442
3443 #[test]
3446 fn test_parse_keys_multiple_spaces() {
3447 let def = ChordDefinition::parse_value("Am keys 0 3 7");
3448 assert_eq!(def.keys, Some(vec![0, 3, 7]));
3449 }
3450
3451 #[test]
3452 fn test_parse_keys_tab_separator() {
3453 let def = ChordDefinition::parse_value("Am keys\t0 3 7");
3454 assert_eq!(def.keys, Some(vec![0, 3, 7]));
3455 }
3456
3457 #[test]
3458 fn test_parse_keys_only_keyword() {
3459 let def = ChordDefinition::parse_value("Am keys");
3461 assert!(def.keys.is_none());
3462 }
3463
3464 #[test]
3467 fn has_src_returns_true_for_non_empty() {
3468 let attrs = ImageAttributes::new("photo.jpg");
3469 assert!(attrs.has_src());
3470 }
3471
3472 #[test]
3473 fn has_src_returns_false_for_empty() {
3474 let attrs = ImageAttributes::default();
3475 assert!(!attrs.has_src());
3476 }
3477
3478 #[test]
3479 fn has_src_returns_false_for_explicit_empty_string() {
3480 let attrs = ImageAttributes::new("");
3481 assert!(!attrs.has_src());
3482 }
3483}
3484
3485#[cfg(test)]
3486mod apply_define_displays_tests {
3487 use super::*;
3488
3489 fn make_song_with_define_and_chords(define_value: &str, chord_names: &[&str]) -> Song {
3490 let mut song = Song::new();
3491
3492 song.lines.push(Line::Directive(Directive::with_value(
3494 "define",
3495 define_value,
3496 )));
3497
3498 let mut lyrics = LyricsLine::new();
3500 for name in chord_names {
3501 lyrics
3502 .segments
3503 .push(LyricsSegment::new(Some(Chord::new(*name)), "word "));
3504 }
3505 song.lines.push(Line::Lyrics(lyrics));
3506
3507 song
3508 }
3509
3510 #[test]
3511 fn applies_display_to_matching_chords() {
3512 let mut song = make_song_with_define_and_chords(
3513 "Am base-fret 1 frets x 0 2 2 1 0 display=\"A minor\"",
3514 &["Am", "G", "Am"],
3515 );
3516 song.apply_define_displays();
3517
3518 if let Line::Lyrics(ref lyrics) = song.lines[1] {
3519 assert_eq!(
3520 lyrics.segments[0].chord.as_ref().unwrap().display_name(),
3521 "A minor"
3522 );
3523 assert_eq!(
3524 lyrics.segments[1].chord.as_ref().unwrap().display_name(),
3525 "G"
3526 );
3527 assert_eq!(
3528 lyrics.segments[2].chord.as_ref().unwrap().display_name(),
3529 "A minor"
3530 );
3531 } else {
3532 panic!("expected lyrics line");
3533 }
3534 }
3535
3536 #[test]
3537 fn no_display_when_not_defined() {
3538 let mut song =
3539 make_song_with_define_and_chords("Am base-fret 1 frets x 0 2 2 1 0", &["Am"]);
3540 song.apply_define_displays();
3541
3542 if let Line::Lyrics(ref lyrics) = song.lines[1] {
3543 assert_eq!(lyrics.segments[0].chord.as_ref().unwrap().display, None);
3544 } else {
3545 panic!("expected lyrics line");
3546 }
3547 }
3548
3549 #[test]
3550 fn later_define_overrides_earlier() {
3551 let mut song = Song::new();
3552 song.lines.push(Line::Directive(Directive::with_value(
3553 "define",
3554 "Am display=\"first\"",
3555 )));
3556 song.lines.push(Line::Directive(Directive::with_value(
3557 "define",
3558 "Am display=\"second\"",
3559 )));
3560 let mut lyrics = LyricsLine::new();
3561 lyrics
3562 .segments
3563 .push(LyricsSegment::new(Some(Chord::new("Am")), "text"));
3564 song.lines.push(Line::Lyrics(lyrics));
3565
3566 song.apply_define_displays();
3567
3568 if let Line::Lyrics(ref lyrics) = song.lines[2] {
3569 assert_eq!(
3570 lyrics.segments[0].chord.as_ref().unwrap().display_name(),
3571 "second"
3572 );
3573 } else {
3574 panic!("expected lyrics line");
3575 }
3576 }
3577
3578 #[test]
3579 fn does_not_overwrite_existing_display() {
3580 let mut song = Song::new();
3581 song.lines.push(Line::Directive(Directive::with_value(
3582 "define",
3583 "Am display=\"from define\"",
3584 )));
3585 let mut lyrics = LyricsLine::new();
3586 let mut chord = Chord::new("Am");
3587 chord.display = Some("already set".to_string());
3588 lyrics
3589 .segments
3590 .push(LyricsSegment::new(Some(chord), "text"));
3591 song.lines.push(Line::Lyrics(lyrics));
3592
3593 song.apply_define_displays();
3594
3595 if let Line::Lyrics(ref lyrics) = song.lines[1] {
3596 assert_eq!(
3597 lyrics.segments[0].chord.as_ref().unwrap().display_name(),
3598 "already set"
3599 );
3600 } else {
3601 panic!("expected lyrics line");
3602 }
3603 }
3604
3605 #[test]
3606 fn format_expands_chord_components() {
3607 let mut song =
3608 make_song_with_define_and_chords("Am format=\"%{root} %{quality}\"", &["Am"]);
3609 song.apply_define_displays();
3610
3611 if let Line::Lyrics(ref lyrics) = song.lines[1] {
3612 assert_eq!(
3613 lyrics.segments[0].chord.as_ref().unwrap().display_name(),
3614 "A m"
3615 );
3616 } else {
3617 panic!("expected lyrics line");
3618 }
3619 }
3620
3621 #[test]
3622 fn format_with_extension() {
3623 let mut song =
3624 make_song_with_define_and_chords("Am7 format=\"%{root}%{quality}%{ext}\"", &["Am7"]);
3625 song.apply_define_displays();
3626
3627 if let Line::Lyrics(ref lyrics) = song.lines[1] {
3628 assert_eq!(
3629 lyrics.segments[0].chord.as_ref().unwrap().display_name(),
3630 "Am7"
3631 );
3632 } else {
3633 panic!("expected lyrics line");
3634 }
3635 }
3636
3637 #[test]
3638 fn format_with_bass_note() {
3639 let mut song = make_song_with_define_and_chords("G/B format=\"%{root}/%{bass}\"", &["G/B"]);
3640 song.apply_define_displays();
3641
3642 if let Line::Lyrics(ref lyrics) = song.lines[1] {
3643 assert_eq!(
3644 lyrics.segments[0].chord.as_ref().unwrap().display_name(),
3645 "G/B"
3646 );
3647 } else {
3648 panic!("expected lyrics line");
3649 }
3650 }
3651
3652 #[test]
3653 fn display_takes_precedence_over_format() {
3654 let mut song = make_song_with_define_and_chords(
3655 "Am display=\"A minor\" format=\"%{root}%{quality}\"",
3656 &["Am"],
3657 );
3658 song.apply_define_displays();
3659
3660 if let Line::Lyrics(ref lyrics) = song.lines[1] {
3661 assert_eq!(
3662 lyrics.segments[0].chord.as_ref().unwrap().display_name(),
3663 "A minor"
3664 );
3665 } else {
3666 panic!("expected lyrics line");
3667 }
3668 }
3669}
3670
3671#[cfg(test)]
3672mod expand_format_tests {
3673 use super::*;
3674
3675 #[test]
3676 fn basic_root_quality() {
3677 let chord = Chord::new("Am");
3678 assert_eq!(
3679 chord.expand_format("%{root}%{quality}"),
3680 Some("Am".to_string())
3681 );
3682 }
3683
3684 #[test]
3685 fn with_accidental() {
3686 let chord = Chord::new("Bb");
3687 assert_eq!(chord.expand_format("%{root}"), Some("Bb".to_string()));
3688 }
3689
3690 #[test]
3691 fn with_extension() {
3692 let chord = Chord::new("Cmaj7");
3693 let result = chord.expand_format("%{root}%{quality}%{ext}");
3694 assert_eq!(result, Some("Cmaj7".to_string()));
3695 }
3696
3697 #[test]
3698 fn with_bass() {
3699 let chord = Chord::new("Am/G");
3700 let result = chord.expand_format("%{root}%{quality}/%{bass}");
3701 assert_eq!(result, Some("Am/G".to_string()));
3702 }
3703
3704 #[test]
3705 fn custom_format() {
3706 let chord = Chord::new("Am");
3707 let result = chord.expand_format("[%{root} minor]");
3708 assert_eq!(result, Some("[A minor]".to_string()));
3709 }
3710
3711 #[test]
3712 fn returns_none_for_unparsed_chord() {
3713 let chord = Chord {
3714 name: "???".to_string(),
3715 detail: None,
3716 display: None,
3717 };
3718 assert_eq!(chord.expand_format("%{root}"), None);
3719 }
3720
3721 #[test]
3722 fn unknown_placeholder_passes_through() {
3723 let chord = Chord::new("Am");
3724 let result = chord.expand_format("%{root}%{unknown}");
3725 assert_eq!(result, Some("A%{unknown}".to_string()));
3726 }
3727
3728 #[test]
3729 fn empty_format_string() {
3730 let chord = Chord::new("Am");
3731 assert_eq!(chord.expand_format(""), Some(String::new()));
3732 }
3733
3734 #[test]
3735 fn slash_chord_bass_with_accidental() {
3736 let chord = Chord::new("G/Bb");
3737 let result = chord.expand_format("%{root}/%{bass}");
3738 assert_eq!(result, Some("G/Bb".to_string()));
3739 }
3740
3741 #[test]
3742 fn no_bass_produces_empty_string() {
3743 let chord = Chord::new("Am");
3744 let result = chord.expand_format("%{root}%{quality} (bass: %{bass})");
3745 assert_eq!(result, Some("Am (bass: )".to_string()));
3746 }
3747
3748 #[test]
3749 fn all_placeholders_combined() {
3750 let chord = Chord::new("Bbm7/Eb");
3751 let result = chord.expand_format("%{root}%{quality}%{ext}/%{bass}");
3752 assert_eq!(result, Some("Bbm7/Eb".to_string()));
3753 }
3754
3755 #[test]
3756 fn literal_text_with_no_placeholders() {
3757 let chord = Chord::new("Am");
3758 let result = chord.expand_format("just text");
3759 assert_eq!(result, Some("just text".to_string()));
3760 }
3761}