Skip to main content

chord_progression_parser/
lib.rs

1mod error_code;
2mod lexer;
3mod model;
4mod parser;
5mod util;
6use serde::Serialize;
7use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
8
9pub use error_code::{ErrorCode, ErrorInfo, ErrorInfoWithPosition};
10pub use model::{
11    accidental::Accidental, ast::Ast, bar::Bar, base::Base, chord::Chord, chord_block::ChordBlock,
12    chord_detailed::ChordDetailed, chord_expression::ChordExpression, chord_info::ChordInfo,
13    chord_info_meta::ChordInfoMeta, chord_type::ChordType, extension::Extension, key::Key,
14    section::Section, section_meta::SectionMeta,
15};
16pub use util::position::Position;
17
18/** Successful JavaScript response serialized as a plain object. */
19#[derive(Serialize)]
20struct JsParseSuccess {
21    success: bool,
22    ast: Ast,
23}
24
25/** Failed JavaScript response serialized as a plain object. */
26#[derive(Serialize)]
27struct JsParseFailure {
28    success: bool,
29    error: JsParseError,
30}
31
32/** JavaScript-facing parse error with camel-case field names. */
33#[derive(Serialize)]
34#[serde(rename_all = "camelCase")]
35struct JsParseError {
36    code: String,
37    additional_info: Option<String>,
38    position: JsPosition,
39}
40
41/** JavaScript-facing source position with camel-case field names. */
42#[derive(Serialize)]
43#[serde(rename_all = "camelCase")]
44struct JsPosition {
45    line_number: usize,
46    column_number: usize,
47    length: usize,
48}
49
50/** Represents either JavaScript response shape without adding an enum tag. */
51#[derive(Serialize)]
52#[serde(untagged)]
53enum JsParseResult {
54    Success(JsParseSuccess),
55    Failure(JsParseFailure),
56}
57
58#[doc(hidden)]
59/// @param {string} input - The chord progression string to parse.
60/// @returns {ParsedResult} - The parsed result.
61#[wasm_bindgen(js_name = "parseChordProgressionString", skip_jsdoc)]
62pub fn parse_chord_progression_string_js(input: &str) -> JsValue {
63    let response = match parse_chord_progression_string(input) {
64        Ok(ast) => JsParseResult::Success(JsParseSuccess { success: true, ast }),
65        Err(error_info) => JsParseResult::Failure(JsParseFailure {
66            success: false,
67            error: JsParseError {
68                code: error_info.error.code.to_string(),
69                additional_info: error_info.error.additional_info,
70                position: JsPosition {
71                    line_number: error_info.position.line_number,
72                    column_number: error_info.position.column_number,
73                    length: error_info.position.length,
74                },
75            },
76        }),
77    };
78
79    response
80        .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
81        .expect("serializing the fixed JavaScript response types should not fail")
82}
83
84/// Parse a chord progression string and return the AST
85///
86/// # Example
87/// ```rust
88/// use chord_progression_parser::parse_chord_progression_string;
89///
90/// let input: &str = "
91/// @section=Intro
92/// [key=E]E-C#m(7)-Bm(7)-C#(7)
93/// F#m(7)-Am(7)-F#(7)-B
94///
95/// @section=Verse
96/// E-C#m(7)-Bm(7)-C#(7)
97/// F#m(7)-Am(7)-F#(7)-B
98/// ";
99///     
100/// let result = parse_chord_progression_string(input);
101/// println!("{:#?}", result);
102/// ```
103///
104/// # Errors
105///
106/// Returns an error code and source position when the input does not follow the grammar.
107pub fn parse_chord_progression_string(input: &str) -> Result<Ast, ErrorInfoWithPosition> {
108    parser::parse(input)
109}
110
111#[cfg(test)]
112mod tests {
113    #[cfg(test)]
114    mod success {
115        use crate::parse_chord_progression_string;
116        use serde_json::json;
117
118        // if C/D, is input, comma is ignored
119        #[test]
120        fn comma_is_ignored_in_dominator_last_char() {
121            let input: &str = "C/D,";
122            let result_json = json!(parse_chord_progression_string(input).unwrap());
123            let expected = json!([
124                {
125                    "chordBlocks": [
126                        {
127                            "type": "bar",
128                            "value": [
129                                {
130                                    "chordExpression": {
131                                        "type": "chord",
132                                        "value": {
133                                            "detailed": {
134                                                "accidental": null,
135                                                "base": "C",
136                                                "chordType": "M",
137                                                "extensions": []
138                                            },
139                                            "plain": "C"
140                                        }
141                                    },
142                                    "denominator": Some("D".to_string()),
143                                    "metaInfos": []
144                                }
145                            ]
146                        }
147                    ],
148                    "metaInfos": []
149                }
150            ]);
151
152            assert_eq!(result_json, expected);
153        }
154
155        // if C/D,E is input, C/D and E are separated
156        #[test]
157        fn comma_separated_chords_with_denominator() {
158            let input: &str = "C/D,E";
159            let result_json = json!(parse_chord_progression_string(input).unwrap());
160            let expected = json!([
161                {
162                    "chordBlocks": [
163                        {
164                            "type": "bar",
165                            "value": [
166                                {
167                                    "chordExpression": {
168                                        "type": "chord",
169                                        "value": {
170                                            "detailed": {
171                                                "accidental": null,
172                                                "base": "C",
173                                                "chordType": "M",
174                                                "extensions": []
175                                            },
176                                            "plain": "C"
177                                        }
178                                    },
179                                    "denominator": "D",
180                                    "metaInfos": []
181                                },
182                                {
183                                    "chordExpression": {
184                                        "type": "chord",
185                                        "value": {
186                                            "detailed": {
187                                                "accidental": null,
188                                                "base": "E",
189                                                "chordType": "M",
190                                                "extensions": []
191                                            },
192                                            "plain": "E"
193                                        }
194                                    },
195                                    "denominator": null,
196                                    "metaInfos": []
197                                }
198                            ]
199                        }
200                    ],
201                    "metaInfos": []
202                }
203            ]);
204
205            assert_eq!(result_json, expected);
206        }
207
208        #[test]
209        fn only_section_meta() {
210            let input: &str = "@section=A";
211
212            let result_json = json!(parse_chord_progression_string(input).unwrap());
213            let expected = json!([
214                {
215                    "chordBlocks": [],
216                    "metaInfos": [
217                        {
218                            "type": "section",
219                            "value": "A"
220                        }
221                    ]
222                }
223            ]);
224
225            assert_eq!(result_json, expected);
226        }
227
228        #[test]
229        fn only_tension() {
230            let input: &str = "C(9,11,13,o)";
231
232            let result_json = json!(parse_chord_progression_string(input).unwrap());
233            let expected = json!([
234                {
235                    "chordBlocks": [
236                        {
237                            "type": "bar",
238                            "value": [{
239                                "chordExpression": {
240                                    "type": "chord",
241                                    "value": {
242                                        "detailed": {
243                                            "accidental": null,
244                                            "base": "C",
245                                            "chordType": "M",
246                                            "extensions": [
247                                                "9",
248                                                "11",
249                                                "13",
250                                                "o"
251                                            ]
252                                        },
253                                        "plain": "C(9,11,13,o)"
254                                    }
255                                },
256                                "denominator": null,
257                                "metaInfos": []
258                            }]
259                        }
260                    ],
261                    "metaInfos": []
262                }
263            ]);
264
265            assert_eq!(result_json, expected);
266        }
267
268        #[test]
269        fn complex_input_snapshot() {
270            let input: &str = "
271@section=Intro
272[key=E]E-C#m(7)-Bm(7)-C#(7)
273F#m(7)-Am(7)-F#(7)-B
274
275@section=Verse
276E-C#m(7)-Bm(7)-C#(7)
277F#m(7)-Am(7)-F#(7)-B
278
279@section=Chorus
280[key=C]C-C(7)-FM(7)-Fm(7)
281C-C(7)-FM(7)-Dm(7)
282Em(7)-E(7)
283        
284@section=Interlude
285C-A,B
286
287[key=C]C(M9)-CM(9)
288";
289
290            insta::assert_debug_snapshot!(parse_chord_progression_string(input));
291        }
292
293        #[test]
294        fn complex_input_can_be_parsed() {
295            let input: &str = "
296@section=Intro
297[key=E]E-C#m(7)-Bm(7)-C#(7)
298F#m(7)-Am(7)-F#(7)-B
299
300@section=Verse
301E-C#m(7)-Bm(7)-C#(7)
302F#m(7)-Am(7)-F#(7)-B
303
304@section=Chorus
305[key=C]C-C(7)-FM(7)-Fm(7)
306C-C(7)-FM(7)-Dm(7)
307Em(7)-E(7)
308        
309@section=Interlude
310C-A,B
311
312[key=C]C(M9)-CM(9)
313";
314
315            let result = parse_chord_progression_string(input);
316            assert!(result.is_ok());
317        }
318
319        #[test]
320        fn differ_major_9_vs_9_of_major() {
321            let input: &str = "
322            @section=Intro
323            [key=C]C(M9)-CM(9)
324            ";
325
326            let result_json = json!(parse_chord_progression_string(input).unwrap());
327            let expected = json!([
328                {
329                    "chordBlocks": [
330                        {
331                            "type": "bar",
332                            "value": [
333                                {
334                                    "chordExpression": {
335                                        "type": "chord",
336                                        "value": {
337                                            "detailed": {
338                                                "accidental": null,
339                                                "base":"C",
340                                                "chordType":"M",
341                                                "extensions": [
342                                                    "M9"
343                                                ]
344                                            },
345                                            "plain":"C(M9)"
346                                        }
347                                    },
348                                    "denominator":null,
349                                    "metaInfos": [
350                                        {
351                                            "type": "key",
352                                            "value": "C",
353                                        }
354                                    ]
355                                },
356                            ]
357                        },
358                        {
359                            "type": "bar",
360                            "value": [
361                                {
362                                    "chordExpression": {
363                                        "type": "chord",
364                                        "value": {
365                                            "detailed": {
366                                                "accidental": null,
367                                                "base":"C",
368                                                "chordType":"M",
369                                                "extensions": [
370                                                    "9"
371                                                ]
372                                            },
373                                            "plain":"CM(9)"
374                                        }
375                                    },
376                                    "denominator":null,
377                                    "metaInfos": []
378                                }
379                            ]
380                        },
381                    ],
382                    "metaInfos": [
383                        {
384                            "type": "section",
385                            "value": "Intro"
386                        }
387                    ]
388                }
389            ]);
390
391            assert_eq!(result_json, expected);
392        }
393    }
394
395    mod failure {
396        use crate::{parse_chord_progression_string, util::position::Position};
397
398        #[test]
399        fn tension_position_when_error() {
400            let input: &str = "C(9,111)";
401
402            let result = parse_chord_progression_string(input);
403            assert_eq!(
404                result.unwrap_err().position,
405                Position {
406                    line_number: 1,
407                    column_number: 5,
408                    length: 3,
409                },
410            )
411        }
412    }
413}