1#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum ListingLine {
7 Link { url: String, label: Option<String> },
10 Text(String),
12}
13
14pub fn parse_listing(text: &str) -> Vec<ListingLine> {
16 text.lines()
17 .map(|line| {
18 let line = line.trim_end_matches('\r');
19 match line.strip_prefix("=> ") {
20 Some(rest) if !rest.trim().is_empty() => {
21 let rest = rest.trim_start();
22 match rest.split_once(char::is_whitespace) {
23 Some((url, label)) => {
24 let label = label.trim();
25 ListingLine::Link {
26 url: url.to_string(),
27 label: (!label.is_empty()).then(|| label.to_string()),
28 }
29 }
30 None => ListingLine::Link {
31 url: rest.to_string(),
32 label: None,
33 },
34 }
35 }
36 _ => ListingLine::Text(line.to_string()),
37 }
38 })
39 .collect()
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 #[test]
47 fn links_and_text_split_per_spec_examples() {
48 let listing = parse_listing(
49 "Welcome!\n=> nex://my-site.net\n=> about.txt\n=> ../nexlog/ my nexlog\nplain line\n",
50 );
51 assert_eq!(listing[0], ListingLine::Text("Welcome!".to_string()));
52 assert_eq!(
53 listing[1],
54 ListingLine::Link {
55 url: "nex://my-site.net".to_string(),
56 label: None
57 }
58 );
59 assert_eq!(
60 listing[2],
61 ListingLine::Link {
62 url: "about.txt".to_string(),
63 label: None
64 }
65 );
66 assert_eq!(
67 listing[3],
68 ListingLine::Link {
69 url: "../nexlog/".to_string(),
70 label: Some("my nexlog".to_string())
71 }
72 );
73 assert_eq!(listing[4], ListingLine::Text("plain line".to_string()));
74 }
75
76 #[test]
77 fn a_bare_arrow_is_text_not_a_link() {
78 assert_eq!(parse_listing("=> ")[0], ListingLine::Text("=> ".to_string()));
79 assert_eq!(
81 parse_listing("=>about.txt")[0],
82 ListingLine::Text("=>about.txt".to_string())
83 );
84 }
85}