use crate::diagnostic::Span;
use crate::parse::field::Spanned;
use crate::parse::music::{classify_quoted_text, is_escaped};
use crate::syntax::{
LyricLineSyntax, LyricTokenKind, LyricTokenSyntax, QuotedTextKind, SymbolLineSyntax,
SymbolTokenKind, SymbolTokenSyntax,
};
pub(super) fn parse_lyric_line(
line_index: usize,
span: Span,
value: Spanned<String>,
) -> LyricLineSyntax {
let tokens = parse_lyric_tokens(&value.value, value.span.start);
LyricLineSyntax {
line_index,
span,
value,
tokens,
}
}
fn parse_lyric_tokens(value: &str, offset: usize) -> Vec<LyricTokenSyntax> {
let mut tokens = Vec::new();
let mut index = 0;
let mut syllable_start = None;
let mut syllable_text = String::new();
while index < value.len() {
let Some(ch) = value[index..].chars().next() else {
break;
};
if is_lyric_separator(ch) {
flush_lyric_syllable(
&mut tokens,
&mut syllable_start,
&mut syllable_text,
offset,
index,
);
index += ch.len_utf8();
continue;
}
match ch {
'\\' => {
let escape_start = index;
if syllable_start.is_none() && value.len() > index + 1 {
syllable_start = Some(escape_start);
}
if let Some((decoded, consumed)) = crate::text::decode_escape_at(&value[index..]) {
syllable_text.push(decoded);
index += consumed;
} else {
index += ch.len_utf8();
if let Some(next) = value[index..].chars().next() {
syllable_text.push(next);
index += next.len_utf8();
}
}
}
'&' if crate::text::decode_entity_at(&value[index..]).is_some() => {
if let Some((decoded, consumed)) = crate::text::decode_entity_at(&value[index..]) {
if syllable_start.is_none() {
syllable_start = Some(index);
}
syllable_text.push(decoded);
index += consumed;
}
}
'-' => {
if syllable_start.is_some() {
flush_lyric_syllable(
&mut tokens,
&mut syllable_start,
&mut syllable_text,
offset,
index,
);
tokens.push(LyricTokenSyntax {
span: Span::new(offset + index, offset + index + 1),
text: "-".to_owned(),
kind: LyricTokenKind::Hyphen,
});
} else {
tokens.push(LyricTokenSyntax {
span: Span::new(offset + index, offset + index + 1),
text: String::new(),
kind: LyricTokenKind::Skip,
});
}
index += ch.len_utf8();
}
'_' => {
flush_lyric_syllable(
&mut tokens,
&mut syllable_start,
&mut syllable_text,
offset,
index,
);
let start = index;
while value[index..].starts_with('_') {
index += 1;
tokens.push(LyricTokenSyntax {
span: Span::new(offset + start, offset + index),
text: "_".to_owned(),
kind: LyricTokenKind::Extender,
});
}
}
'*' => {
flush_lyric_syllable(
&mut tokens,
&mut syllable_start,
&mut syllable_text,
offset,
index,
);
tokens.push(LyricTokenSyntax {
span: Span::new(offset + index, offset + index + 1),
text: String::new(),
kind: LyricTokenKind::Skip,
});
index += ch.len_utf8();
}
'~' => {
if syllable_start.is_none() {
syllable_start = Some(index);
}
syllable_text.push(' ');
index += ch.len_utf8();
}
'|' => {
flush_lyric_syllable(
&mut tokens,
&mut syllable_start,
&mut syllable_text,
offset,
index,
);
tokens.push(LyricTokenSyntax {
span: Span::new(offset + index, offset + index + 1),
text: "|".to_owned(),
kind: LyricTokenKind::Bar,
});
index += ch.len_utf8();
}
_ => {
if syllable_start.is_none() {
syllable_start = Some(index);
}
syllable_text.push(ch);
index += ch.len_utf8();
}
}
}
flush_lyric_syllable(
&mut tokens,
&mut syllable_start,
&mut syllable_text,
offset,
value.len(),
);
tokens
}
fn is_lyric_separator(ch: char) -> bool {
matches!(ch, ' ' | '\t' | '\n' | '\r')
}
fn flush_lyric_syllable(
tokens: &mut Vec<LyricTokenSyntax>,
syllable_start: &mut Option<usize>,
syllable_text: &mut String,
offset: usize,
end: usize,
) {
let Some(start) = syllable_start.take() else {
return;
};
tokens.push(LyricTokenSyntax {
span: Span::new(offset + start, offset + end),
text: std::mem::take(syllable_text),
kind: LyricTokenKind::Syllable,
});
}
pub(super) fn parse_symbol_line(
line_index: usize,
span: Span,
value: Spanned<String>,
) -> SymbolLineSyntax {
let tokens = parse_symbol_tokens(&value.value, value.span.start);
SymbolLineSyntax {
line_index,
span,
value,
tokens,
}
}
fn parse_symbol_tokens(value: &str, offset: usize) -> Vec<SymbolTokenSyntax> {
let mut tokens = Vec::new();
let mut index = 0;
while index < value.len() {
let Some(ch) = value[index..].chars().next() else {
break;
};
if ch.is_whitespace() {
index += ch.len_utf8();
continue;
}
match ch {
'*' => {
tokens.push(SymbolTokenSyntax {
span: Span::new(offset + index, offset + index + 1),
text: String::new(),
kind: SymbolTokenKind::Skip,
});
index += ch.len_utf8();
}
'|' => {
tokens.push(SymbolTokenSyntax {
span: Span::new(offset + index, offset + index + 1),
text: "|".to_owned(),
kind: SymbolTokenKind::Bar,
});
index += ch.len_utf8();
}
'"' => {
let start = index;
index += 1;
while index < value.len() {
let Some(ch) = value[index..].chars().next() else {
break;
};
index += ch.len_utf8();
if ch == '"' && !is_escaped(value, index - ch.len_utf8()) {
break;
}
}
let raw = &value[start..index];
let text = raw
.strip_prefix('"')
.and_then(|text| text.strip_suffix('"'))
.unwrap_or(raw)
.to_owned();
let kind = match classify_quoted_text(&text) {
QuotedTextKind::ChordSymbol => SymbolTokenKind::ChordSymbol,
QuotedTextKind::Annotation(_) => SymbolTokenKind::Annotation,
};
tokens.push(SymbolTokenSyntax {
span: Span::new(offset + start, offset + index),
text,
kind,
});
}
'!' | '+' => {
let delimiter = ch;
let start = index;
index += delimiter.len_utf8();
while index < value.len() {
let Some(ch) = value[index..].chars().next() else {
break;
};
index += ch.len_utf8();
if ch == delimiter {
break;
}
}
let raw = &value[start..index];
let text = raw
.strip_prefix(delimiter)
.and_then(|text| text.strip_suffix(delimiter))
.unwrap_or(raw)
.to_owned();
tokens.push(SymbolTokenSyntax {
span: Span::new(offset + start, offset + index),
text,
kind: SymbolTokenKind::Decoration,
});
}
_ => {
let start = index;
while index < value.len() {
let Some(ch) = value[index..].chars().next() else {
break;
};
if ch.is_whitespace() || matches!(ch, '*' | '|') {
break;
}
index += ch.len_utf8();
}
tokens.push(SymbolTokenSyntax {
span: Span::new(offset + start, offset + index),
text: value[start..index].to_owned(),
kind: SymbolTokenKind::Raw,
});
}
}
}
tokens
}