mermaid_cli/domain/
image_token.rs1use std::sync::OnceLock;
12
13use regex::Regex;
14
15fn token_re() -> &'static Regex {
19 static RE: OnceLock<Regex> = OnceLock::new();
20 RE.get_or_init(|| Regex::new(r"\[Image #(\d+)\]").expect("valid image-token regex"))
21}
22
23pub fn render_token(n: u64) -> String {
27 format!("[Image #{n}] ")
28}
29
30pub fn token_ending_at(buf: &str, cursor: usize) -> Option<(usize, u64)> {
35 token_re().captures_iter(buf).find_map(|c| {
36 let whole = c.get(0)?;
37 if whole.end() != cursor {
38 return None;
39 }
40 let n = c.get(1)?.as_str().parse::<u64>().ok()?;
41 Some((whole.start(), n))
42 })
43}
44
45pub fn token_starting_at(buf: &str, cursor: usize) -> Option<(usize, u64)> {
48 token_re().captures_iter(buf).find_map(|c| {
49 let whole = c.get(0)?;
50 if whole.start() != cursor {
51 return None;
52 }
53 let n = c.get(1)?.as_str().parse::<u64>().ok()?;
54 Some((whole.end(), n))
55 })
56}
57
58pub fn numbers_in_order(text: &str) -> Vec<u64> {
62 let mut out = Vec::new();
63 for c in token_re().captures_iter(text) {
64 if let Some(n) = c.get(1).and_then(|m| m.as_str().parse::<u64>().ok())
65 && !out.contains(&n)
66 {
67 out.push(n);
68 }
69 }
70 out
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn render_token_has_trailing_space() {
79 assert_eq!(render_token(7), "[Image #7] ");
80 assert_eq!(render_token(16), "[Image #16] ");
81 }
82
83 #[test]
84 fn token_ending_at_hits_only_the_exact_boundary() {
85 let buf = "see [Image #3] here";
86 assert_eq!(token_ending_at(buf, 14), Some((4, 3)));
88 assert_eq!(token_ending_at(buf, 13), None);
90 assert_eq!(token_ending_at(buf, 15), None);
91 assert_eq!(token_ending_at(buf, 9), None);
93 }
94
95 #[test]
96 fn token_ending_at_picks_the_right_one_among_many() {
97 let buf = "[Image #1][Image #2]";
98 assert_eq!(token_ending_at(buf, 10), Some((0, 1))); assert_eq!(token_ending_at(buf, 20), Some((10, 2))); }
101
102 #[test]
103 fn token_starting_at_is_symmetric() {
104 let buf = "[Image #2] a [Image #5]";
105 assert_eq!(token_starting_at(buf, 0), Some((10, 2)));
106 assert_eq!(token_starting_at(buf, 13), Some((23, 5)));
107 assert_eq!(token_starting_at(buf, 1), None);
108 }
109
110 #[test]
111 fn numbers_in_order_dedups_and_preserves_first_appearance() {
112 assert_eq!(numbers_in_order("hello"), Vec::<u64>::new());
113 assert_eq!(numbers_in_order("[Image #1]"), vec![1]);
114 assert_eq!(numbers_in_order("[Image #5] x [Image #3]"), vec![5, 3]);
116 assert_eq!(numbers_in_order("[Image #2] [Image #2]"), vec![2]);
118 assert_eq!(
119 numbers_in_order("a [Image #7] b [Image #7] c [Image #1]"),
120 vec![7, 1]
121 );
122 }
123
124 #[test]
125 fn overflowing_numbers_are_ignored_not_panicked() {
126 let huge = "[Image #99999999999999999999999999]"; assert_eq!(numbers_in_order(huge), Vec::<u64>::new());
128 assert_eq!(token_ending_at(huge, huge.len()), None);
129 }
130
131 #[test]
132 fn non_token_brackets_are_not_matched() {
133 assert_eq!(
134 numbers_in_order("[Image] [Img #3] [Image #x]"),
135 Vec::<u64>::new()
136 );
137 }
138}