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