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