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 is_within_autodoc_block(content: &str, position: usize) -> bool {
119 let lines: Vec<&str> = content.lines().collect();
120 let mut byte_pos = 0;
121 let mut in_autodoc = false;
122 let mut autodoc_indent = 0;
123
124 for line in lines {
125 let line_end = byte_pos + line.len();
126
127 if is_autodoc_marker(line) {
129 in_autodoc = true;
130 autodoc_indent = get_autodoc_indent(line).unwrap_or(0);
131 if byte_pos <= position && position <= line_end {
133 return true;
134 }
135 } else if in_autodoc {
136 if is_autodoc_options(line, autodoc_indent) {
138 if byte_pos <= position && position <= line_end {
140 return true;
141 }
142 } else {
143 if line.is_empty() {
146 } else {
148 in_autodoc = false;
150 autodoc_indent = 0;
151 if byte_pos <= position && position <= line_end {
154 return false;
155 }
156 }
157 }
158 }
159
160 byte_pos = line_end + 1;
162 }
163
164 false
165}
166
167pub fn is_valid_crossref(ref_text: &str) -> bool {
169 ref_text.contains('.') || ref_text.contains(':')
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_autodoc_marker_detection() {
180 assert!(is_autodoc_marker("::: mymodule.MyClass"));
181 assert!(is_autodoc_marker("::: package.module.Class"));
182 assert!(is_autodoc_marker(" ::: indented.Class"));
183 assert!(is_autodoc_marker("::: module:function"));
184 assert!(!is_autodoc_marker(":: Wrong number"));
185 assert!(!is_autodoc_marker("Regular text"));
186 }
187
188 #[test]
189 fn test_crossref_detection() {
190 assert!(contains_crossref("See [module.Class][]"));
191 assert!(contains_crossref("The [text][module.Class] here"));
192 assert!(contains_crossref("[package.module.Class][]"));
193 assert!(contains_crossref("[custom text][module:function]"));
194 assert!(!contains_crossref("Regular [link](url)"));
195 assert!(!contains_crossref("No references here"));
196 }
197
198 #[test]
199 fn test_autodoc_options() {
200 assert!(is_autodoc_options(" handler: python", 0));
201 assert!(is_autodoc_options(" options:", 0));
202 assert!(is_autodoc_options(" show_source: true", 0));
203 assert!(!is_autodoc_options("", 0)); assert!(!is_autodoc_options("Not indented", 0));
205 assert!(!is_autodoc_options(" Only 2 spaces", 0));
206 assert!(is_autodoc_options(" - window", 0));
208 assert!(is_autodoc_options(" - app", 0));
209 }
210
211 #[test]
212 fn test_within_autodoc_block() {
213 let content = r#"# API Documentation
214
215::: mymodule.MyClass
216 handler: python
217 options:
218 show_source: true
219 show_root_heading: true
220
221Regular text here.
222
223::: another.Class
224
225More text."#;
226
227 let handler_pos = content.find("handler:").unwrap();
228 let options_pos = content.find("show_source:").unwrap();
229 let regular_pos = content.find("Regular text").unwrap();
230 let more_pos = content.find("More text").unwrap();
231
232 assert!(is_within_autodoc_block(content, handler_pos));
233 assert!(is_within_autodoc_block(content, options_pos));
234 assert!(!is_within_autodoc_block(content, regular_pos));
235 assert!(!is_within_autodoc_block(content, more_pos));
236 }
237
238 #[test]
239 fn test_valid_crossref() {
240 assert!(is_valid_crossref("module.Class"));
241 assert!(is_valid_crossref("package.module.Class"));
242 assert!(is_valid_crossref("module:function"));
243 assert!(is_valid_crossref("numpy.ndarray"));
244 assert!(!is_valid_crossref("simple_word"));
245 assert!(!is_valid_crossref("no-dots-here"));
246 }
247}