pub(crate) const COMPACT_NAMESPACE: &str = "cr";
pub(crate) const COMPACT_CARRIERS: [(&str, &str); 8] = [
("dp", "croma-direction-placement"),
("htx", "croma-harmony-text"),
("ht", "croma-harmony-text"),
("le", "croma-lyric-extend"),
("mr", "croma-meter-restatement"),
("kr", "croma-key-restatement"),
("ec", "croma-ending-close"),
("mf", "croma-musicxml-forward"),
];
pub(crate) fn expand_compact_carrier(value: &str) -> Option<String> {
let rest = value.trim().strip_prefix(COMPACT_NAMESPACE)?;
if !rest.starts_with(char::is_whitespace) {
return None;
}
let rest = rest.trim_start();
let (code, fields) = match rest.find(char::is_whitespace) {
Some(index) => (&rest[..index], rest[index..].trim_start()),
None => (rest, ""),
};
let (code, inline_value) = match code.split_once('=') {
Some((code, value)) => (code, Some(value)),
None => (code, None),
};
match (code, inline_value) {
("dp", Some(value)) => {
let placement = match value {
"a" => "above",
"b" => "below",
_ => return None,
};
Some(format!("croma-direction-placement placement={placement}"))
}
("le", Some(verse)) => Some(format!("croma-lyric-extend verse={verse}")),
("htx", None) => Some("croma-harmony-text textless=1".to_owned()),
("ht", None) if !fields.is_empty() => Some(format!("croma-harmony-text {fields}")),
("mr", None) => Some("croma-meter-restatement".to_owned()),
("kr", None) => Some("croma-key-restatement".to_owned()),
("mf", None) => Some("croma-musicxml-forward".to_owned()),
("ec", None) => Some(format!(
"croma-ending-close {}",
expand_ending_close(fields)?
)),
_ => None,
}
}
pub(crate) fn unknown_directive_name(value: &str) -> String {
let mut words = value.split_whitespace();
let first = words.next().unwrap_or_default();
if first.eq_ignore_ascii_case(COMPACT_NAMESPACE)
&& let Some(code) = words.next()
{
let code = directive_token(code);
return format!("{first} {code}");
}
directive_token(first).to_owned()
}
fn directive_token(word: &str) -> &str {
word.split('=').next().unwrap_or_default()
}
pub(crate) fn compact_code_for_long(value: &str) -> Option<(&'static str, &'static str)> {
let value = value.trim();
let (matched_code, long) = COMPACT_CARRIERS
.iter()
.copied()
.find(|(_, long)| matches_long_carrier(value, long))?;
let code = if long == "croma-harmony-text" {
if value.contains("textless") {
"htx"
} else {
"ht"
}
} else {
matched_code
};
Some((long, code))
}
fn matches_long_carrier(value: &str, long: &str) -> bool {
value
.strip_prefix(long)
.is_some_and(|rest| rest.is_empty() || rest.starts_with(char::is_whitespace))
}
fn expand_ending_close(fields: &str) -> Option<String> {
let mut out: Vec<String> = Vec::new();
for field in split_fields(fields) {
let (key, value) = field.split_once('=')?;
let expanded = match (key, value) {
("t", "s") => "type=stop".to_owned(),
("t", "d") => "type=discontinue".to_owned(),
("l", "l") => "location=left".to_owned(),
("l", "r") => "location=right".to_owned(),
("n", value) => format!("number={value}"),
_ => return None,
};
out.push(expanded);
}
(!out.is_empty()).then(|| out.join(" "))
}
fn split_fields(fields: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut start = None;
let mut quoted = false;
let mut escaped = false;
for (index, ch) in fields.char_indices() {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' if quoted => escaped = true,
'"' => quoted = !quoted,
ch if ch.is_whitespace() && !quoted => {
if let Some(begin) = start.take() {
out.push(&fields[begin..index]);
}
}
_ => {
if start.is_none() {
start = Some(index);
}
}
}
}
if let Some(begin) = start {
out.push(&fields[begin..]);
}
out
}
#[cfg(test)]
#[path = "carrier_tests.rs"]
mod tests;