Skip to main content

music21/common/
formats.rs

1//! music21's `common.formats`: the formats music21 reads and writes, by
2//! name and by file extension, as its converters register them.
3
4use crate::common::stringtools::is_python_space;
5
6/// The formats `show` takes: music21's `VALID_SHOW_FORMATS`.
7pub 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
27/// The formats `write` takes: music21's `VALID_WRITE_FORMATS`, which are
28/// the show formats and RomanText.
29pub 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
50/// The answers the `autoDownload` setting takes: music21's
51/// `VALID_AUTO_DOWNLOAD`.
52pub const VALID_AUTO_DOWNLOAD: [&str; 3] = ["ask", "deny", "allow"];
53
54/// What one of music21's converters registers itself for.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct SubConverter {
57    /// The class's name in `music21.converter.subConverters`.
58    pub class_name: &'static str,
59    /// Its format names, the first being the format's own.
60    pub formats: &'static [&'static str],
61    /// The extensions it reads, without their dots.
62    pub input_extensions: &'static [&'static str],
63    /// The extensions it writes, without their dots.
64    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
81/// music21's converters, in the order its `Converter.subConvertersList`
82/// gives them where none has been registered: by class name, with the
83/// plain `SubConverter` last.
84pub 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
142/// A format name lowered and trimmed, as music21 reads one.
143fn cleaned(format: &str) -> String {
144    format
145        .to_lowercase()
146        .trim_matches(is_python_space)
147        .to_string()
148}
149
150/// The converter registered for a format name, which is not an
151/// extension: music21's `findSubConverterForFormat`, by its class.
152///
153/// ```
154/// use music21::common::formats::sub_converter_for_format;
155///
156/// assert_eq!(sub_converter_for_format("t").unwrap().class_name, "ConverterText");
157/// assert!(sub_converter_for_format("mxl").is_none());
158/// ```
159pub 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
166/// The format a name, an abbreviation or an extension stands for, with or
167/// without its dot and with anything after a further dot ignored: music21's
168/// `Converter.regularizeFormat`.
169///
170/// ```
171/// use music21::common::formats::regularize_format;
172///
173/// assert_eq!(regularize_format("mxl"), Some("musicxml"));
174/// assert_eq!(regularize_format("lily.png"), Some("lilypond"));
175/// assert_eq!(regularize_format("blah"), None);
176/// ```
177pub 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
192/// The format a name or an extension stands for and the extension it is
193/// written with, dot and all: music21's `findFormat`.
194///
195/// ```
196/// use music21::common::formats::find_format;
197///
198/// assert_eq!(find_format(".mxl"), (Some("musicxml"), Some(".musicxml".to_string())));
199/// assert_eq!(find_format("abc"), (Some("abc"), Some(".abc".to_string())));
200/// assert_eq!(find_format("wpd"), (None, None));
201/// ```
202pub 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
225/// Each extension, dotted, that a format reads, the format given by name
226/// or by one of its extensions: music21's `findInputExtension`.
227///
228/// ```
229/// use music21::common::formats::find_input_extension;
230///
231/// assert_eq!(find_input_extension("musicxml"), [".xml", ".mxl", ".musicxml"]);
232/// assert_eq!(find_input_extension(".mxl"), [".xml", ".mxl", ".musicxml"]);
233/// assert!(find_input_extension("blah").is_empty());
234/// ```
235pub 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
261/// The format and extension a URL names, read from its query or from how
262/// it ends: music21's `findFormatExtURL`.
263///
264/// ```
265/// use music21::common::formats::find_format_ext_url;
266///
267/// let url = "http://somesite.com/cgi-bin/ksdata?l=cc/schubert/d0576&file=d0576-06.krn&f=kern";
268/// assert_eq!(find_format_ext_url(url), (Some("humdrum"), Some(".krn".to_string())));
269/// assert_eq!(find_format_ext_url("https://junk"), (None, None));
270/// ```
271pub 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        // Each converter's first extension the URL ends with, the last
280        // converter that has one winning, as music21's loop leaves it.
281        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    /// music21's own examples for `findFormat`.
303    #[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}