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 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 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;