1pub const PREFIX: &str = "hammer://image/";
7
8pub fn reference(image_id: i32) -> String {
10 format!("{PREFIX}{image_id}")
11}
12
13pub fn parse_reference(reference: &str) -> Option<i32> {
17 let source = reference.strip_prefix('<').and_then(|it| it.strip_suffix('>')).unwrap_or(reference);
18 let digits = source.strip_prefix(PREFIX)?;
19 if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
20 return None;
21 }
22 digits.parse().ok()
23}
24
25pub fn find_ids(markdown: &str) -> Vec<i32> {
31 let bytes = markdown.as_bytes();
32 let mut ids = Vec::new();
33 let mut from = 0;
34 while let Some(found) = markdown[from..].find(PREFIX) {
35 let at = from + found;
36 let start = at + PREFIX.len();
37 let end = start + bytes[start..].iter().take_while(|b| b.is_ascii_digit()).count();
38 let bounded = (at == 0 || !is_lead(bytes[at - 1])) && (end == bytes.len() || !is_word(bytes[end]));
39 if end > start && bounded {
40 if let Ok(id) = markdown[start..end].parse() {
41 if !ids.contains(&id) {
42 ids.push(id);
43 }
44 }
45 }
46 from = end;
47 }
48 ids
49}
50
51pub fn get(project: &str, reference: &str) -> crate::Result<crate::ops::ImageContent> {
55 match parse_reference(reference) {
56 Some(id) => crate::ops::image::get(project, id),
57 None => Err(format!("InvalidInput: Not an image reference: {reference}").into()),
58 }
59}
60
61fn is_word(b: u8) -> bool {
62 b.is_ascii_alphanumeric() || b == b'_'
63}
64
65fn is_lead(b: u8) -> bool {
66 is_word(b) || matches!(b, b'+' | b'.' | b'-')
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn a_reference_names_its_image_id() {
75 assert_eq!(parse_reference("hammer://image/3"), Some(3));
76 assert_eq!(parse_reference("<hammer://image/3>"), Some(3));
77 assert_eq!(parse_reference("hammer://image/007"), Some(7));
78 assert_eq!(parse_reference(&reference(42)), Some(42));
79 assert_eq!(reference(42), "hammer://image/42");
80 assert_eq!(parse_reference("hammer://image/2147483647"), Some(i32::MAX));
81 }
82
83 #[test]
84 fn anything_else_names_no_image() {
85 for text in [
86 "https://example.com/3.png", "hammer://scene/3", "hammer://image/", "hammer://image/3x",
87 "hammer://image/-3", "hammer://image/+3", "hammer://image/3 ", " hammer://image/3",
88 "hammer://image/99999999999", "hammer://image/2147483648", "<hammer://image/3", "hammer://image/3>",
89 "<<hammer://image/3>>", "<>", "", "hammer://image/3",
90 ] {
91 assert_eq!(parse_reference(text), None, "{text}");
92 }
93 }
94
95 #[test]
96 fn finds_every_referenced_id() {
97 let text = "\n\nSee  and [c]: hammer://image/7";
98 assert_eq!(find_ids(text), vec![1, 12, 7]);
99 assert_eq!(find_ids("hammer://image/9 hammer://image/2 hammer://image/9"), vec![9, 2]);
100 assert_eq!(find_ids("```\n\n```\n"), vec![5]);
101 assert_eq!(find_ids("A [link](hammer://image/3)."), vec![3]);
102 assert_eq!(find_ids("éhammer://image/4é"), vec![4]);
103 assert_eq!(find_ids("No images here."), Vec::<i32>::new());
104 }
105
106 #[test]
107 fn an_id_runs_to_its_last_digit_and_the_scheme_stands_alone() {
108 assert_eq!(find_ids(""), vec![12]);
109 assert_eq!(find_ids("hammer://image/12"), vec![12]);
110 assert_eq!(find_ids("hammer://image/12.png"), vec![12]);
111 for text in [
112 "hammer://image/12x", "hammer://image/12_", "xhammer://image/3", "_hammer://image/3", "+hammer://image/3",
113 ".hammer://image/3", "-hammer://image/3", "9hammer://image/3", "hammer://image/", "hammer://image/x",
114 ] {
115 assert_eq!(find_ids(text), Vec::<i32>::new(), "{text}");
116 }
117 }
118
119 #[test]
120 fn an_id_too_large_for_an_i32_names_no_image() {
121 assert_eq!(find_ids(" "), vec![3]);
122 }
123}