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 {
35 if !AUTODOC_MARKER.is_match(line) {
37 return false;
38 }
39
40 let trimmed = line.trim();
43 if let Some(start) = trimmed.find(":::") {
44 let after_marker = &trimmed[start + 3..].trim();
45 if let Some(module_path) = after_marker.split_whitespace().next() {
47 if module_path.starts_with('.') || module_path.starts_with(':') {
49 return false; }
51 if module_path.ends_with('.') || module_path.ends_with(':') {
52 return false; }
54 if module_path.contains("..")
55 || module_path.contains("::")
56 || module_path.contains(".:")
57 || module_path.contains(":.")
58 {
59 return false; }
61 }
62 }
63
64 true
68}
69
70pub fn contains_crossref(line: &str) -> bool {
72 CROSSREF_PATTERN.is_match(line)
73}
74
75pub fn get_autodoc_indent(line: &str) -> Option<usize> {
77 if AUTODOC_MARKER.is_match(line) {
78 return Some(super::mkdocs_common::get_line_indent(line));
80 }
81 None
82}
83
84pub fn is_autodoc_options(line: &str, base_indent: usize) -> bool {
86 let line_indent = super::mkdocs_common::get_line_indent(line);
88
89 if line_indent >= base_indent + 4 {
91 if line.trim().is_empty() {
93 return true;
94 }
95
96 if line.contains(':') {
98 return true;
99 }
100 let trimmed = line.trim_start();
102 if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
103 return true;
104 }
105 }
106
107 false
108}
109
110pub fn detect_autodoc_block_ranges(content: &str) -> Vec<crate::utils::skip_context::ByteRange> {
113 let mut ranges = Vec::new();
114 let lines: Vec<&str> = content.lines().collect();
115 let mut byte_pos = 0;
116 let mut in_autodoc = false;
117 let mut autodoc_indent = 0;
118 let mut block_start = 0;
119
120 for line in lines {
121 let line_end = byte_pos + line.len();
122
123 if is_autodoc_marker(line) {
125 in_autodoc = true;
126 autodoc_indent = get_autodoc_indent(line).unwrap_or(0);
127 block_start = byte_pos;
128 } else if in_autodoc {
129 if is_autodoc_options(line, autodoc_indent) {
131 } else {
133 if line.is_empty() {
136 } else {
138 ranges.push(crate::utils::skip_context::ByteRange {
141 start: block_start,
142 end: byte_pos.saturating_sub(1), });
144 in_autodoc = false;
145 autodoc_indent = 0;
146 }
147 }
148 }
149
150 byte_pos = line_end + 1;
152 }
153
154 if in_autodoc {
156 ranges.push(crate::utils::skip_context::ByteRange {
157 start: block_start,
158 end: byte_pos.saturating_sub(1),
159 });
160 }
161
162 ranges
163}
164
165pub fn is_within_autodoc_block_ranges(ranges: &[crate::utils::skip_context::ByteRange], position: usize) -> bool {
167 crate::utils::skip_context::is_in_html_comment_ranges(ranges, position)
168}
169
170pub fn is_within_autodoc_block(content: &str, position: usize) -> bool {
172 let lines: Vec<&str> = content.lines().collect();
173 let mut byte_pos = 0;
174 let mut in_autodoc = false;
175 let mut autodoc_indent = 0;
176
177 for line in lines {
178 let line_end = byte_pos + line.len();
179
180 if is_autodoc_marker(line) {
182 in_autodoc = true;
183 autodoc_indent = get_autodoc_indent(line).unwrap_or(0);
184 if byte_pos <= position && position <= line_end {
186 return true;
187 }
188 } else if in_autodoc {
189 if is_autodoc_options(line, autodoc_indent) {
191 if byte_pos <= position && position <= line_end {
193 return true;
194 }
195 } else {
196 if line.is_empty() {
199 } else {
201 in_autodoc = false;
203 autodoc_indent = 0;
204 if byte_pos <= position && position <= line_end {
207 return false;
208 }
209 }
210 }
211 }
212
213 byte_pos = line_end + 1;
215 }
216
217 false
218}
219
220pub fn is_valid_crossref(ref_text: &str) -> bool {
222 ref_text.contains('.') || ref_text.contains(':')
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_autodoc_marker_detection() {
233 assert!(is_autodoc_marker("::: mymodule.MyClass"));
234 assert!(is_autodoc_marker("::: package.module.Class"));
235 assert!(is_autodoc_marker(" ::: indented.Class"));
236 assert!(is_autodoc_marker("::: module:function"));
237 assert!(!is_autodoc_marker(":: Wrong number"));
238 assert!(!is_autodoc_marker("Regular text"));
239 }
240
241 #[test]
242 fn test_crossref_detection() {
243 assert!(contains_crossref("See [module.Class][]"));
244 assert!(contains_crossref("The [text][module.Class] here"));
245 assert!(contains_crossref("[package.module.Class][]"));
246 assert!(contains_crossref("[custom text][module:function]"));
247 assert!(!contains_crossref("Regular [link](url)"));
248 assert!(!contains_crossref("No references here"));
249 }
250
251 #[test]
252 fn test_autodoc_options() {
253 assert!(is_autodoc_options(" handler: python", 0));
254 assert!(is_autodoc_options(" options:", 0));
255 assert!(is_autodoc_options(" show_source: true", 0));
256 assert!(!is_autodoc_options("", 0)); assert!(!is_autodoc_options("Not indented", 0));
258 assert!(!is_autodoc_options(" Only 2 spaces", 0));
259 assert!(is_autodoc_options(" - window", 0));
261 assert!(is_autodoc_options(" - app", 0));
262 }
263
264 #[test]
265 fn test_within_autodoc_block() {
266 let content = r#"# API Documentation
267
268::: mymodule.MyClass
269 handler: python
270 options:
271 show_source: true
272 show_root_heading: true
273
274Regular text here.
275
276::: another.Class
277
278More text."#;
279
280 let handler_pos = content.find("handler:").unwrap();
281 let options_pos = content.find("show_source:").unwrap();
282 let regular_pos = content.find("Regular text").unwrap();
283 let more_pos = content.find("More text").unwrap();
284
285 assert!(is_within_autodoc_block(content, handler_pos));
286 assert!(is_within_autodoc_block(content, options_pos));
287 assert!(!is_within_autodoc_block(content, regular_pos));
288 assert!(!is_within_autodoc_block(content, more_pos));
289 }
290
291 #[test]
292 fn test_valid_crossref() {
293 assert!(is_valid_crossref("module.Class"));
294 assert!(is_valid_crossref("package.module.Class"));
295 assert!(is_valid_crossref("module:function"));
296 assert!(is_valid_crossref("numpy.ndarray"));
297 assert!(!is_valid_crossref("simple_word"));
298 assert!(!is_valid_crossref("no-dots-here"));
299 }
300}