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}"#;