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