rumdl_lib/utils/
mkdocs_common.rs1pub const MKDOCS_CONTENT_INDENT: usize = 4;
9
10pub struct BytePositionTracker<'a> {
13 pub content: &'a str,
14 pub lines: Vec<&'a str>,
15}
16
17impl<'a> BytePositionTracker<'a> {
18 pub fn new(content: &'a str) -> Self {
20 Self {
21 content,
22 lines: content.lines().collect(),
23 }
24 }
25
26 pub fn iter_with_positions(&self) -> impl Iterator<Item = (usize, &'a str, usize, usize)> + '_ {
29 let mut byte_pos = 0;
30 self.lines.iter().enumerate().map(move |(idx, line)| {
31 let start = byte_pos;
32 let end = byte_pos + line.len();
33 byte_pos = end + 1; (idx, *line, start, end)
35 })
36 }
37
38 pub fn is_position_in_matching_lines<F>(&self, position: usize, predicate: F) -> bool
40 where
41 F: Fn(usize, &str) -> bool,
42 {
43 for (idx, line, start, end) in self.iter_with_positions() {
44 if start <= position && position <= end && predicate(idx, line) {
45 return true;
46 }
47 }
48 false
49 }
50}
51
52pub fn get_line_indent(line: &str) -> usize {
54 line.chars()
55 .take_while(|&c| c == ' ' || c == '\t')
56 .map(|c| if c == '\t' { 4 } else { 1 }) .sum()
58}
59
60pub struct ContextStateMachine {
62 in_context: bool,
63 context_indent: usize,
64 context_type: Option<String>,
65}
66
67impl ContextStateMachine {
68 pub fn new() -> Self {
69 Self {
70 in_context: false,
71 context_indent: 0,
72 context_type: None,
73 }
74 }
75
76 pub fn enter_context(&mut self, indent: usize, context_type: String) {
78 self.in_context = true;
79 self.context_indent = indent;
80 self.context_type = Some(context_type);
81 }
82
83 pub fn exit_context(&mut self) {
85 self.in_context = false;
86 self.context_indent = 0;
87 self.context_type = None;
88 }
89
90 pub fn is_in_context(&self) -> bool {
92 self.in_context
93 }
94
95 pub fn context_indent(&self) -> usize {
97 self.context_indent
98 }
99
100 pub fn context_type(&self) -> Option<&str> {
102 self.context_type.as_deref()
103 }
104}
105
106impl Default for ContextStateMachine {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_get_line_indent() {
118 assert_eq!(get_line_indent("no indent"), 0);
119 assert_eq!(get_line_indent(" two spaces"), 2);
120 assert_eq!(get_line_indent(" four spaces"), 4);
121 assert_eq!(get_line_indent("\tone tab"), 4);
122 assert_eq!(get_line_indent("\t\ttwo tabs"), 8);
123 assert_eq!(get_line_indent(" \tmixed"), 6); }
125
126 #[test]
127 fn test_byte_position_tracker() {
128 let content = "line1\nline2\nline3";
129 let tracker = BytePositionTracker::new(content);
130
131 let positions: Vec<_> = tracker.iter_with_positions().collect();
132 assert_eq!(positions.len(), 3);
133 assert_eq!(positions[0], (0, "line1", 0, 5));
134 assert_eq!(positions[1], (1, "line2", 6, 11));
135 assert_eq!(positions[2], (2, "line3", 12, 17));
136 }
137
138 #[test]
139 fn test_position_in_matching_lines() {
140 let content = "normal\nspecial\nnormal";
141 let tracker = BytePositionTracker::new(content);
142
143 assert!(tracker.is_position_in_matching_lines(8, |_, line| line == "special"));
145 assert!(!tracker.is_position_in_matching_lines(2, |_, line| line == "special"));
147 }
148
149 #[test]
150 fn test_context_state_machine() {
151 let mut sm = ContextStateMachine::new();
152 assert!(!sm.is_in_context());
153
154 sm.enter_context(4, "admonition".to_string());
155 assert!(sm.is_in_context());
156 assert_eq!(sm.context_indent(), 4);
157 assert_eq!(sm.context_type(), Some("admonition"));
158
159 sm.exit_context();
160 assert!(!sm.is_in_context());
161 assert_eq!(sm.context_type(), None);
162 }
163}