Skip to main content

croma_core/parse/
mod.rs

1pub mod barline;
2pub mod decoration;
3pub mod directive;
4pub mod field;
5pub mod lyric;
6pub mod music;
7pub mod note;
8
9use crate::diagnostic::{Diagnostic, RecoveryNote, Severity, Span, SpecReference};
10use crate::error::{CromaError, Result};
11use crate::lower::{ScoreModelInput, build_score_model, lower_tune_music};
12use crate::model::{Score, TextLine, Tune};
13use crate::options::ParseOptions;
14use crate::parse::field::{InterpretationField, ParsedAbcFields, ParsedFieldKind, parse_fields};
15use crate::parse::music::parse_music_document;
16use crate::source::SourceText;
17use crate::syntax::ParsedMusicDocument;
18use crate::syntax::tune::{SurfaceMap, analyze_source};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ParseReport<T> {
22    pub value: T,
23    pub diagnostics: Vec<Diagnostic>,
24}
25
26impl<T> ParseReport<T> {
27    pub fn new(value: T, diagnostics: Vec<Diagnostic>) -> Self {
28        Self { value, diagnostics }
29    }
30
31    pub fn has_errors(&self) -> bool {
32        self.diagnostics
33            .iter()
34            .any(|diagnostic| diagnostic.severity == Severity::Error)
35    }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct AbcDocument {
40    pub source: SourceText,
41    pub options: ParseOptions,
42    pub surface: SurfaceMap,
43    pub fields: ParsedAbcFields,
44    pub music: ParsedMusicDocument,
45}
46
47pub fn parse_document(source: &str, options: ParseOptions) -> ParseReport<AbcDocument> {
48    parse_document_source(SourceText::new(source), options)
49}
50
51pub fn parse_document_source(
52    source: SourceText,
53    options: ParseOptions,
54) -> ParseReport<AbcDocument> {
55    let mut diagnostics = source_level_diagnostics(&source);
56    let surface = analyze_source(&source);
57    let (fields, field_diagnostics) = parse_fields(&source, &surface, options);
58    diagnostics.extend(field_diagnostics);
59    let ParseReport {
60        value: music,
61        diagnostics: music_diagnostics,
62    } = parse_music_document(&source, &surface, &fields);
63    diagnostics.extend(music_diagnostics);
64
65    ParseReport::new(
66        AbcDocument {
67            source,
68            options,
69            surface,
70            fields,
71            music,
72        },
73        diagnostics,
74    )
75}
76
77pub fn parse_tune(
78    source: &SourceText,
79    _surface: &SurfaceMap,
80    _options: ParseOptions,
81) -> Result<Tune> {
82    let report = parse_tune_report(source, _surface, _options);
83    report
84        .value
85        .ok_or_else(|| CromaError::from_diagnostics(report.diagnostics))
86}
87
88pub fn parse_tune_report(
89    source: &SourceText,
90    surface: &SurfaceMap,
91    options: ParseOptions,
92) -> ParseReport<Option<Tune>> {
93    if source.content().trim().is_empty() {
94        return ParseReport::new(None, source_level_diagnostics(source));
95    }
96
97    let fallback_surface;
98    let surface = if surface.line_map.lines.is_empty() && source.line_count() > 0 {
99        fallback_surface = analyze_source(source);
100        &fallback_surface
101    } else {
102        surface
103    };
104
105    let (fields, mut diagnostics) = parse_fields(source, surface, options);
106    let music_report = parse_music_document(source, surface, &fields);
107    diagnostics.extend(music_report.diagnostics);
108    parse_tune_report_with_fields(
109        source,
110        surface,
111        &fields,
112        Some(&music_report.value),
113        diagnostics,
114    )
115}
116
117pub fn parse_tune_report_from_document(document: &AbcDocument) -> ParseReport<Option<Tune>> {
118    parse_tune_report_with_fields(
119        &document.source,
120        &document.surface,
121        &document.fields,
122        Some(&document.music),
123        Vec::new(),
124    )
125}
126
127fn header_time_symbol_from_fields(
128    fields: &ParsedAbcFields,
129    header_field_indices: &[usize],
130) -> Option<String> {
131    let mut pending = None;
132    for field_index in header_field_indices {
133        let Some(field) = fields.field(*field_index) else {
134            continue;
135        };
136        match &field.kind {
137            ParsedFieldKind::Interpretation(InterpretationField::CromaTimeSymbol { value }) => {
138                pending = parse_time_symbol_value(&value.value);
139            }
140            ParsedFieldKind::Meter(_) => return pending,
141            _ => {}
142        }
143    }
144    None
145}
146
147fn parse_time_symbol_value(value: &str) -> Option<String> {
148    for item in value.split_whitespace() {
149        let Some(symbol) = item.strip_prefix("symbol=") else {
150            continue;
151        };
152        if matches!(symbol, "common" | "cut") {
153            return Some(symbol.to_owned());
154        }
155    }
156    None
157}
158
159fn parse_tune_report_with_fields(
160    source: &SourceText,
161    surface: &SurfaceMap,
162    fields: &ParsedAbcFields,
163    music: Option<&ParsedMusicDocument>,
164    mut diagnostics: Vec<Diagnostic>,
165) -> ParseReport<Option<Tune>> {
166    if source.content().trim().is_empty() {
167        return ParseReport::new(None, source_level_diagnostics(source));
168    }
169
170    let mut reference = String::new();
171    let mut reference_line = TextLine {
172        text: String::new(),
173        span: Span::new(0, 0),
174    };
175    let mut title = String::new();
176    let mut title_line = None;
177    let mut composers = Vec::new();
178    let mut tempo_line = None;
179    let mut meter = String::from("4/4");
180    let mut key = String::new();
181    let mut has_key = false;
182    let mut events = Vec::new();
183    let mut divisions = 8;
184    let mut voices = Vec::new();
185    let mut header_time_symbol = None;
186    let mut score_directives = Vec::new();
187    let mut preserved_directives = Vec::new();
188    let mut post_tune_lyrics = Vec::new();
189    let mut score = None;
190    let mut body_start = None;
191    let mut tune_field_state = None;
192
193    let Some(tune) = surface.line_map.tunes.first() else {
194        diagnostics.push(missing_key_diagnostic(source, surface.line_map.tunes.len()));
195        return ParseReport::new(None, diagnostics);
196    };
197
198    if let Some(tune_fields) = fields.tune(tune.index) {
199        if let Some(parsed_meter) = tune_fields.header.meter.as_ref() {
200            meter = parsed_meter.value.raw.clone();
201        }
202        header_time_symbol =
203            header_time_symbol_from_fields(fields, &tune_fields.header_field_indices);
204        tune_field_state = Some(&tune_fields.header);
205
206        for field_index in &tune_fields.header_field_indices {
207            let Some(field) = fields.field(*field_index) else {
208                continue;
209            };
210            match &field.kind {
211                ParsedFieldKind::Reference(value) => {
212                    reference = value.value.clone();
213                    reference_line = TextLine {
214                        text: value.value.clone(),
215                        span: value.span,
216                    };
217                }
218                // Titles and composers are ABC 2.1 §8.2 text strings: decode
219                // their accent mnemonics and entities for the score model.
220                ParsedFieldKind::Title(value) if title.is_empty() => {
221                    title = crate::text::decode_text_string(&value.value);
222                    title_line = Some(TextLine {
223                        text: title.clone(),
224                        span: value.span,
225                    });
226                }
227                ParsedFieldKind::TextMetadata(metadata) if metadata.code == 'C' => {
228                    composers.push(TextLine {
229                        text: crate::text::decode_text_string(&metadata.value.value),
230                        span: metadata.value.span,
231                    });
232                }
233                ParsedFieldKind::Tempo(value) => {
234                    tempo_line = Some(TextLine {
235                        text: value.value.clone(),
236                        span: value.span,
237                    });
238                }
239                ParsedFieldKind::Key(value) => {
240                    has_key = true;
241                    key = if value.value.raw.is_empty() {
242                        String::from("none")
243                    } else {
244                        value.value.raw.clone()
245                    };
246                    body_start = Some(field.line_span.end);
247                }
248                _ => {}
249            }
250        }
251    }
252
253    if let (Some(tune_music), Some(field_state)) = (
254        music.and_then(|music| music.tune(tune.index)),
255        tune_field_state,
256    ) {
257        let lower_report = lower_tune_music(tune_music, field_state);
258        diagnostics.extend(lower_report.diagnostics);
259        events = lower_report.value.events;
260        divisions = lower_report.value.divisions;
261        voices = lower_report.value.voices;
262        score_directives = lower_report.value.score_directives;
263        preserved_directives = lower_report.value.preserved_directives;
264        post_tune_lyrics = lower_report.value.post_tune_lyrics;
265        score = Some(build_score_model(ScoreModelInput {
266            reference: reference_line.clone(),
267            title: title_line.clone(),
268            composers: composers.clone(),
269            tempo: tempo_line.clone(),
270            source_span: tune.span,
271            field_state,
272            voices: &voices,
273            header_time_symbol,
274            score_directives: &score_directives,
275            preserved_directives: &preserved_directives,
276            post_tune_lyrics: &post_tune_lyrics,
277            diagnostics: &diagnostics,
278            divisions,
279        }));
280    }
281
282    if !has_key {
283        diagnostics.push(missing_key_diagnostic(source, surface.line_map.tunes.len()));
284        return ParseReport::new(None, diagnostics);
285    }
286    if events.iter().all(|event| !event.is_time_bearing()) {
287        diagnostics.push(no_music_diagnostic(source, body_start));
288        return ParseReport::new(None, diagnostics);
289    }
290
291    ParseReport::new(
292        Some(Tune {
293            reference,
294            title,
295            meter,
296            key,
297            divisions,
298            events,
299            voices,
300            score_directives,
301            preserved_directives,
302            post_tune_lyrics,
303            score: score.unwrap_or_else(|| Score::empty(tune.span, diagnostics.clone())),
304        }),
305        diagnostics,
306    )
307}
308
309fn source_level_diagnostics(source: &SourceText) -> Vec<Diagnostic> {
310    if source.content().trim().is_empty() {
311        return vec![
312            Diagnostic::new(
313                Severity::Error,
314                "abc.file.empty",
315                "ABC source is empty",
316                Span::new(source.content_start(), source.content_start()),
317            )
318            .with_spec_reference(abc_file_structure_reference()),
319        ];
320    }
321
322    let has_non_comment_content = (0..source.line_count()).any(|index| {
323        source
324            .line_text(index)
325            .map(|line| {
326                let trimmed = line.trim_start();
327                !trimmed.trim_end().is_empty() && !trimmed.starts_with('%')
328            })
329            .unwrap_or(false)
330    });
331
332    if has_non_comment_content {
333        Vec::new()
334    } else {
335        vec![
336            Diagnostic::new(
337                Severity::Error,
338                "abc.file.no_tune",
339                "ABC source contains comments only and no tune content",
340                source.content_span(),
341            )
342            .with_spec_reference(abc_file_structure_reference()),
343        ]
344    }
345}
346
347fn abc_file_structure_reference() -> SpecReference {
348    SpecReference::new("ABC 2.1 file structure")
349        .with_url("https://abcnotation.com/wiki/abc:standard:v2.1")
350}
351
352fn missing_key_diagnostic(source: &SourceText, tune_count: usize) -> Diagnostic {
353    let diagnostic = Diagnostic::new(
354        Severity::Error,
355        "abc.file.missing_k",
356        "ABC source is missing a K: field",
357        Span::new(source.len(), source.len()),
358    )
359    .with_spec_reference(abc_file_structure_reference());
360    if tune_count > 1 {
361        // Clarify the otherwise-confusing message when K: is plainly present on a
362        // LATER tune: croma processes a single tune per file, and the first one
363        // (the one missing K:) was selected (ABC 2.1 §2.2 separates tunes by blank
364        // lines — two adjacent `X:` with no blank line is malformed).
365        diagnostic.with_recovery_note(RecoveryNote::new(
366            "This file holds multiple tunes; croma processes only the first, which has no K: field. Separate tunes with a blank line.",
367        ))
368    } else {
369        diagnostic
370    }
371}
372
373fn no_music_diagnostic(source: &SourceText, body_start: Option<usize>) -> Diagnostic {
374    Diagnostic::new(
375        Severity::Error,
376        "abc.file.no_music",
377        "ABC source does not contain body music",
378        Span::new(body_start.unwrap_or_else(|| source.len()), source.len()),
379    )
380    .with_spec_reference(abc_file_structure_reference())
381}
382
383#[cfg(test)]
384#[path = "mod_tests.rs"]
385mod tests;