use crate::date::detect_and_replace;
use crate::SlugOpts;
use chrono::{Datelike, Local};
use regex::Regex;
use slug_preserve::slugify_with_sentinel;
use std::sync::OnceLock;
const PIPELINE_SEP: char = '_';
fn re_camelcase() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
#[allow(clippy::expect_used)]
Regex::new(r"([a-z])([A-Z]+)").expect("static camelcase regex compiles")
})
}
fn re_existing_time() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
#[allow(clippy::expect_used)]
Regex::new(r"(-[0-9]{2})[ _]?[Aa]?[Tt][ _]?([0-9]{2}[\-._])")
.expect("static existing-time regex compiles")
})
}
fn re_multiple_underscore() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| {
#[allow(clippy::expect_used)]
Regex::new(r"_+").expect("static underscore-collapse regex compiles")
})
}
#[must_use]
pub fn slugify_camel_iso(input: &str, opts: &SlugOpts) -> String {
let current_year = Local::now().year();
slugify_camel_iso_with_year(input, opts, current_year)
}
#[must_use]
pub fn slugify_camel_iso_with_year(input: &str, opts: &SlugOpts, current_year: i32) -> String {
let nfkc: String = unicode_normalization::UnicodeNormalization::nfkc(input).collect();
let with_time = re_existing_time()
.replace_all(&nfkc, |c: ®ex::Captures<'_>| {
#[allow(clippy::expect_used)]
let g1 = c.get(1).expect("regex group 1").as_str();
#[allow(clippy::expect_used)]
let g2 = c.get(2).expect("regex group 2").as_str();
format!("{g1}_{g2}")
})
.into_owned();
let with_camel = if opts.split_camel {
re_camelcase()
.replace_all(&with_time, |c: ®ex::Captures<'_>| {
#[allow(clippy::expect_used)]
let g1 = c.get(1).expect("regex group 1").as_str();
#[allow(clippy::expect_used)]
let g2 = c.get(2).expect("regex group 2").as_str();
format!("{g1}_{g2}")
})
.into_owned()
} else {
with_time
};
let pipeline_opts = SlugOpts {
separator: PIPELINE_SEP,
case: slug_preserve::CaseMode::Preserve,
split_camel: opts.split_camel,
};
let slugged = slugify_with_sentinel(&with_camel, PIPELINE_SEP, &pipeline_opts);
let dated = detect_and_replace(&slugged, PIPELINE_SEP, current_year);
let cased = slug_preserve_apply_case(&dated, opts.case);
let collapsed = re_multiple_underscore()
.replace_all(&cased, "_")
.into_owned();
let final_str = if PIPELINE_SEP == opts.separator {
collapsed
} else {
collapsed.replace(PIPELINE_SEP, &opts.separator.to_string())
};
final_str.trim_matches(opts.separator).to_string()
}
fn slug_preserve_apply_case(input: &str, mode: slug_preserve::CaseMode) -> String {
use slug_preserve::CaseMode;
match mode {
CaseMode::Preserve => input.to_string(),
CaseMode::Lower => input.to_lowercase(),
CaseMode::Upper => input.to_uppercase(),
CaseMode::Title | CaseMode::Capitalize => title_case_after_alnum_boundary(input),
}
}
fn title_case_after_alnum_boundary(input: &str) -> String {
let lowered = input.to_lowercase();
let bytes = lowered.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
for (i, &b) in bytes.iter().enumerate() {
let mut ch = b;
if ch.is_ascii_lowercase() {
let after_underscore = i > 0 && bytes[i - 1] == b'_';
let at_start = i == 0;
let between_digits = ch == b't'
&& i > 0
&& i + 1 < bytes.len()
&& bytes[i - 1].is_ascii_digit()
&& bytes[i + 1].is_ascii_digit();
if at_start || after_underscore || between_digits {
ch = ch.to_ascii_uppercase();
}
}
out.push(ch);
}
#[allow(clippy::expect_used)]
String::from_utf8(out).expect("ASCII-only mutations preserve UTF-8")
}