|
1 |
| -use glib::{DateTime, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags}; |
| 1 | +use glib::{DateTime, TimeZone, Uri, UriFlags}; |
| 2 | +const S: char = ' '; |
2 | 3 |
|
3 | 4 | pub const TAG: &str = "=>";
|
4 | 5 |
|
5 | 6 | /// [Link](https://geminiprotocol.net/docs/gemtext-specification.gmi#link-lines) entity holder
|
6 | 7 | pub struct Link {
|
7 |
| - pub alt: Option<String>, // [optional] alternative link description |
8 |
| - pub timestamp: Option<DateTime>, // [optional] valid link DateTime object |
9 |
| - pub uri: Uri, // [required] valid link URI object |
| 8 | + /// For performance reasons, hold Gemtext date and alternative together as the optional String |
| 9 | + /// * to extract valid [DateTime](https://docs.gtk.org/glib/struct.DateTime.html) use `time` implementation method |
| 10 | + pub alt: Option<String>, |
| 11 | + /// For performance reasons, hold URL as the raw String |
| 12 | + /// * to extract valid [Uri](https://docs.gtk.org/glib/struct.Uri.html) use `uri` implementation method |
| 13 | + pub url: String, |
10 | 14 | }
|
11 | 15 |
|
12 | 16 | impl Link {
|
13 | 17 | // Constructors
|
14 | 18 |
|
15 | 19 | /// Parse `Self` from line string
|
16 |
| - pub fn from(line: &str, base: Option<&Uri>, timezone: Option<&TimeZone>) -> Option<Self> { |
17 |
| - // Skip next operations on prefix mismatch |
18 |
| - // * replace regex implementation @TODO |
19 |
| - if !line.starts_with(TAG) { |
| 20 | + pub fn parse(line: &str) -> Option<Self> { |
| 21 | + let l = line.strip_prefix(TAG)?.trim(); |
| 22 | + let u = l.find(S).map_or(l, |i| &l[..i]); |
| 23 | + if u.is_empty() { |
20 | 24 | return None;
|
21 | 25 | }
|
| 26 | + Some(Self { |
| 27 | + alt: l |
| 28 | + .get(u.len()..) |
| 29 | + .map(|a| a.trim()) |
| 30 | + .filter(|a| !a.is_empty()) |
| 31 | + .map(|a| a.to_string()), |
| 32 | + url: u.to_string(), |
| 33 | + }) |
| 34 | + } |
22 | 35 |
|
23 |
| - // Define initial values |
24 |
| - let mut alt = None; |
25 |
| - let mut timestamp = None; |
| 36 | + // Converters |
26 | 37 |
|
27 |
| - // Begin line parse |
28 |
| - let regex = Regex::split_simple( |
29 |
| - r"^=>\s*([^\s]+)\s*(\d{4}-\d{2}-\d{2})?\s*(.+)?$", |
30 |
| - line, |
31 |
| - RegexCompileFlags::DEFAULT, |
32 |
| - RegexMatchFlags::DEFAULT, |
| 38 | + /// Convert `Self` to [Gemtext](https://geminiprotocol.net/docs/gemtext-specification.gmi) line |
| 39 | + pub fn to_source(&self) -> String { |
| 40 | + let mut s = String::with_capacity( |
| 41 | + TAG.len() + self.url.len() + self.alt.as_ref().map_or(0, |a| a.len()) + 2, |
33 | 42 | );
|
| 43 | + s.push_str(TAG); |
| 44 | + s.push(S); |
| 45 | + s.push_str(&self.url); |
| 46 | + if let Some(ref alt) = self.alt { |
| 47 | + s.push(S); |
| 48 | + s.push_str(alt); |
| 49 | + } |
| 50 | + s |
| 51 | + } |
| 52 | + |
| 53 | + // Getters |
34 | 54 |
|
35 |
| - // Detect address required to continue |
36 |
| - let mut unresolved_address = regex.get(1)?.to_string(); |
| 55 | + /// Get valid [DateTime](https://docs.gtk.org/glib/struct.DateTime.html) for `Self` |
| 56 | + pub fn time(&self, timezone: Option<&TimeZone>) -> Option<DateTime> { |
| 57 | + let a = self.alt.as_ref()?; |
| 58 | + let t = &a[..a.find(S).unwrap_or(a.len())]; |
| 59 | + DateTime::from_iso8601(&format!("{t}T00:00:00"), timezone).ok() |
| 60 | + } |
37 | 61 |
|
| 62 | + /// Get valid [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `Self` |
| 63 | + pub fn uri(&self, base: Option<&Uri>) -> Option<Uri> { |
38 | 64 | // Relative scheme patch
|
39 | 65 | // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2
|
40 |
| - if let Some(p) = unresolved_address.strip_prefix("//") { |
41 |
| - let b = base?; |
42 |
| - let postfix = p.trim_start_matches(":"); |
43 |
| - unresolved_address = format!( |
44 |
| - "{}://{}", |
45 |
| - b.scheme(), |
46 |
| - if postfix.is_empty() { |
47 |
| - format!("{}/", b.host()?) |
48 |
| - } else { |
49 |
| - postfix.into() |
50 |
| - } |
51 |
| - ) |
52 |
| - } |
53 |
| - // Convert address to the valid URI |
54 |
| - let uri = match base { |
55 |
| - // Base conversion requested |
56 |
| - Some(base_uri) => { |
57 |
| - // Convert relative address to absolute |
58 |
| - match Uri::resolve_relative( |
59 |
| - Some(&base_uri.to_str()), |
60 |
| - unresolved_address.as_str(), |
61 |
| - UriFlags::NONE, |
62 |
| - ) { |
63 |
| - Ok(resolved_str) => { |
64 |
| - // Try convert string to the valid URI |
65 |
| - match Uri::parse(&resolved_str, UriFlags::NONE) { |
66 |
| - Ok(resolved_uri) => resolved_uri, |
67 |
| - Err(_) => return None, |
68 |
| - } |
| 66 | + let unresolved_address = match self.url.strip_prefix("//") { |
| 67 | + Some(p) => { |
| 68 | + let b = base?; |
| 69 | + let s = p.trim_start_matches(":"); |
| 70 | + &format!( |
| 71 | + "{}://{}", |
| 72 | + b.scheme(), |
| 73 | + if s.is_empty() { |
| 74 | + format!("{}/", b.host()?) |
| 75 | + } else { |
| 76 | + s.into() |
69 | 77 | }
|
70 |
| - Err(_) => return None, |
71 |
| - } |
72 |
| - } |
73 |
| - // Base resolve not requested |
74 |
| - None => { |
75 |
| - // Try convert address to valid URI |
76 |
| - match Uri::parse(&unresolved_address, UriFlags::NONE) { |
77 |
| - Ok(unresolved_uri) => unresolved_uri, |
78 |
| - Err(_) => return None, |
79 |
| - } |
| 78 | + ) |
80 | 79 | }
|
| 80 | + None => &self.url, |
81 | 81 | };
|
82 |
| - |
83 |
| - // Timestamp |
84 |
| - if let Some(date) = regex.get(2) { |
85 |
| - timestamp = match DateTime::from_iso8601(&format!("{date}T00:00:00"), timezone) { |
86 |
| - Ok(value) => Some(value), |
| 82 | + // Convert address to the valid URI, |
| 83 | + // resolve to absolute URL format if the target is relative |
| 84 | + match base { |
| 85 | + Some(base_uri) => match Uri::resolve_relative( |
| 86 | + Some(&base_uri.to_str()), |
| 87 | + unresolved_address, |
| 88 | + UriFlags::NONE, |
| 89 | + ) { |
| 90 | + Ok(resolved_str) => Uri::parse(&resolved_str, UriFlags::NONE).ok(), |
87 | 91 | Err(_) => None,
|
88 |
| - } |
| 92 | + }, |
| 93 | + None => Uri::parse(unresolved_address, UriFlags::NONE).ok(), |
89 | 94 | }
|
| 95 | + } |
| 96 | +} |
90 | 97 |
|
91 |
| - // Alt |
92 |
| - if let Some(value) = regex.get(3) { |
93 |
| - if !value.is_empty() { |
94 |
| - alt = Some(value.to_string()) |
95 |
| - } |
96 |
| - }; |
| 98 | +#[test] |
| 99 | +fn test() { |
| 100 | + use crate::line::Link; |
97 | 101 |
|
98 |
| - Some(Self { |
99 |
| - alt, |
100 |
| - timestamp, |
101 |
| - uri, |
102 |
| - }) |
103 |
| - } |
| 102 | + const SOURCE: &str = "=> gemini://geminiprotocol.net 1965-01-19 Gemini"; |
| 103 | + |
| 104 | + let link = Link::parse(SOURCE).unwrap(); |
| 105 | + |
| 106 | + assert_eq!(link.alt, Some("1965-01-19 Gemini".to_string())); |
| 107 | + assert_eq!(link.url, "gemini://geminiprotocol.net"); |
| 108 | + |
| 109 | + let uri = link.uri(None).unwrap(); |
| 110 | + assert_eq!(uri.scheme(), "gemini"); |
| 111 | + assert_eq!(uri.host().unwrap(), "geminiprotocol.net"); |
| 112 | + |
| 113 | + let time = link.time(Some(&glib::TimeZone::local())).unwrap(); |
| 114 | + assert_eq!(time.year(), 1965); |
| 115 | + assert_eq!(time.month(), 1); |
| 116 | + assert_eq!(time.day_of_month(), 19); |
| 117 | + |
| 118 | + assert_eq!(link.to_source(), SOURCE); |
104 | 119 | }
|
0 commit comments