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}