| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | 6× | pub fn bluesky(input: &str) -> Option<String> { |
| 8 | 6× | let h = input.trim().trim_start_matches('@').trim_end_matches('/'); |
| 9 | 6× | let h = h.strip_prefix("https://bsky.app/profile/").unwrap_or(h).to_ascii_lowercase(); |
| 10 | 6× | (!h.is_empty() && h.len() <= 253 && h.contains('.') && h.split('.').all(label_ok)).then_some(h) |
| 11 | 6× | } |
| 12 | |
|
| 13 | |
|
| 14 | |
|
| 15 | 5× | pub fn mastodon(input: &str) -> Option<String> { |
| 16 | 5× | let s = input.trim().trim_end_matches('/'); |
| 17 | 5× | let (user, instance) = if let Some(rest) = s.strip_prefix("https://") { |
| 18 | 2× | let (instance, user) = rest.split_once("/@")?; |
| 19 | 2× | (user, instance) |
| 20 | | } else { |
| 21 | 3× | s.trim_start_matches('@').split_once('@')? |
| 22 | | }; |
| 23 | 16× | let user_ok = !user.is_empty() && user.len() <= 64 && user.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.'); |
| 24 | 4× | let instance = instance.to_ascii_lowercase(); |
| 25 | 4× | let instance_ok = instance.contains('.') && instance.len() <= 253 && instance.split('.').all(label_ok); |
| 26 | 4× | (user_ok && instance_ok).then(|| format!("{user}@{instance}")) |
| 27 | 5× | } |
| 28 | |
|
| 29 | |
|
| 30 | |
|
| 31 | 5× | pub fn github(input: &str) -> Option<String> { |
| 32 | 5× | let u = input.trim().trim_start_matches('@').trim_end_matches('/'); |
| 33 | 5× | let u = u.strip_prefix("https://github.com/").unwrap_or(u); |
| 34 | 5× | let ok = !u.is_empty() |
| 35 | 5× | && u.len() <= 39 |
| 36 | 41× | && u.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') |
| 37 | 4× | && !u.starts_with('-') |
| 38 | 3× | && !u.ends_with('-'); |
| 39 | 5× | ok.then(|| u.to_string()) |
| 40 | 5× | } |
| 41 | |
|
| 42 | 16× | fn label_ok(l: &str) -> bool { |
| 43 | 83× | !l.is_empty() && l.len() <= 63 && l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') && !l.starts_with('-') && !l.ends_with('-') |
| 44 | 16× | } |
| 45 | |
|
| 46 | 2× | pub fn bluesky_url(handle: &str) -> String { |
| 47 | 2× | format!("https://bsky.app/profile/{handle}") |
| 48 | 2× | } |
| 49 | |
|
| 50 | |
|
| 51 | 3× | pub fn mastodon_url(address: &str) -> String { |
| 52 | 3× | match address.split_once('@') { |
| 53 | 3× | Some((user, instance)) => format!("https://{instance}/@{user}"), |
| 54 | 0× | None => String::new(), |
| 55 | | } |
| 56 | 3× | } |
| 57 | |
|
| 58 | 2× | pub fn github_url(user: &str) -> String { |
| 59 | 2× | format!("https://github.com/{user}") |
| 60 | 2× | } |
| 61 | |
|
| 62 | | #[cfg(test)] |
| 63 | | mod tests { |
| 64 | | use super::*; |
| 65 | |
|
| 66 | | #[test] |
| 67 | 1× | fn handles_are_checked_and_normalised() { |
| 68 | 1× | assert_eq!(bluesky("@Andy.bsky.social").as_deref(), Some("andy.bsky.social")); |
| 69 | 1× | assert_eq!(bluesky("https://bsky.app/profile/andy.example.org/").as_deref(), Some("andy.example.org")); |
| 70 | 1× | assert_eq!(bluesky("andy"), None, "a handle is a domain"); |
| 71 | 1× | assert_eq!(bluesky("an dy.bsky.social"), None); |
| 72 | 1× | assert_eq!(mastodon("@[email protected]").as_deref(), Some("[email protected]")); |
| 73 | 1× | assert_eq!(mastodon("https://Fosstodon.org/@andy").as_deref(), Some("[email protected]")); |
| 74 | 1× | assert_eq!(mastodon("andy"), None); |
| 75 | 1× | assert_eq!(mastodon("andy@nodot"), None); |
| 76 | 1× | assert_eq!(github("https://github.com/andymccall/").as_deref(), Some("andymccall")); |
| 77 | 1× | assert_eq!(github("@andy-mccall").as_deref(), Some("andy-mccall")); |
| 78 | 1× | assert_eq!(github("-andy"), None); |
| 79 | 1× | assert_eq!(github("andy mccall"), None); |
| 80 | 1× | assert_eq!(mastodon_url("[email protected]"), "https://fosstodon.org/@andy"); |
| 81 | 1× | } |
| 82 | | } |