Skip to main content

nex_protocol/
listing.rs

1//! The nex directory-listing format: plain text where each line beginning
2//! `=> ` followed by a URL is a link. The URL may be absolute or relative.
3
4/// One line of a directory listing.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum ListingLine {
7    /// A `=> ` link line. The spec defines only the URL; text after the
8    /// first whitespace is preserved as a label by client convention.
9    Link { url: String, label: Option<String> },
10    /// Any other line, verbatim.
11    Text(String),
12}
13
14/// Parse a directory listing into its lines.
15pub 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        // No space after the arrow: the spec requires "=> ".
80        assert_eq!(
81            parse_listing("=>about.txt")[0],
82            ListingLine::Text("=>about.txt".to_string())
83        );
84    }
85}