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