#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Token {
Alpha,
Range(u32, u32),
Code(u32),
Literal(char),
}
impl Token {
fn parse(token: &str) -> Option<Token> {
if token == "@" {
return Some(Token::Alpha);
}
if let Some((lo, hi)) = token.split_once('-')
&& let (Ok(lo), Ok(hi)) = (lo.parse::<u32>(), hi.parse::<u32>())
{
return Some(Token::Range(lo, hi));
}
if let Ok(n) = token.parse::<u32>() {
return Some(Token::Code(n));
}
let mut chars = token.chars();
if let (Some(only), None) = (chars.next(), chars.next()) {
return Some(Token::Literal(only));
}
None
}
#[inline]
fn matches(self, c: char) -> bool {
match self {
Token::Alpha => c.is_alphabetic(),
Token::Range(lo, hi) => (lo..=hi).contains(&(c as u32)),
Token::Code(n) => c as u32 == n,
Token::Literal(only) => c == only,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct KeywordSpec {
tokens: Vec<Token>,
}
impl KeywordSpec {
pub fn parse(spec: &str) -> Self {
let tokens = spec
.split(',')
.filter_map(|raw| {
let token = raw.trim();
if token.is_empty() {
None
} else {
Token::parse(token)
}
})
.collect();
Self { tokens }
}
#[inline]
pub fn matches(&self, c: char) -> bool {
self.tokens.iter().any(|token| token.matches(c))
}
}
pub fn is_keyword_char(c: char, spec: &str) -> bool {
spec.split(',').any(|raw| {
let token = raw.trim();
!token.is_empty() && Token::parse(token).is_some_and(|t| t.matches(c))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iskeyword_alphabetic_via_at() {
assert!(is_keyword_char('a', "@"));
assert!(is_keyword_char('Z', "@"));
assert!(!is_keyword_char('1', "@"));
}
#[test]
fn iskeyword_numeric_range() {
assert!(is_keyword_char('0', "48-57"));
assert!(is_keyword_char('9', "48-57"));
assert!(!is_keyword_char('a', "48-57"));
}
#[test]
fn iskeyword_literal_punctuation() {
assert!(is_keyword_char('_', "_"));
assert!(!is_keyword_char('.', "_"));
}
#[test]
fn iskeyword_default_spec() {
let spec = "@,48-57,_,192-255";
assert!(is_keyword_char('a', spec));
assert!(is_keyword_char('5', spec));
assert!(is_keyword_char('_', spec));
assert!(!is_keyword_char(' ', spec));
assert!(!is_keyword_char('.', spec));
}
}