use crate::TokenType;
use std::sync::Arc;
#[allow(dead_code)]
pub struct ModSpec {
pub run: &'static [char], pub allow_charset: bool, }
pub const QR_SPEC: ModSpec = ModSpec { run: &['i', 'm', 's', 'x', 'p', 'n'], allow_charset: true };
pub const M_SPEC: ModSpec =
ModSpec { run: &['i', 'm', 's', 'x', 'p', 'n', 'g', 'c'], allow_charset: true };
pub const S_SPEC: ModSpec =
ModSpec { run: &['i', 'm', 's', 'x', 'p', 'n', 'e', 'r'], allow_charset: true };
pub const TR_SPEC: ModSpec = ModSpec { run: &['c', 'd', 's', 'r'], allow_charset: false };
pub fn paired_close(open: char) -> Option<char> {
match open {
'(' => Some(')'),
'[' => Some(']'),
'{' => Some('}'),
'<' => Some('>'),
_ => None,
}
}
#[allow(dead_code)]
pub fn canon_run(run: &str, spec: &ModSpec) -> String {
let mut out = String::new();
for &c in spec.run {
if run.contains(c) {
out.push(c);
}
}
out
}
#[allow(dead_code)]
pub fn split_tail_for_spec(tail: &str, spec: &ModSpec) -> Option<(String, Option<&'static str>)> {
if !tail.chars().all(|c| c.is_ascii_alphabetic()) {
return None;
}
if !spec.allow_charset {
return if tail.chars().all(|c| spec.run.contains(&c)) {
Some((canon_run(tail, spec), None))
} else {
None
};
}
let (run_part, charset): (&str, Option<&'static str>) =
if let Some(stripped) = tail.strip_suffix("aa") {
(stripped, Some("aa"))
} else if let Some(stripped) = tail.strip_suffix('a') {
(stripped, Some("a"))
} else if let Some(stripped) = tail.strip_suffix('d') {
(stripped, Some("d"))
} else if let Some(stripped) = tail.strip_suffix('l') {
(stripped, Some("l"))
} else if let Some(stripped) = tail.strip_suffix('u') {
(stripped, Some("u"))
} else {
(tail, None)
};
if !run_part.chars().all(|c| spec.run.contains(&c)) {
return None;
}
let run = canon_run(run_part, spec);
Some((run, charset))
}
#[derive(Debug, Clone)]
pub struct QuoteOperatorInfo {
pub operator: String, pub delimiter: char, pub start_pos: usize, }
#[derive(Debug)]
#[allow(dead_code)] pub struct QuoteResult {
pub token_type: TokenType,
pub text: Arc<str>,
pub start: usize,
pub end: usize,
}
pub fn is_quote_operator(word: &str) -> bool {
matches!(word, "q" | "qq" | "qw" | "qr" | "qx" | "m" | "s" | "tr" | "y")
}
pub fn get_quote_token_type(operator: &str) -> TokenType {
match operator {
"q" => TokenType::QuoteSingle,
"qq" => TokenType::QuoteDouble,
"qw" => TokenType::QuoteWords,
"qr" => TokenType::QuoteRegex,
"qx" => TokenType::QuoteCommand,
"m" => TokenType::RegexMatch,
"s" => TokenType::Substitution,
"tr" | "y" => TokenType::Transliteration,
_ => TokenType::Error(Arc::from(format!("Unknown quote operator: {}", operator))),
}
}
#[allow(dead_code)]
pub fn get_mod_spec(operator: &str) -> Option<&'static ModSpec> {
match operator {
"qr" => Some(&QR_SPEC),
"m" => Some(&M_SPEC),
"s" => Some(&S_SPEC),
"tr" | "y" => Some(&TR_SPEC),
_ => None, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paired_close_handles_balanced_and_unbalanced_delimiters() {
assert_eq!(paired_close('('), Some(')'));
assert_eq!(paired_close('['), Some(']'));
assert_eq!(paired_close('{'), Some('}'));
assert_eq!(paired_close('<'), Some('>'));
assert_eq!(paired_close('/'), None);
}
#[test]
fn split_tail_for_spec_rejects_invalid_input() {
assert_eq!(split_tail_for_spec("im1", &QR_SPEC), None);
assert_eq!(split_tail_for_spec("z", &QR_SPEC), None);
assert_eq!(split_tail_for_spec("ca", &TR_SPEC), None);
}
#[test]
fn split_tail_for_spec_supports_charset_suffix_and_canonical_run_order() {
assert_eq!(split_tail_for_spec("mixa", &QR_SPEC), Some(("imx".to_string(), Some("a"))));
assert_eq!(split_tail_for_spec("ximaa", &QR_SPEC), Some(("imx".to_string(), Some("aa"))));
assert_eq!(split_tail_for_spec("d", &QR_SPEC), Some(("".to_string(), Some("d"))));
}
#[test]
fn split_tail_for_spec_without_charset_only_accepts_run_flags() {
assert_eq!(split_tail_for_spec("rsc", &TR_SPEC), Some(("csr".to_string(), None)));
assert_eq!(split_tail_for_spec("rsu", &TR_SPEC), None);
}
#[test]
fn quote_operator_helpers_cover_known_and_unknown_operators() {
assert!(is_quote_operator("qq"));
assert!(is_quote_operator("tr"));
assert!(!is_quote_operator("foo"));
assert_eq!(get_quote_token_type("q"), TokenType::QuoteSingle);
assert_eq!(get_quote_token_type("y"), TokenType::Transliteration);
let unknown = get_quote_token_type("unknown");
assert!(matches!(unknown, TokenType::Error(_)));
if let TokenType::Error(message) = unknown {
assert!(message.contains("unknown"));
}
}
}