1#[must_use]
10pub fn is_manual_section(value: &str) -> bool {
11 if value.is_empty() || value.len() > 16 {
12 return false;
13 }
14 let mut characters = value.chars();
15 match characters.next() {
16 Some(first) if first.is_ascii_digit() => {
17 characters.all(|character| character.is_ascii_alphanumeric())
18 }
19 Some('l' | 'n') => characters.next().is_none(),
20 _ => false,
21 }
22}
23
24#[must_use]
30pub fn is_command_manual_section(value: &str) -> bool {
31 if !is_manual_section(value) || !matches!(value.as_bytes().first(), Some(b'1' | b'8')) {
32 return false;
33 }
34 let suffix = &value[1..];
35 suffix.is_empty()
36 || suffix
37 .chars()
38 .next()
39 .is_some_and(|character| character.is_ascii_alphabetic())
40}
41
42#[must_use]
44pub fn parenthesized_manual_reference(selector: &str) -> Option<(&str, &str)> {
45 if selector.contains(['/', '\\']) || !selector.ends_with(')') {
46 return None;
47 }
48 let opening = selector.rfind('(')?;
49 let name = &selector[..opening];
50 let section = &selector[opening + 1..selector.len() - 1];
51 let valid_name = !name.is_empty()
52 && name.chars().all(|character| {
53 !character.is_whitespace() && !character.is_control() && !matches!(character, '(' | ')')
54 });
55 (valid_name && is_manual_section(section)).then_some((name, section))
56}
57
58#[cfg(test)]
59mod tests {
60 use super::{is_command_manual_section, is_manual_section, parenthesized_manual_reference};
61
62 #[test]
63 fn recognizes_conventional_sections_and_command_families() {
64 for section in ["0", "1", "1p", "3type", "8x", "l", "n"] {
65 assert!(is_manual_section(section), "{section}");
66 }
67 for section in ["1", "1p", "8", "8x"] {
68 assert!(is_command_manual_section(section), "{section}");
69 }
70 for section in ["", "qgroup", "17!", "3-type", "ll"] {
71 assert!(!is_manual_section(section), "{section}");
72 }
73 for section in ["0", "10", "17", "3", "5", "l", "n"] {
74 assert!(!is_command_manual_section(section), "{section}");
75 }
76 }
77
78 #[test]
79 fn splits_parenthesized_manual_references_without_paths() {
80 assert_eq!(
81 parenthesized_manual_reference("systemd.slice(5)"),
82 Some(("systemd.slice", "5"))
83 );
84 assert_eq!(parenthesized_manual_reference("manual/1/git"), None);
85 assert_eq!(parenthesized_manual_reference("two words(1)"), None);
86 assert_eq!(parenthesized_manual_reference("function(arg)(3)"), None);
87 }
88}