use regex::Regex;
use std::sync::LazyLock;
static AUTODOC_MARKER: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"^(\s*):::\s+\S+.*$", )
.unwrap()
});
static CROSSREF_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
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_]*)*\]\[\]"
).unwrap()
});
pub fn is_autodoc_marker(line: &str) -> bool {
if !AUTODOC_MARKER.is_match(line) {
return false;
}
let trimmed = line.trim();
if let Some(start) = trimmed.find(":::") {
let after_marker = &trimmed[start + 3..].trim();
if let Some(module_path) = after_marker.split_whitespace().next() {
if module_path.starts_with('{') {
return false;
}
if !module_path.contains('.') && !module_path.contains(':') {
return false;
}
if module_path.starts_with('.') || module_path.starts_with(':') {
return false;
}
if module_path.ends_with('.') || module_path.ends_with(':') {
return false;
}
if module_path.contains("..")
|| module_path.contains("::")
|| module_path.contains(".:")
|| module_path.contains(":.")
{
return false;
}
}
}
true
}
pub fn contains_crossref(line: &str) -> bool {
CROSSREF_PATTERN.is_match(line)
}
pub fn get_autodoc_indent(line: &str) -> Option<usize> {
if is_autodoc_marker(line) {
return Some(super::mkdocs_common::get_line_indent(line));
}
None
}
pub fn is_autodoc_options(line: &str, base_indent: usize) -> bool {
let line_indent = super::mkdocs_common::get_line_indent(line);
if line_indent >= base_indent + 4 {
if line.trim().is_empty() {
return true;
}
if line.contains(':') {
return true;
}
let trimmed = line.trim_start();
if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
return true;
}
}
false
}
pub fn detect_autodoc_block_ranges(content: &str) -> Vec<crate::utils::skip_context::ByteRange> {
let mut ranges = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let mut byte_pos = 0;
let mut in_autodoc = false;
let mut autodoc_indent = 0;
let mut block_start = 0;
for line in lines {
let line_end = byte_pos + line.len();
if is_autodoc_marker(line) {
in_autodoc = true;
autodoc_indent = get_autodoc_indent(line).unwrap_or(0);
block_start = byte_pos;
} else if in_autodoc {
if is_autodoc_options(line, autodoc_indent) {
} else {
if line.is_empty() {
} else {
ranges.push(crate::utils::skip_context::ByteRange {
start: block_start,
end: byte_pos.saturating_sub(1), });
in_autodoc = false;
autodoc_indent = 0;
}
}
}
byte_pos = line_end + 1;
}
if in_autodoc {
ranges.push(crate::utils::skip_context::ByteRange {
start: block_start,
end: byte_pos.saturating_sub(1),
});
}
ranges
}
pub fn is_within_autodoc_block_ranges(ranges: &[crate::utils::skip_context::ByteRange], position: usize) -> bool {
crate::utils::skip_context::is_in_html_comment_ranges(ranges, position)
}
pub fn is_valid_crossref(ref_text: &str) -> bool {
ref_text.contains('.') || ref_text.contains(':')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_autodoc_marker_detection() {
assert!(is_autodoc_marker("::: mymodule.MyClass"));
assert!(is_autodoc_marker("::: package.module.Class"));
assert!(is_autodoc_marker(" ::: indented.Class"));
assert!(is_autodoc_marker("::: module:function"));
assert!(is_autodoc_marker("::: handler:package.module"));
assert!(is_autodoc_marker("::: a.b"));
assert!(!is_autodoc_marker(":: Wrong number"));
assert!(!is_autodoc_marker("Regular text"));
assert!(!is_autodoc_marker(":::"));
assert!(!is_autodoc_marker("::: "));
assert!(!is_autodoc_marker("::: warning"));
assert!(!is_autodoc_marker("::: note"));
assert!(!is_autodoc_marker("::: danger"));
assert!(!is_autodoc_marker("::: sidebar"));
assert!(!is_autodoc_marker(" ::: callout"));
assert!(!is_autodoc_marker("::: {.note}"));
assert!(!is_autodoc_marker("::: {#myid .warning}"));
assert!(!is_autodoc_marker("::: {.note .important}"));
assert!(!is_autodoc_marker("::: .starts.with.dot"));
assert!(!is_autodoc_marker("::: ends.with.dot."));
assert!(!is_autodoc_marker("::: has..consecutive.dots"));
assert!(!is_autodoc_marker("::: :starts.with.colon"));
}
#[test]
fn test_crossref_detection() {
assert!(contains_crossref("See [module.Class][]"));
assert!(contains_crossref("The [text][module.Class] here"));
assert!(contains_crossref("[package.module.Class][]"));
assert!(contains_crossref("[custom text][module:function]"));
assert!(!contains_crossref("Regular [link](url)"));
assert!(!contains_crossref("No references here"));
}
#[test]
fn test_autodoc_options() {
assert!(is_autodoc_options(" handler: python", 0));
assert!(is_autodoc_options(" options:", 0));
assert!(is_autodoc_options(" show_source: true", 0));
assert!(!is_autodoc_options("", 0)); assert!(!is_autodoc_options("Not indented", 0));
assert!(!is_autodoc_options(" Only 2 spaces", 0));
assert!(is_autodoc_options(" - window", 0));
assert!(is_autodoc_options(" - app", 0));
}
#[test]
fn test_valid_crossref() {
assert!(is_valid_crossref("module.Class"));
assert!(is_valid_crossref("package.module.Class"));
assert!(is_valid_crossref("module:function"));
assert!(is_valid_crossref("numpy.ndarray"));
assert!(!is_valid_crossref("simple_word"));
assert!(!is_valid_crossref("no-dots-here"));
}
}