#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dependency {
pub alias: String,
pub uri: String,
pub start: usize,
pub end: usize,
}
pub fn scan_dependencies(source: &str) -> Vec<Dependency> {
let mut dependencies = Vec::new();
let mut in_abstract = false;
let mut abstract_started = false;
let mut current_pos = 0;
for line in source.lines() {
let line_start = current_pos;
let trimmed = line.trim();
current_pos += line.len() + 1;
if trimmed.is_empty() {
if abstract_started && in_abstract {
break;
}
continue;
}
if trimmed.starts_with("# ") && !trimmed.starts_with("## ") {
continue;
}
if trimmed.starts_with("## ") {
break;
}
in_abstract = true;
abstract_started = true;
scan_line_for_links(line, line_start, &mut dependencies);
}
dependencies
}
fn scan_line_for_links(line: &str, line_start: usize, deps: &mut Vec<Dependency>) {
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'[' {
let link_start = line_start + i;
i += 1;
let alias_start = i;
while i < bytes.len() && bytes[i] != b']' {
i += 1;
}
if i >= bytes.len() {
break;
}
let alias = &line[alias_start..i];
i += 1;
if i >= bytes.len() || bytes[i] != b'(' {
continue;
}
i += 1;
let uri_start = i;
let mut paren_depth = 1;
while i < bytes.len() && paren_depth > 0 {
if bytes[i] == b'(' {
paren_depth += 1;
} else if bytes[i] == b')' {
paren_depth -= 1;
}
if paren_depth > 0 {
i += 1;
}
}
if paren_depth != 0 {
break;
}
let uri = &line[uri_start..i];
let link_end = line_start + i + 1;
i += 1;
if alias.is_empty() || uri.is_empty() {
continue;
}
deps.push(Dependency {
alias: alias.trim().to_string(),
uri: uri.trim().to_string(),
start: link_start,
end: link_end,
});
} else {
i += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_dependency_scanning() {
let source = r#"
# My Game
This uses [Geometry](file:./geo.md) and [Physics](logos:std).
## Main
Let x be 1.
"#;
let deps = scan_dependencies(source);
assert_eq!(deps.len(), 2);
assert_eq!(deps[0].alias, "Geometry");
assert_eq!(deps[0].uri, "file:./geo.md");
assert_eq!(deps[1].alias, "Physics");
assert_eq!(deps[1].uri, "logos:std");
}
#[test]
fn ignores_links_after_abstract() {
let source = r#"
# Header
This is the abstract with [Dep1](file:a.md).
This second paragraph has [Dep2](file:b.md).
## Main
Let x be 1.
"#;
let deps = scan_dependencies(source);
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].alias, "Dep1");
}
#[test]
fn no_dependencies_without_abstract() {
let source = r#"
# Module
## Main
Let x be 1.
"#;
let deps = scan_dependencies(source);
assert_eq!(deps.len(), 0);
}
#[test]
fn multiline_abstract() {
let source = r#"
# My Project
This project uses [Math](file:./math.md) for calculations
and [IO](file:./io.md) for input/output operations.
## Main
Let x be 1.
"#;
let deps = scan_dependencies(source);
assert_eq!(deps.len(), 2);
assert_eq!(deps[0].alias, "Math");
assert_eq!(deps[1].alias, "IO");
}
#[test]
fn handles_spaces_in_alias() {
let source = r#"
# App
Uses the [Standard Library](logos:std).
## Main
"#;
let deps = scan_dependencies(source);
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].alias, "Standard Library");
assert_eq!(deps[0].uri, "logos:std");
}
#[test]
fn handles_https_urls() {
let source = r#"
# App
Uses [Physics](https://logicaffeine.dev/pkg/physics).
## Main
"#;
let deps = scan_dependencies(source);
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].alias, "Physics");
assert_eq!(deps[0].uri, "https://logicaffeine.dev/pkg/physics");
}
#[test]
fn handles_multiple_links_on_one_line() {
let source = r#"
# App
Uses [A](file:a.md), [B](file:b.md), and [C](file:c.md).
## Main
"#;
let deps = scan_dependencies(source);
assert_eq!(deps.len(), 3);
assert_eq!(deps[0].alias, "A");
assert_eq!(deps[1].alias, "B");
assert_eq!(deps[2].alias, "C");
}
}