rumdl_lib/utils/
mkdocs_footnotes.rs1use super::mkdocs_common::{BytePositionTracker, ContextStateMachine, MKDOCS_CONTENT_INDENT, get_line_indent};
2use regex::Regex;
3use std::sync::LazyLock;
14
15static FOOTNOTE_REF: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[\^[a-zA-Z0-9_-]+\]").unwrap());
17
18static FOOTNOTE_DEF: LazyLock<Regex> = LazyLock::new(|| {
22 Regex::new(
23 r"^(\s*)\[\^([a-zA-Z0-9_-]+)\]:\s*", )
25 .unwrap()
26});
27
28pub fn is_footnote_definition(line: &str) -> bool {
30 FOOTNOTE_DEF.is_match(line)
31}
32
33pub fn contains_footnote_reference(line: &str) -> bool {
35 FOOTNOTE_REF.is_match(line)
36}
37
38pub fn get_footnote_indent(line: &str) -> Option<usize> {
40 if FOOTNOTE_DEF.is_match(line) {
41 return Some(get_line_indent(line));
43 }
44 None
45}
46
47pub fn is_footnote_continuation(line: &str, base_indent: usize) -> bool {
49 if line.trim().is_empty() {
51 return true;
52 }
53
54 get_line_indent(line) >= base_indent + MKDOCS_CONTENT_INDENT
56}
57
58pub fn is_within_footnote_definition(content: &str, position: usize) -> bool {
60 let tracker = BytePositionTracker::new(content);
61 let mut state = ContextStateMachine::new();
62
63 for (_idx, line, start, end) in tracker.iter_with_positions() {
64 if is_footnote_definition(line) {
66 let indent = get_footnote_indent(line).unwrap_or(0);
67 state.enter_context(indent, "footnote".to_string());
68 } else if state.is_in_context() {
69 if !line.trim().is_empty() && !is_footnote_continuation(line, state.context_indent()) {
71 state.exit_context();
73
74 if is_footnote_definition(line) {
76 let indent = get_footnote_indent(line).unwrap_or(0);
77 state.enter_context(indent, "footnote".to_string());
78 }
79 }
80 }
81
82 if start <= position && position <= end && state.is_in_context() {
84 return true;
85 }
86 }
87
88 false
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn test_footnote_definition_detection() {
97 assert!(is_footnote_definition("[^1]: This is a footnote"));
98 assert!(is_footnote_definition("[^note]: Named footnote"));
99 assert!(is_footnote_definition(" [^2]: Indented footnote"));
100 assert!(!is_footnote_definition("[^1] Reference in text"));
101 assert!(!is_footnote_definition("Regular text"));
102 }
103
104 #[test]
105 fn test_footnote_reference_detection() {
106 assert!(contains_footnote_reference("Text with [^1] reference"));
107 assert!(contains_footnote_reference("Multiple [^1] and [^2] refs"));
108 assert!(contains_footnote_reference("[^named-ref]"));
109 assert!(!contains_footnote_reference("No references here"));
110 }
111
112 #[test]
113 fn test_footnote_continuation() {
114 assert!(is_footnote_continuation(" Continued content", 0));
115 assert!(is_footnote_continuation(" More indented", 0));
116 assert!(is_footnote_continuation("", 0)); assert!(!is_footnote_continuation("Not indented enough", 0));
118 assert!(!is_footnote_continuation(" Only 2 spaces", 0));
119 }
120
121 #[test]
122 fn test_within_footnote_definition() {
123 let content = r#"Regular text here.
124
125[^1]: This is a footnote definition
126 with multiple lines
127 of content.
128
129More regular text.
130
131[^2]: Another footnote
132 Also multi-line.
133
134End text."#;
135
136 let def_pos = content.find("footnote definition").unwrap();
137 let multi_pos = content.find("with multiple").unwrap();
138 let regular_pos = content.find("More regular").unwrap();
139 let end_pos = content.find("End text").unwrap();
140
141 assert!(is_within_footnote_definition(content, def_pos));
142 assert!(is_within_footnote_definition(content, multi_pos));
143 assert!(!is_within_footnote_definition(content, regular_pos));
144 assert!(!is_within_footnote_definition(content, end_pos));
145 }
146}