rumdl_lib/utils/
mkdocstrings_refs.rs1use lazy_static::lazy_static;
12use regex::Regex;
13
14lazy_static! {
15 static ref AUTODOC_MARKER: Regex = Regex::new(
20 r"^(\s*):::\s+\S+.*$" ).unwrap();
22
23 static ref CROSSREF_PATTERN: Regex = Regex::new(
26 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_]*)*\]\[\]"
27 ).unwrap();
28
29 static ref HANDLER_OPTIONS: Regex = Regex::new(
31 r"^(\s{4,})\w+:"
32 ).unwrap();
33
34 static ref VALID_MODULE_PATH: Regex = Regex::new(
36 r"^[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*$"
37 ).unwrap();
38}
39
40pub fn is_autodoc_marker(line: &str) -> bool {
42 if !AUTODOC_MARKER.is_match(line) {
44 return false;
45 }
46
47 let trimmed = line.trim();
50 if let Some(start) = trimmed.find(":::") {
51 let after_marker = &trimmed[start + 3..].trim();
52 if let Some(module_path) = after_marker.split_whitespace().next() {
54 if module_path.starts_with('.') || module_path.starts_with(':') {
56 return false; }
58 if module_path.ends_with('.') || module_path.ends_with(':') {
59 return false; }
61 if module_path.contains("..")
62 || module_path.contains("::")
63 || module_path.contains(".:")
64 || module_path.contains(":.")
65 {
66 return false; }
68 }
69 }
70
71 true
75}
76
77pub fn contains_crossref(line: &str) -> bool {
79 CROSSREF_PATTERN.is_match(line)
80}
81
82pub fn get_autodoc_indent(line: &str) -> Option<usize> {
84 if AUTODOC_MARKER.is_match(line) {
85 return Some(super::mkdocs_common::get_line_indent(line));
87 }
88 None
89}
90
91pub fn is_autodoc_options(line: &str, base_indent: usize) -> bool {
93 let line_indent = super::mkdocs_common::get_line_indent(line);
95
96 if line_indent >= base_indent + 4 {
98 if line.trim().is_empty() {
100 return true;
101 }
102
103 if line.contains(':') {
105 return true;
106 }
107 let trimmed = line.trim_start();
109 if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
110 return true;
111 }
112 }
113
114 false
115}
116
117pub fn detect_autodoc_block_ranges(content: &str) -> Vec<crate::utils::skip_context::ByteRange> {
120 let mut ranges = Vec::new();
121 let lines: Vec<&str> = content.lines().collect();
122 let mut byte_pos = 0;
123 let mut in_autodoc = false;
124 let mut autodoc_indent = 0;
125 let mut block_start = 0;
126
127 for line in lines {
128 let line_end = byte_pos + line.len();
129
130 if is_autodoc_marker(line) {
132 in_autodoc = true;
133 autodoc_indent = get_autodoc_indent(line).unwrap_or(0);
134 block_start = byte_pos;
135 } else if in_autodoc {
136 if is_autodoc_options(line, autodoc_indent) {
138 } else {
140 if line.is_empty() {
143 } else {
145 ranges.push(crate::utils::skip_context::ByteRange {
148 start: block_start,
149 end: byte_pos.saturating_sub(1), });
151 in_autodoc = false;
152 autodoc_indent = 0;
153 }
154 }
155 }
156
157 byte_pos = line_end + 1;
159 }
160
161 if in_autodoc {
163 ranges.push(crate::utils::skip_context::ByteRange {
164 start: block_start,
165 end: byte_pos.saturating_sub(1),
166 });
167 }
168
169 ranges
170}
171
172pub fn is_within_autodoc_block_ranges(ranges: &[crate::utils::skip_context::ByteRange], position: usize) -> bool {
174 crate::utils::skip_context::is_in_html_comment_ranges(ranges, position)
175}
176
177pub fn is_within_autodoc_block(content: &str, position: usize) -> bool {
179 let lines: Vec<&str> = content.lines().collect();
180 let mut byte_pos = 0;
181 let mut in_autodoc = false;
182 let mut autodoc_indent = 0;
183
184 for line in lines {
185 let line_end = byte_pos + line.len();
186
187 if is_autodoc_marker(line) {
189 in_autodoc = true;
190 autodoc_indent = get_autodoc_indent(line).unwrap_or(0);
191 if byte_pos <= position && position <= line_end {
193 return true;
194 }
195 } else if in_autodoc {
196 if is_autodoc_options(line, autodoc_indent) {
198 if byte_pos <= position && position <= line_end {
200 return true;
201 }
202 } else {
203 if line.is_empty() {
206 } else {
208 in_autodoc = false;
210 autodoc_indent = 0;
211 if byte_pos <= position && position <= line_end {
214 return false;
215 }
216 }
217 }
218 }
219
220 byte_pos = line_end + 1;
222 }
223
224 false
225}
226
227pub fn is_valid_crossref(ref_text: &str) -> bool {
229 ref_text.contains('.') || ref_text.contains(':')
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_autodoc_marker_detection() {
240 assert!(is_autodoc_marker("::: mymodule.MyClass"));
241 assert!(is_autodoc_marker("::: package.module.Class"));
242 assert!(is_autodoc_marker(" ::: indented.Class"));
243 assert!(is_autodoc_marker("::: module:function"));
244 assert!(!is_autodoc_marker(":: Wrong number"));
245 assert!(!is_autodoc_marker("Regular text"));
246 }
247
248 #[test]
249 fn test_crossref_detection() {
250 assert!(contains_crossref("See [module.Class][]"));
251 assert!(contains_crossref("The [text][module.Class] here"));
252 assert!(contains_crossref("[package.module.Class][]"));
253 assert!(contains_crossref("[custom text][module:function]"));
254 assert!(!contains_crossref("Regular [link](url)"));
255 assert!(!contains_crossref("No references here"));
256 }
257
258 #[test]
259 fn test_autodoc_options() {
260 assert!(is_autodoc_options(" handler: python", 0));
261 assert!(is_autodoc_options(" options:", 0));
262 assert!(is_autodoc_options(" show_source: true", 0));
263 assert!(!is_autodoc_options("", 0)); assert!(!is_autodoc_options("Not indented", 0));
265 assert!(!is_autodoc_options(" Only 2 spaces", 0));
266 assert!(is_autodoc_options(" - window", 0));
268 assert!(is_autodoc_options(" - app", 0));
269 }
270
271 #[test]
272 fn test_within_autodoc_block() {
273 let content = r#"# API Documentation
274
275::: mymodule.MyClass
276 handler: python
277 options:
278 show_source: true
279 show_root_heading: true
280
281Regular text here.
282
283::: another.Class
284
285More text."#;
286
287 let handler_pos = content.find("handler:").unwrap();
288 let options_pos = content.find("show_source:").unwrap();
289 let regular_pos = content.find("Regular text").unwrap();
290 let more_pos = content.find("More text").unwrap();
291
292 assert!(is_within_autodoc_block(content, handler_pos));
293 assert!(is_within_autodoc_block(content, options_pos));
294 assert!(!is_within_autodoc_block(content, regular_pos));
295 assert!(!is_within_autodoc_block(content, more_pos));
296 }
297
298 #[test]
299 fn test_valid_crossref() {
300 assert!(is_valid_crossref("module.Class"));
301 assert!(is_valid_crossref("package.module.Class"));
302 assert!(is_valid_crossref("module:function"));
303 assert!(is_valid_crossref("numpy.ndarray"));
304 assert!(!is_valid_crossref("simple_word"));
305 assert!(!is_valid_crossref("no-dots-here"));
306 }
307}