rumdl_lib/utils/
mkdocstrings_refs.rs1use regex::Regex;
2use std::sync::LazyLock;
13
14static AUTODOC_MARKER: LazyLock<Regex> = LazyLock::new(|| {
19 Regex::new(
20 r"^(\s*):::\s+\S+.*$", )
22 .unwrap()
23});
24
25static CROSSREF_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
28 Regex::new(
29 r"\[(?:[^\]]*)\]\[[a-zA-Z_][a-zA-Z0-9_]*(?:[:\.][a-zA-Z_][a-zA-Z0-9_]*)*\]|\[[a-zA-Z_][a-zA-Z0-9_]*(?:[:\.][a-zA-Z_][a-zA-Z0-9_]*)*\]\[\]"
30 ).unwrap()
31});
32
33pub fn is_autodoc_marker(line: &str) -> bool {
40 if !AUTODOC_MARKER.is_match(line) {
42 return false;
43 }
44
45 let trimmed = line.trim();
46 if let Some(start) = trimmed.find(":::") {
47 let after_marker = &trimmed[start + 3..].trim();
48 if let Some(module_path) = after_marker.split_whitespace().next() {
50 if module_path.starts_with('{') {
52 return false;
53 }
54
55 if !module_path.contains('.') && !module_path.contains(':') {
59 return false;
60 }
61
62 if module_path.starts_with('.') || module_path.starts_with(':') {
64 return false;
65 }
66 if module_path.ends_with('.') || module_path.ends_with(':') {
67 return false;
68 }
69 if module_path.contains("..")
71 || module_path.contains("::")
72 || module_path.contains(".:")
73 || module_path.contains(":.")
74 {
75 return false;
76 }
77 }
78 }
79
80 true
81}
82
83pub fn contains_crossref(line: &str) -> bool {
85 CROSSREF_PATTERN.is_match(line)
86}
87
88pub fn get_autodoc_indent(line: &str) -> Option<usize> {
90 if is_autodoc_marker(line) {
91 return Some(super::mkdocs_common::get_line_indent(line));
92 }
93 None
94}
95
96pub fn is_autodoc_options(line: &str, base_indent: usize) -> bool {
98 let line_indent = super::mkdocs_common::get_line_indent(line);
100
101 if line_indent >= base_indent + 4 {
103 if line.trim().is_empty() {
105 return true;
106 }
107
108 if line.contains(':') {
110 return true;
111 }
112 let trimmed = line.trim_start();
114 if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
115 return true;
116 }
117 }
118
119 false
120}
121
122pub fn detect_autodoc_block_ranges(content: &str) -> Vec<crate::utils::skip_context::ByteRange> {
125 let mut ranges = Vec::new();
126 let lines: Vec<&str> = content.lines().collect();
127 let mut byte_pos = 0;
128 let mut in_autodoc = false;
129 let mut autodoc_indent = 0;
130 let mut block_start = 0;
131
132 for line in lines {
133 let line_end = byte_pos + line.len();
134
135 if is_autodoc_marker(line) {
137 in_autodoc = true;
138 autodoc_indent = get_autodoc_indent(line).unwrap_or(0);
139 block_start = byte_pos;
140 } else if in_autodoc {
141 if is_autodoc_options(line, autodoc_indent) {
143 } else {
145 if line.is_empty() {
148 } else {
150 ranges.push(crate::utils::skip_context::ByteRange {
153 start: block_start,
154 end: byte_pos.saturating_sub(1), });
156 in_autodoc = false;
157 autodoc_indent = 0;
158 }
159 }
160 }
161
162 byte_pos = line_end + 1;
164 }
165
166 if in_autodoc {
168 ranges.push(crate::utils::skip_context::ByteRange {
169 start: block_start,
170 end: byte_pos.saturating_sub(1),
171 });
172 }
173
174 ranges
175}
176
177pub fn is_within_autodoc_block_ranges(ranges: &[crate::utils::skip_context::ByteRange], position: usize) -> bool {
179 crate::utils::skip_context::is_in_html_comment_ranges(ranges, position)
180}
181
182pub fn is_valid_crossref(ref_text: &str) -> bool {
184 ref_text.contains('.') || ref_text.contains(':')
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_autodoc_marker_detection() {
195 assert!(is_autodoc_marker("::: mymodule.MyClass"));
197 assert!(is_autodoc_marker("::: package.module.Class"));
198 assert!(is_autodoc_marker(" ::: indented.Class"));
199 assert!(is_autodoc_marker("::: module:function"));
200 assert!(is_autodoc_marker("::: handler:package.module"));
201 assert!(is_autodoc_marker("::: a.b"));
202
203 assert!(!is_autodoc_marker(":: Wrong number"));
205 assert!(!is_autodoc_marker("Regular text"));
206 assert!(!is_autodoc_marker(":::"));
207 assert!(!is_autodoc_marker("::: "));
208
209 assert!(!is_autodoc_marker("::: warning"));
211 assert!(!is_autodoc_marker("::: note"));
212 assert!(!is_autodoc_marker("::: danger"));
213 assert!(!is_autodoc_marker("::: sidebar"));
214 assert!(!is_autodoc_marker(" ::: callout"));
215
216 assert!(!is_autodoc_marker("::: {.note}"));
218 assert!(!is_autodoc_marker("::: {#myid .warning}"));
219 assert!(!is_autodoc_marker("::: {.note .important}"));
220
221 assert!(!is_autodoc_marker("::: .starts.with.dot"));
223 assert!(!is_autodoc_marker("::: ends.with.dot."));
224 assert!(!is_autodoc_marker("::: has..consecutive.dots"));
225 assert!(!is_autodoc_marker("::: :starts.with.colon"));
226 }
227
228 #[test]
229 fn test_crossref_detection() {
230 assert!(contains_crossref("See [module.Class][]"));
231 assert!(contains_crossref("The [text][module.Class] here"));
232 assert!(contains_crossref("[package.module.Class][]"));
233 assert!(contains_crossref("[custom text][module:function]"));
234 assert!(!contains_crossref("Regular [link](url)"));
235 assert!(!contains_crossref("No references here"));
236 }
237
238 #[test]
239 fn test_autodoc_options() {
240 assert!(is_autodoc_options(" handler: python", 0));
241 assert!(is_autodoc_options(" options:", 0));
242 assert!(is_autodoc_options(" show_source: true", 0));
243 assert!(!is_autodoc_options("", 0)); assert!(!is_autodoc_options("Not indented", 0));
245 assert!(!is_autodoc_options(" Only 2 spaces", 0));
246 assert!(is_autodoc_options(" - window", 0));
248 assert!(is_autodoc_options(" - app", 0));
249 }
250
251 #[test]
252 fn test_valid_crossref() {
253 assert!(is_valid_crossref("module.Class"));
254 assert!(is_valid_crossref("package.module.Class"));
255 assert!(is_valid_crossref("module:function"));
256 assert!(is_valid_crossref("numpy.ndarray"));
257 assert!(!is_valid_crossref("simple_word"));
258 assert!(!is_valid_crossref("no-dots-here"));
259 }
260}