Rust workspace with nomina-core (rename engine) and nomina-app (Tauri v2 shell). React/TypeScript frontend with tabbed rule panels, virtual-scrolled file list, and Zustand state management. All 9 rule types implemented with 25 passing tests.
101 lines
2.5 KiB
Rust
101 lines
2.5 KiB
Rust
use regex::Regex;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{NominaError, RenameContext, RenameRule};
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RegexRule {
|
|
pub pattern: String,
|
|
pub replace_with: String,
|
|
pub case_insensitive: bool,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl RegexRule {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
pattern: String::new(),
|
|
replace_with: String::new(),
|
|
case_insensitive: false,
|
|
enabled: true,
|
|
}
|
|
}
|
|
|
|
fn build_regex(&self) -> std::result::Result<Regex, NominaError> {
|
|
let pat = if self.case_insensitive {
|
|
format!("(?i){}", self.pattern)
|
|
} else {
|
|
self.pattern.clone()
|
|
};
|
|
Regex::new(&pat).map_err(|e| NominaError::InvalidRegex {
|
|
pattern: self.pattern.clone(),
|
|
reason: e.to_string(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl RenameRule for RegexRule {
|
|
fn apply(&self, filename: &str, _context: &RenameContext) -> String {
|
|
if self.pattern.is_empty() {
|
|
return filename.to_string();
|
|
}
|
|
match self.build_regex() {
|
|
Ok(re) => re.replace_all(filename, self.replace_with.as_str()).into_owned(),
|
|
Err(_) => filename.to_string(),
|
|
}
|
|
}
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Regex"
|
|
}
|
|
|
|
fn rule_type(&self) -> &str {
|
|
"regex"
|
|
}
|
|
|
|
fn is_enabled(&self) -> bool {
|
|
self.enabled
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn basic_regex() {
|
|
let rule = RegexRule {
|
|
pattern: r"(\d+)".into(),
|
|
replace_with: "NUM".into(),
|
|
case_insensitive: false,
|
|
enabled: true,
|
|
};
|
|
let ctx = RenameContext::dummy(0);
|
|
assert_eq!(rule.apply("file123", &ctx), "fileNUM");
|
|
}
|
|
|
|
#[test]
|
|
fn capture_groups() {
|
|
let rule = RegexRule {
|
|
pattern: r"([a-z]+)-([a-z]+)".into(),
|
|
replace_with: "${2}_${1}".into(),
|
|
case_insensitive: false,
|
|
enabled: true,
|
|
};
|
|
let ctx = RenameContext::dummy(0);
|
|
assert_eq!(rule.apply("hello-world", &ctx), "world_hello");
|
|
}
|
|
|
|
#[test]
|
|
fn empty_pattern() {
|
|
let rule = RegexRule {
|
|
pattern: String::new(),
|
|
replace_with: "x".into(),
|
|
case_insensitive: false,
|
|
enabled: true,
|
|
};
|
|
let ctx = RenameContext::dummy(0);
|
|
assert_eq!(rule.apply("test", &ctx), "test");
|
|
}
|
|
}
|