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}