1use super::clone::string_to_owned;
22use crate::data::PageRef;
23use crate::settings::WikitextSettings;
24use crate::url::is_url;
25use std::borrow::Cow;
26use strum_macros::EnumIter;
27
28#[derive(Serialize, Deserialize, Debug, Hash, Clone, PartialEq, Eq)]
29#[serde(untagged)]
30pub enum LinkLocation<'a> {
31 Page(PageRef),
33
34 Url(Cow<'a, str>),
36}
37
38impl<'a> LinkLocation<'a> {
39 pub fn parse_with_interwiki(
41 link: Cow<'a, str>,
42 settings: &WikitextSettings,
43 ) -> Option<(Self, LinkType)> {
44 match link.as_ref().strip_prefix('!') {
46 None => {
48 let interwiki = Self::parse(link);
49 let ltype = interwiki.link_type();
50 Some((interwiki, ltype))
51 }
52
53 Some(link) => settings
55 .interwiki
56 .build(link)
57 .map(|url| (LinkLocation::Url(Cow::Owned(url)), LinkType::Interwiki)),
58 }
59 }
60
61 pub fn parse(link: Cow<'a, str>) -> Self {
62 let link_str = &link;
66 if is_url(link_str) || link_str.starts_with('#') || link_str.starts_with("/") {
67 return LinkLocation::Url(link);
68 }
69
70 match PageRef::parse(link_str) {
71 Err(_) => LinkLocation::Url(link),
72 Ok(page_ref) => LinkLocation::Page(page_ref),
73 }
74 }
75
76 pub fn to_owned(&self) -> LinkLocation<'static> {
77 match self {
78 LinkLocation::Page(page) => LinkLocation::Page(page.to_owned()),
79 LinkLocation::Url(url) => LinkLocation::Url(string_to_owned(url)),
80 }
81 }
82
83 pub fn link_type(&self) -> LinkType {
84 match self {
85 LinkLocation::Page(_) => LinkType::Page,
86 LinkLocation::Url(_) => LinkType::Direct,
87 }
88 }
89}
90
91#[test]
92fn test_link_location() {
93 #[inline]
95 fn convert_opt(s: Option<&str>) -> Option<String> {
96 s.map(String::from)
97 }
98
99 macro_rules! test {
100 ($input:expr => $site:expr, $page:expr, $extra:expr $(,)?) => {{
102 let expected = LinkLocation::Page(PageRef {
103 site: convert_opt($site),
104 page: str!($page),
105 extra: convert_opt($extra),
106 });
107 test!($input; expected);
108 }};
109
110 ($input:expr => $url:expr $(,)?) => {
112 let url = cow!($url);
113 let expected = LinkLocation::Url(url);
114 test!($input; expected);
115 };
116
117 ($input:expr; $expected:expr $(,)?) => {{
119 let actual = LinkLocation::parse(cow!($input));
120 assert_eq!(
121 actual,
122 $expected,
123 "Actual link location result doesn't match expected",
124 );
125 }};
126 }
127
128 test!("" => "");
129 test!("#" => "#");
130 test!("#anchor" => "#anchor");
131
132 test!("page" => None, "page", None);
133 test!("page/edit" => None, "page", Some("/edit"));
134 test!("page#toc0" => None, "page", Some("#toc0"));
135 test!("page/comments#main" => None, "page", Some("/comments#main"));
136
137 test!("/page" => "/page");
138 test!("/page/edit" => "/page/edit");
139 test!("/page#toc0" => "/page#toc0");
140
141 test!("component:theme" => None, "component:theme", None);
142 test!(":scp-wiki:scp-1000" => Some("scp-wiki"), "scp-1000", None);
143 test!(
144 ":scp-wiki:scp-1000#page-options-bottom" =>
145 Some("scp-wiki"), "scp-1000", Some("#page-options-bottom"),
146 );
147 test!(
148 ":scp-wiki:component:theme" =>
149 Some("scp-wiki"), "component:theme", None,
150 );
151 test!(
152 ":scp-wiki:component:theme/edit/true" =>
153 Some("scp-wiki"), "component:theme", Some("/edit/true"),
154 );
155
156 test!("http://blog.wikidot.com/" => "http://blog.wikidot.com/");
157 test!("https://example.com" => "https://example.com");
158 test!("mailto:test@example.net" => "mailto:test@example.net");
159
160 test!("::page" => "::page");
161 test!("::component:theme" => "::component:theme");
162 test!("multiple:category:page" => None, "multiple-category:page", None);
163}
164
165#[derive(Serialize, Deserialize, Debug, Hash, Clone, PartialEq, Eq)]
166#[serde(rename_all = "kebab-case")]
167pub enum LinkLabel<'a> {
168 Text(Cow<'a, str>),
172
173 Slug(Cow<'a, str>),
183
184 Url,
188
189 Page,
193}
194
195impl LinkLabel<'_> {
196 pub fn to_owned(&self) -> LinkLabel<'static> {
197 match self {
198 LinkLabel::Text(text) => LinkLabel::Text(string_to_owned(text)),
199 LinkLabel::Slug(text) => LinkLabel::Slug(string_to_owned(text)),
200 LinkLabel::Url => LinkLabel::Url,
201 LinkLabel::Page => LinkLabel::Page,
202 }
203 }
204}
205
206#[derive(EnumIter, Serialize, Deserialize, Debug, Hash, Copy, Clone, PartialEq, Eq)]
207#[serde(rename_all = "kebab-case")]
208pub enum LinkType {
209 Direct,
213
214 Page,
218
219 Interwiki,
221
222 Anchor,
224
225 TableOfContents,
227}
228
229impl LinkType {
230 pub fn name(self) -> &'static str {
231 match self {
232 LinkType::Direct => "direct",
233 LinkType::Page => "page",
234 LinkType::Interwiki => "interwiki",
235 LinkType::Anchor => "anchor",
236 LinkType::TableOfContents => "table-of-contents",
237 }
238 }
239}
240
241impl<'a> TryFrom<&'a str> for LinkType {
242 type Error = &'a str;
243
244 fn try_from(value: &'a str) -> Result<LinkType, &'a str> {
245 match value {
246 "direct" => Ok(LinkType::Direct),
247 "page" => Ok(LinkType::Page),
248 "interwiki" => Ok(LinkType::Interwiki),
249 "anchor" => Ok(LinkType::Anchor),
250 "table-of-contents" => Ok(LinkType::TableOfContents),
251 _ => Err(value),
252 }
253 }
254}
255
256#[test]
258fn link_type_name_serde() {
259 use strum::IntoEnumIterator;
260
261 for variant in LinkType::iter() {
262 let output = serde_json::to_string(&variant).expect("Unable to serialize JSON");
263 let serde_name: String =
264 serde_json::from_str(&output).expect("Unable to deserialize JSON");
265
266 assert_eq!(
267 &serde_name,
268 variant.name(),
269 "Serde name does not match variant name",
270 );
271
272 let converted: LinkType = serde_name
273 .as_str()
274 .try_into()
275 .expect("Could not convert item");
276
277 assert_eq!(converted, variant, "Converted item does not match variant");
278 }
279}