1pub fn is_slug(s: &str) -> bool {
7 !s.is_empty()
8 && s.split('-').all(|w| {
9 !w.is_empty()
10 && w.bytes()
11 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
12 })
13}
14
15pub fn is_url(s: &str) -> bool {
17 let Some(i) = s.find(':') else {
18 return false;
19 };
20 let scheme = &s[..i];
21 let mut chars = scheme.chars();
22 chars.next().is_some_and(|c| c.is_ascii_alphabetic())
23 && chars.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
24 && (s[i + 1..].starts_with("//") || scheme.eq_ignore_ascii_case("mailto"))
25}
26
27pub fn code_list<T: std::fmt::Display>(items: impl IntoIterator<Item = T>, sep: &str) -> String {
29 items
30 .into_iter()
31 .map(|i| format!("`{i}`"))
32 .collect::<Vec<_>>()
33 .join(sep)
34}
35
36pub fn article(word: &str) -> &'static str {
40 let w = word
41 .trim_start_matches(|c: char| !c.is_ascii_alphanumeric())
42 .to_ascii_lowercase();
43 const YOU: [&str; 13] = [
44 "uni", "uin", "uid", "uli", "use", "usu", "uti", "utf", "uri", "url", "uk", "uu", "eu",
45 ];
46 const SILENT: [&str; 5] = ["hour", "honest", "honor", "heir", "html"];
47 let vowel = w.starts_with(['a', 'e', 'i', 'o', 'u'])
48 && !YOU.iter().any(|p| w.starts_with(p))
49 && !w.starts_with("one");
50 let eight = w.starts_with('8') || w.starts_with("11") || w.starts_with("18");
52 if vowel || eight || SILENT.iter().any(|p| w.starts_with(p)) {
53 "an"
54 } else {
55 "a"
56 }
57}
58
59pub fn count(n: usize, one: &str, many: &str) -> String {
61 format!("{n} {}", if n == 1 { one } else { many })
62}
63
64pub fn is_are(n: usize) -> &'static str {
66 if n == 1 { "is" } else { "are" }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn a_count_takes_its_number() {
75 assert_eq!(count(1, "item", "items"), "1 item");
76 assert_eq!(count(0, "item", "items"), "0 items");
77 assert_eq!((is_are(1), is_are(2)), ("is", "are"));
78 }
79
80 #[test]
81 fn the_article_follows_the_sound() {
82 for (word, said) in [
83 ("image", "an"),
84 ("int", "an"),
85 ("`amount`", "an"),
86 ("item", "an"),
87 ("html", "an"),
88 ("integer", "an"),
89 ("unit", "a"),
90 ("url", "a"),
91 ("slug", "a"),
92 ("paragraph", "a"),
93 ("row", "a"),
94 ("bullet-list", "a"),
95 ("uint", "a"),
96 ("ulid", "a"),
97 ("utf8", "a"),
98 ("8bit", "an"),
99 ("one-of", "a"),
100 ] {
101 assert_eq!(article(word), said, "{word}");
102 }
103 }
104
105 #[test]
106 fn a_slug_is_lower_case_words_joined_by_hyphens() {
107 assert!(is_slug("issue-1"));
108 assert!(!is_slug("Issue"));
109 assert!(!is_slug("a--b"));
110 assert!(!is_slug(""));
111 }
112
113 #[test]
114 fn a_url_has_a_scheme() {
115 assert!(is_url("https://a"));
116 assert!(is_url("mailto:a@b"));
117 assert!(!is_url("a:b"));
118 assert!(!is_url("1http://x"));
119 }
120}