initial project scaffold
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.
This commit is contained in:
100
crates/nomina-core/src/rules/regex.rs
Normal file
100
crates/nomina-core/src/rules/regex.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user