1use std::fmt;
11use std::str::FromStr;
12
13use crate::{
14 defaults::IntegerType,
15 duration::DurationType,
16 error::{Error, Result},
17};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub enum TieType {
23 Start,
25 Stop,
27 Continue,
29 LetRing,
31 ContinueLetRing,
33}
34
35impl TieType {
36 pub const ALL: [TieType; 5] = [
38 TieType::Start,
39 TieType::Stop,
40 TieType::Continue,
41 TieType::LetRing,
42 TieType::ContinueLetRing,
43 ];
44
45 pub fn as_str(self) -> &'static str {
47 match self {
48 TieType::Start => "start",
49 TieType::Stop => "stop",
50 TieType::Continue => "continue",
51 TieType::LetRing => "let-ring",
52 TieType::ContinueLetRing => "continue-let-ring",
53 }
54 }
55
56 pub fn from_name(name: &str) -> Result<Self> {
58 Self::ALL
59 .into_iter()
60 .find(|candidate| candidate.as_str() == name)
61 .ok_or_else(|| {
62 let valid = Self::ALL
63 .iter()
64 .map(|candidate| format!("'{}'", candidate.as_str()))
65 .collect::<Vec<_>>()
66 .join(", ");
67 Error::Notation(format!("Type must be one of ({valid}), not {name}"))
68 })
69 }
70}
71
72impl fmt::Display for TieType {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str(self.as_str())
75 }
76}
77
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
81pub enum TieStyle {
82 #[default]
84 Normal,
85 Dotted,
87 Dashed,
89 Hidden,
91}
92
93impl TieStyle {
94 pub const ALL: [TieStyle; 4] = [
96 TieStyle::Normal,
97 TieStyle::Dotted,
98 TieStyle::Dashed,
99 TieStyle::Hidden,
100 ];
101
102 pub fn as_str(self) -> &'static str {
104 match self {
105 TieStyle::Normal => "normal",
106 TieStyle::Dotted => "dotted",
107 TieStyle::Dashed => "dashed",
108 TieStyle::Hidden => "hidden",
109 }
110 }
111
112 pub fn from_name(name: &str) -> Result<Self> {
114 Self::ALL
115 .into_iter()
116 .find(|candidate| candidate.as_str() == name)
117 .ok_or_else(|| Error::Notation(format!("not a valid tie style: {name}")))
118 }
119}
120
121impl fmt::Display for TieStyle {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 f.write_str(self.as_str())
124 }
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub enum Placement {
131 Above,
133 Below,
135}
136
137impl Placement {
138 pub fn as_str(self) -> &'static str {
140 match self {
141 Placement::Above => "above",
142 Placement::Below => "below",
143 }
144 }
145
146 pub fn from_name(name: &str) -> Result<Self> {
148 match name {
149 "above" => Ok(Placement::Above),
150 "below" => Ok(Placement::Below),
151 other => Err(Error::Notation(format!("not a valid placement: {other}"))),
152 }
153 }
154}
155
156impl fmt::Display for Placement {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 f.write_str(self.as_str())
159 }
160}
161
162#[derive(Clone, Debug, PartialEq, Eq, Hash)]
168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
169#[must_use]
170pub struct Tie {
171 tie_type: TieType,
172 style: TieStyle,
173 placement: Option<Placement>,
174}
175
176impl Tie {
177 pub fn new(tie_type: TieType) -> Self {
179 Self {
180 tie_type,
181 style: TieStyle::default(),
182 placement: None,
183 }
184 }
185
186 pub fn from_name(name: &str) -> Result<Self> {
189 Ok(Self::new(TieType::from_name(name)?))
190 }
191
192 pub fn tie_type(&self) -> TieType {
194 self.tie_type
195 }
196
197 pub fn set_tie_type(&mut self, tie_type: TieType) {
199 self.tie_type = tie_type;
200 }
201
202 pub fn style(&self) -> TieStyle {
204 self.style
205 }
206
207 pub fn set_style(&mut self, style: TieStyle) {
209 self.style = style;
210 }
211
212 pub fn placement(&self) -> Option<Placement> {
214 self.placement
215 }
216
217 pub fn set_placement(&mut self, placement: Option<Placement>) {
219 self.placement = placement;
220 }
221}
222
223impl Default for Tie {
224 fn default() -> Self {
226 Self::new(TieType::Start)
227 }
228}
229
230impl fmt::Display for Tie {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 write!(f, "Tie {}", self.tie_type)
233 }
234}
235
236impl FromStr for Tie {
237 type Err = Error;
238
239 fn from_str(name: &str) -> Result<Self> {
240 Self::from_name(name)
241 }
242}
243
244#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
246#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
247#[must_use]
248pub enum Notehead {
249 ArrowDown,
251 ArrowUp,
253 BackSlashed,
255 CircleDot,
257 CircleX,
259 Circled,
261 Cluster,
263 Cross,
265 Diamond,
267 Do,
269 Fa,
271 FaUp,
273 InvertedTriangle,
275 La,
277 LeftTriangle,
279 Mi,
281 NoneShape,
283 #[default]
285 Normal,
286 Other,
288 Re,
290 Rectangle,
292 Slash,
294 Slashed,
296 So,
298 Square,
300 Ti,
302 Triangle,
304 X,
306}
307
308impl Notehead {
309 pub const ALL: [Notehead; 28] = [
311 Notehead::ArrowDown,
312 Notehead::ArrowUp,
313 Notehead::BackSlashed,
314 Notehead::CircleDot,
315 Notehead::CircleX,
316 Notehead::Circled,
317 Notehead::Cluster,
318 Notehead::Cross,
319 Notehead::Diamond,
320 Notehead::Do,
321 Notehead::Fa,
322 Notehead::FaUp,
323 Notehead::InvertedTriangle,
324 Notehead::La,
325 Notehead::LeftTriangle,
326 Notehead::Mi,
327 Notehead::NoneShape,
328 Notehead::Normal,
329 Notehead::Other,
330 Notehead::Re,
331 Notehead::Rectangle,
332 Notehead::Slash,
333 Notehead::Slashed,
334 Notehead::So,
335 Notehead::Square,
336 Notehead::Ti,
337 Notehead::Triangle,
338 Notehead::X,
339 ];
340
341 pub fn as_str(self) -> &'static str {
343 match self {
344 Notehead::ArrowDown => "arrow down",
345 Notehead::ArrowUp => "arrow up",
346 Notehead::BackSlashed => "back slashed",
347 Notehead::CircleDot => "circle dot",
348 Notehead::CircleX => "circle-x",
349 Notehead::Circled => "circled",
350 Notehead::Cluster => "cluster",
351 Notehead::Cross => "cross",
352 Notehead::Diamond => "diamond",
353 Notehead::Do => "do",
354 Notehead::Fa => "fa",
355 Notehead::FaUp => "fa up",
356 Notehead::InvertedTriangle => "inverted triangle",
357 Notehead::La => "la",
358 Notehead::LeftTriangle => "left triangle",
359 Notehead::Mi => "mi",
360 Notehead::NoneShape => "none",
361 Notehead::Normal => "normal",
362 Notehead::Other => "other",
363 Notehead::Re => "re",
364 Notehead::Rectangle => "rectangle",
365 Notehead::Slash => "slash",
366 Notehead::Slashed => "slashed",
367 Notehead::So => "so",
368 Notehead::Square => "square",
369 Notehead::Ti => "ti",
370 Notehead::Triangle => "triangle",
371 Notehead::X => "x",
372 }
373 }
374
375 pub fn from_name(name: &str) -> Result<Self> {
377 Self::ALL
378 .into_iter()
379 .find(|candidate| candidate.as_str() == name)
380 .ok_or_else(|| Error::Notation(format!("not a valid notehead type name: '{name}'")))
381 }
382}
383
384impl fmt::Display for Notehead {
385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 f.write_str(self.as_str())
387 }
388}
389
390#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
399#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
400pub enum BeamType {
401 #[default]
403 Start,
404 Continue,
406 Stop,
408 PartialBeam,
410}
411
412impl BeamType {
413 pub fn as_str(self) -> &'static str {
415 match self {
416 Self::Start => "start",
417 Self::Continue => "continue",
418 Self::Stop => "stop",
419 Self::PartialBeam => "partial",
420 }
421 }
422
423 pub fn from_music21_name(name: &str) -> Option<Self> {
425 match name {
426 "start" => Some(Self::Start),
427 "continue" => Some(Self::Continue),
428 "stop" => Some(Self::Stop),
429 "partial" => Some(Self::PartialBeam),
430 _ => None,
431 }
432 }
433}
434
435impl fmt::Display for BeamType {
436 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437 f.write_str(self.as_str())
438 }
439}
440
441#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
443#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
444pub enum BeamDirection {
445 Left,
447 Right,
449}
450
451impl BeamDirection {
452 pub fn as_str(self) -> &'static str {
454 match self {
455 Self::Left => "left",
456 Self::Right => "right",
457 }
458 }
459
460 pub fn from_music21_name(name: &str) -> Option<Self> {
462 match name {
463 "left" => Some(Self::Left),
464 "right" => Some(Self::Right),
465 _ => None,
466 }
467 }
468}
469
470impl fmt::Display for BeamDirection {
471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472 f.write_str(self.as_str())
473 }
474}
475
476#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
478#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
479#[must_use]
480pub struct Beam {
481 beam_type: Option<BeamType>,
485 direction: Option<BeamDirection>,
486 number: Option<u32>,
487}
488
489impl Beam {
490 pub fn new(beam_type: impl Into<Option<BeamType>>, direction: Option<BeamDirection>) -> Self {
492 Self {
493 beam_type: beam_type.into(),
494 direction,
495 number: None,
496 }
497 }
498
499 pub fn beam_type(&self) -> Option<BeamType> {
502 self.beam_type
503 }
504
505 pub fn set_beam_type(&mut self, beam_type: Option<BeamType>) {
507 self.beam_type = beam_type;
508 }
509
510 pub fn direction(&self) -> Option<BeamDirection> {
512 self.direction
513 }
514
515 pub fn set_direction(&mut self, direction: Option<BeamDirection>) {
517 self.direction = direction;
518 }
519
520 pub fn number(&self) -> Option<u32> {
522 self.number
523 }
524
525 pub fn set_number(&mut self, number: Option<u32>) {
527 self.number = number;
528 }
529}
530
531impl fmt::Display for Beam {
532 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535 let number = match self.number {
536 Some(number) => number.to_string(),
537 None => "None".to_string(),
538 };
539 let beam_type = match self.beam_type {
540 Some(beam_type) => beam_type.to_string(),
541 None => "None".to_string(),
542 };
543 match self.direction {
544 Some(direction) => write!(f, "{number}/{beam_type}/{direction}"),
545 None => write!(f, "{number}/{beam_type}"),
546 }
547 }
548}
549
550#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
553#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
554#[must_use]
555pub struct Beams {
556 beams: Vec<Beam>,
557 feathered: bool,
559}
560
561const BEAMABLE: [(DurationType, u32); 9] = [
564 (DurationType::Eighth, 1),
565 (DurationType::Sixteenth, 2),
566 (DurationType::ThirtySecond, 3),
567 (DurationType::SixtyFourth, 4),
568 (DurationType::HundredTwentyEighth, 5),
569 (DurationType::TwoHundredFiftySixth, 6),
570 (DurationType::FiveHundredTwelfth, 7),
571 (DurationType::TenTwentyFourth, 8),
572 (DurationType::TwentyFortyEighth, 9),
573];
574
575impl Beams {
576 pub fn new() -> Self {
578 Self::default()
579 }
580
581 pub fn beams(&self) -> &[Beam] {
583 &self.beams
584 }
585
586 pub fn len(&self) -> usize {
588 self.beams.len()
589 }
590
591 pub fn is_empty(&self) -> bool {
593 self.beams.is_empty()
594 }
595
596 pub fn feathered(&self) -> bool {
598 self.feathered
599 }
600
601 pub fn set_feathered(&mut self, feathered: bool) {
603 self.feathered = feathered;
604 }
605
606 pub fn beams_mut(&mut self) -> &mut Vec<Beam> {
608 &mut self.beams
609 }
610
611 pub fn set_beams(&mut self, beams: Vec<Beam>) {
613 self.beams = beams;
614 }
615
616 pub fn append(
618 &mut self,
619 beam_type: impl Into<Option<BeamType>>,
620 direction: Option<BeamDirection>,
621 ) {
622 let mut beam = Beam::new(beam_type, direction);
623 beam.set_number(Some(self.beams.len() as u32 + 1));
624 self.beams.push(beam);
625 }
626
627 pub const LEVELS: usize = BEAMABLE.len();
631
632 pub fn levels_for(duration_type: DurationType) -> Option<u32> {
634 BEAMABLE
635 .into_iter()
636 .find(|(candidate, _)| *candidate == duration_type)
637 .map(|(_, levels)| levels)
638 }
639
640 pub fn naive_beams<'a>(
665 elements: impl IntoIterator<Item = &'a crate::stream::StreamElement>,
666 ) -> Vec<Option<Beams>> {
667 use crate::stream::StreamElement;
668 elements
669 .into_iter()
670 .map(|element| {
671 let sounds = matches!(
672 element,
673 StreamElement::Note(_)
674 | StreamElement::Chord(_)
675 | StreamElement::Unpitched(_)
676 | StreamElement::PercussionChord(_)
677 | StreamElement::ChordSymbol(_)
678 );
679 let written = element.duration().and_then(|duration| {
680 match duration.components().as_slice() {
681 [(duration_type, _)] => Some(*duration_type),
682 _ => None,
683 }
684 });
685 Self::naive(written, sounds)
686 })
687 .collect()
688 }
689
690 pub fn naive(written: Option<DurationType>, sounds: bool) -> Option<Beams> {
694 let levels = written.and_then(Self::levels_for).filter(|_| sounds)?;
695 let mut made = Beams::new();
696 made.fill_levels(levels, None).ok()?;
697 Some(made)
698 }
699
700 pub fn fill(&mut self, duration_type: DurationType, beam_type: Option<BeamType>) -> Result<()> {
706 let Some(levels) = Self::levels_for(duration_type) else {
707 return Err(Error::Notation(format!(
708 "cannot fill beams for a {} note",
709 duration_type.music21_name()
710 )));
711 };
712 self.fill_levels(levels, beam_type)
713 }
714
715 pub fn fill_levels(&mut self, levels: u32, beam_type: Option<BeamType>) -> Result<()> {
721 if levels == 0 || levels > BEAMABLE.len() as u32 {
722 return Err(Error::Notation(format!(
723 "cannot fill beams for level {levels}"
724 )));
725 }
726 self.beams.clear();
727 for _ in 0..levels {
728 self.append(None, None);
729 }
730 if let Some(beam_type) = beam_type {
731 self.set_all(beam_type, None);
732 }
733 Ok(())
734 }
735
736 pub fn set_all(&mut self, beam_type: BeamType, direction: Option<BeamDirection>) {
738 for beam in &mut self.beams {
739 beam.beam_type = Some(beam_type);
740 beam.direction = direction;
741 }
742 }
743
744 pub fn by_number(&self, number: u32) -> Option<&Beam> {
746 self.beams.iter().find(|beam| beam.number == Some(number))
747 }
748
749 pub fn set_by_number(
751 &mut self,
752 number: u32,
753 beam_type: BeamType,
754 direction: Option<BeamDirection>,
755 ) -> Result<()> {
756 let Some(beam) = self
757 .beams
758 .iter_mut()
759 .find(|beam| beam.number == Some(number))
760 else {
761 return Err(Error::Notation(format!(
762 "beam number {number} does not exist"
763 )));
764 };
765 beam.beam_type = Some(beam_type);
766 beam.direction = direction;
767 Ok(())
768 }
769
770 pub fn types(&self) -> Vec<Option<BeamType>> {
772 self.beams.iter().map(Beam::beam_type).collect()
773 }
774
775 pub fn numbers(&self) -> Vec<Option<u32>> {
777 self.beams.iter().map(Beam::number).collect()
778 }
779}
780
781impl fmt::Display for Beams {
782 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
784 let written: Vec<String> = self
785 .beams
786 .iter()
787 .map(|beam| format!("<music21.beam.Beam {beam}>"))
788 .collect();
789 f.write_str(&written.join("/"))
790 }
791}
792
793pub fn remove_sandwiched_unbeamables(beams: &mut [Option<Beams>]) {
799 let mut previous: Option<Beams> = None;
800 for index in 0..beams.len() {
801 let next = beams.get(index + 1).cloned().flatten();
802 if previous.is_none() && next.is_none() {
803 beams[index] = None;
804 }
805 previous = beams[index].clone();
806 }
807}
808
809pub fn sanitize_partial_beams(beams: &mut [Option<Beams>]) {
812 for entry in beams.iter_mut() {
813 let Some(group) = entry else {
814 continue;
815 };
816 let joined = group.types().into_iter().flatten().any(|beam_type| {
817 matches!(
818 beam_type,
819 BeamType::Start | BeamType::Stop | BeamType::Continue
820 )
821 });
822 if !joined {
823 *entry = None;
824 continue;
825 }
826 let mut after_start = false;
827 let mut after_stop = false;
828 for beam in group.beams_mut() {
829 match beam.beam_type() {
830 Some(BeamType::Start) => after_start = true,
831 Some(BeamType::Stop) => after_stop = true,
832 Some(BeamType::PartialBeam) => {
833 if after_start && beam.direction() == Some(BeamDirection::Left) {
834 beam.set_direction(Some(BeamDirection::Right));
835 } else if after_stop && beam.direction() == Some(BeamDirection::Right) {
836 beam.set_direction(Some(BeamDirection::Left));
837 }
838 }
839 _ => {}
840 }
841 }
842 }
843}
844
845pub fn merge_connecting_partial_beams(beams: &mut [Option<Beams>]) {
848 for index in 0..beams.len().saturating_sub(1) {
849 let Some(numbers) = beams[index]
850 .as_ref()
851 .map(|group| group.numbers().into_iter().flatten().collect::<Vec<_>>())
852 else {
853 continue;
854 };
855 if beams[index + 1].is_none() {
856 continue;
857 }
858 for number in numbers {
859 let this = beams[index]
860 .as_ref()
861 .and_then(|group| group.by_number(number))
862 .copied();
863 let next = beams[index + 1]
864 .as_ref()
865 .and_then(|group| group.by_number(number))
866 .copied();
867 let (Some(this), Some(next)) = (this, next) else {
868 continue;
869 };
870 if this.beam_type() != Some(BeamType::PartialBeam)
871 || this.direction() != Some(BeamDirection::Right)
872 {
873 continue;
874 }
875 if next.beam_type() == Some(BeamType::PartialBeam)
876 && next.direction() == Some(BeamDirection::Right)
877 {
878 continue;
879 }
880 if matches!(
883 next.beam_type(),
884 Some(BeamType::Continue) | Some(BeamType::Stop)
885 ) {
886 continue;
887 }
888 set_beam(&mut beams[index], number, BeamType::Start, None);
889 let merged = match next.beam_type() {
890 Some(BeamType::PartialBeam) => BeamType::Stop,
891 Some(BeamType::Start) => BeamType::Continue,
892 other => other.unwrap_or(BeamType::Start),
893 };
894 set_beam(&mut beams[index + 1], number, merged, None);
895 }
896 }
897
898 for index in 1..beams.len() {
900 let Some(numbers) = beams[index]
901 .as_ref()
902 .map(|group| group.numbers().into_iter().flatten().collect::<Vec<_>>())
903 else {
904 continue;
905 };
906 if beams[index - 1].is_none() {
907 continue;
908 }
909 for number in numbers {
910 let this = beams[index]
911 .as_ref()
912 .and_then(|group| group.by_number(number))
913 .copied();
914 let previous = beams[index - 1]
915 .as_ref()
916 .and_then(|group| group.by_number(number))
917 .copied();
918 let (Some(this), Some(previous)) = (this, previous) else {
919 continue;
920 };
921 if this.beam_type() != Some(BeamType::PartialBeam)
922 || this.direction() != Some(BeamDirection::Left)
923 || previous.beam_type() != Some(BeamType::Stop)
924 {
925 continue;
926 }
927 set_beam(&mut beams[index], number, BeamType::Stop, None);
928 set_beam(&mut beams[index - 1], number, BeamType::Continue, None);
929 }
930 }
931}
932
933fn set_beam(
935 group: &mut Option<Beams>,
936 number: u32,
937 beam_type: BeamType,
938 direction: Option<BeamDirection>,
939) {
940 if let Some(group) = group {
941 let _ = group.set_by_number(number, beam_type, direction);
942 }
943}
944
945#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
948#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
949pub enum NoteSize {
950 Full,
952 Cue,
954 Grace,
956 GraceCue,
958 Large,
960}
961
962impl NoteSize {
963 pub fn as_str(self) -> &'static str {
966 match self {
967 Self::Full => "full",
968 Self::Cue => "cue",
969 Self::Grace => "grace",
970 Self::GraceCue => "grace-cue",
971 Self::Large => "large",
972 }
973 }
974
975 pub fn from_name(name: &str) -> Option<Self> {
977 [
978 Self::Full,
979 Self::Cue,
980 Self::Grace,
981 Self::GraceCue,
982 Self::Large,
983 ]
984 .into_iter()
985 .find(|size| size.as_str() == name)
986 }
987}
988
989#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
991#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
992pub enum StemDirection {
993 Double,
995 Down,
997 NoStem,
999 #[default]
1001 Unspecified,
1002 Up,
1004}
1005
1006impl StemDirection {
1007 pub const ALL: [StemDirection; 5] = [
1013 StemDirection::Double,
1014 StemDirection::Down,
1015 StemDirection::NoStem,
1016 StemDirection::Unspecified,
1017 StemDirection::Up,
1018 ];
1019
1020 pub const NAMES: [&'static str; 6] = ["double", "down", "noStem", "none", "unspecified", "up"];
1023
1024 pub fn as_str(self) -> &'static str {
1026 match self {
1027 StemDirection::Double => "double",
1028 StemDirection::Down => "down",
1029 StemDirection::NoStem => "noStem",
1030 StemDirection::Unspecified => "unspecified",
1031 StemDirection::Up => "up",
1032 }
1033 }
1034
1035 pub fn from_name(name: &str) -> Result<Self> {
1040 if name == "none" {
1041 return Ok(StemDirection::NoStem);
1042 }
1043 Self::ALL
1044 .into_iter()
1045 .find(|candidate| candidate.as_str() == name)
1046 .ok_or_else(|| Error::Notation(format!("not a valid stem direction name: {name}")))
1047 }
1048}
1049
1050impl fmt::Display for StemDirection {
1051 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1052 f.write_str(self.as_str())
1053 }
1054}
1055
1056#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1058#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1059pub enum Syllabic {
1060 Single,
1062 Begin,
1064 Middle,
1066 End,
1068 Composite,
1070}
1071
1072impl Syllabic {
1073 pub const ALL: [Syllabic; 5] = [
1075 Syllabic::Single,
1076 Syllabic::Begin,
1077 Syllabic::Middle,
1078 Syllabic::End,
1079 Syllabic::Composite,
1080 ];
1081
1082 pub fn as_str(self) -> &'static str {
1084 match self {
1085 Syllabic::Single => "single",
1086 Syllabic::Begin => "begin",
1087 Syllabic::Middle => "middle",
1088 Syllabic::End => "end",
1089 Syllabic::Composite => "composite",
1090 }
1091 }
1092
1093 pub fn from_name(name: &str) -> Result<Self> {
1095 Self::ALL
1096 .into_iter()
1097 .find(|candidate| candidate.as_str() == name)
1098 .ok_or_else(|| {
1099 Error::Notation(format!(
1102 "Syllabic value '{name}' is not in note.SYLLABIC_CHOICES, namely: [None, 'begin', 'single', 'end', 'middle', 'composite']"
1103 ))
1104 })
1105 }
1106}
1107
1108impl fmt::Display for Syllabic {
1109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1110 f.write_str(self.as_str())
1111 }
1112}
1113
1114#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1121#[must_use]
1122pub struct Lyric {
1123 text: Option<String>,
1127 number: IntegerType,
1128 syllabic: Syllabic,
1129 said_syllabic: bool,
1135 identifier: Option<String>,
1136 components: Vec<Lyric>,
1137 elision_before: String,
1138 #[cfg_attr(feature = "serde", serde(default))]
1139 color: crate::display::Color,
1140 #[cfg_attr(feature = "serde", serde(default))]
1141 justify: crate::display::Drawn<Option<Justification>>,
1142 #[cfg_attr(feature = "serde", serde(default))]
1143 placement: crate::display::Drawn<Option<Placement>>,
1144 #[cfg_attr(feature = "serde", serde(default))]
1145 hidden: crate::display::Drawn<bool>,
1146}
1147
1148#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1151#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1152pub enum Justification {
1153 Left,
1155 Center,
1157 Right,
1159 Full,
1161}
1162
1163impl Justification {
1164 pub fn as_str(self) -> &'static str {
1166 match self {
1167 Self::Left => "left",
1168 Self::Center => "center",
1169 Self::Right => "right",
1170 Self::Full => "full",
1171 }
1172 }
1173
1174 pub fn from_name(name: &str) -> Result<Self> {
1180 match name.to_lowercase().as_str() {
1181 "left" => Ok(Self::Left),
1182 "center" => Ok(Self::Center),
1183 "right" => Ok(Self::Right),
1184 "full" => Ok(Self::Full),
1185 _ => Err(Error::Notation(format!(
1186 "Not a supported justification: '{name}'"
1187 ))),
1188 }
1189 }
1190}
1191
1192impl fmt::Display for Justification {
1193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1194 f.write_str(self.as_str())
1195 }
1196}
1197
1198const DEFAULT_ELISION: &str = " ";
1201
1202impl Lyric {
1203 pub fn new(text: impl Into<String>) -> Self {
1205 let mut lyric = Self::unsung();
1206 lyric.text = Some(text.into());
1207 lyric.said_syllabic = true;
1208 lyric
1209 }
1210
1211 pub fn unsung() -> Self {
1215 Self {
1216 text: Some(String::new()),
1217 number: 1,
1218 syllabic: Syllabic::Single,
1219 said_syllabic: false,
1220 identifier: None,
1221 components: Vec::new(),
1222 elision_before: DEFAULT_ELISION.to_string(),
1223 color: crate::display::Drawn(None),
1224 justify: crate::display::Drawn(None),
1225 placement: crate::display::Drawn(None),
1226 hidden: crate::display::Drawn(false),
1227 }
1228 }
1229
1230 pub fn color(&self) -> Option<&str> {
1232 self.color.0.as_deref()
1233 }
1234
1235 pub fn set_color(&mut self, color: Option<String>) {
1237 self.color.0 = color;
1238 }
1239
1240 pub fn justify(&self) -> Option<Justification> {
1242 self.justify.0
1243 }
1244
1245 pub fn set_justify(&mut self, justify: Option<Justification>) {
1247 self.justify.0 = justify;
1248 }
1249
1250 pub fn placement(&self) -> Option<Placement> {
1253 self.placement.0
1254 }
1255
1256 pub fn set_placement(&mut self, placement: Option<Placement>) {
1258 self.placement.0 = placement;
1259 }
1260
1261 pub fn is_hidden(&self) -> bool {
1264 self.hidden.0
1265 }
1266
1267 pub fn set_hidden(&mut self, hidden: bool) {
1269 self.hidden.0 = hidden;
1270 }
1271
1272 pub fn from_raw_text(raw_text: &str) -> Self {
1275 let mut lyric = Self::unsung();
1276 if !raw_text.is_empty() {
1278 lyric.set_raw_text(raw_text);
1279 }
1280 lyric
1281 }
1282
1283 pub fn text(&self) -> String {
1288 let Some((first, rest)) = self.components.split_first() else {
1289 return self.text.clone().unwrap_or_default();
1290 };
1291 let mut text = first.text();
1292 for component in rest {
1293 text.push_str(&component.elision_before);
1294 text.push_str(&component.text());
1295 }
1296 text
1297 }
1298
1299 pub fn set_text(&mut self, text: impl Into<String>) {
1304 self.components.clear();
1305 self.text = Some(text.into());
1306 }
1307
1308 pub fn explicit_text(&self) -> Option<String> {
1312 if self.is_composite() {
1313 return Some(self.text());
1314 }
1315 self.text.clone()
1316 }
1317
1318 pub fn clear_text(&mut self) {
1322 self.components.clear();
1323 self.text = None;
1324 }
1325
1326 pub fn is_composite(&self) -> bool {
1329 !self.components.is_empty()
1330 }
1331
1332 pub fn components(&self) -> &[Lyric] {
1334 &self.components
1335 }
1336
1337 pub fn set_components(&mut self, components: Vec<Lyric>) {
1340 self.components = components;
1341 }
1342
1343 pub fn elision_before(&self) -> &str {
1346 &self.elision_before
1347 }
1348
1349 pub fn set_elision_before(&mut self, elision: impl Into<String>) {
1351 self.elision_before = elision.into();
1352 }
1353
1354 pub fn number(&self) -> IntegerType {
1356 self.number
1357 }
1358
1359 pub fn set_number(&mut self, number: IntegerType) {
1367 self.number = number;
1368 }
1369
1370 pub fn syllabic(&self) -> Syllabic {
1373 if self.is_composite() {
1374 return Syllabic::Composite;
1375 }
1376 self.syllabic
1377 }
1378
1379 pub fn explicit_syllabic(&self) -> Option<Syllabic> {
1382 if self.is_composite() {
1383 return Some(Syllabic::Composite);
1384 }
1385 self.said_syllabic.then_some(self.syllabic)
1386 }
1387
1388 pub fn set_syllabic(&mut self, syllabic: Syllabic) {
1390 self.syllabic = syllabic;
1391 self.said_syllabic = true;
1392 }
1393
1394 pub fn identifier(&self) -> String {
1397 self.identifier
1398 .clone()
1399 .unwrap_or_else(|| self.number.to_string())
1400 }
1401
1402 pub fn explicit_identifier(&self) -> Option<&str> {
1406 self.identifier.as_deref()
1407 }
1408
1409 pub fn set_identifier(&mut self, identifier: Option<String>) {
1411 self.identifier = identifier;
1412 }
1413
1414 pub fn raw_text(&self) -> String {
1420 if self.explicit_text().is_none() {
1421 return String::new();
1422 }
1423 let text = self.text();
1424 let (Some(first), Some(last)) = (self.components.first(), self.components.last()) else {
1425 return match self.syllabic {
1426 Syllabic::Begin => format!("{text}-"),
1427 Syllabic::Middle => format!("-{text}-"),
1428 Syllabic::End => format!("-{text}"),
1429 Syllabic::Single | Syllabic::Composite => text,
1430 };
1431 };
1432 let leading = matches!(first.syllabic(), Syllabic::Middle | Syllabic::End);
1433 let trailing = matches!(last.syllabic(), Syllabic::Begin | Syllabic::Middle);
1434 format!(
1435 "{}{text}{}",
1436 if leading { "-" } else { "" },
1437 if trailing { "-" } else { "" }
1438 )
1439 }
1440
1441 pub fn set_raw_text(&mut self, raw_text: &str) {
1444 self.set_text_and_syllabic(raw_text, false);
1445 }
1446
1447 pub fn set_text_and_syllabic(&mut self, raw_text: &str, apply_raw: bool) {
1453 if apply_raw {
1454 self.set_text(raw_text);
1455 if !self.said_syllabic {
1456 self.set_syllabic(Syllabic::Single);
1457 }
1458 return;
1459 }
1460 self.components.clear();
1461 let starts = raw_text.starts_with('-');
1464 let ends = raw_text.ends_with('-');
1465 let trimmed = raw_text.strip_prefix('-').unwrap_or(raw_text);
1466 let trimmed = if ends {
1467 trimmed.strip_suffix('-').unwrap_or(trimmed)
1468 } else {
1469 trimmed
1470 }
1471 .to_string();
1472 self.set_syllabic(match (starts, ends) {
1473 (true, true) => Syllabic::Middle,
1474 (true, false) => Syllabic::End,
1475 (false, true) => Syllabic::Begin,
1476 (false, false) => Syllabic::Single,
1477 });
1478 self.text = Some(trimmed);
1479 }
1480}
1481
1482impl fmt::Display for Lyric {
1483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1484 f.write_str(&self.text())
1485 }
1486}
1487
1488pub(crate) mod verses {
1491 use super::{Lyric, Syllabic};
1492 use crate::defaults::IntegerType;
1493
1494 pub(crate) fn joined(lyrics: &[Lyric]) -> Option<String> {
1496 if lyrics.is_empty() {
1497 return None;
1498 }
1499 Some(
1500 lyrics
1501 .iter()
1502 .map(Lyric::text)
1503 .collect::<Vec<_>>()
1504 .join("\n"),
1505 )
1506 }
1507
1508 pub(crate) fn set(lyrics: &mut Vec<Lyric>, text: Option<&str>) {
1512 lyrics.clear();
1513 let Some(text) = text else {
1514 return;
1515 };
1516 for (index, line) in text.split('\n').enumerate() {
1517 let mut parsed = Lyric::from_raw_text(line);
1518 parsed.set_number(index as IntegerType + 1);
1519 lyrics.push(parsed);
1520 }
1521 }
1522
1523 pub(crate) fn add(
1528 lyrics: &mut Vec<Lyric>,
1529 text: &str,
1530 number: Option<IntegerType>,
1531 apply_raw: bool,
1532 ) {
1533 let Some(number) = number else {
1534 let mut lyric = built(text, apply_raw);
1535 lyric.set_number(lyrics.len() as IntegerType + 1);
1536 lyrics.push(lyric);
1537 return;
1538 };
1539 if let Some(existing) = lyrics.iter_mut().find(|lyric| lyric.number() == number) {
1540 existing.set_text(text);
1541 return;
1542 }
1543 let mut lyric = built(text, apply_raw);
1544 lyric.set_number(number);
1545 lyrics.push(lyric);
1546 }
1547
1548 pub(crate) fn insert(lyrics: &mut Vec<Lyric>, text: &str, index: usize, apply_raw: bool) {
1552 let index = index.min(lyrics.len());
1553 for (offset, lyric) in lyrics[index..].iter_mut().enumerate() {
1554 lyric.set_number(index as IntegerType + offset as IntegerType + 2);
1555 }
1556 let mut lyric = built(text, apply_raw);
1557 lyric.set_number(index as IntegerType + 1);
1558 lyrics.insert(index, lyric);
1559 }
1560
1561 fn built(text: &str, apply_raw: bool) -> Lyric {
1564 if apply_raw && !text.is_empty() {
1565 let mut lyric = Lyric::new(text);
1566 lyric.set_syllabic(Syllabic::Single);
1567 lyric
1568 } else {
1569 Lyric::from_raw_text(text)
1570 }
1571 }
1572}
1573
1574#[cfg(test)]
1575mod tests {
1576 #[test]
1577 fn ties_beams_and_placements_are_written_and_read_by_name() {
1578 use super::{
1579 Beam, BeamDirection, BeamType, Beams, Lyric, Placement, Syllabic, Tie, TieStyle,
1580 TieType,
1581 };
1582 use std::str::FromStr;
1583
1584 let mut tie = Tie::from_str("start").unwrap();
1585 assert_eq!(tie.tie_type(), TieType::Start);
1586 tie.set_tie_type(TieType::Stop);
1587 assert_eq!(tie.tie_type(), TieType::Stop);
1588 assert!(Tie::from_str("sideways").is_err());
1589 assert_eq!(TieStyle::Dotted.to_string(), "dotted");
1590 assert_eq!(Placement::Above.to_string(), "above");
1591 assert_eq!(Syllabic::Middle.to_string(), "middle");
1592
1593 assert_eq!(
1594 BeamType::from_music21_name("partial"),
1595 Some(BeamType::PartialBeam)
1596 );
1597 assert_eq!(BeamType::from_music21_name("sideways"), None);
1598 assert_eq!(
1599 BeamDirection::from_music21_name("left"),
1600 Some(BeamDirection::Left)
1601 );
1602 assert_eq!(BeamDirection::from_music21_name("up"), None);
1603
1604 let mut beam = Beam::new(BeamType::Start, None);
1605 beam.set_beam_type(Some(BeamType::Continue));
1606 beam.set_direction(Some(BeamDirection::Right));
1607 assert_eq!(beam.beam_type(), Some(BeamType::Continue));
1608 assert_eq!(beam.direction(), Some(BeamDirection::Right));
1609
1610 let mut beams = Beams::default();
1611 assert!(beams.is_empty());
1612 beams.set_beams(vec![beam]);
1613 assert_eq!(beams.beams().len(), 1);
1614 beams.beams_mut()[0].set_beam_type(Some(BeamType::Stop));
1615 assert_eq!(beams.beams()[0].beam_type(), Some(BeamType::Stop));
1616 assert!(!beams.feathered());
1617 beams.set_feathered(true);
1618 assert!(beams.feathered());
1619
1620 let mut lyric = Lyric::new("hel");
1621 assert_eq!(lyric.elision_before(), " ");
1622 assert_eq!(lyric.explicit_syllabic(), Some(Syllabic::Single));
1623 lyric.clear_text();
1624 assert_eq!(lyric.explicit_text(), None);
1625 assert_eq!(Lyric::unsung().explicit_syllabic(), None);
1626 }
1627
1628 #[test]
1631 fn the_beam_passes_tidy_a_run_of_beams() {
1632 use super::{
1633 BeamDirection, BeamType, Beams, merge_connecting_partial_beams,
1634 remove_sandwiched_unbeamables, sanitize_partial_beams,
1635 };
1636 use crate::duration::DurationType;
1637
1638 let filled = |beam_type: BeamType| {
1639 let mut beams = Beams::default();
1640 beams.fill(DurationType::Eighth, Some(beam_type)).unwrap();
1641 Some(beams)
1642 };
1643 let partial = |direction: BeamDirection| {
1644 let mut beams = Beams::default();
1645 beams
1646 .fill(DurationType::Eighth, Some(BeamType::PartialBeam))
1647 .unwrap();
1648 beams.beams_mut()[0].set_direction(Some(direction));
1649 Some(beams)
1650 };
1651
1652 let mut run = vec![
1654 None,
1655 filled(BeamType::Start),
1656 None,
1657 filled(BeamType::Start),
1658 filled(BeamType::Stop),
1659 ];
1660 remove_sandwiched_unbeamables(&mut run);
1661 assert!(run[1].is_none());
1662 assert!(run[3].is_some());
1663
1664 let mut lone = vec![partial(BeamDirection::Left)];
1667 sanitize_partial_beams(&mut lone);
1668 assert!(lone[0].is_none());
1669 let mut mixed = Beams::default();
1670 mixed.append(BeamType::Start, None);
1671 mixed.append(BeamType::PartialBeam, Some(BeamDirection::Left));
1672 let mut run = vec![Some(mixed)];
1673 sanitize_partial_beams(&mut run);
1674 let group = run[0].as_ref().unwrap();
1675 assert_eq!(group.beams()[1].direction(), Some(BeamDirection::Right));
1676
1677 let mut run = vec![partial(BeamDirection::Right), partial(BeamDirection::Left)];
1679 merge_connecting_partial_beams(&mut run);
1680 assert_eq!(
1681 run[0].as_ref().unwrap().beams()[0].beam_type(),
1682 Some(BeamType::Start)
1683 );
1684 assert_eq!(
1685 run[1].as_ref().unwrap().beams()[0].beam_type(),
1686 Some(BeamType::Stop)
1687 );
1688 let mut run = vec![partial(BeamDirection::Right), filled(BeamType::Start)];
1690 merge_connecting_partial_beams(&mut run);
1691 assert_eq!(
1692 run[1].as_ref().unwrap().beams()[0].beam_type(),
1693 Some(BeamType::Continue)
1694 );
1695 let mut run = vec![partial(BeamDirection::Right), filled(BeamType::Stop)];
1697 merge_connecting_partial_beams(&mut run);
1698 assert_eq!(
1699 run[0].as_ref().unwrap().beams()[0].beam_type(),
1700 Some(BeamType::PartialBeam)
1701 );
1702 }
1703
1704 #[test]
1706 fn text_and_syllabic_are_read_from_the_hyphens_unless_told_not_to() {
1707 use super::{Lyric, Syllabic};
1708
1709 let mut lyric = Lyric::unsung();
1710 lyric.set_text_and_syllabic("hel-", false);
1711 assert_eq!(
1712 (lyric.text(), lyric.syllabic()),
1713 ("hel".to_string(), Syllabic::Begin)
1714 );
1715 lyric.set_text_and_syllabic("-lo", false);
1716 assert_eq!(
1717 (lyric.text(), lyric.syllabic()),
1718 ("lo".to_string(), Syllabic::End)
1719 );
1720 lyric.set_text_and_syllabic("the", false);
1721 assert_eq!(
1722 (lyric.text(), lyric.syllabic()),
1723 ("the".to_string(), Syllabic::Single)
1724 );
1725
1726 let mut raw = Lyric::unsung();
1727 raw.set_text_and_syllabic("hel-", true);
1728 assert_eq!(
1729 (raw.text(), raw.syllabic()),
1730 ("hel-".to_string(), Syllabic::Single)
1731 );
1732 raw.set_syllabic(Syllabic::Begin);
1733 raw.set_text_and_syllabic("-lo", true);
1734 assert_eq!(
1735 (raw.text(), raw.syllabic()),
1736 ("-lo".to_string(), Syllabic::Begin)
1737 );
1738 }
1739
1740 #[test]
1741 fn beams_fill_one_level_for_each_flag() {
1742 let mut beams = Beams::new();
1744 beams
1745 .fill(DurationType::Sixteenth, Some(BeamType::Start))
1746 .unwrap();
1747 assert_eq!(beams.len(), 2);
1748 assert_eq!(
1749 beams.types(),
1750 [Some(BeamType::Start), Some(BeamType::Start)]
1751 );
1752 assert_eq!(beams.numbers(), [Some(1), Some(2)]);
1753 assert_eq!(
1754 beams.to_string(),
1755 "<music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>"
1756 );
1757
1758 beams.set_all(BeamType::Stop, None);
1759 assert_eq!(beams.types(), [Some(BeamType::Stop), Some(BeamType::Stop)]);
1760 assert_eq!(
1761 beams.by_number(1).and_then(Beam::beam_type),
1762 Some(BeamType::Stop)
1763 );
1764
1765 let mut counted = Beams::new();
1768 counted.fill(DurationType::Sixteenth, None).unwrap();
1769 assert_eq!(counted.types(), [None, None]);
1770 assert_eq!(
1771 counted.to_string(),
1772 "<music21.beam.Beam 1/None>/<music21.beam.Beam 2/None>"
1773 );
1774 assert!(Beams::new().fill_levels(12, None).is_err());
1775 assert!(beams.set_by_number(3, BeamType::Start, None).is_err());
1776
1777 assert!(Beams::new().fill(DurationType::Quarter, None).is_err());
1779
1780 let mut stub = Beams::new();
1782 stub.append(BeamType::PartialBeam, Some(BeamDirection::Left));
1783 assert_eq!(stub.to_string(), "<music21.beam.Beam 1/partial/left>");
1784 }
1785 use super::*;
1786
1787 #[test]
1788 fn tie_types_round_trip_and_reject_others() {
1789 for tie_type in TieType::ALL {
1790 assert_eq!(TieType::from_name(tie_type.as_str()).unwrap(), tie_type);
1791 }
1792 assert_eq!(Tie::default().tie_type(), TieType::Start);
1793 assert_eq!(Tie::from_name("stop").unwrap().to_string(), "Tie stop");
1794 let error = TieType::from_name("hello").unwrap_err().to_string();
1795 assert!(error.contains("Type must be one of"), "{error}");
1796 assert!(error.ends_with("not hello"), "{error}");
1797 }
1798
1799 #[test]
1800 fn tie_carries_style_and_placement() {
1801 let mut tie = Tie::new(TieType::Continue);
1802 assert_eq!(tie.style(), TieStyle::Normal);
1803 assert_eq!(tie.placement(), None);
1804 tie.set_style(TieStyle::Dashed);
1805 tie.set_placement(Some(Placement::Above));
1806 assert_eq!(tie.style().as_str(), "dashed");
1807 assert_eq!(tie.placement().map(Placement::as_str), Some("above"));
1808 assert!(TieStyle::from_name("wavy").is_err());
1809 assert!(Placement::from_name("sideways").is_err());
1810 }
1811
1812 #[test]
1813 fn notehead_and_stem_names_round_trip() {
1814 for notehead in Notehead::ALL {
1815 assert_eq!(Notehead::from_name(notehead.as_str()).unwrap(), notehead);
1816 }
1817 for direction in StemDirection::ALL {
1818 assert_eq!(
1819 StemDirection::from_name(direction.as_str()).unwrap(),
1820 direction
1821 );
1822 }
1823 assert_eq!(Notehead::default(), Notehead::Normal);
1824 assert_eq!(
1826 StemDirection::from_name("none").unwrap(),
1827 StemDirection::NoStem
1828 );
1829 assert_eq!(StemDirection::NAMES.len(), StemDirection::ALL.len() + 1);
1830 assert_eq!(StemDirection::default(), StemDirection::Unspecified);
1831 assert_eq!(Notehead::Diamond.to_string(), "diamond");
1832 assert_eq!(StemDirection::NoStem.to_string(), "noStem");
1833 assert!(Notehead::from_name("blah").is_err());
1834 assert!(StemDirection::from_name("sideways").is_err());
1835 }
1836
1837 #[test]
1838 fn lyric_hyphens_say_where_the_syllable_falls() {
1839 let cases = [
1840 ("hello", Syllabic::Single, "hello"),
1841 ("dic-", Syllabic::Begin, "dic"),
1842 ("-ci-", Syllabic::Middle, "ci"),
1843 ("-us", Syllabic::End, "us"),
1844 ];
1845 for (raw, syllabic, text) in cases {
1846 let lyric = Lyric::from_raw_text(raw);
1847 assert_eq!(lyric.syllabic(), syllabic, "{raw}");
1848 assert_eq!(lyric.text(), text, "{raw}");
1849 assert_eq!(lyric.raw_text(), raw, "{raw}");
1850 }
1851 let mut lyric = Lyric::new("shine");
1852 assert_eq!(lyric.number(), 1);
1853 assert_eq!(lyric.identifier(), "1");
1854 lyric.set_number(3);
1855 assert_eq!(lyric.identifier(), "3");
1856 lyric.set_identifier(Some("chorus".to_string()));
1857 assert_eq!(lyric.identifier(), "chorus");
1858 lyric.set_number(0);
1861 assert_eq!(lyric.number(), 0);
1862 assert_eq!(lyric.to_string(), "shine");
1863 assert_eq!(Syllabic::from_name("middle").unwrap(), Syllabic::Middle);
1864 assert!(Syllabic::from_name("half").is_err());
1865 }
1866
1867 #[test]
1868 fn a_composite_lyric_reads_as_its_components_run_together() {
1869 let mut co = Lyric::new("co");
1871 co.set_syllabic(Syllabic::End);
1872 let mut e = Lyric::new("e");
1873 e.set_syllabic(Syllabic::Single);
1874
1875 let mut bianco = Lyric::new("");
1876 assert!(!bianco.is_composite());
1877 bianco.set_components(vec![co, e]);
1878 assert!(bianco.is_composite());
1879 assert_eq!(bianco.text(), "co e");
1880 assert_eq!(bianco.syllabic(), Syllabic::Composite);
1881 assert_eq!(bianco.raw_text(), "-co e");
1883
1884 let mut components = bianco.components().to_vec();
1887 components[1].set_elision_before("_");
1888 components[1].set_syllabic(Syllabic::Middle);
1889 bianco.set_components(components);
1890 assert_eq!(bianco.text(), "co_e");
1891 assert_eq!(bianco.raw_text(), "-co_e-");
1892
1893 bianco.set_text("bianco");
1895 assert!(!bianco.is_composite());
1896 assert_eq!(bianco.text(), "bianco");
1897 assert_eq!(bianco.syllabic(), Syllabic::Single);
1898 }
1899
1900 #[test]
1901 fn an_identifier_is_only_there_when_it_was_named() {
1902 let mut lyric = Lyric::new("shine");
1903 assert_eq!(lyric.explicit_identifier(), None);
1904 assert_eq!(lyric.identifier(), "1");
1905 lyric.set_identifier(Some("chorus".to_string()));
1906 assert_eq!(lyric.explicit_identifier(), Some("chorus"));
1907 }
1908
1909 #[test]
1910 fn a_lone_hyphen_is_the_middle_of_a_word() {
1911 for raw in ["-", "--"] {
1914 let lyric = Lyric::from_raw_text(raw);
1915 assert_eq!(lyric.syllabic(), Syllabic::Middle);
1916 assert_eq!(lyric.text(), "");
1917 }
1918 assert_eq!(Lyric::from_raw_text("-a").syllabic(), Syllabic::End);
1919 assert_eq!(Lyric::from_raw_text("a-").syllabic(), Syllabic::Begin);
1920 }
1921}