use super::token::Token;
#[expect(
clippy::match_same_arms,
reason = "the arms are grouped by lexical family so each carries the grammar production and citation its reading comes from; collapsing equal bodies would erase which production a refusal belongs to"
)]
pub(super) fn reclassify(token: &Token, slice: &str, src: &str, start: usize) -> Option<Token> {
match token {
Token::Bom => Some(Token::Bom),
Token::SymMatches
| Token::SymAnd
| Token::SymOr
| Token::SymXor
| Token::SymNot
| Token::SymImplies
| Token::SymForAll
| Token::SymExists
| Token::SymThereExists
| Token::SymIn
| Token::SymThen
| Token::SymNotMatches
| Token::SymOccurrences
| Token::SymExistence
| Token::SymCardinality
| Token::SymOrdered
| Token::SymUnordered
| Token::SymUnique
| Token::SymUseNode
| Token::SymUseArchetype
| Token::SymAllowArchetype
| Token::SymInclude
| Token::SymExclude
| Token::SymAfter
| Token::SymBefore
| Token::SymClosed => demote_word(slice),
Token::SymIff | Token::SymBrokenBar => None,
Token::SymSelf
| Token::SymResult
| Token::SymCase
| Token::SymChoice
| Token::SymAssert => None,
Token::SymInfinity => Some(token.clone()),
Token::SymTrue | Token::SymFalse => Some(token.clone()),
Token::RootIdCode(_) | Token::IdCode(_) | Token::AtCode(_) | Token::AcCode(_) => {
demote_word(slice)
}
Token::ArchetypeId(_) | Token::VersionId(_) | Token::Guid(_) => None,
Token::DateTimeConstraintPattern(_)
| Token::DateConstraintPattern(_)
| Token::TimeConstraintPattern(_)
| Token::DurationConstraintPattern(_) => demote_word(slice),
Token::VariableWithPath(_) | Token::VariableId(_) => None,
Token::Iso8601DateTime(text) => Some(Token::Iso8601DateTime(text.replacen(' ', "T", 1))),
Token::Iso8601Date(_) | Token::Iso8601Time(_) | Token::Iso8601Duration(_) => {
Some(token.clone())
}
Token::ContainedRegexp(_) | Token::LCurly | Token::RCurly => None,
Token::TermCodeRef(_) => Some(token.clone()),
Token::EmbeddedUri(_) => Some(token.clone()),
Token::LocalTermCodeRef(_) => Some(token.clone()),
Token::PlugInBlock(_) => Some(token.clone()),
Token::AdlPath(text) => odin_path(text).then(|| token.clone()),
Token::Real(_) | Token::Character(_) | Token::Integer(_) => Some(token.clone()),
Token::String(text) => Some(Token::String(strip_line_leaders(
text,
leader_budget(src, start),
))),
Token::AlphaUcId(_) | Token::AlphaLcId(_) => Some(token.clone()),
Token::AlphaUnderscoreId(_) => None,
Token::LParen
| Token::RParen
| Token::LBracket
| Token::RBracket
| Token::SymComma
| Token::SymSemiColon
| Token::SymEq
| Token::SymLe
| Token::SymGe
| Token::SymGt
| Token::SymLt
| Token::SymIvlSep
| Token::SymDot
| Token::SymIvlDelim
| Token::SymPlus
| Token::SymMinus
| Token::SymSlash => Some(token.clone()),
Token::SymAssignment | Token::SymColon | Token::SymPercent | Token::SymCarat => None,
Token::SymAt => Some(token.clone()),
Token::SymNe => None,
Token::SymListContinue => Some(token.clone()),
Token::SymPlusOrMinus => Some(token.clone()),
Token::SymStar => (slice == "*").then(|| token.clone()),
}
}
fn demote_word(slice: &str) -> Option<Token> {
let mut chars = slice.chars();
let head = chars.next()?;
if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
return None;
}
match head {
'a'..='z' => Some(Token::AlphaLcId(slice.to_owned())),
'A'..='Z' => Some(Token::AlphaUcId(slice.to_owned())),
_ => None,
}
}
fn odin_path(text: &str) -> bool {
let bytes = text.as_bytes();
let mut at = 0usize;
let mut leading_slashes = 0usize;
while bytes.get(at) == Some(&b'/') {
leading_slashes += 1;
at += 1;
}
if leading_slashes > 1 {
return false;
}
let mut segments = 0usize;
loop {
match bytes.get(at) {
Some(head) if head.is_ascii_lowercase() => at += 1,
_ => return false,
}
while matches!(bytes.get(at), Some(c) if c.is_ascii_alphanumeric() || *c == b'_') {
at += 1;
}
if bytes.get(at) == Some(&b'[') {
at += 1;
loop {
match bytes.get(at) {
Some(b']') => break,
Some(b'\r' | b'\n') | None => return false,
Some(_) => at += 1,
}
}
at += 1;
}
segments += 1;
if bytes.get(at) == Some(&b'/') {
at += 1;
} else {
break;
}
}
at == bytes.len()
&& ((leading_slashes == 1 && segments >= 1) || (leading_slashes == 0 && segments >= 2))
}
fn leader_budget(src: &str, start: usize) -> usize {
let Some(before) = src.get(..start) else {
return 0;
};
let line_start = before.rfind('\n').map_or(0, |index| index + 1);
before
.get(line_start..)
.map_or(0, |indent| indent.chars().count() + 1)
}
fn strip_line_leaders(raw: &str, budget: usize) -> String {
if !raw.contains('\n') {
return raw.to_owned();
}
let mut out = String::with_capacity(raw.len());
for (index, line) in raw.split_inclusive('\n').enumerate() {
if index == 0 {
out.push_str(line);
continue;
}
let kept = line
.char_indices()
.take(budget)
.take_while(|(_, c)| *c == ' ' || *c == '\t')
.last()
.map_or(0, |(i, c)| i + c.len_utf8());
out.push_str(line.get(kept..).unwrap_or(line));
}
out
}