logicaffeine_language/analysis/
dependencies.rs1#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Dependency {
13 pub alias: String,
15 pub uri: String,
17 pub start: usize,
19 pub end: usize,
21}
22
23pub fn scan_dependencies(source: &str) -> Vec<Dependency> {
43 let mut dependencies = Vec::new();
44 let mut in_abstract = false;
45 let mut abstract_started = false;
46 let mut current_pos = 0;
47
48 for line in source.lines() {
49 let line_start = current_pos;
50 let trimmed = line.trim();
51
52 current_pos += line.len() + 1; if trimmed.is_empty() {
57 if abstract_started && in_abstract {
58 break;
60 }
61 continue;
62 }
63
64 if trimmed.starts_with("# ") && !trimmed.starts_with("## ") {
66 continue;
67 }
68
69 if trimmed.starts_with("## ") {
71 break;
72 }
73
74 in_abstract = true;
76 abstract_started = true;
77
78 scan_line_for_links(line, line_start, &mut dependencies);
80 }
81
82 dependencies
83}
84
85fn scan_line_for_links(line: &str, line_start: usize, deps: &mut Vec<Dependency>) {
87 let bytes = line.as_bytes();
88 let mut i = 0;
89
90 while i < bytes.len() {
91 if bytes[i] == b'[' {
93 let link_start = line_start + i;
94 i += 1;
95
96 let alias_start = i;
98 while i < bytes.len() && bytes[i] != b']' {
99 i += 1;
100 }
101
102 if i >= bytes.len() {
103 break;
105 }
106
107 let alias = &line[alias_start..i];
108 i += 1; if i >= bytes.len() || bytes[i] != b'(' {
112 continue;
113 }
114 i += 1; let uri_start = i;
118 let mut paren_depth = 1;
119 while i < bytes.len() && paren_depth > 0 {
120 if bytes[i] == b'(' {
121 paren_depth += 1;
122 } else if bytes[i] == b')' {
123 paren_depth -= 1;
124 }
125 if paren_depth > 0 {
126 i += 1;
127 }
128 }
129
130 if paren_depth != 0 {
131 break;
133 }
134
135 let uri = &line[uri_start..i];
136 let link_end = line_start + i + 1;
137 i += 1; if alias.is_empty() || uri.is_empty() {
141 continue;
142 }
143
144 deps.push(Dependency {
145 alias: alias.trim().to_string(),
146 uri: uri.trim().to_string(),
147 start: link_start,
148 end: link_end,
149 });
150 } else {
151 i += 1;
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn basic_dependency_scanning() {
162 let source = r#"
163# My Game
164
165This uses [Geometry](file:./geo.md) and [Physics](logos:std).
166
167## Main
168Let x be 1.
169"#;
170 let deps = scan_dependencies(source);
171 assert_eq!(deps.len(), 2);
172 assert_eq!(deps[0].alias, "Geometry");
173 assert_eq!(deps[0].uri, "file:./geo.md");
174 assert_eq!(deps[1].alias, "Physics");
175 assert_eq!(deps[1].uri, "logos:std");
176 }
177
178 #[test]
179 fn ignores_links_after_abstract() {
180 let source = r#"
181# Header
182
183This is the abstract with [Dep1](file:a.md).
184
185This second paragraph has [Dep2](file:b.md).
186
187## Main
188Let x be 1.
189"#;
190 let deps = scan_dependencies(source);
191 assert_eq!(deps.len(), 1);
192 assert_eq!(deps[0].alias, "Dep1");
193 }
194
195 #[test]
196 fn no_dependencies_without_abstract() {
197 let source = r#"
198# Module
199
200## Main
201Let x be 1.
202"#;
203 let deps = scan_dependencies(source);
204 assert_eq!(deps.len(), 0);
205 }
206
207 #[test]
208 fn multiline_abstract() {
209 let source = r#"
210# My Project
211
212This project uses [Math](file:./math.md) for calculations
213and [IO](file:./io.md) for input/output operations.
214
215## Main
216Let x be 1.
217"#;
218 let deps = scan_dependencies(source);
219 assert_eq!(deps.len(), 2);
220 assert_eq!(deps[0].alias, "Math");
221 assert_eq!(deps[1].alias, "IO");
222 }
223
224 #[test]
225 fn handles_spaces_in_alias() {
226 let source = r#"
227# App
228
229Uses the [Standard Library](logos:std).
230
231## Main
232"#;
233 let deps = scan_dependencies(source);
234 assert_eq!(deps.len(), 1);
235 assert_eq!(deps[0].alias, "Standard Library");
236 assert_eq!(deps[0].uri, "logos:std");
237 }
238
239 #[test]
240 fn handles_https_urls() {
241 let source = r#"
242# App
243
244Uses [Physics](https://logicaffeine.dev/pkg/physics).
245
246## Main
247"#;
248 let deps = scan_dependencies(source);
249 assert_eq!(deps.len(), 1);
250 assert_eq!(deps[0].alias, "Physics");
251 assert_eq!(deps[0].uri, "https://logicaffeine.dev/pkg/physics");
252 }
253
254 #[test]
255 fn handles_multiple_links_on_one_line() {
256 let source = r#"
257# App
258
259Uses [A](file:a.md), [B](file:b.md), and [C](file:c.md).
260
261## Main
262"#;
263 let deps = scan_dependencies(source);
264 assert_eq!(deps.len(), 3);
265 assert_eq!(deps[0].alias, "A");
266 assert_eq!(deps[1].alias, "B");
267 assert_eq!(deps[2].alias, "C");
268 }
269}