1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/// Extracts all markdown links from a file
///
/// # Examples
///
/// ```ignore
/// use pmat::services::doc_validator::extract_links;
/// use std::path::PathBuf;
///
/// let content = "[example](https://example.com) and [local](./file.md)";
/// let links = extract_links(content, &PathBuf::from("test.md"));
/// assert_eq!(links.len(), 2);
/// ```ignore
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub fn extract_links(content: &str, source_file: &Path) -> Vec<Link> {
let mut links = Vec::new();
let regex = Regex::new(r"\[([^\]]+)\]\(([^)]+)\)").expect("Invalid regex");
for (line_num, line) in content.lines().enumerate() {
// Skip code blocks (lines starting with backticks)
if line.trim_start().starts_with("```") {
continue;
}
for cap in regex.captures_iter(line) {
let text = cap[1].to_string();
let target = cap[2].to_string();
let link_type = classify_link(&target);
links.push(Link {
text,
target,
source_file: source_file.to_path_buf(),
line_number: line_num + 1,
link_type,
});
}
}
links
}
/// Classifies a link target into its type
///
/// # Examples
///
/// ```ignore
/// use pmat::services::doc_validator::{classify_link, LinkType};
///
/// assert_eq!(classify_link("https://example.com"), LinkType::ExternalHttp);
/// assert_eq!(classify_link("./local.md"), LinkType::Internal);
/// assert_eq!(classify_link("#anchor"), LinkType::Anchor);
/// assert_eq!(classify_link("mailto:user@example.com"), LinkType::Email);
/// ```ignore
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn classify_link(target: &str) -> LinkType {
if target.starts_with("http://") || target.starts_with("https://") {
LinkType::ExternalHttp
} else if target.starts_with('#') {
LinkType::Anchor
} else if target.starts_with("mailto:") {
LinkType::Email
} else if target.contains("://") {
LinkType::Other(
target
.split("://")
.next()
.expect("split should have at least one element")
.to_string(),
)
} else {
LinkType::Internal
}
}
/// Normalizes a path by resolving `.` and `..` components
fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
}
std::path::Component::CurDir => {
// Skip current directory
}
_ => {
components.push(component);
}
}
}
components.iter().collect()
}