1pub mod anchor_styles;
6pub mod blockquote;
7pub mod code_block_utils;
8pub mod emphasis_utils;
9pub mod fix_utils;
10pub mod header_id_utils;
11pub mod jinja_utils;
12pub mod kramdown_utils;
13pub mod line_ending;
14pub mod mkdocs_admonitions;
15pub mod mkdocs_attr_list;
16pub mod mkdocs_common;
17pub mod mkdocs_config;
18pub mod mkdocs_critic;
19pub mod mkdocs_definition_lists;
20pub mod mkdocs_extensions;
21pub mod mkdocs_footnotes;
22pub mod mkdocs_html_markdown;
23pub mod mkdocs_icons;
24pub mod mkdocs_patterns;
25pub mod mkdocs_snippets;
26pub mod mkdocs_tabs;
27pub mod mkdocstrings_refs;
28pub mod obsidian_config;
29pub mod parser_options;
30pub mod project_root;
31pub mod pymdown_blocks;
32pub mod quarto_divs;
33pub mod range_utils;
34pub mod regex_cache;
35pub mod sentence_utils;
36pub mod skip_context;
37pub mod string_interner;
38pub mod table_utils;
39pub mod text_reflow;
40pub mod thematic_break;
41pub mod utf8_offsets;
42
43pub use code_block_utils::CodeBlockUtils;
44pub use line_ending::{
45 LineEnding, detect_line_ending, detect_line_ending_enum, ensure_consistent_line_endings, get_line_ending_str,
46 normalize_line_ending,
47};
48pub use parser_options::rumdl_parser_options;
49pub use range_utils::LineIndex;
50
51pub fn calculate_indentation_width(indent_str: &str, tab_width: usize) -> usize {
55 let mut width = 0;
56 for ch in indent_str.chars() {
57 if ch == '\t' {
58 width = ((width / tab_width) + 1) * tab_width;
59 } else if ch == ' ' {
60 width += 1;
61 } else {
62 break;
63 }
64 }
65 width
66}
67
68pub fn calculate_indentation_width_default(indent_str: &str) -> usize {
70 calculate_indentation_width(indent_str, 4)
71}
72
73pub fn is_definition_list_item(line: &str) -> bool {
83 let trimmed = line.trim_start();
84 trimmed.starts_with(": ")
85 || (trimmed.starts_with(':') && trimmed.len() > 1 && trimmed.chars().nth(1).is_some_and(char::is_whitespace))
86}
87
88pub fn is_template_directive_only(line: &str) -> bool {
98 let trimmed = line.trim();
99 if trimmed.is_empty() {
100 return false;
101 }
102 (trimmed.starts_with("{{") && trimmed.ends_with("}}")) || (trimmed.starts_with("{%") && trimmed.ends_with("%}"))
103}
104
105pub trait StrExt {
107 fn replace_trailing_spaces(&self, replacement: &str) -> String;
109
110 fn has_trailing_spaces(&self) -> bool;
112
113 fn trailing_spaces(&self) -> usize;
115}
116
117impl StrExt for str {
118 fn replace_trailing_spaces(&self, replacement: &str) -> String {
119 let (content, ends_with_newline) = if let Some(stripped) = self.strip_suffix('\n') {
123 (stripped, true)
124 } else {
125 (self, false)
126 };
127
128 let mut non_space_len = content.len();
130 for c in content.chars().rev() {
131 if c == ' ' {
132 non_space_len -= 1;
133 } else {
134 break;
135 }
136 }
137
138 let mut result = String::with_capacity(non_space_len + replacement.len() + usize::from(ends_with_newline));
140 result.push_str(&content[..non_space_len]);
141 result.push_str(replacement);
142 if ends_with_newline {
143 result.push('\n');
144 }
145
146 result
147 }
148
149 fn has_trailing_spaces(&self) -> bool {
150 self.trailing_spaces() > 0
151 }
152
153 fn trailing_spaces(&self) -> usize {
154 let content = self.strip_suffix('\n').unwrap_or(self);
158
159 let mut space_count = 0;
161 for c in content.chars().rev() {
162 if c == ' ' {
163 space_count += 1;
164 } else {
165 break;
166 }
167 }
168
169 space_count
170 }
171}
172
173use std::collections::hash_map::DefaultHasher;
174use std::hash::{Hash, Hasher};
175
176pub fn fast_hash(content: &str) -> u64 {
189 let mut hasher = DefaultHasher::new();
190 content.hash(&mut hasher);
191 hasher.finish()
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_detect_line_ending_pure_lf() {
200 let content = "First line\nSecond line\nThird line\n";
202 assert_eq!(detect_line_ending(content), "\n");
203 }
204
205 #[test]
206 fn test_detect_line_ending_pure_crlf() {
207 let content = "First line\r\nSecond line\r\nThird line\r\n";
209 assert_eq!(detect_line_ending(content), "\r\n");
210 }
211
212 #[test]
213 fn test_detect_line_ending_mixed_more_lf() {
214 let content = "First line\nSecond line\r\nThird line\nFourth line\n";
216 assert_eq!(detect_line_ending(content), "\n");
217 }
218
219 #[test]
220 fn test_detect_line_ending_mixed_more_crlf() {
221 let content = "First line\r\nSecond line\r\nThird line\nFourth line\r\n";
223 assert_eq!(detect_line_ending(content), "\r\n");
224 }
225
226 #[test]
227 fn test_detect_line_ending_empty_string() {
228 let content = "";
230 assert_eq!(detect_line_ending(content), "\n");
231 }
232
233 #[test]
234 fn test_detect_line_ending_single_line_no_ending() {
235 let content = "This is a single line with no line ending";
237 assert_eq!(detect_line_ending(content), "\n");
238 }
239
240 #[test]
241 fn test_detect_line_ending_equal_lf_and_crlf() {
242 let content = "Line 1\r\nLine 2\nLine 3\r\nLine 4\n";
246 assert_eq!(detect_line_ending(content), "\n");
247 }
248
249 #[test]
250 fn test_detect_line_ending_single_lf() {
251 let content = "Line 1\n";
253 assert_eq!(detect_line_ending(content), "\n");
254 }
255
256 #[test]
257 fn test_detect_line_ending_single_crlf() {
258 let content = "Line 1\r\n";
260 assert_eq!(detect_line_ending(content), "\r\n");
261 }
262
263 #[test]
264 fn test_detect_line_ending_embedded_cr() {
265 let content = "Line 1\rLine 2\nLine 3\r\nLine 4\n";
268 assert_eq!(detect_line_ending(content), "\n");
270 }
271}