use super::mkdocs_common::{BytePositionTracker, ContextStateMachine, MKDOCS_CONTENT_INDENT, get_line_indent};
use regex::Regex;
use std::sync::LazyLock;
static FOOTNOTE_REF: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[\^[a-zA-Z0-9_-]+\]").unwrap());
static FOOTNOTE_DEF: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"^(\s*)\[\^([a-zA-Z0-9_-]+)\]:\s*", )
.unwrap()
});
pub fn is_footnote_definition(line: &str) -> bool {
FOOTNOTE_DEF.is_match(line)
}
pub fn contains_footnote_reference(line: &str) -> bool {
FOOTNOTE_REF.is_match(line)
}
pub fn get_footnote_indent(line: &str) -> Option<usize> {
if FOOTNOTE_DEF.is_match(line) {
return Some(get_line_indent(line));
}
None
}
pub fn is_footnote_continuation(line: &str, base_indent: usize) -> bool {
if line.trim().is_empty() {
return true;
}
get_line_indent(line) >= base_indent + MKDOCS_CONTENT_INDENT
}
pub fn is_within_footnote_definition(content: &str, position: usize) -> bool {
let tracker = BytePositionTracker::new(content);
let mut state = ContextStateMachine::new();
for (_idx, line, start, end) in tracker.iter_with_positions() {
if is_footnote_definition(line) {
let indent = get_footnote_indent(line).unwrap_or(0);
state.enter_context(indent, "footnote".to_string());
} else if state.is_in_context() {
if !line.trim().is_empty() && !is_footnote_continuation(line, state.context_indent()) {
state.exit_context();
if is_footnote_definition(line) {
let indent = get_footnote_indent(line).unwrap_or(0);
state.enter_context(indent, "footnote".to_string());
}
}
}
if start <= position && position <= end && state.is_in_context() {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_footnote_definition_detection() {
assert!(is_footnote_definition("[^1]: This is a footnote"));
assert!(is_footnote_definition("[^note]: Named footnote"));
assert!(is_footnote_definition(" [^2]: Indented footnote"));
assert!(!is_footnote_definition("[^1] Reference in text"));
assert!(!is_footnote_definition("Regular text"));
}
#[test]
fn test_footnote_reference_detection() {
assert!(contains_footnote_reference("Text with [^1] reference"));
assert!(contains_footnote_reference("Multiple [^1] and [^2] refs"));
assert!(contains_footnote_reference("[^named-ref]"));
assert!(!contains_footnote_reference("No references here"));
}
#[test]
fn test_footnote_continuation() {
assert!(is_footnote_continuation(" Continued content", 0));
assert!(is_footnote_continuation(" More indented", 0));
assert!(is_footnote_continuation("", 0)); assert!(!is_footnote_continuation("Not indented enough", 0));
assert!(!is_footnote_continuation(" Only 2 spaces", 0));
}
#[test]
fn test_within_footnote_definition() {
let content = r#"Regular text here.
[^1]: This is a footnote definition
with multiple lines
of content.
More regular text.
[^2]: Another footnote
Also multi-line.
End text."#;
let def_pos = content.find("footnote definition").unwrap();
let multi_pos = content.find("with multiple").unwrap();
let regular_pos = content.find("More regular").unwrap();
let end_pos = content.find("End text").unwrap();
assert!(is_within_footnote_definition(content, def_pos));
assert!(is_within_footnote_definition(content, multi_pos));
assert!(!is_within_footnote_definition(content, regular_pos));
assert!(!is_within_footnote_definition(content, end_pos));
}
}