use super::clone::string_to_owned;
use crate::data::PageRef;
use crate::settings::WikitextSettings;
use crate::url::is_url;
use std::borrow::Cow;
use strum_macros::EnumIter;
#[derive(Serialize, Deserialize, Debug, Hash, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum LinkLocation<'a> {
Page(PageRef),
Url(Cow<'a, str>),
}
impl<'a> LinkLocation<'a> {
pub fn parse_with_interwiki(
link: Cow<'a, str>,
settings: &WikitextSettings,
) -> Option<(Self, LinkType)> {
match link.as_ref().strip_prefix('!') {
None => {
let interwiki = Self::parse(link);
let ltype = interwiki.link_type();
Some((interwiki, ltype))
}
Some(link) => settings
.interwiki
.build(link)
.map(|url| (LinkLocation::Url(Cow::Owned(url)), LinkType::Interwiki)),
}
}
pub fn parse(link: Cow<'a, str>) -> Self {
let link_str = &link;
if is_url(link_str) || link_str.starts_with('#') || link_str.starts_with("/") {
return LinkLocation::Url(link);
}
match PageRef::parse(link_str) {
Err(_) => LinkLocation::Url(link),
Ok(page_ref) => LinkLocation::Page(page_ref),
}
}
pub fn to_owned(&self) -> LinkLocation<'static> {
match self {
LinkLocation::Page(page) => LinkLocation::Page(page.to_owned()),
LinkLocation::Url(url) => LinkLocation::Url(string_to_owned(url)),
}
}
pub fn link_type(&self) -> LinkType {
match self {
LinkLocation::Page(_) => LinkType::Page,
LinkLocation::Url(_) => LinkType::Direct,
}
}
}
#[test]
fn test_link_location() {
#[inline]
fn convert_opt(s: Option<&str>) -> Option<String> {
s.map(String::from)
}
macro_rules! test {
($input:expr => $site:expr, $page:expr, $extra:expr $(,)?) => {{
let expected = LinkLocation::Page(PageRef {
site: convert_opt($site),
page: str!($page),
extra: convert_opt($extra),
});
test!($input; expected);
}};
($input:expr => $url:expr $(,)?) => {
let url = cow!($url);
let expected = LinkLocation::Url(url);
test!($input; expected);
};
($input:expr; $expected:expr $(,)?) => {{
let actual = LinkLocation::parse(cow!($input));
assert_eq!(
actual,
$expected,
"Actual link location result doesn't match expected",
);
}};
}
test!("" => "");
test!("#" => "#");
test!("#anchor" => "#anchor");
test!("page" => None, "page", None);
test!("page/edit" => None, "page", Some("/edit"));
test!("page#toc0" => None, "page", Some("#toc0"));
test!("page/comments#main" => None, "page", Some("/comments#main"));
test!("/page" => "/page");
test!("/page/edit" => "/page/edit");
test!("/page#toc0" => "/page#toc0");
test!("component:theme" => None, "component:theme", None);
test!(":scp-wiki:scp-1000" => Some("scp-wiki"), "scp-1000", None);
test!(
":scp-wiki:scp-1000#page-options-bottom" =>
Some("scp-wiki"), "scp-1000", Some("#page-options-bottom"),
);
test!(
":scp-wiki:component:theme" =>
Some("scp-wiki"), "component:theme", None,
);
test!(
":scp-wiki:component:theme/edit/true" =>
Some("scp-wiki"), "component:theme", Some("/edit/true"),
);
test!("http://blog.wikidot.com/" => "http://blog.wikidot.com/");
test!("https://example.com" => "https://example.com");
test!("mailto:test@example.net" => "mailto:test@example.net");
test!("::page" => "::page");
test!("::component:theme" => "::component:theme");
test!("multiple:category:page" => None, "multiple-category:page", None);
}
#[derive(Serialize, Deserialize, Debug, Hash, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum LinkLabel<'a> {
Text(Cow<'a, str>),
Slug(Cow<'a, str>),
Url,
Page,
}
impl LinkLabel<'_> {
pub fn to_owned(&self) -> LinkLabel<'static> {
match self {
LinkLabel::Text(text) => LinkLabel::Text(string_to_owned(text)),
LinkLabel::Slug(text) => LinkLabel::Slug(string_to_owned(text)),
LinkLabel::Url => LinkLabel::Url,
LinkLabel::Page => LinkLabel::Page,
}
}
}
#[derive(EnumIter, Serialize, Deserialize, Debug, Hash, Copy, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum LinkType {
Direct,
Page,
Interwiki,
Anchor,
TableOfContents,
}
impl LinkType {
pub fn name(self) -> &'static str {
match self {
LinkType::Direct => "direct",
LinkType::Page => "page",
LinkType::Interwiki => "interwiki",
LinkType::Anchor => "anchor",
LinkType::TableOfContents => "table-of-contents",
}
}
}
impl<'a> TryFrom<&'a str> for LinkType {
type Error = &'a str;
fn try_from(value: &'a str) -> Result<LinkType, &'a str> {
match value {
"direct" => Ok(LinkType::Direct),
"page" => Ok(LinkType::Page),
"interwiki" => Ok(LinkType::Interwiki),
"anchor" => Ok(LinkType::Anchor),
"table-of-contents" => Ok(LinkType::TableOfContents),
_ => Err(value),
}
}
}
#[test]
fn link_type_name_serde() {
use strum::IntoEnumIterator;
for variant in LinkType::iter() {
let output = serde_json::to_string(&variant).expect("Unable to serialize JSON");
let serde_name: String =
serde_json::from_str(&output).expect("Unable to deserialize JSON");
assert_eq!(
&serde_name,
variant.name(),
"Serde name does not match variant name",
);
let converted: LinkType = serde_name
.as_str()
.try_into()
.expect("Could not convert item");
assert_eq!(converted, variant, "Converted item does not match variant");
}
}