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