1pub fn parse_citations(content: &str) -> Vec<(usize, String)> {
14 let mut out = Vec::new();
15 let mut in_section = false;
16 for line in content.lines() {
17 let t = line.trim();
18 if !in_section {
19 let heading = t.trim_start_matches('#').trim();
20 if t.eq_ignore_ascii_case("Sources:") || heading.eq_ignore_ascii_case("Sources") {
21 in_section = true;
22 }
23 continue;
24 }
25 if t.is_empty() {
26 continue;
27 }
28 let Some((num, rest)) = t.split_once(['.', ')']) else {
29 break;
30 };
31 let Ok(n) = num.trim().parse::<usize>() else {
32 break;
33 };
34 let url = rest.trim().to_string();
35 if url.is_empty() {
36 break;
37 }
38 out.push((n, url));
39 }
40 out
41}
42
43pub fn citation_number_in(text: &str) -> Option<usize> {
45 let mut rest = text;
46 while let Some(start) = rest.find('[') {
47 rest = &rest[start + 1..];
48 let end = rest.find(']')?;
49 let inner = &rest[..end];
50 if !inner.is_empty() && inner.chars().all(|c| c.is_ascii_digit()) {
51 return inner.parse().ok();
52 }
53 rest = &rest[end + 1..];
54 }
55 None
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn parse_citations_reads_a_sources_heading_section() {
64 let content = "# Report\n\nBody text [1] and more [2].\n\n## Sources\n1. https://a.example\n2. https://b.example/page\n";
65 assert_eq!(
66 parse_citations(content),
67 vec![
68 (1, "https://a.example".into()),
69 (2, "https://b.example/page".into())
70 ]
71 );
72 }
73
74 #[test]
75 fn parse_citations_reads_a_plain_sources_colon_line() {
76 let content = "findings text [1]\nSources:\n1. https://a.example\n";
77 assert_eq!(
78 parse_citations(content),
79 vec![(1, "https://a.example".into())]
80 );
81 }
82
83 #[test]
84 fn parse_citations_returns_empty_when_no_sources_section() {
85 assert!(parse_citations("just prose, no citations").is_empty());
86 }
87
88 #[test]
89 fn citation_number_in_finds_first_bracketed_number() {
90 assert_eq!(
91 citation_number_in("supported by research [3] and also [4]"),
92 Some(3)
93 );
94 assert_eq!(citation_number_in("no citation here"), None);
95 assert_eq!(citation_number_in("[not a number] but [5] later"), Some(5));
96 assert_eq!(citation_number_in("[not a number]"), None);
97 }
98}