gabc_parser/
lib.rs

1//Copyright (c) 2018 Lydia Simmons
2//This software is licensed under the GNU General Public License v3.0.
3//See the LICENSE file in this distribution for license terms.
4
5//! Library for parsing and manipulating gabc code. The intended use case is to parse an entire
6//! gabc file into a `GabcFile` struct with `GabcFile::new()`. The `GABCParser` struct
7//! provides access to the parse tree itself for lower-level processing.
8//! Documentation for gabc is available at http://gregorio-project.github.io/gabc/index.html.
9
10extern crate itertools;
11extern crate pest;
12#[macro_use]
13extern crate pest_derive;
14extern crate serde;
15extern crate serde_json;
16#[macro_use]
17extern crate serde_derive;
18
19use itertools::Itertools;
20use pest::iterators::Pairs;
21use pest::Parser;
22
23//-----------------------------------------------------------------------
24//Pest boilerplate from the book (https://pest-parser.github.io/book/)
25
26const _GRAMMAR: &str = include_str!("gabc.pest");
27
28#[derive(Parser)]
29#[grammar = "gabc.pest"]
30///Parser that recognizes gabc, generated from `gabc.pest`.
31///# Examples
32///```
33/// # extern crate pest;
34/// # extern crate gabc_parser;
35/// # use gabc_parser::{GABCParser, Rule};
36/// # use pest::Parser;
37/// # fn main() {
38/// let pairs = GABCParser::parse(Rule::syllable, "Po(ev/)");
39/// assert!(pairs.is_ok());
40/// //pairs is a Result containing an iterator with a single Pair;
41/// //we unwrap the syllable pair and examine its sub-pairs
42/// let mut pair_iter = pairs.unwrap().next().unwrap().into_inner();
43/// let a = pair_iter.next().unwrap();
44/// assert_eq!(a.as_rule(), Rule::string);
45/// assert_eq!(a.as_str(), "Po");
46/// let b = pair_iter.next().unwrap();
47/// assert_eq!(b.as_rule(), Rule::note);
48/// assert_eq!(b.as_str(), "ev");
49/// let c = pair_iter.next().unwrap();
50/// assert_eq!(c.as_rule(), Rule::spacer);
51/// assert_eq!(c.as_str(), "/");
52/// assert_eq!(pair_iter.next(), None);
53/// # }
54///```
55pub struct GABCParser;
56
57//-----------------------------------------------------------------------
58
59///Struct representing a gabc note.
60#[derive(Debug, Serialize)]
61pub struct Note<'a> {
62    ///Entire prefix of the note (usually empty)
63    pub prefix: &'a str,
64    ///Main character of the note: its position in the gabc staff (a-m)
65    pub position: char,
66    ///Entire suffix string of the note, including shape indicators and rhythmic signs
67    pub suffix: &'a str,
68    ///Clef governing this note in its original context
69    pub current_clef: &'a str,
70}
71
72impl<'a> Note<'a> {
73    ///Create a new note from well-formed gabc input.
74    ///# Examples
75    ///```
76    ///# use gabc_parser::*;
77    ///let n = Note::new("h..", "c1");
78    ///assert_eq!(n.prefix, "");
79    ///assert_eq!(n.position, 'h');
80    ///assert_eq!(n.suffix, "..");
81    ///assert_eq!(n.current_clef, "c1");
82    ///```
83    pub fn new<'b>(gabc_input: &'b str, current_clef: &'b str) -> Note<'b> {
84        let mut parse_result = parse_gabc(gabc_input, Rule::note);
85        parsed_note_to_struct(parse_result.next().unwrap(), current_clef)
86    }
87    ///Get the absolute pitch of this note in modern (Lilypond) notation, between a, and a'''.
88    ///Assumes that the clef indicates middle C or the F above middle C.
89    ///# Examples
90    ///```
91    ///# use gabc_parser::*;
92    ///let n = Note::new("h..", "c1");
93    ///assert_eq!(n.absolute_pitch(), "g'");
94    ///```
95    pub fn absolute_pitch(&self) -> &str {
96        let ly_notes = vec![
97            "a,", "b,", "c", "d", "e", "f", "g", "a", "b", "c'", "d'", "e'", "f'", "g'", "a'",
98            "b'", "c''", "d''", "e''", "f''", "g''", "a'''",
99        ];
100        let start_index = match self.current_clef {
101            "c1" => 6,
102            "c2" => 4,
103            "c3" => 2,
104            "c4" => 0,
105            "f1" => 9,
106            "f2" => 7,
107            "f3" => 5,
108            "f4" => 3,
109            x => panic!("invalid clef: {}", x),
110        };
111        let position = self.position.to_lowercase().next().unwrap() as usize - 'a' as usize;
112        assert!(position < ly_notes.len());
113        return ly_notes.get(position + start_index).unwrap();
114    }
115}
116
117///Any element that can appear in a gabc music string.
118#[derive(Debug, Serialize)]
119pub enum NoteElem<'a> {
120    ///A gabc spacer, e.g. "/"
121    Spacer(&'a str),
122    ///A gabc bar separator, e.g. "::"
123    Barline(&'a str),
124    ///A `Note` struct
125    Note(Note<'a>),
126}
127
128impl<'a> NoteElem<'a> {
129    ///Get the Lilypond representation of this note element. gabc spacers (e.g. "/") are ignored;
130    ///`Note` suffixes (e.g. ".") that have Lilypond equivalents are not yet implemented.
131    ///# Examples
132    ///```
133    ///# use gabc_parser::*;
134    ///let n = NoteElem::Note(Note::new("h..", "c1"));
135    ///assert_eq!(n.to_ly(), "g'");
136    ///let s = NoteElem::Spacer("/");
137    ///assert_eq!(s.to_ly(), "");
138    ///let b = NoteElem::Barline(":");
139    ///assert_eq!(b.to_ly(), "\\divisioMaior");
140    ///```
141    pub fn to_ly(&self) -> &str {
142        match self {
143            NoteElem::Barline(s) => match *s {
144                "'" => "\\divisioMinima",
145                ";" => "\\divisioMaior",
146                ":" => "\\divisioMaior",
147                "::" => "\\finalis",
148                _ => "\\divisioMinima",
149            },
150            NoteElem::Note(n) => n.absolute_pitch(),
151            NoteElem::Spacer(_) => "",
152        }
153    }
154}
155
156///Struct representing a gabc syllable with text and music, e.g. "Po(eh/hi)"
157#[derive(Debug, Serialize)]
158pub struct Syllable<'a> {
159    ///Text part of the syllable
160    pub text: &'a str,
161    ///Music part of the syllable
162    pub music: Vec<NoteElem<'a>>,
163}
164
165impl<'a> Syllable<'a> {
166    ///Create a new syllable from well-formed gabc input.
167    ///# Examples
168    ///```
169    ///# use gabc_parser::*;
170    ///let s = Syllable::new("Po(eh/hi)", "c3");
171    ///assert_eq!(s.text, "Po");
172    ///assert_eq!(s.music.len(), 5);
173    ///```
174    pub fn new<'b>(gabc_input: &'b str, current_clef: &'b str) -> Syllable<'b> {
175        let mut parse_result = parse_gabc(gabc_input, Rule::syllable);
176        parsed_syllable_to_struct(parse_result.next().unwrap(), current_clef)
177    }
178    ///Translate this syllable's music string into a tied sequence of Lilypond notes.
179    ///# Examples
180    ///```
181    ///# use gabc_parser::*;
182    ///let s = Syllable::new("Po(eh/hi)", "c3");
183    ///assert_eq!(s.ly_notes(), "g(c' c' d')");
184    ///```
185    pub fn ly_notes(&self) -> String {
186        let mut result = String::new();
187        let mut notes_iter = self.music.iter();
188        match notes_iter.next() {
189            None => return result,
190            Some(s) => result.push_str(s.to_ly()),
191        }
192        match notes_iter.next() {
193            None => return result,
194            Some(s) => {
195                result.push_str("(");
196                result.push_str(s.to_ly());
197            }
198        }
199        while let Some(s) = notes_iter.next() {
200            let t = s.to_ly();
201            if t.trim() != "" { result.push_str(" "); };
202            result.push_str(t);
203        }
204        result.push_str(")");
205        result
206    }
207    ///Translate this syllable's text into valid Lilypond lyrics. If there are no Notes in this
208    ///syllable's music string, add "\set stanza = " to prevent Lilypond matching this text
209    ///to a note.
210    ///# Examples
211    ///```
212    ///# use gabc_parser::*;
213    ///let s = Syllable::new("*()", "c3");
214    ///assert_eq!(s.ly_text(), " \\set stanza = \"*\" ");
215    ///```
216    pub fn ly_text(&self) -> String {
217        //Filter out Lilypond control characters
218        let text = sanitize_ly_syllable(self.text);
219        //If there are no notes, use "set stanza"
220        let mut flag = false;
221        for ne in &self.music {
222            if let NoteElem::Note(_) = ne {
223                flag = true;
224            }
225        }
226        if !flag && !(text.trim() == "") {
227            return format!(" \\set stanza = \"{}\" ", text);
228        } else {
229            return text.to_string();
230        }
231    }
232}
233
234///Sanitize a syllable for Lilypond by removing control characters, replacing interior spaces with
235///underscores, and surrounding anything starting with a number with "double quotes" (this is
236///a pretty hacky way to prevent Lilypond errors)
237fn sanitize_ly_syllable(text: &str) -> String {
238    let start = text.trim_left() != text;
239    let end = text.trim_right() != text;
240    let mut t = text.trim().chars().filter(|c| {
241        match c {
242            '{' | '}' => false,
243            _ => true,
244        }
245    }).map(|c| match c {
246        ' ' => '_',
247        x => x,
248    }).collect::<String>();
249    if let Some(c) = t.chars().next() {
250        if c.is_numeric() {
251            t = format!("\"{}\"", t);
252        }
253    }
254    let mut result = String::new();
255    if start {result.push(' ')};
256    result.push_str(&t);
257    if end {result.push(' ')};
258    result
259}
260
261///Struct representing an entire gabc file.
262#[derive(Debug, Serialize)]
263pub struct GabcFile<'a> {
264    ///This file's attributes, e.g. "name: Populus Sion", as key/value tuples
265    pub attributes: Vec<(&'a str, &'a str)>,
266    ///This file's `Syllable`s
267    pub syllables: Vec<Syllable<'a>>,
268}
269
270impl<'a> GabcFile<'a> {
271    ///Create a new `GabcFile` from well-formed gabc input.
272    ///# Examples
273    ///```
274    ///# use gabc_parser::*;
275    ///let s = "name:Test;
276    ///%%
277    ///(c1) Hel(e.)lo(hi~) (::)";
278    ///let f = GabcFile::new(s);
279    ///assert_eq!(f.attributes[0], ("name", "Test"));
280    ///assert_eq!(f.syllables.len(), 4); //clefs currently produce an empty syllable
281    ///```
282    pub fn new(gabc_input: &str) -> GabcFile {
283        let parse_result = parse_gabc(gabc_input, Rule::file);
284        parsed_file_to_struct(parse_result)
285    }
286    ///Translate this `GabcFile` into JSON.
287    pub fn as_json(&self) -> String {
288        serde_json::to_string(self).unwrap()
289    }
290    ///Translate this `GabcFile` into a well-formed Lilypond file, by translating its text and music
291    ///and inserting them into a template derived from
292    ///<http://lilypond.org/doc/v2.18/Documentation/snippets/templates#templates-ancient-notation-template-_002d-modern-transcription-of-gregorian-music>
293    pub fn as_lilypond(&self) -> String {
294        format!("{}{}{}{}{}", LY_1, &self.ly_notes(), LY_2, &self.ly_lyrics(), LY_3)
295    }
296    ///Extract the notes of this file into well-formed Lilypond music, with a newline between each
297    ///syllable
298    ///# Examples
299    ///```
300    ///# use gabc_parser::*;
301    ///let s = "name:Test;
302    ///%%
303    ///(c1) Hel(e.)lo(hi~) (::)";
304    ///let f = GabcFile::new(s);
305    ///assert_eq!(f.ly_notes(), r#"
306    ///d'
307    ///g'(a')
308    ///\finalis
309    ///"#);
310    ///```
311    pub fn ly_notes(&self) -> String {
312        let mut notes = String::new();
313        for syllable in &self.syllables {
314            notes.push_str(&syllable.ly_notes());
315            notes.push_str("\n");
316        }
317        notes
318    }
319    ///Extract the text of this file into well-formed Lilypond lyrics, inserting " -- " to join
320    ///syllables where appropriate.
321    ///# Examples
322    ///```
323    ///# use gabc_parser::*;
324    ///let s = "name:Test;
325    ///%%
326    ///(c1) Hel(e.)lo(hi~) (::)";
327    ///let f = GabcFile::new(s);
328    ///assert_eq!(f.ly_lyrics(), " Hel -- lo  ");
329    pub fn ly_lyrics(&self) -> String {
330        let mut result = String::new();
331        let syllable_iter = &mut self.syllables.iter().peekable();
332        while let Some(syll) = syllable_iter.next() {
333            let s = &syll.ly_text();
334            result.push_str(&s);
335            if let Some(next_syll) = syllable_iter.peek() {
336                let next_s = next_syll.ly_text();
337                if s.trim_right() == s && next_s.trim_left() == next_s {
338                    result.push_str(" -- ");
339                }
340            }
341        }
342        result
343    }
344}
345
346///Wrapper for GABCParser::parse() that prints a helpful error and exits the process if parsing
347///fails (a friendly alternative to panicking).
348pub fn parse_gabc(text: &str, rule: Rule) -> Pairs<Rule> {
349    let parse_result = GABCParser::parse(rule, &text);
350    match parse_result {
351        Err(e) => {
352            println!("Parse error: {}", e);
353            std::process::exit(1);
354        }
355        Ok(pairs) => {
356            return pairs;
357        }
358    }
359}
360
361///Pretty string representation of a `Pairs` parse tree. Useful for directly debugging the output of
362///`GABCParser::parse()` or `parse_gabc()`.
363pub fn debug_print(rules: Pairs<Rule>) -> String {
364    print_rule_tree(rules, 0)
365}
366
367///Pretty-print parsed `Pairs` (recursive version).
368fn print_rule_tree(rules: Pairs<Rule>, tabs: usize) -> String {
369    let mut output = String::new();
370    for rule in rules {
371        for _ in 0..tabs {
372            output.push_str("\t");
373        }
374        output.push_str(format!("{:?}: {}\n", rule.as_rule(), rule.as_str()).as_ref());
375        output.push_str(print_rule_tree(rule.into_inner(), tabs + 1).as_ref());
376    }
377    output
378}
379
380///Turns a file parse result into a `GabcFile`. This relies on unchecked unwrap() calls that should not
381///fail because of the characteristics of the pest PEG.
382fn parsed_file_to_struct<'b>(mut parsed_file: pest::iterators::Pairs<'b, Rule>) -> GabcFile<'b> {
383    let mut syllables: Vec<Syllable> = Vec::new();
384    let mut attributes: Vec<(&str, &str)> = Vec::new();
385    let mut current_clef = "no clef set";
386    for pair in parsed_file.next().unwrap().into_inner() {
387        match pair.as_rule() {
388            Rule::attribute => {
389                let attribute: (&str, &str) =
390                    pair.into_inner().map(|x| x.as_str()).next_tuple().unwrap();
391                attributes.push(attribute);
392            }
393            Rule::syllable => {
394                let mut syllable_components = pair.into_inner();
395                let text = syllable_components.next().unwrap().as_str();
396                let mut music: Vec<NoteElem> = Vec::new();
397                while let Some(pair) = syllable_components.next() {
398                    match pair.as_rule() {
399                        Rule::note => {
400                            music.push(NoteElem::Note(parsed_note_to_struct(pair, current_clef)));
401                        }
402                        Rule::barline => {
403                            music.push(NoteElem::Barline(pair.as_str()));
404                        }
405                        Rule::spacer => {
406                            music.push(NoteElem::Spacer(pair.as_str()));
407                        }
408                        Rule::clef => {
409                            current_clef = pair.as_str();
410                        }
411                        _ => unreachable!("impossible syllable sub-rule"),
412                    }
413                }
414                syllables.push(Syllable { text, music }); //strings[1..].to_vec() } );
415            }
416            _ => {}
417        }
418    }
419    GabcFile {
420        attributes,
421        syllables,
422    }
423}
424
425///Turns a syllable parse result into a `Syllable`. This relies on unchecked unwrap() calls that should not
426///fail because of the characteristics of the pest PEG.
427///This isn't used in the main parse pipeline because it can't update the current_clef tracker,
428///but it is used in `Syllable::new()`.
429fn parsed_syllable_to_struct<'a>(parsed_syllable: pest::iterators::Pair<'a, Rule>, current_clef: &'a str) -> Syllable<'a> {
430    let mut syllable_components = parsed_syllable.into_inner();
431    let text = syllable_components.next().unwrap().as_str();
432    let mut music: Vec<NoteElem> = Vec::new();
433    while let Some(pair) = syllable_components.next() {
434        match pair.as_rule() {
435            Rule::note => {
436                music.push(NoteElem::Note(parsed_note_to_struct(pair, current_clef)));
437            }
438            Rule::barline => {
439                music.push(NoteElem::Barline(pair.as_str()));
440            }
441            Rule::spacer => {
442                music.push(NoteElem::Spacer(pair.as_str()));
443            }
444            _ => unreachable!("impossible syllable sub-rule"),
445        }
446    }
447    Syllable { text, music }
448}
449
450///Turns a note parse result into a `Note`. This relies on unchecked unwrap() calls that should not
451///fail because of the characteristics of the pest PEG.
452fn parsed_note_to_struct<'b>(parsed_note: pest::iterators::Pair<'b, Rule>, current_clef: &'b str) -> Note<'b> {
453        let mut prefix = "";
454        let mut position = 'z';
455        let mut suffix = "";
456        for p in parsed_note.into_inner() {
457            match &p.as_rule() {
458                Rule::prefix => prefix = p.as_str(),
459                Rule::position => position = p.as_str().chars().next().unwrap(),
460                Rule::suffix => suffix = p.as_str(),
461                _ => unreachable!("impossible note sub-rule"),
462            }
463        }
464        assert!(position != 'z'); //note rule MUST have a position sub-rule
465        Note {
466            prefix,
467            position,
468            suffix,
469            current_clef,
470        }
471}
472
473//Lilypond template below derived from
474//<http://lilypond.org/doc/v2.18/Documentation/snippets/templates#templates-ancient-notation-template-_002d-modern-transcription-of-gregorian-music>
475static LY_1: &'static str = r#"\include "gregorian.ly"
476
477chant = \absolute { \transpose c c' {
478  \set Score.timing = ##f
479  "#;
480// f4 a2 \divisioMinima
481// g4 b a2 f2 \divisioMaior
482// g4( f) f( g) a2 \finalis
483static LY_2: &'static str = r#"
484}}
485
486verba = \lyricmode {
487  "#;
488// Lo -- rem ip -- sum do -- lor sit a -- met
489static LY_3: &'static str = r#"
490}
491
492\score {
493  \new Staff <<
494    \new Voice = "melody" \chant
495    \new Lyrics = "one" \lyricsto melody \verba
496  >>
497  \layout {
498    \context {
499      \Staff
500      \remove "Time_signature_engraver"
501      \remove "Bar_engraver"
502      \hide Stem
503    }
504    \context {
505      \Voice
506      \override Stem.length = #0
507    }
508    \context {
509      \Score
510      barAlways = ##t
511    }
512  }
513}"#;