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.trim().is_empty() {
98 return true;
99 }
100
101 if line_indent >= base_indent + 4 && line.contains(':') {
103 return true;
104 }
105
106 false
107}
108
109pub fn is_within_autodoc_block(content: &str, position: usize) -> bool {
111 let lines: Vec<&str> = content.lines().collect();
112 let mut byte_pos = 0;
113 let mut in_autodoc = false;
114 let mut autodoc_indent = 0;
115
116 for line in lines {
117 let line_end = byte_pos + line.len();
118
119 if is_autodoc_marker(line) {
121 in_autodoc = true;
122 autodoc_indent = get_autodoc_indent(line).unwrap_or(0);
123 } else if in_autodoc {
124 if !is_autodoc_options(line, autodoc_indent) && !line.trim().is_empty() {
126 in_autodoc = false;
128 autodoc_indent = 0;
129 }
130 }
131
132 if byte_pos <= position && position <= line_end && in_autodoc {
134 return true;
135 }
136
137 byte_pos = line_end + 1;
139 }
140
141 false
142}
143
144pub fn is_valid_crossref(ref_text: &str) -> bool {
146 ref_text.contains('.') || ref_text.contains(':')
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_autodoc_marker_detection() {
157 assert!(is_autodoc_marker("::: mymodule.MyClass"));
158 assert!(is_autodoc_marker("::: package.module.Class"));
159 assert!(is_autodoc_marker(" ::: indented.Class"));
160 assert!(is_autodoc_marker("::: module:function"));
161 assert!(!is_autodoc_marker(":: Wrong number"));
162 assert!(!is_autodoc_marker("Regular text"));
163 }
164
165 #[test]
166 fn test_crossref_detection() {
167 assert!(contains_crossref("See [module.Class][]"));
168 assert!(contains_crossref("The [text][module.Class] here"));
169 assert!(contains_crossref("[package.module.Class][]"));
170 assert!(contains_crossref("[custom text][module:function]"));
171 assert!(!contains_crossref("Regular [link](url)"));
172 assert!(!contains_crossref("No references here"));
173 }
174
175 #[test]
176 fn test_autodoc_options() {
177 assert!(is_autodoc_options(" handler: python", 0));
178 assert!(is_autodoc_options(" options:", 0));
179 assert!(is_autodoc_options(" show_source: true", 0));
180 assert!(is_autodoc_options("", 0)); assert!(!is_autodoc_options("Not indented", 0));
182 assert!(!is_autodoc_options(" Only 2 spaces", 0));
183 }
184
185 #[test]
186 fn test_within_autodoc_block() {
187 let content = r#"# API Documentation
188
189::: mymodule.MyClass
190 handler: python
191 options:
192 show_source: true
193 show_root_heading: true
194
195Regular text here.
196
197::: another.Class
198
199More text."#;
200
201 let handler_pos = content.find("handler:").unwrap();
202 let options_pos = content.find("show_source:").unwrap();
203 let regular_pos = content.find("Regular text").unwrap();
204 let more_pos = content.find("More text").unwrap();
205
206 assert!(is_within_autodoc_block(content, handler_pos));
207 assert!(is_within_autodoc_block(content, options_pos));
208 assert!(!is_within_autodoc_block(content, regular_pos));
209 assert!(!is_within_autodoc_block(content, more_pos));
210 }
211
212 #[test]
213 fn test_valid_crossref() {
214 assert!(is_valid_crossref("module.Class"));
215 assert!(is_valid_crossref("package.module.Class"));
216 assert!(is_valid_crossref("module:function"));
217 assert!(is_valid_crossref("numpy.ndarray"));
218 assert!(!is_valid_crossref("simple_word"));
219 assert!(!is_valid_crossref("no-dots-here"));
220 }
221}