1use crate::{
16 defaults::IntegerType,
17 error::{Error, Result},
18 notation::{Lyric, Syllabic},
19 stream::{Stream, StreamElement},
20};
21
22pub const ARTICLES: [(&str, &[&str]); 8] = [
25 ("ar", &["al-"]),
26 ("en", &["the", "a", "an"]),
27 (
28 "de",
29 &[
30 "der", "die", "das", "des", "dem", "den", "ein", "eine", "einer", "einem", "einen",
31 ],
32 ),
33 ("nl", &["de", "het", "'t", "een"]),
34 (
35 "es",
36 &["el", "la", "los", "las", "un", "una", "unos", "unas"],
37 ),
38 ("pt", &["o", "a", "os", "as", "um", "uma", "uns", "umas"]),
39 (
40 "fr",
41 &[
42 "le", "la", "les", "l'", "un", "une", "des", "du", "de la", "des",
43 ],
44 ),
45 (
46 "it",
47 &[
48 "il", "lo", "la", "l'", "i", "gli", "le", "un'", "un", "uno", "una", "del", "dello",
49 "della", "dei", "degli", "delle",
50 ],
51 ),
52];
53
54fn articles(language: Option<&str>) -> Result<Vec<&'static str>> {
56 match language {
57 None => Ok(ARTICLES
58 .iter()
59 .flat_map(|(_, articles)| articles.iter().copied())
60 .collect()),
61 Some(code) => ARTICLES
62 .iter()
63 .find(|(known, _)| *known == code)
64 .map(|(_, articles)| articles.to_vec())
65 .ok_or_else(|| Error::Text(format!("no articles known for language {code:?}"))),
66 }
67}
68
69pub fn prepend_article(text: &str, language: Option<&str>) -> Result<String> {
79 if !text.contains(',') {
80 return Ok(text.to_string());
81 }
82 let articles = articles(language)?;
83 let Some((before, trailing)) = text.rsplit_once(',') else {
84 return Ok(text.to_string());
85 };
86 let trailing = trailing.trim();
87 if articles.contains(&trailing.to_lowercase().as_str()) {
88 Ok(format!("{trailing} {before}"))
89 } else {
90 Ok(text.to_string())
91 }
92}
93
94pub fn postpend_article(text: &str, language: Option<&str>) -> Result<String> {
102 if !text.contains(' ') {
103 return Ok(text.to_string());
104 }
105 let articles = articles(language)?;
106 let Some((leading, after)) = text.split_once(' ') else {
107 return Ok(text.to_string());
108 };
109 let leading = leading.trim();
110 if articles.contains(&leading.to_lowercase().as_str()) {
111 Ok(format!("{after}, {leading}"))
112 } else {
113 Ok(text.to_string())
114 }
115}
116
117pub fn assemble_lyrics(stream: &Stream, line_number: IntegerType, word_separator: &str) -> String {
124 let mut word: Vec<String> = Vec::new();
125 let mut words: Vec<String> = Vec::new();
126 for event in stream.flatten().events() {
127 let lyrics: &[Lyric] = match event.element() {
128 StreamElement::Note(note) => note.lyrics(),
129 StreamElement::Chord(chord) => chord.lyrics(),
130 StreamElement::Rest(rest) => rest.lyrics(),
131 _ => continue,
132 };
133 let Some(lyric) = line(lyrics, line_number) else {
134 continue;
135 };
136 let text = lyric.explicit_text();
137 if text.as_deref() == Some("_") {
138 continue;
139 }
140 match lyric.explicit_syllabic() {
141 Some(Syllabic::Begin | Syllabic::Middle) => word.extend(text),
142 Some(Syllabic::Composite) => {
143 word.extend(text);
144 let last = lyric.components().last().and_then(Lyric::explicit_syllabic);
145 if matches!(last, None | Some(Syllabic::End | Syllabic::Single)) {
146 words.push(word.concat());
147 word.clear();
148 }
149 }
150 None | Some(Syllabic::End | Syllabic::Single) => {
151 word.extend(text);
152 words.push(word.concat());
153 word.clear();
154 }
155 }
156 }
157 words.join(word_separator)
158}
159
160fn line(lyrics: &[Lyric], line_number: IntegerType) -> Option<&Lyric> {
163 let index = i64::from(line_number) - 1;
164 let index = if index < 0 {
165 index + lyrics.len() as i64
166 } else {
167 index
168 };
169 usize::try_from(index)
170 .ok()
171 .and_then(|index| lyrics.get(index))
172}
173
174pub fn assemble_all_lyrics(
180 stream: &Stream,
181 max_lyrics: IntegerType,
182 lyric_separation: &str,
183 word_separator: &str,
184) -> String {
185 let mut lyrics = String::new();
186 for line_number in 1..max_lyrics {
187 let line = assemble_lyrics(stream, line_number, word_separator);
188 if !line.is_empty() {
189 if line_number > 1 {
190 lyrics.push_str(lyric_separation);
191 }
192 lyrics.push_str(&line);
193 }
194 }
195 lyrics
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::note::Note;
202
203 #[test]
204 fn articles_move_to_the_front_and_back() -> Result<()> {
205 assert_eq!(
207 prepend_article("Ale is Dear, The", None)?,
208 "The Ale is Dear"
209 );
210 assert_eq!(prepend_article("The Ale is Dear", None)?, "The Ale is Dear");
211 assert_eq!(
212 prepend_article("Ale is Dear, The", Some("fr"))?,
213 "Ale is Dear, The"
214 );
215 assert_eq!(
216 prepend_article("Combattimento di Tancredi e Clorinda, Il", Some("it"))?,
217 "Il Combattimento di Tancredi e Clorinda"
218 );
219 assert_eq!(
220 postpend_article("The Ale is Dear", None)?,
221 "Ale is Dear, The"
222 );
223 assert_eq!(
224 postpend_article("Ale is Dear, The", Some("en"))?,
225 "Ale is Dear, The"
226 );
227 assert!(prepend_article("Ale, The", Some("xx")).is_err());
228 Ok(())
229 }
230
231 #[test]
232 fn syllables_are_joined_into_words() {
233 let mut stream = Stream::new();
234 for (offset, (text, syllabic)) in [
235 ("Hi", Syllabic::Single),
236 ("the", Syllabic::Begin),
237 ("re", Syllabic::End),
238 ("_", Syllabic::Single),
239 ]
240 .into_iter()
241 .enumerate()
242 {
243 let mut lyric = Lyric::new(text);
244 lyric.set_syllabic(syllabic);
245 let mut note = Note::from_name("C4").expect("a note");
246 note.lyrics_mut().push(lyric);
247 stream.insert(offset as crate::defaults::FloatType, note);
248 }
249 assert_eq!(assemble_lyrics(&stream, 1, " "), "Hi there");
250 assert_eq!(assemble_lyrics(&stream, 0, "-"), "Hi-there");
251 assert_eq!(assemble_lyrics(&stream, 2, " "), "");
252 assert_eq!(assemble_all_lyrics(&stream, 10, "\n", " "), "Hi there");
253 }
254}
255
256#[derive(Clone, Debug, PartialEq)]
259#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
260pub struct TextBox {
261 pub content: String,
263 pub page: i64,
265 pub style: crate::style::TextStyle,
267}
268
269impl TextBox {
270 pub fn new(content: impl Into<String>) -> Self {
273 let mut style = crate::style::TextStyle::default();
274 style.absolute_x = Some(crate::style::StyleValue::Whole(500));
275 style.absolute_y = Some(crate::style::StyleValue::Whole(500));
276 style
277 .set_align_vertical(Some("top"))
278 .expect("top is an alignment");
279 style
280 .set_align_horizontal(Some("center"))
281 .expect("center is an alignment");
282 Self {
283 content: content.into(),
284 page: 1,
285 style,
286 }
287 }
288}