Skip to main content

croma_core/
options.rs

1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2pub enum AbcSpecVersion {
3    #[default]
4    V21,
5    V22Draft,
6}
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum ParseMode {
10    #[default]
11    Strict,
12    Loose,
13    Recover,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub struct DiagnosticOptions {
18    pub suppress_croma_carrier_warnings: bool,
19}
20
21impl DiagnosticOptions {
22    pub fn suppress_croma_carrier_warnings(mut self) -> Self {
23        self.suppress_croma_carrier_warnings = true;
24        self
25    }
26
27    pub(crate) fn should_emit_croma_carrier_warning(self, name: &str) -> bool {
28        !self.suppress_croma_carrier_warnings
29            || !(is_croma_carrier_name(name) || is_croma_managed_midi_name(name))
30    }
31}
32
33/// Whether `name` is in croma's private carrier namespace, in either spelling:
34/// the long `croma-<name>` prefix, or the compact `cr <code>` pair the lowerer
35/// reports for an unrecognised compact carrier (see
36/// `crate::lower::carrier::unknown_directive_name`).
37///
38/// The two are matched differently on purpose. `croma-` is a *prefix* of one
39/// word, so a leading-slice test is exactly right. `cr` is a whole word: a
40/// leading-slice test would swallow `credits` and every other directive
41/// starting with those two letters, so the compact form is recognised only as
42/// the namespace word followed by a code.
43pub fn is_croma_carrier_name(name: &str) -> bool {
44    let name = name.trim_start();
45    name.get(.."croma-".len())
46        .is_some_and(|prefix| prefix.eq_ignore_ascii_case("croma-"))
47        || is_compact_croma_carrier_name(name)
48}
49
50fn is_compact_croma_carrier_name(name: &str) -> bool {
51    name.split_once(char::is_whitespace)
52        .is_some_and(|(head, code)| {
53            head.eq_ignore_ascii_case(crate::lower::carrier::COMPACT_NAMESPACE)
54                && !code.trim().is_empty()
55        })
56}
57
58fn is_croma_managed_midi_name(name: &str) -> bool {
59    name.trim_start().eq_ignore_ascii_case("MIDI")
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63pub struct ParseOptions {
64    pub spec: AbcSpecVersion,
65    pub mode: ParseMode,
66    pub diagnostics: DiagnosticOptions,
67}
68
69impl ParseOptions {
70    pub fn new(spec: AbcSpecVersion, mode: ParseMode) -> Self {
71        Self {
72            spec,
73            mode,
74            diagnostics: DiagnosticOptions::default(),
75        }
76    }
77
78    pub fn suppress_croma_carrier_warnings(mut self) -> Self {
79        self.diagnostics = self.diagnostics.suppress_croma_carrier_warnings();
80        self
81    }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub struct ExportOptions {
86    pub spec: AbcSpecVersion,
87    pub parse_mode: ParseMode,
88    pub diagnostics: DiagnosticOptions,
89    /// Render-oriented MusicXML hints (beams, stems, tuplet display, ...). Default
90    /// leaves the writer's byte-for-byte round-trip output untouched.
91    pub write: MusicXmlWriteOptions,
92}
93
94impl ExportOptions {
95    pub fn parse_options(self) -> ParseOptions {
96        ParseOptions {
97            spec: self.spec,
98            mode: self.parse_mode,
99            diagnostics: self.diagnostics,
100        }
101    }
102
103    pub fn suppress_croma_carrier_warnings(mut self) -> Self {
104        self.diagnostics = self.diagnostics.suppress_croma_carrier_warnings();
105        self
106    }
107
108    /// Turn on the full engraving profile (all render hints).
109    pub fn engrave(mut self) -> Self {
110        self.write = MusicXmlWriteOptions::engrave();
111        self
112    }
113}
114
115/// Opt-in engraving hints the MusicXML writer computes at write time from data the
116/// score already holds (meter, durations, clef, voice). Every field defaults off, so
117/// [`MusicXmlWriteOptions::default`] reproduces the writer's byte-for-byte round-trip
118/// output; the reader self-loop and the abc2xml whitelist depend on that default.
119///
120/// The rules are croma's own, written from published engraving references; see
121/// `docs/engraving-export.md` and `docs/engraving/rules.md`.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
123pub struct MusicXmlWriteOptions {
124    /// Emit `<beam>` grouping computed from meter + note durations.
125    pub beams: bool,
126    /// How to spell `<tuplet>` display detail when the source carried no directive.
127    pub tuplet_display: TupletDisplay,
128    /// Emit `<stem>up|down` computed from clef, pitch, voice, and beam membership.
129    pub stems: bool,
130    /// Emit multi-voice rest `<display-step>`/`<display-octave>` so voices do not collide.
131    pub rest_placement: bool,
132    /// Emit `<slur placement="above|below">` computed from stem direction / voice.
133    pub slur_placement: bool,
134    /// Emit `<tied orientation="over|under">` computed from stem direction / voice.
135    pub tie_orientation: bool,
136}
137
138impl MusicXmlWriteOptions {
139    /// The `--engrave` umbrella: every hint on, tuplet display = engraving default.
140    pub fn engrave() -> Self {
141        Self {
142            beams: true,
143            tuplet_display: TupletDisplay::EngravingDefault,
144            stems: true,
145            rest_placement: true,
146            slur_placement: true,
147            tie_orientation: true,
148        }
149    }
150
151    /// Whether any hint is enabled (lets the writer skip plan construction entirely
152    /// on the default path).
153    pub fn any_enabled(self) -> bool {
154        self.beams
155            || self.stems
156            || self.rest_placement
157            || self.slur_placement
158            || self.tie_orientation
159            || self.tuplet_display != TupletDisplay::AsEncoded
160    }
161}
162
163/// How the writer spells a `<tuplet>` whose ABC source carried no explicit
164/// tuplet-display directive.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
166pub enum TupletDisplay {
167    /// Emit only what the source encoded (current behaviour — a bare `<tuplet>`).
168    #[default]
169    AsEncoded,
170    /// Emit the engraving default (`docs/engraving/rules.md` §2): number-only, no
171    /// bracket, for a tuplet that is exactly one beam group; bracketed otherwise.
172    EngravingDefault,
173}
174
175impl From<ExportOptions> for ParseOptions {
176    fn from(options: ExportOptions) -> Self {
177        options.parse_options()
178    }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
182pub struct LowerOptions;
183
184#[cfg(test)]
185#[path = "options_tests.rs"]
186mod tests;