Skip to content

Commit 22a05a9

Browse files
author
yggverse
committed
remove regex dependency, rename constructor, add tests
1 parent 0c90bba commit 22a05a9

File tree

3 files changed

+168
-138
lines changed

3 files changed

+168
-138
lines changed

README.md

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ for line in gemtext.lines() {
3434

3535
``` rust
3636
use ggemtext::line::code::Inline;
37-
match Inline::from("```inline```") {
37+
match Inline::parse("```inline```") {
3838
Some(inline) => assert_eq!(inline.value, "inline"),
3939
None => assert!(false),
4040
}
@@ -93,31 +93,25 @@ assert_eq!("H1".to_source(&Level::H1), "# H1");
9393
#### Link
9494

9595
``` rust
96-
use ggemtext::line::Link;
97-
match Link::from(
98-
"=> gemini://geminiprotocol.net 1965-01-19 Gemini",
99-
None, // absolute path given, base not wanted
100-
Some(&glib::TimeZone::local()),
101-
) {
102-
Some(link) => {
103-
// Alt
104-
assert_eq!(link.alt, Some("Gemini".into()));
105-
106-
// Date
107-
match link.timestamp {
108-
Some(timestamp) => {
109-
assert_eq!(timestamp.year(), 1965);
110-
assert_eq!(timestamp.month(), 1);
111-
assert_eq!(timestamp.day_of_month(), 19);
112-
}
113-
None => assert!(false),
114-
}
115-
116-
// URI
117-
assert_eq!(link.uri.to_string(), "gemini://geminiprotocol.net");
118-
}
119-
None => assert!(false),
120-
}
96+
use crate::line::Link;
97+
98+
const SOURCE: &str = "=> gemini://geminiprotocol.net 1965-01-19 Gemini";
99+
100+
let link = Link::parse(SOURCE).unwrap();
101+
102+
assert_eq!(link.alt, Some("1965-01-19 Gemini".to_string()));
103+
assert_eq!(link.url, "gemini://geminiprotocol.net");
104+
105+
let uri = link.uri(None).unwrap();
106+
assert_eq!(uri.scheme(), "gemini");
107+
assert_eq!(uri.host().unwrap(), "geminiprotocol.net");
108+
109+
let time = link.time(Some(&glib::TimeZone::local())).unwrap();
110+
assert_eq!(time.year(), 1965);
111+
assert_eq!(time.month(), 1);
112+
assert_eq!(time.day_of_month(), 19);
113+
114+
assert_eq!(link.to_source(), SOURCE);
121115
```
122116

123117
#### List

src/line/link.rs

Lines changed: 91 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,119 @@
1-
use glib::{DateTime, Regex, RegexCompileFlags, RegexMatchFlags, TimeZone, Uri, UriFlags};
1+
use glib::{DateTime, TimeZone, Uri, UriFlags};
2+
const S: char = ' ';
23

34
pub const TAG: &str = "=>";
45

56
/// [Link](https://geminiprotocol.net/docs/gemtext-specification.gmi#link-lines) entity holder
67
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,
1014
}
1115

1216
impl Link {
1317
// Constructors
1418

1519
/// 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() {
2024
return None;
2125
}
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+
}
2235

23-
// Define initial values
24-
let mut alt = None;
25-
let mut timestamp = None;
36+
// Converters
2637

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,
3342
);
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
3454

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+
}
3761

62+
/// Get valid [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `Self`
63+
pub fn uri(&self, base: Option<&Uri>) -> Option<Uri> {
3864
// Relative scheme patch
3965
// 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()
6977
}
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+
)
8079
}
80+
None => &self.url,
8181
};
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(),
8791
Err(_) => None,
88-
}
92+
},
93+
None => Uri::parse(unresolved_address, UriFlags::NONE).ok(),
8994
}
95+
}
96+
}
9097

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;
97101

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);
104119
}

0 commit comments

Comments
 (0)