1use crate::error::{Error, Result};
5use unicode_normalization::UnicodeNormalization;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Prim {
12 Boundary,
14 NonWordBoundary,
16 LineStart,
18 LineEnd,
20 Space,
22 None_,
24 AnyCharacter,
26 Word,
28 NotWord,
30 Alpha,
32 NotAlpha,
34 Digit,
36 NotDigit,
38}
39
40impl Prim {
41 pub fn from_name(name: &str) -> Option<Prim> {
42 Some(match name {
43 "boundary" => Prim::Boundary,
44 "non_boundary" | "non_word_boundary" => Prim::NonWordBoundary,
45 "line_start" => Prim::LineStart,
46 "line_end" => Prim::LineEnd,
47 "space" => Prim::Space,
48 "none" => Prim::None_,
49 "any_character" => Prim::AnyCharacter,
50 "word" => Prim::Word,
51 "not_word" => Prim::NotWord,
52 "alpha" => Prim::Alpha,
53 "not_alpha" => Prim::NotAlpha,
54 "digit" => Prim::Digit,
55 "not_digit" => Prim::NotDigit,
56 _ => return None,
57 })
58 }
59
60 pub fn name(&self) -> &'static str {
61 match self {
62 Prim::Boundary => "boundary",
63 Prim::NonWordBoundary => "non_word_boundary",
64 Prim::LineStart => "line_start",
65 Prim::LineEnd => "line_end",
66 Prim::Space => "space",
67 Prim::None_ => "none",
68 Prim::AnyCharacter => "any_character",
69 Prim::Word => "word",
70 Prim::NotWord => "not_word",
71 Prim::Alpha => "alpha",
72 Prim::NotAlpha => "not_alpha",
73 Prim::Digit => "digit",
74 Prim::NotDigit => "not_digit",
75 }
76 }
77
78 pub fn re_only(&self) -> bool {
81 !matches!(self, Prim::None_ | Prim::Space)
82 }
83
84 pub fn matches_char(&self, ch: char) -> Option<bool> {
87 Some(match self {
88 Prim::Space => ch == ' ',
89 Prim::None_ => false, Prim::AnyCharacter => ch != '\n',
91 Prim::Word => ch.is_ascii_alphanumeric() || ch == '_',
92 Prim::NotWord => !(ch.is_ascii_alphanumeric() || ch == '_'),
93 Prim::Alpha => ch.is_ascii_alphabetic(),
94 Prim::NotAlpha => !ch.is_ascii_alphabetic(),
95 Prim::Digit => ch.is_ascii_digit(),
96 Prim::NotDigit => !ch.is_ascii_digit(),
97 Prim::Boundary | Prim::NonWordBoundary | Prim::LineStart | Prim::LineEnd => return None,
98 })
99 }
100
101 pub fn matches_zero_width(&self, chars: &[char], pos: usize) -> Option<bool> {
104 Some(match self {
105 Prim::Boundary => is_word_boundary(chars, pos),
106 Prim::NonWordBoundary => !is_word_boundary(chars, pos),
107 Prim::LineStart => pos == 0 || chars[pos - 1] == '\n',
108 Prim::LineEnd => pos == chars.len() || chars[pos] == '\n',
109 _ => return None,
110 })
111 }
112}
113
114fn is_word_boundary(chars: &[char], pos: usize) -> bool {
118 let before = pos > 0 && is_ruby_word_char(chars[pos - 1]);
119 let after = pos < chars.len() && is_ruby_word_char(chars[pos]);
120 before != after
121}
122
123pub fn is_ruby_word_char(ch: char) -> bool {
131 if matches!(ch, '\u{200C}' | '\u{200D}') {
132 return true;
133 }
134 use unicode_general_category::{get_general_category, GeneralCategory as Gc};
135 matches!(
136 get_general_category(ch),
137 Gc::UppercaseLetter
138 | Gc::LowercaseLetter
139 | Gc::TitlecaseLetter
140 | Gc::ModifierLetter
141 | Gc::OtherLetter
142 | Gc::LetterNumber
143 | Gc::DecimalNumber
144 | Gc::NonspacingMark
145 | Gc::SpacingMark
146 | Gc::EnclosingMark
147 | Gc::ConnectorPunctuation
148 )
149}
150
151#[derive(Debug, Clone)]
155pub struct Funcall {
156 pub name: FuncallName,
157 pub kwargs: Vec<(String, String)>,
158 pub reverse_run: Option<bool>,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum FuncallName {
164 Upcase,
165 Downcase,
166 TitleCase,
167 Separate,
168 Unseparate,
169 Compose,
170 Decompose,
171}
172
173impl FuncallName {
174 pub fn from_name(name: &str) -> Option<FuncallName> {
175 Some(match name {
176 "upcase" => FuncallName::Upcase,
177 "downcase" => FuncallName::Downcase,
178 "title_case" => FuncallName::TitleCase,
179 "separate" => FuncallName::Separate,
180 "unseparate" => FuncallName::Unseparate,
181 "compose" => FuncallName::Compose,
182 "decompose" => FuncallName::Decompose,
183 _ => return None,
184 })
185 }
186
187 pub fn reverse_name(&self) -> Option<FuncallName> {
191 Some(match self {
192 FuncallName::TitleCase => FuncallName::Downcase, FuncallName::Downcase => FuncallName::TitleCase, FuncallName::Compose => FuncallName::Decompose,
195 FuncallName::Decompose => FuncallName::Compose,
196 FuncallName::Separate => FuncallName::Unseparate,
197 FuncallName::Unseparate => FuncallName::Separate,
198 FuncallName::Upcase => return None,
199 })
200 }
201
202 pub fn apply(&self, s: String, kwargs: &[(String, String)]) -> Result<String> {
203 let kw = |name: &str| kwargs.iter().find(|(k, _)| k == name).map(|(_, v)| v.clone());
204 match self {
205 FuncallName::Upcase => Ok(s.to_uppercase()),
206 FuncallName::Downcase => match kw("word_separator") {
207 Some(sep) => Ok(downcase_per_word(s, &sep)),
208 None => Ok(s.to_lowercase()),
209 },
210 FuncallName::TitleCase => {
211 let sep = kw("word_separator").unwrap_or_else(|| " ".to_string());
212 Ok(title_case(s, &sep))
213 }
214 FuncallName::Separate => {
215 let sep = kw("separator").unwrap_or_else(|| " ".to_string());
216 let mut out = String::with_capacity(s.len() * 2);
217 for (i, c) in s.chars().enumerate() {
218 if i > 0 {
219 out.push_str(&sep);
220 }
221 out.push(c);
222 }
223 Ok(out)
224 }
225 FuncallName::Unseparate => {
226 let sep = kw("separator").unwrap_or_else(|| " ".to_string());
227 Ok(s.split(&sep).collect::<String>())
228 }
229 FuncallName::Compose => Ok(s.nfc().collect()),
231 FuncallName::Decompose => Ok(s.nfd().collect()),
232 }
233 }
234}
235
236pub fn title_case(s: String, word_separator: &str) -> String {
241 let out = upcase_first(s);
242 if word_separator.is_empty() {
243 return out;
244 }
245 let sep: Vec<char> = word_separator.chars().collect();
246 let chars: Vec<char> = out.chars().collect();
247 let mut result = String::new();
248 let mut i = 0usize;
249 while i < chars.len() {
250 if i + sep.len() < chars.len() && chars[i..i + sep.len()] == sep[..] {
251 let mut m: String = sep.iter().collect();
252 m.extend(chars[i + sep.len()].to_uppercase());
253 result.push_str(&m);
254 i += sep.len() + 1;
255 } else {
256 result.push(chars[i]);
257 i += 1;
258 }
259 }
260 result
261}
262
263pub fn downcase_per_word(s: String, word_separator: &str) -> String {
266 let out = downcase_first(s);
267 if word_separator.is_empty() {
268 return out;
269 }
270 let sep: Vec<char> = word_separator.chars().collect();
271 let chars: Vec<char> = out.chars().collect();
272 let mut result = String::new();
273 let mut i = 0usize;
274 while i < chars.len() {
275 if i + sep.len() < chars.len() && chars[i..i + sep.len()] == sep[..] {
276 let mut m: String = sep.iter().collect();
277 m.extend(chars[i + sep.len()].to_lowercase());
278 result.push_str(&m);
279 i += sep.len() + 1;
280 } else {
281 result.push(chars[i]);
282 i += 1;
283 }
284 }
285 result
286}
287
288fn upcase_first(s: String) -> String {
289 let mut chars = s.chars();
290 match chars.next() {
291 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
292 None => s,
293 }
294}
295
296fn downcase_first(s: String) -> String {
297 let mut chars = s.chars();
298 match chars.next() {
299 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
300 None => s,
301 }
302}
303
304pub fn unsupported_funcall(name: &str) -> Error {
306 Error::Unsupported(format!(
307 "funcall {name:?} (rababa/secryst need external ML engines; not in slice 1)"
308 ))
309}