use crate::error::{Error, Result};
use unicode_normalization::UnicodeNormalization;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Prim {
Boundary,
NonWordBoundary,
LineStart,
LineEnd,
Space,
None_,
AnyCharacter,
Word,
NotWord,
Alpha,
NotAlpha,
Digit,
NotDigit,
}
impl Prim {
pub fn from_name(name: &str) -> Option<Prim> {
Some(match name {
"boundary" => Prim::Boundary,
"non_boundary" | "non_word_boundary" => Prim::NonWordBoundary,
"line_start" => Prim::LineStart,
"line_end" => Prim::LineEnd,
"space" => Prim::Space,
"none" => Prim::None_,
"any_character" => Prim::AnyCharacter,
"word" => Prim::Word,
"not_word" => Prim::NotWord,
"alpha" => Prim::Alpha,
"not_alpha" => Prim::NotAlpha,
"digit" => Prim::Digit,
"not_digit" => Prim::NotDigit,
_ => return None,
})
}
pub fn name(&self) -> &'static str {
match self {
Prim::Boundary => "boundary",
Prim::NonWordBoundary => "non_word_boundary",
Prim::LineStart => "line_start",
Prim::LineEnd => "line_end",
Prim::Space => "space",
Prim::None_ => "none",
Prim::AnyCharacter => "any_character",
Prim::Word => "word",
Prim::NotWord => "not_word",
Prim::Alpha => "alpha",
Prim::NotAlpha => "not_alpha",
Prim::Digit => "digit",
Prim::NotDigit => "not_digit",
}
}
pub fn re_only(&self) -> bool {
!matches!(self, Prim::None_ | Prim::Space)
}
pub fn matches_char(&self, ch: char) -> Option<bool> {
Some(match self {
Prim::Space => ch == ' ',
Prim::None_ => false, Prim::AnyCharacter => ch != '\n',
Prim::Word => ch.is_ascii_alphanumeric() || ch == '_',
Prim::NotWord => !(ch.is_ascii_alphanumeric() || ch == '_'),
Prim::Alpha => ch.is_ascii_alphabetic(),
Prim::NotAlpha => !ch.is_ascii_alphabetic(),
Prim::Digit => ch.is_ascii_digit(),
Prim::NotDigit => !ch.is_ascii_digit(),
Prim::Boundary | Prim::NonWordBoundary | Prim::LineStart | Prim::LineEnd => return None,
})
}
pub fn matches_zero_width(&self, chars: &[char], pos: usize) -> Option<bool> {
Some(match self {
Prim::Boundary => is_word_boundary(chars, pos),
Prim::NonWordBoundary => !is_word_boundary(chars, pos),
Prim::LineStart => pos == 0 || chars[pos - 1] == '\n',
Prim::LineEnd => pos == chars.len() || chars[pos] == '\n',
_ => return None,
})
}
}
fn is_word_boundary(chars: &[char], pos: usize) -> bool {
let before = pos > 0 && is_ruby_word_char(chars[pos - 1]);
let after = pos < chars.len() && is_ruby_word_char(chars[pos]);
before != after
}
pub fn is_ruby_word_char(ch: char) -> bool {
if matches!(ch, '\u{200C}' | '\u{200D}') {
return true;
}
use unicode_general_category::{get_general_category, GeneralCategory as Gc};
matches!(
get_general_category(ch),
Gc::UppercaseLetter
| Gc::LowercaseLetter
| Gc::TitlecaseLetter
| Gc::ModifierLetter
| Gc::OtherLetter
| Gc::LetterNumber
| Gc::DecimalNumber
| Gc::NonspacingMark
| Gc::SpacingMark
| Gc::EnclosingMark
| Gc::ConnectorPunctuation
)
}
#[derive(Debug, Clone)]
pub struct Funcall {
pub name: FuncallName,
pub kwargs: Vec<(String, String)>,
pub reverse_run: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FuncallName {
Upcase,
Downcase,
TitleCase,
Separate,
Unseparate,
Compose,
Decompose,
}
impl FuncallName {
pub fn from_name(name: &str) -> Option<FuncallName> {
Some(match name {
"upcase" => FuncallName::Upcase,
"downcase" => FuncallName::Downcase,
"title_case" => FuncallName::TitleCase,
"separate" => FuncallName::Separate,
"unseparate" => FuncallName::Unseparate,
"compose" => FuncallName::Compose,
"decompose" => FuncallName::Decompose,
_ => return None,
})
}
pub fn reverse_name(&self) -> Option<FuncallName> {
Some(match self {
FuncallName::TitleCase => FuncallName::Downcase, FuncallName::Downcase => FuncallName::TitleCase, FuncallName::Compose => FuncallName::Decompose,
FuncallName::Decompose => FuncallName::Compose,
FuncallName::Separate => FuncallName::Unseparate,
FuncallName::Unseparate => FuncallName::Separate,
FuncallName::Upcase => return None,
})
}
pub fn apply(&self, s: String, kwargs: &[(String, String)]) -> Result<String> {
let kw = |name: &str| kwargs.iter().find(|(k, _)| k == name).map(|(_, v)| v.clone());
match self {
FuncallName::Upcase => Ok(s.to_uppercase()),
FuncallName::Downcase => match kw("word_separator") {
Some(sep) => Ok(downcase_per_word(s, &sep)),
None => Ok(s.to_lowercase()),
},
FuncallName::TitleCase => {
let sep = kw("word_separator").unwrap_or_else(|| " ".to_string());
Ok(title_case(s, &sep))
}
FuncallName::Separate => {
let sep = kw("separator").unwrap_or_else(|| " ".to_string());
let mut out = String::with_capacity(s.len() * 2);
for (i, c) in s.chars().enumerate() {
if i > 0 {
out.push_str(&sep);
}
out.push(c);
}
Ok(out)
}
FuncallName::Unseparate => {
let sep = kw("separator").unwrap_or_else(|| " ".to_string());
Ok(s.split(&sep).collect::<String>())
}
FuncallName::Compose => Ok(s.nfc().collect()),
FuncallName::Decompose => Ok(s.nfd().collect()),
}
}
}
pub fn title_case(s: String, word_separator: &str) -> String {
let out = upcase_first(s);
if word_separator.is_empty() {
return out;
}
let sep: Vec<char> = word_separator.chars().collect();
let chars: Vec<char> = out.chars().collect();
let mut result = String::new();
let mut i = 0usize;
while i < chars.len() {
if i + sep.len() < chars.len() && chars[i..i + sep.len()] == sep[..] {
let mut m: String = sep.iter().collect();
m.extend(chars[i + sep.len()].to_uppercase());
result.push_str(&m);
i += sep.len() + 1;
} else {
result.push(chars[i]);
i += 1;
}
}
result
}
pub fn downcase_per_word(s: String, word_separator: &str) -> String {
let out = downcase_first(s);
if word_separator.is_empty() {
return out;
}
let sep: Vec<char> = word_separator.chars().collect();
let chars: Vec<char> = out.chars().collect();
let mut result = String::new();
let mut i = 0usize;
while i < chars.len() {
if i + sep.len() < chars.len() && chars[i..i + sep.len()] == sep[..] {
let mut m: String = sep.iter().collect();
m.extend(chars[i + sep.len()].to_lowercase());
result.push_str(&m);
i += sep.len() + 1;
} else {
result.push(chars[i]);
i += 1;
}
}
result
}
fn upcase_first(s: String) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => s,
}
}
fn downcase_first(s: String) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
None => s,
}
}
pub fn unsupported_funcall(name: &str) -> Error {
Error::Unsupported(format!(
"funcall {name:?} (rababa/secryst need external ML engines; not in slice 1)"
))
}