use super::misc::tokens_with_spans;
use super::voice::parse_voice_properties;
use super::*;
use crate::diagnostic::Span;
pub(crate) fn parse_key(value: &str, value_span: Span) -> KeySignature {
let trimmed = value.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("none") {
return KeySignature {
raw: trimmed.to_owned(),
tonic: None,
mode: KeyMode::None,
accidentals: Vec::new(),
explicit: false,
compact_accidentals_ignored: false,
tonic_trailing_junk_ignored: false,
properties: VoiceProperties::default(),
};
}
if trimmed == "HP" || trimmed == "Hp" {
return KeySignature {
raw: trimmed.to_owned(),
tonic: None,
mode: if trimmed == "HP" {
KeyMode::HighlandPipes
} else {
KeyMode::HighlandPipesMarked
},
accidentals: Vec::new(),
explicit: false,
compact_accidentals_ignored: false,
tonic_trailing_junk_ignored: false,
properties: VoiceProperties::default(),
};
}
let (tokens, compact_accidentals_ignored) = key_tokens_with_spans(trimmed, value_span.start);
let mut tonic = None;
let mut mode = KeyMode::Major;
let mut explicit = false;
let mut tonic_trailing_junk_ignored = false;
let mut token_start = 0;
if let Some(first) = tokens.first()
&& let Some((parsed_tonic, inline_mode, junk_ignored)) = parse_tonic_token(&first.value)
{
tonic = Some(parsed_tonic);
if let Some(inline_mode) = inline_mode {
mode = inline_mode;
explicit = mode == KeyMode::Explicit;
}
tonic_trailing_junk_ignored = junk_ignored;
token_start = 1;
}
if let Some(token) = tokens.get(token_start)
&& let Some(parsed_mode) = parse_key_mode(&token.value)
{
explicit = parsed_mode == KeyMode::Explicit;
mode = parsed_mode;
token_start += 1;
}
let accidentals = tokens[token_start..]
.iter()
.flat_map(parse_key_accidentals)
.collect();
let properties = if let Some(first_remaining) = tokens.get(token_start) {
let remainder_start = first_remaining.span.start;
let remainder_offset = remainder_start.saturating_sub(value_span.start);
let remainder = trimmed.get(remainder_offset..).unwrap_or("");
parse_voice_properties(&Spanned::new(remainder.to_owned(), {
Span::new(remainder_start, value_span.end)
}))
} else {
VoiceProperties::default()
};
KeySignature {
raw: trimmed.to_owned(),
tonic,
mode,
accidentals,
explicit,
compact_accidentals_ignored,
tonic_trailing_junk_ignored,
properties,
}
}
fn key_tokens_with_spans(value: &str, offset: usize) -> (Vec<Spanned<String>>, bool) {
let tokens = tokens_with_spans(value, offset);
let Some(first) = tokens.first() else {
return (tokens, false);
};
let Some(mut expanded_first) = split_compact_key_accidentals(first) else {
return (tokens, false);
};
let mut expanded = Vec::with_capacity(expanded_first.len() + tokens.len().saturating_sub(1));
expanded.append(&mut expanded_first);
expanded.extend(tokens.into_iter().skip(1));
(expanded, true)
}
fn split_compact_key_accidentals(token: &Spanned<String>) -> Option<Vec<Spanned<String>>> {
let value = token.value.as_str();
for (tail_start, _) in value.char_indices().skip(1) {
if compact_key_accidental_len(&value[tail_start..]).is_none() {
continue;
}
let tonic = &value[..tail_start];
if parse_tonic_token(tonic).is_none() {
continue;
}
if !is_compact_key_accidental_tail(&value[tail_start..]) {
continue;
}
return Some(vec![Spanned::new(
tonic.to_owned(),
Span::new(token.span.start, token.span.start + tail_start),
)]);
}
None
}
fn is_compact_key_accidental_tail(value: &str) -> bool {
let mut start = 0;
while start < value.len() {
let Some(token_len) = compact_key_accidental_len(&value[start..]) else {
return false;
};
start += token_len;
}
true
}
fn compact_key_accidental_len(value: &str) -> Option<usize> {
let sign_len = if value.starts_with("__") || value.starts_with("^^") {
2
} else if value.starts_with('_') || value.starts_with('^') || value.starts_with('=') {
1
} else {
return None;
};
let note = value[sign_len..].chars().next()?;
if !matches!(note.to_ascii_uppercase(), 'A'..='G') {
return None;
}
Some(sign_len + note.len_utf8())
}
fn parse_tonic_token(token: &str) -> Option<(KeyTonic, Option<KeyMode>, bool)> {
let mut chars = token.char_indices();
let (_, first) = chars.next()?;
if !matches!(first.to_ascii_uppercase(), 'A'..='G') {
return None;
}
let mut accidental = None;
let mut mode_start = first.len_utf8();
if let Some((offset, ch)) = chars.next() {
match ch {
'#' => {
accidental = Some(KeyTonicAccidental::Sharp);
mode_start = offset + ch.len_utf8();
}
'b' => {
accidental = Some(KeyTonicAccidental::Flat);
mode_start = offset + ch.len_utf8();
}
_ => {
mode_start = offset;
}
}
}
let mut junk_ignored = false;
let mode = if mode_start < token.len() {
let remainder = &token[mode_start..];
match parse_key_mode(remainder) {
Some(mode) => Some(mode),
None if remainder.starts_with(|c: char| c.is_ascii_alphabetic()) => {
return None;
}
None => {
junk_ignored = true;
None
}
}
} else {
None
};
Some((
KeyTonic {
step: first.to_ascii_uppercase(),
accidental,
},
mode,
junk_ignored,
))
}
fn parse_key_mode(value: &str) -> Option<KeyMode> {
let lower = value.to_ascii_lowercase();
if lower == "m" {
return Some(KeyMode::Minor);
}
let prefix = &lower[..lower.len().min(3)];
match prefix {
"maj" => Some(KeyMode::Major),
"ion" => Some(KeyMode::Ionian),
"min" => Some(KeyMode::Minor),
"aeo" => Some(KeyMode::Aeolian),
"mix" => Some(KeyMode::Mixolydian),
"dor" => Some(KeyMode::Dorian),
"phr" => Some(KeyMode::Phrygian),
"lyd" => Some(KeyMode::Lydian),
"loc" => Some(KeyMode::Locrian),
"exp" => Some(KeyMode::Explicit),
_ => None,
}
}
fn parse_key_accidentals(token: &Spanned<String>) -> Vec<KeyAccidental> {
let value = token.value.as_str();
let mut accidentals = Vec::new();
let mut pos = 0;
while pos < value.len() {
let rest = &value[pos..];
let (sign, sign_len) = if rest.starts_with("__") {
(AccidentalSign::DoubleFlat, 2)
} else if rest.starts_with("^^") {
(AccidentalSign::DoubleSharp, 2)
} else if rest.starts_with('_') {
(AccidentalSign::Flat, 1)
} else if rest.starts_with('^') {
(AccidentalSign::Sharp, 1)
} else if rest.starts_with('=') {
(AccidentalSign::Natural, 1)
} else {
break;
};
let Some(note) = rest[sign_len..].chars().next() else {
break;
};
if !matches!(note.to_ascii_uppercase(), 'A'..='G') {
break;
}
let sign_start = token.span.start + pos;
let note_start = sign_start + sign_len;
let note_span = Span::new(note_start, note_start + note.len_utf8());
accidentals.push(KeyAccidental {
sign,
note: Spanned::new(note, note_span),
span: Span::new(sign_start, note_span.end),
});
pos += sign_len + note.len_utf8();
}
accidentals
}