use crate::config::SiteConfig;
use crate::model::Redirect;
use crate::title::Title;
pub(crate) fn detect(text: &str, config: &SiteConfig) -> Option<Redirect> {
let after_keyword = strip_keyword(text.trim_start(), config)?;
let rest = after_keyword.trim_start();
let inner = rest.strip_prefix("[[")?;
let inner = &inner[..inner.find("]]")?];
let target = match inner.find('|') {
Some(pipe) => &inner[..pipe],
None => inner,
};
let (page, fragment) = match target.split_once('#') {
Some((page, fragment)) if !fragment.is_empty() => (page, Some(fragment.to_owned())),
Some((page, _)) => (page, None),
None => (target, None),
};
let title = Title::parse(page, config).ok()?;
Some(Redirect::new(title, fragment))
}
fn strip_keyword<'a>(text: &'a str, config: &SiteConfig) -> Option<&'a str> {
let mut longest: Option<usize> = None;
for alias in config.redirect_aliases() {
if let Some(len) = folded_prefix_len(text, alias) {
longest = Some(longest.map_or(len, |best| best.max(len)));
}
}
longest.map(|len| &text[len..])
}
fn folded_prefix_len(text: &str, folded: &str) -> Option<usize> {
let mut expected = folded.chars();
let mut len = 0_usize;
for c in text.chars() {
if expected.as_str().is_empty() {
break;
}
for lower in c.to_lowercase() {
if expected.next() != Some(lower) {
return None;
}
}
len += c.len_utf8();
}
if expected.as_str().is_empty() {
Some(len)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{MagicWordDef, NamespaceDef};
fn cfg() -> SiteConfig {
SiteConfig::builder("ru", "Википедия")
.namespace(NamespaceDef::main())
.magic_word(MagicWordDef::new(
"redirect",
["#REDIRECT", "#REDIR", "#перенаправление"],
false,
))
.build()
.unwrap()
}
#[test]
fn english_and_russian_keywords_detect() {
let r = detect("#REDIRECT [[Цель]]", &cfg()).unwrap();
assert_eq!(r.target().dbkey(), "Цель");
assert_eq!(r.fragment(), None);
let r = detect("#перенаправление [[Foo]]", &cfg()).unwrap();
assert_eq!(r.target().dbkey(), "Foo");
}
#[test]
fn matching_is_case_insensitive() {
assert!(detect("#ReDiReCt [[X]]", &cfg()).is_some());
assert!(detect("#ПЕРЕНАПРАВЛЕНИЕ [[X]]", &cfg()).is_some());
assert!(detect("#redir [[X]]", &cfg()).is_some());
}
#[test]
fn leading_blank_lines_and_glued_brackets_are_fine() {
assert!(detect("\n\n #redirect [[X]]", &cfg()).is_some());
assert!(detect("#REDIRECT[[X]]", &cfg()).is_some());
}
#[test]
fn longest_keyword_wins_over_its_prefix_alias() {
let r = detect("#REDIRECT [[Цель]]", &cfg()).unwrap();
assert_eq!(r.target().dbkey(), "Цель");
}
#[test]
fn fragments_split_off_and_empty_fragments_drop() {
let r = detect("#REDIRECT [[Страница#Раздел]]", &cfg()).unwrap();
assert_eq!(r.target().dbkey(), "Страница");
assert_eq!(r.fragment(), Some("Раздел"));
let r = detect("#REDIRECT [[Страница#]]", &cfg()).unwrap();
assert_eq!(r.fragment(), None);
}
#[test]
fn pipe_labels_and_trailing_junk_are_ignored() {
let r = detect("#REDIRECT [[Цель|метка]] {{R from move}}\nещё", &cfg()).unwrap();
assert_eq!(r.target().dbkey(), "Цель");
}
#[test]
fn keyword_without_a_link_is_no_redirect() {
assert_eq!(detect("#REDIRECT", &cfg()), None);
assert_eq!(detect("#REDIRECT ", &cfg()), None);
assert_eq!(detect("#REDIRECT to [[X]]", &cfg()), None);
assert_eq!(detect("#REDIRECT [[X", &cfg()), None);
}
#[test]
fn missing_or_wrong_keyword_is_no_redirect() {
assert_eq!(detect("REDIRECT [[X]]", &cfg()), None);
assert_eq!(detect("#GO [[X]]", &cfg()), None);
assert_eq!(detect("#redirec [[X]]", &cfg()), None);
assert_eq!(detect("#red", &cfg()), None);
assert_eq!(detect("", &cfg()), None);
}
#[test]
fn invalid_targets_are_no_redirect() {
assert_eq!(detect("#REDIRECT [[<плохо]]", &cfg()), None);
assert_eq!(detect("#REDIRECT [[]]", &cfg()), None);
assert_eq!(detect("#REDIRECT [[#Фраг]]", &cfg()), None);
}
}