chord_progression_parser/
lib.rs

1mod error_code;
2mod parser;
3mod tokenizer;
4mod util;
5use error_code::ErrorInfoWithPosition;
6use parser::{parse, Ast};
7use serde_json::json;
8use tokenizer::tokenize;
9use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
10
11// FIXME:
12//  "serde_wasm_bindgen::to_value(&json_result).unwrap()" makes Map.
13//  But I want to generate JSON.
14//  So currently use deprecated from_serde() instead.
15#[doc(hidden)]
16/// @param {string} input - The chord progression string to parse.
17/// @returns {ParsedResult} - The parsed result.
18/// @throws {string} - The error information.
19#[wasm_bindgen(js_name = "parseChordProgressionString", skip_jsdoc)]
20pub fn parse_chord_progression_string_js(input: &str) -> JsValue {
21    let result = parse_chord_progression_string(input);
22
23    let json_result = if let Err(error_info) = result {
24        json!({
25            "success": false,
26            "error": {
27                "code": error_info.error.code.to_string(),
28                "additionalInfo": error_info.error.additional_info,
29                "position": {
30                    "lineNumber": error_info.position.line_number,
31                    "columnNumber": error_info.position.column_number,
32                    "length": error_info.position.length,
33                },
34            }
35        })
36    } else {
37        json!({
38            "success": true,
39            "ast": result.unwrap(),
40        })
41    };
42
43    JsValue::from_serde(&json_result).unwrap()
44}
45
46/// Parse a chord progression string and return the AST
47///
48/// # Example
49/// ```rust
50/// use chord_progression_parser::parse_chord_progression_string;
51///
52/// let input: &str = "
53/// @section=Intro
54/// |[key=E]E|C#m(7)|Bm(7)|C#(7)|
55/// |F#m(7)|Am(7)|F#(7)|B|
56///
57/// @section=Verse
58/// |E|C#m(7)|Bm(7)|C#(7)|
59/// |F#m(7)|Am(7)|F#(7)|B|
60/// ";
61///     
62/// let result = parse_chord_progression_string(input);
63/// println!("{:#?}", result);
64/// ```
65///
66/// # Panics
67///
68/// Panics if unhandled error occurs.
69pub fn parse_chord_progression_string(input: &str) -> Result<Ast, ErrorInfoWithPosition> {
70    let tokenized_result = tokenize(input);
71    if tokenized_result.is_err() {
72        return Err(tokenized_result.err().unwrap());
73    }
74
75    let parsed_result = parse(&tokenized_result.unwrap());
76    if parsed_result.is_err() {
77        return Err(parsed_result.err().unwrap());
78    }
79
80    Ok(parsed_result.unwrap())
81}
82
83#[cfg(test)]
84mod tests {
85    #[cfg(test)]
86    mod success {
87        use crate::parse_chord_progression_string;
88        use serde_json::json;
89
90        // if C/D, is input, comma is ignored
91        #[test]
92        fn comma_is_ignored_in_dominator_last_char() {
93            let input: &str = "C/D,";
94            let result_json = json!(parse_chord_progression_string(input).unwrap());
95            let expected = json!([
96                {
97                    "chordBlocks": [
98                        {
99                            "type": "bar",
100                            "value": [
101                                {
102                                    "chordExpression": {
103                                        "type": "chord",
104                                        "value": {
105                                            "detailed": {
106                                                "accidental": null,
107                                                "base": "C",
108                                                "chordType": "M",
109                                                "extensions": []
110                                            },
111                                            "plain": "C"
112                                        }
113                                    },
114                                    "denominator": Some("D".to_string()),
115                                    "metaInfos": []
116                                }
117                            ]
118                        }
119                    ],
120                    "metaInfos": []
121                }
122            ]);
123
124            assert_eq!(result_json, expected);
125        }
126
127        // if C/D,E is input, C/D and E are separated
128        #[test]
129        fn comma_separated_chords_with_denominator() {
130            let input: &str = "C/D,E";
131            let result_json = json!(parse_chord_progression_string(input).unwrap());
132            let expected = json!([
133                {
134                    "chordBlocks": [
135                        {
136                            "type": "bar",
137                            "value": [
138                                {
139                                    "chordExpression": {
140                                        "type": "chord",
141                                        "value": {
142                                            "detailed": {
143                                                "accidental": null,
144                                                "base": "C",
145                                                "chordType": "M",
146                                                "extensions": []
147                                            },
148                                            "plain": "C"
149                                        }
150                                    },
151                                    "denominator": "D",
152                                    "metaInfos": []
153                                },
154                                {
155                                    "chordExpression": {
156                                        "type": "chord",
157                                        "value": {
158                                            "detailed": {
159                                                "accidental": null,
160                                                "base": "E",
161                                                "chordType": "M",
162                                                "extensions": []
163                                            },
164                                            "plain": "E"
165                                        }
166                                    },
167                                    "denominator": null,
168                                    "metaInfos": []
169                                }
170                            ]
171                        }
172                    ],
173                    "metaInfos": []
174                }
175            ]);
176
177            assert_eq!(result_json, expected);
178        }
179
180        #[test]
181        fn only_section_meta() {
182            let input: &str = "@section=A";
183
184            let result_json = json!(parse_chord_progression_string(input).unwrap());
185            let expected = json!([
186                {
187                    "chordBlocks": [],
188                    "metaInfos": [
189                        {
190                            "type": "section",
191                            "value": "A"
192                        }
193                    ]
194                }
195            ]);
196
197            assert_eq!(result_json, expected);
198        }
199
200        #[test]
201        fn only_tension() {
202            let input: &str = "C(9,11,13,o)";
203
204            let result_json = json!(parse_chord_progression_string(input).unwrap());
205            let expected = json!([
206                {
207                    "chordBlocks": [
208                        {
209                            "type": "bar",
210                            "value": [{
211                                "chordExpression": {
212                                    "type": "chord",
213                                    "value": {
214                                        "detailed": {
215                                            "accidental": null,
216                                            "base": "C",
217                                            "chordType": "M",
218                                            "extensions": [
219                                                "9",
220                                                "11",
221                                                "13",
222                                                "o"
223                                            ]
224                                        },
225                                        "plain": "C(9,11,13,o)"
226                                    }
227                                },
228                                "denominator": null,
229                                "metaInfos": []
230                            }]
231                        }
232                    ],
233                    "metaInfos": []
234                }
235            ]);
236
237            assert_eq!(result_json, expected);
238        }
239
240        #[test]
241        fn complex_input_snapshot() {
242            let input: &str = "
243@section=Intro
244[key=E]E-C#m(7)-Bm(7)-C#(7)
245F#m(7)-Am(7)-F#(7)-B
246
247@section=Verse
248E-C#m(7)-Bm(7)-C#(7)
249F#m(7)-Am(7)-F#(7)-B
250
251@section=Chorus
252[key=C]C-C(7)-FM(7)-Fm(7)
253C-C(7)-FM(7)-Dm(7)
254Em(7)-E(7)
255        
256@section=Interlude
257C-A,B
258
259[key=C]C(M9)-CM(9)
260";
261
262            insta::assert_debug_snapshot!(parse_chord_progression_string(input));
263        }
264
265        #[test]
266        fn complex_input_can_be_parsed() {
267            let input: &str = "
268@section=Intro
269[key=E]E-C#m(7)-Bm(7)-C#(7)
270F#m(7)-Am(7)-F#(7)-B
271
272@section=Verse
273E-C#m(7)-Bm(7)-C#(7)
274F#m(7)-Am(7)-F#(7)-B
275
276@section=Chorus
277[key=C]C-C(7)-FM(7)-Fm(7)
278C-C(7)-FM(7)-Dm(7)
279Em(7)-E(7)
280        
281@section=Interlude
282C-A,B
283
284[key=C]C(M9)-CM(9)
285";
286
287            let result = parse_chord_progression_string(input);
288            assert!(result.is_ok());
289        }
290
291        #[test]
292        fn differ_major_9_vs_9_of_major() {
293            let input: &str = "
294            @section=Intro
295            [key=C]C(M9)-CM(9)
296            ";
297
298            let result_json = json!(parse_chord_progression_string(input).unwrap());
299            let expected = json!([
300                {
301                    "chordBlocks": [
302                        {
303                            "type": "bar",
304                            "value": [
305                                {
306                                    "chordExpression": {
307                                        "type": "chord",
308                                        "value": {
309                                            "detailed": {
310                                                "accidental": null,
311                                                "base":"C",
312                                                "chordType":"M",
313                                                "extensions": [
314                                                    "M9"
315                                                ]
316                                            },
317                                            "plain":"C(M9)"
318                                        }
319                                    },
320                                    "denominator":null,
321                                    "metaInfos": [
322                                        {
323                                            "type": "key",
324                                            "value": "C",
325                                        }
326                                    ]
327                                },
328                            ]
329                        },
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                                                    "9"
343                                                ]
344                                            },
345                                            "plain":"CM(9)"
346                                        }
347                                    },
348                                    "denominator":null,
349                                    "metaInfos": []
350                                }
351                            ]
352                        },
353                    ],
354                    "metaInfos": [
355                        {
356                            "type": "section",
357                            "value": "Intro"
358                        }
359                    ]
360                }
361            ]);
362
363            assert_eq!(result_json, expected);
364        }
365    }
366
367    mod failure {
368        use crate::{parse_chord_progression_string, util::position::Position};
369
370        #[test]
371        fn tension_position_when_error() {
372            let input: &str = "C(9,111)";
373
374            let result = parse_chord_progression_string(input);
375            assert_eq!(
376                result.unwrap_err().position,
377                Position {
378                    line_number: 1,
379                    column_number: 5,
380                    length: 3,
381                },
382            )
383        }
384    }
385}