1use crate::common::stringtools::is_python_space;
5
6pub const VALID_SHOW_FORMATS: [&str; 17] = [
8 "musicxml",
9 "lilypond",
10 "text",
11 "textline",
12 "midi",
13 "png",
14 "pdf",
15 "svg",
16 "lily.pdf",
17 "lily.png",
18 "lily.svg",
19 "braille",
20 "ipython",
21 "ipython.png",
22 "ipython.midi",
23 "musicxml.png",
24 "musicxml.pdf",
25];
26
27pub const VALID_WRITE_FORMATS: [&str; 18] = [
30 "musicxml",
31 "lilypond",
32 "text",
33 "textline",
34 "midi",
35 "png",
36 "pdf",
37 "svg",
38 "lily.pdf",
39 "lily.png",
40 "lily.svg",
41 "braille",
42 "ipython",
43 "ipython.png",
44 "ipython.midi",
45 "musicxml.png",
46 "musicxml.pdf",
47 "rntxt",
48];
49
50pub const VALID_AUTO_DOWNLOAD: [&str; 3] = ["ask", "deny", "allow"];
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct SubConverter {
57 pub class_name: &'static str,
59 pub formats: &'static [&'static str],
61 pub input_extensions: &'static [&'static str],
63 pub output_extensions: &'static [&'static str],
65}
66
67const fn converter(
68 class_name: &'static str,
69 formats: &'static [&'static str],
70 input_extensions: &'static [&'static str],
71 output_extensions: &'static [&'static str],
72) -> SubConverter {
73 SubConverter {
74 class_name,
75 formats,
76 input_extensions,
77 output_extensions,
78 }
79}
80
81pub const SUB_CONVERTERS: [SubConverter; 20] = [
85 converter("ConverterABC", &["abc"], &["abc"], &[]),
86 converter("ConverterBraille", &["braille"], &[], &["txt"]),
87 converter("ConverterCapella", &["capella"], &["capx"], &[]),
88 converter(
89 "ConverterClercqTemperley",
90 &["cttxt", "har", "clercqTemperley"],
91 &["cttxt", "har", "clercqTemperley"],
92 &[],
93 ),
94 converter("ConverterHumdrum", &["humdrum"], &["krn"], &[]),
95 converter("ConverterIPython", &["ipython", "jupyter"], &[], &[]),
96 converter(
97 "ConverterLilypond",
98 &["lilypond", "lily"],
99 &[],
100 &["ly", "png", "pdf", "svg"],
101 ),
102 converter("ConverterMEI", &["mei"], &["mei"], &[]),
103 converter("ConverterMidi", &["midi"], &["mid", "midi"], &["mid"]),
104 converter(
105 "ConverterMuseData",
106 &["musedata"],
107 &["md", "musedata", "zip"],
108 &[],
109 ),
110 converter(
111 "ConverterMusicXML",
112 &["musicxml", "xml"],
113 &["xml", "mxl", "musicxml"],
114 &["musicxml", "xml", "mxl"],
115 ),
116 converter("ConverterNoteworthy", &["noteworthytext"], &["nwctxt"], &[]),
117 converter("ConverterNoteworthyBinary", &["noteworthy"], &["nwc"], &[]),
118 converter(
119 "ConverterRomanText",
120 &["romantext", "rntext"],
121 &["rntxt", "rntext", "romantext", "rtxt"],
122 &["rntxt"],
123 ),
124 converter("ConverterScala", &["scala"], &["scl"], &["scl"]),
125 converter("ConverterText", &["text", "txt", "t"], &[], &["txt"]),
126 converter("ConverterTextLine", &["textline"], &[], &["txt"]),
127 converter(
128 "ConverterTinyNotation",
129 &["tinynotation"],
130 &["tntxt", "tinynotation"],
131 &[],
132 ),
133 converter(
134 "ConverterVolpiano",
135 &["volpiano"],
136 &["volpiano", "vp"],
137 &["txt", "vp"],
138 ),
139 converter("SubConverter", &[], &[], &[]),
140];
141
142fn cleaned(format: &str) -> String {
144 format
145 .to_lowercase()
146 .trim_matches(is_python_space)
147 .to_string()
148}
149
150pub fn sub_converter_for_format(format: &str) -> Option<&'static SubConverter> {
160 let format = cleaned(format);
161 SUB_CONVERTERS
162 .iter()
163 .find(|converter| converter.formats.contains(&format.as_str()))
164}
165
166pub fn regularize_format(format: &str) -> Option<&'static str> {
178 let format = cleaned(format);
179 let format = format.strip_prefix('.').unwrap_or(&format);
180 let format = format.split('.').next().unwrap_or_default();
181 let found = SUB_CONVERTERS
182 .iter()
183 .find(|converter| converter.formats.contains(&format))
184 .or_else(|| {
185 SUB_CONVERTERS
186 .iter()
187 .find(|converter| converter.input_extensions.contains(&format))
188 })?;
189 found.formats.first().copied()
190}
191
192pub fn find_format(format: &str) -> (Option<&'static str>, Option<String>) {
203 let Some(name) = regularize_format(format) else {
204 return (None, None);
205 };
206 let first_lowered = |converter: &&SubConverter| {
207 converter
208 .formats
209 .iter()
210 .any(|registered| registered.to_lowercase() == name)
211 };
212 let extension = SUB_CONVERTERS
213 .iter()
214 .find(first_lowered)
215 .and_then(|converter| {
216 converter
217 .output_extensions
218 .first()
219 .or(converter.input_extensions.first())
220 .map(|extension| format!(".{extension}"))
221 });
222 (Some(name), extension)
223}
224
225pub fn find_input_extension(format: &str) -> Vec<String> {
236 let format = cleaned(format);
237 let format = format.strip_prefix('.').unwrap_or(&format);
238 let dotted = |converter: &SubConverter| -> Vec<String> {
239 converter
240 .input_extensions
241 .iter()
242 .map(|extension| {
243 if extension.starts_with('.') {
244 (*extension).to_string()
245 } else {
246 format!(".{extension}")
247 }
248 })
249 .collect()
250 };
251 if let Some(converter) = sub_converter_for_format(format) {
252 return dotted(converter);
253 }
254 SUB_CONVERTERS
255 .iter()
256 .find(|converter| converter.input_extensions.contains(&format))
257 .map(dotted)
258 .unwrap_or_default()
259}
260
261pub fn find_format_ext_url(url: &str) -> (Option<&'static str>, Option<String>) {
272 let extension = if url.contains("=xml") {
273 Some(".xml".to_string())
274 } else if url.contains("=kern") {
275 Some(".krn".to_string())
276 } else if url.contains("format=stage2") || url.contains("format=stage1") {
277 Some(".md".to_string())
278 } else {
279 SUB_CONVERTERS
282 .iter()
283 .filter_map(|converter| {
284 converter
285 .input_extensions
286 .iter()
287 .find(|extension| url.ends_with(&format!(".{extension}")))
288 })
289 .next_back()
290 .map(|extension| format!(".{extension}"))
291 };
292 match extension {
293 Some(extension) => (find_format(&extension).0, Some(extension)),
294 None => (None, None),
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
304 fn formats_are_found_as_music21_finds_them() {
305 let cases = [
306 ("musicxml", "musicxml", ".musicxml"),
307 ("lily", "lilypond", ".ly"),
308 ("lily.png", "lilypond", ".ly"),
309 ("humdrum", "humdrum", ".krn"),
310 ("txt", "text", ".txt"),
311 ("textline", "textline", ".txt"),
312 ("midi", "midi", ".mid"),
313 ("scl", "scala", ".scl"),
314 ("braille", "braille", ".txt"),
315 ("capx", "capella", ".capx"),
316 ("md", "musedata", ".md"),
317 (".md", "musedata", ".md"),
318 ];
319 for (given, format, extension) in cases {
320 assert_eq!(
321 find_format(given),
322 (Some(format), Some(extension.to_string())),
323 "{given}"
324 );
325 }
326 }
327
328 #[test]
329 fn a_format_registered_with_a_capital_is_not_found_lowered() {
330 assert_eq!(regularize_format("clercqTemperley"), None);
331 assert_eq!(regularize_format("cttxt"), Some("cttxt"));
332 }
333
334 #[test]
335 fn urls_are_read_by_query_then_ending() {
336 let url = "http://somesite.com/?l=cc/schubert/piano/d0576&file=d0576-06.krn&f=xml";
337 assert_eq!(
338 find_format_ext_url(url),
339 (Some("musicxml"), Some(".xml".to_string()))
340 );
341 assert_eq!(
342 find_format_ext_url("http://somesite.com/files/mid001.mid"),
343 (Some("midi"), Some(".mid".to_string()))
344 );
345 }
346}