1pub mod anchor_styles;
6pub mod atomic_write;
7pub mod blank_lines;
8pub mod blockquote;
9pub mod code_block_utils;
10pub mod emphasis_utils;
11pub mod fix_utils;
12pub mod frontmatter_values;
13pub(crate) mod gh_aw;
14pub(crate) mod hard_break;
15pub mod header_id_utils;
16pub mod html_block;
17pub mod html_elements;
18pub mod jinja_utils;
19pub mod kramdown_utils;
20pub mod line_ending;
21pub(crate) mod list_fix_guard;
22pub mod list_indent_shift;
23pub mod mdg;
24pub(crate) mod mdx_comments;
25pub mod mkdocs_admonitions;
26pub mod mkdocs_attr_list;
27pub mod mkdocs_common;
28pub mod mkdocs_config;
29pub mod mkdocs_critic;
30pub mod mkdocs_definition_lists;
31pub mod mkdocs_extensions;
32pub mod mkdocs_footnotes;
33pub mod mkdocs_html_markdown;
34pub mod mkdocs_icons;
35pub mod mkdocs_patterns;
36pub mod mkdocs_snippets;
37pub mod mkdocs_tabs;
38pub mod mkdocstrings_refs;
39pub mod obsidian_config;
40pub mod obsidian_tag;
41pub mod pandoc;
42pub mod parser_options;
43pub mod project_root;
44pub mod pymdown_blocks;
45pub mod quarto_chunks;
46pub mod range_utils;
47pub mod regex_cache;
48pub mod sentence_utils;
49pub(crate) mod shortcode_utils;
50pub mod skip_context;
51pub mod table_utils;
52pub mod text_reflow;
53pub mod thematic_break;
54pub mod unicode;
55pub mod upward_walk;
56pub mod utf8_offsets;
57
58pub use code_block_utils::CodeBlockUtils;
59pub use line_ending::{
60 LineEnding, NormalizedLineEndingMap, detect_line_ending, detect_line_ending_enum, ensure_consistent_line_endings,
61 get_line_ending_str, line_ending_before, normalize_line_ending,
62};
63pub use parser_options::rumdl_parser_options;
64pub use range_utils::LineIndex;
65
66pub fn calculate_indentation_width(indent_str: &str, tab_width: usize) -> usize {
70 let mut width = 0;
71 for ch in indent_str.chars() {
72 if ch == '\t' {
73 width = ((width / tab_width) + 1) * tab_width;
74 } else if ch == ' ' {
75 width += 1;
76 } else {
77 break;
78 }
79 }
80 width
81}
82
83pub fn calculate_indentation_width_default(indent_str: &str) -> usize {
85 calculate_indentation_width(indent_str, 4)
86}
87
88pub fn is_definition_list_item(line: &str) -> bool {
98 let trimmed = line.trim_start();
99 trimmed.starts_with(": ")
100 || (trimmed.starts_with(':') && trimmed.len() > 1 && trimmed.chars().nth(1).is_some_and(char::is_whitespace))
101}
102
103pub fn is_template_directive_only(line: &str) -> bool {
113 let trimmed = line.trim();
114 if trimmed.is_empty() {
115 return false;
116 }
117 (trimmed.starts_with("{{") && trimmed.ends_with("}}")) || (trimmed.starts_with("{%") && trimmed.ends_with("%}"))
118}
119
120pub trait StrExt {
122 fn replace_trailing_spaces(&self, replacement: &str) -> String;
124
125 fn has_trailing_spaces(&self) -> bool;
127
128 fn trailing_spaces(&self) -> usize;
130}
131
132impl StrExt for str {
133 fn replace_trailing_spaces(&self, replacement: &str) -> String {
134 let (content, ends_with_newline) = if let Some(stripped) = self.strip_suffix('\n') {
138 (stripped, true)
139 } else {
140 (self, false)
141 };
142
143 let mut non_space_len = content.len();
145 for c in content.chars().rev() {
146 if c == ' ' {
147 non_space_len -= 1;
148 } else {
149 break;
150 }
151 }
152
153 let mut result = String::with_capacity(non_space_len + replacement.len() + usize::from(ends_with_newline));
155 result.push_str(&content[..non_space_len]);
156 result.push_str(replacement);
157 if ends_with_newline {
158 result.push('\n');
159 }
160
161 result
162 }
163
164 fn has_trailing_spaces(&self) -> bool {
165 self.trailing_spaces() > 0
166 }
167
168 fn trailing_spaces(&self) -> usize {
169 let content = self.strip_suffix('\n').unwrap_or(self);
173
174 let mut space_count = 0;
176 for c in content.chars().rev() {
177 if c == ' ' {
178 space_count += 1;
179 } else {
180 break;
181 }
182 }
183
184 space_count
185 }
186}
187
188use std::collections::hash_map::DefaultHasher;
189use std::hash::{Hash, Hasher};
190
191pub fn fast_hash(content: &str) -> u64 {
204 let mut hasher = DefaultHasher::new();
205 content.hash(&mut hasher);
206 hasher.finish()
207}
208
209pub fn warning_label(kind: &str) -> String {
213 let label = format!("[{kind}]");
214 #[cfg(feature = "colored")]
215 {
216 use colored::Colorize;
217 label.yellow().to_string()
218 }
219 #[cfg(not(feature = "colored"))]
220 {
221 label
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_detect_line_ending_pure_lf() {
231 let content = "First line\nSecond line\nThird line\n";
233 assert_eq!(detect_line_ending(content), "\n");
234 }
235
236 #[test]
237 fn test_detect_line_ending_pure_crlf() {
238 let content = "First line\r\nSecond line\r\nThird line\r\n";
240 assert_eq!(detect_line_ending(content), "\r\n");
241 }
242
243 #[test]
244 fn test_detect_line_ending_mixed_more_lf() {
245 let content = "First line\nSecond line\r\nThird line\nFourth line\n";
247 assert_eq!(detect_line_ending(content), "\n");
248 }
249
250 #[test]
251 fn test_detect_line_ending_mixed_more_crlf() {
252 let content = "First line\r\nSecond line\r\nThird line\nFourth line\r\n";
254 assert_eq!(detect_line_ending(content), "\r\n");
255 }
256
257 #[test]
258 fn test_detect_line_ending_empty_string() {
259 let content = "";
261 assert_eq!(detect_line_ending(content), "\n");
262 }
263
264 #[test]
265 fn test_detect_line_ending_single_line_no_ending() {
266 let content = "This is a single line with no line ending";
268 assert_eq!(detect_line_ending(content), "\n");
269 }
270
271 #[test]
272 fn test_detect_line_ending_equal_lf_and_crlf() {
273 let content = "Line 1\r\nLine 2\nLine 3\r\nLine 4\n";
277 assert_eq!(detect_line_ending(content), "\n");
278 }
279
280 #[test]
281 fn test_detect_line_ending_single_lf() {
282 let content = "Line 1\n";
284 assert_eq!(detect_line_ending(content), "\n");
285 }
286
287 #[test]
288 fn test_detect_line_ending_single_crlf() {
289 let content = "Line 1\r\n";
291 assert_eq!(detect_line_ending(content), "\r\n");
292 }
293
294 #[test]
295 fn test_detect_line_ending_embedded_cr() {
296 let content = "Line 1\rLine 2\nLine 3\r\nLine 4\n";
299 assert_eq!(detect_line_ending(content), "\n");
301 }
302}