1use crate::utils::fast_hash;
2use crate::utils::regex_cache::{escape_regex, get_cached_fancy_regex};
3
4use crate::rule::{Fix, LintError, LintResult, LintWarning, Rule, Severity};
5use std::collections::{HashMap, HashSet};
6use std::sync::{Arc, Mutex};
7
8mod md044_config;
9pub use md044_config::MD044Config;
10
11type WarningPosition = (usize, usize, String); fn is_inline_config_comment(trimmed: &str) -> bool {
70 trimmed.starts_with("<!-- rumdl-")
71 || trimmed.starts_with("<!-- markdownlint-")
72 || trimmed.starts_with("<!-- vale off")
73 || trimmed.starts_with("<!-- vale on")
74 || (trimmed.starts_with("<!-- vale ") && trimmed.contains(" = "))
75 || trimmed.starts_with("<!-- vale style")
76 || trimmed.starts_with("<!-- lint disable ")
77 || trimmed.starts_with("<!-- lint enable ")
78 || trimmed.starts_with("<!-- lint ignore ")
79}
80
81#[derive(Clone)]
82pub struct MD044ProperNames {
83 config: MD044Config,
84 combined_pattern: Option<String>,
86 name_variants: Vec<String>,
88 content_cache: Arc<Mutex<HashMap<u64, Vec<WarningPosition>>>>,
90}
91
92impl MD044ProperNames {
93 pub fn new(names: Vec<String>, code_blocks: bool) -> Self {
94 let config = MD044Config {
95 names,
96 code_blocks,
97 html_elements: true, html_comments: true, };
100 let combined_pattern = Self::create_combined_pattern(&config);
101 let name_variants = Self::build_name_variants(&config);
102 Self {
103 config,
104 combined_pattern,
105 name_variants,
106 content_cache: Arc::new(Mutex::new(HashMap::new())),
107 }
108 }
109
110 fn ascii_normalize(s: &str) -> String {
112 s.replace(['é', 'è', 'ê', 'ë'], "e")
113 .replace(['à', 'á', 'â', 'ä', 'ã', 'å'], "a")
114 .replace(['ï', 'î', 'í', 'ì'], "i")
115 .replace(['ü', 'ú', 'ù', 'û'], "u")
116 .replace(['ö', 'ó', 'ò', 'ô', 'õ'], "o")
117 .replace('ñ', "n")
118 .replace('ç', "c")
119 }
120
121 pub fn from_config_struct(config: MD044Config) -> Self {
122 let combined_pattern = Self::create_combined_pattern(&config);
123 let name_variants = Self::build_name_variants(&config);
124 Self {
125 config,
126 combined_pattern,
127 name_variants,
128 content_cache: Arc::new(Mutex::new(HashMap::new())),
129 }
130 }
131
132 fn create_combined_pattern(config: &MD044Config) -> Option<String> {
134 if config.names.is_empty() {
135 return None;
136 }
137
138 let mut patterns: Vec<String> = config
140 .names
141 .iter()
142 .flat_map(|name| {
143 let mut variations = vec![];
144 let lower_name = name.to_lowercase();
145
146 variations.push(escape_regex(&lower_name));
148
149 let lower_name_no_dots = lower_name.replace('.', "");
151 if lower_name != lower_name_no_dots {
152 variations.push(escape_regex(&lower_name_no_dots));
153 }
154
155 let ascii_normalized = Self::ascii_normalize(&lower_name);
157
158 if ascii_normalized != lower_name {
159 variations.push(escape_regex(&ascii_normalized));
160
161 let ascii_no_dots = ascii_normalized.replace('.', "");
163 if ascii_normalized != ascii_no_dots {
164 variations.push(escape_regex(&ascii_no_dots));
165 }
166 }
167
168 variations
169 })
170 .collect();
171
172 patterns.sort_by_key(|b| std::cmp::Reverse(b.len()));
174
175 Some(format!(r"(?i)({})", patterns.join("|")))
178 }
179
180 fn build_name_variants(config: &MD044Config) -> Vec<String> {
181 let mut variants = HashSet::new();
182 for name in &config.names {
183 let lower_name = name.to_lowercase();
184 variants.insert(lower_name.clone());
185
186 let lower_no_dots = lower_name.replace('.', "");
187 if lower_name != lower_no_dots {
188 variants.insert(lower_no_dots);
189 }
190
191 let ascii_normalized = Self::ascii_normalize(&lower_name);
192 if ascii_normalized != lower_name {
193 variants.insert(ascii_normalized.clone());
194
195 let ascii_no_dots = ascii_normalized.replace('.', "");
196 if ascii_normalized != ascii_no_dots {
197 variants.insert(ascii_no_dots);
198 }
199 }
200 }
201
202 variants.into_iter().collect()
203 }
204
205 fn find_name_violations(
208 &self,
209 content: &str,
210 ctx: &crate::lint_context::LintContext,
211 content_lower: &str,
212 ) -> Vec<WarningPosition> {
213 if self.config.names.is_empty() || content.is_empty() || self.combined_pattern.is_none() {
215 return Vec::new();
216 }
217
218 let has_potential_matches = self.name_variants.iter().any(|name| content_lower.contains(name));
220
221 if !has_potential_matches {
222 return Vec::new();
223 }
224
225 let hash = fast_hash(content);
227 {
228 if let Ok(cache) = self.content_cache.lock()
230 && let Some(cached) = cache.get(&hash)
231 {
232 return cached.clone();
233 }
234 }
235
236 let mut violations = Vec::new();
237
238 let combined_regex = match &self.combined_pattern {
240 Some(pattern) => match get_cached_fancy_regex(pattern) {
241 Ok(regex) => regex,
242 Err(_) => return Vec::new(),
243 },
244 None => return Vec::new(),
245 };
246
247 for (line_idx, line_info) in ctx.lines.iter().enumerate() {
249 let line_num = line_idx + 1;
250 let line = line_info.content(ctx.content);
251
252 let trimmed = line.trim_start();
254 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
255 continue;
256 }
257
258 if !self.config.code_blocks && line_info.in_code_block {
260 continue;
261 }
262
263 if !self.config.html_elements && line_info.in_html_block {
265 continue;
266 }
267
268 if !self.config.html_comments && line_info.in_html_comment {
270 continue;
271 }
272
273 if line_info.in_jsx_expression || line_info.in_mdx_comment {
275 continue;
276 }
277
278 if line_info.in_obsidian_comment {
280 continue;
281 }
282
283 let fm_value_offset = if line_info.in_front_matter {
286 Self::frontmatter_value_offset(line)
287 } else {
288 0
289 };
290 if fm_value_offset == usize::MAX {
291 continue;
292 }
293
294 if is_inline_config_comment(trimmed) {
296 continue;
297 }
298
299 let line_lower = line.to_lowercase();
301 let has_line_matches = self.name_variants.iter().any(|name| line_lower.contains(name));
302
303 if !has_line_matches {
304 continue;
305 }
306
307 for cap_result in combined_regex.find_iter(line) {
309 match cap_result {
310 Ok(cap) => {
311 let found_name = &line[cap.start()..cap.end()];
312
313 let start_pos = cap.start();
315 let end_pos = cap.end();
316
317 if start_pos < fm_value_offset {
319 continue;
320 }
321
322 let byte_pos = line_info.byte_offset + start_pos;
324 if ctx.is_in_html_tag(byte_pos) {
325 continue;
326 }
327
328 if !Self::is_at_word_boundary(line, start_pos, true)
329 || !Self::is_at_word_boundary(line, end_pos, false)
330 {
331 continue; }
333
334 if !self.config.code_blocks {
336 if ctx.is_in_code_block_or_span(byte_pos) {
337 continue;
338 }
339 if (line_info.in_html_comment || line_info.in_html_block)
343 && Self::is_in_backtick_code_in_line(line, start_pos)
344 {
345 continue;
346 }
347 }
348
349 if Self::is_in_link(ctx, byte_pos) {
351 continue;
352 }
353
354 if Self::is_in_angle_bracket_url(line, start_pos) {
358 continue;
359 }
360
361 if let Some(proper_name) = self.get_proper_name_for(found_name) {
363 if found_name != proper_name {
365 violations.push((line_num, cap.start() + 1, found_name.to_string()));
366 }
367 }
368 }
369 Err(e) => {
370 eprintln!("Regex execution error on line {line_num}: {e}");
371 }
372 }
373 }
374 }
375
376 if let Ok(mut cache) = self.content_cache.lock() {
378 cache.insert(hash, violations.clone());
379 }
380 violations
381 }
382
383 fn is_in_link(ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
390 use pulldown_cmark::LinkType;
391
392 let link_idx = ctx.links.partition_point(|link| link.byte_offset <= byte_pos);
394 if link_idx > 0 {
395 let link = &ctx.links[link_idx - 1];
396 if byte_pos < link.byte_end {
397 let text_start = if matches!(link.link_type, LinkType::WikiLink { .. }) {
399 link.byte_offset + 2
400 } else {
401 link.byte_offset + 1
402 };
403 let text_end = text_start + link.text.len();
404
405 if byte_pos >= text_start && byte_pos < text_end {
407 return Self::link_text_is_url(&link.text);
408 }
409 return true;
411 }
412 }
413
414 let image_idx = ctx.images.partition_point(|img| img.byte_offset <= byte_pos);
416 if image_idx > 0 {
417 let image = &ctx.images[image_idx - 1];
418 if byte_pos < image.byte_end {
419 let alt_start = image.byte_offset + 2;
421 let alt_end = alt_start + image.alt_text.len();
422
423 if byte_pos >= alt_start && byte_pos < alt_end {
425 return false;
426 }
427 return true;
429 }
430 }
431
432 ctx.is_in_reference_def(byte_pos)
434 }
435
436 fn link_text_is_url(text: &str) -> bool {
439 let lower = text.trim().to_ascii_lowercase();
440 lower.starts_with("http://") || lower.starts_with("https://") || lower.starts_with("www.")
441 }
442
443 fn is_in_angle_bracket_url(line: &str, pos: usize) -> bool {
449 let bytes = line.as_bytes();
450 let len = bytes.len();
451 let mut i = 0;
452 while i < len {
453 if bytes[i] == b'<' {
454 let after_open = i + 1;
455 if after_open < len && bytes[after_open].is_ascii_alphabetic() {
459 let mut s = after_open + 1;
460 let scheme_max = (after_open + 32).min(len);
461 while s < scheme_max
462 && (bytes[s].is_ascii_alphanumeric()
463 || bytes[s] == b'+'
464 || bytes[s] == b'-'
465 || bytes[s] == b'.')
466 {
467 s += 1;
468 }
469 if s < len && bytes[s] == b':' {
470 let mut j = s + 1;
472 let mut found_close = false;
473 while j < len {
474 match bytes[j] {
475 b'>' => {
476 found_close = true;
477 break;
478 }
479 b' ' | b'<' => break,
480 _ => j += 1,
481 }
482 }
483 if found_close && pos >= i && pos <= j {
484 return true;
485 }
486 if found_close {
487 i = j + 1;
488 continue;
489 }
490 }
491 }
492 }
493 i += 1;
494 }
495 false
496 }
497
498 fn is_in_backtick_code_in_line(line: &str, pos: usize) -> bool {
506 let bytes = line.as_bytes();
507 let len = bytes.len();
508 let mut i = 0;
509 while i < len {
510 if bytes[i] == b'`' {
511 let open_start = i;
513 while i < len && bytes[i] == b'`' {
514 i += 1;
515 }
516 let tick_len = i - open_start;
517
518 while i < len {
520 if bytes[i] == b'`' {
521 let close_start = i;
522 while i < len && bytes[i] == b'`' {
523 i += 1;
524 }
525 if i - close_start == tick_len {
526 let content_start = open_start + tick_len;
530 let content_end = close_start;
531 if pos >= content_start && pos < content_end {
532 return true;
533 }
534 break;
536 }
537 } else {
539 i += 1;
540 }
541 }
542 } else {
543 i += 1;
544 }
545 }
546 false
547 }
548
549 fn is_word_boundary_char(c: char) -> bool {
551 !c.is_alphanumeric()
552 }
553
554 fn is_at_word_boundary(content: &str, pos: usize, is_start: bool) -> bool {
556 if is_start {
557 if pos == 0 {
558 return true;
559 }
560 match content[..pos].chars().next_back() {
561 None => true,
562 Some(c) => Self::is_word_boundary_char(c),
563 }
564 } else {
565 if pos >= content.len() {
566 return true;
567 }
568 match content[pos..].chars().next() {
569 None => true,
570 Some(c) => Self::is_word_boundary_char(c),
571 }
572 }
573 }
574
575 fn frontmatter_value_offset(line: &str) -> usize {
579 let trimmed = line.trim();
580
581 if trimmed == "---" || trimmed == "+++" || trimmed.is_empty() {
583 return usize::MAX;
584 }
585
586 if trimmed.starts_with('#') {
588 return usize::MAX;
589 }
590
591 let stripped = line.trim_start();
593 if let Some(after_dash) = stripped.strip_prefix("- ") {
594 let leading = line.len() - stripped.len();
595 if let Some(result) = Self::kv_value_offset(line, after_dash, leading + 2) {
597 return result;
598 }
599 return leading + 2;
601 }
602 if stripped == "-" {
603 return usize::MAX;
604 }
605
606 if let Some(result) = Self::kv_value_offset(line, stripped, line.len() - stripped.len()) {
608 return result;
609 }
610
611 if let Some(eq_pos) = line.find('=') {
613 let after_eq = eq_pos + 1;
614 if after_eq < line.len() && line.as_bytes()[after_eq] == b' ' {
615 let value_start = after_eq + 1;
616 let value_slice = &line[value_start..];
617 let value_trimmed = value_slice.trim();
618 if value_trimmed.is_empty() {
619 return usize::MAX;
620 }
621 if (value_trimmed.starts_with('"') && value_trimmed.ends_with('"'))
623 || (value_trimmed.starts_with('\'') && value_trimmed.ends_with('\''))
624 {
625 let quote_offset = value_slice.find(['"', '\'']).unwrap_or(0);
626 return value_start + quote_offset + 1;
627 }
628 return value_start;
629 }
630 return usize::MAX;
632 }
633
634 0
636 }
637
638 fn kv_value_offset(line: &str, content: &str, base_offset: usize) -> Option<usize> {
642 let colon_pos = content.find(':')?;
643 let abs_colon = base_offset + colon_pos;
644 let after_colon = abs_colon + 1;
645 if after_colon < line.len() && line.as_bytes()[after_colon] == b' ' {
646 let value_start = after_colon + 1;
647 let value_slice = &line[value_start..];
648 let value_trimmed = value_slice.trim();
649 if value_trimmed.is_empty() {
650 return Some(usize::MAX);
651 }
652 if value_trimmed.starts_with('{') || value_trimmed.starts_with('[') {
654 return Some(usize::MAX);
655 }
656 if (value_trimmed.starts_with('"') && value_trimmed.ends_with('"'))
658 || (value_trimmed.starts_with('\'') && value_trimmed.ends_with('\''))
659 {
660 let quote_offset = value_slice.find(['"', '\'']).unwrap_or(0);
661 return Some(value_start + quote_offset + 1);
662 }
663 return Some(value_start);
664 }
665 Some(usize::MAX)
667 }
668
669 fn get_proper_name_for(&self, found_name: &str) -> Option<String> {
671 let found_lower = found_name.to_lowercase();
672
673 for name in &self.config.names {
675 let lower_name = name.to_lowercase();
676 let lower_name_no_dots = lower_name.replace('.', "");
677
678 if found_lower == lower_name || found_lower == lower_name_no_dots {
680 return Some(name.clone());
681 }
682
683 let ascii_normalized = Self::ascii_normalize(&lower_name);
685
686 let ascii_no_dots = ascii_normalized.replace('.', "");
687
688 if found_lower == ascii_normalized || found_lower == ascii_no_dots {
689 return Some(name.clone());
690 }
691 }
692 None
693 }
694}
695
696impl Rule for MD044ProperNames {
697 fn name(&self) -> &'static str {
698 "MD044"
699 }
700
701 fn description(&self) -> &'static str {
702 "Proper names should have the correct capitalization"
703 }
704
705 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
706 if self.config.names.is_empty() {
707 return true;
708 }
709 let content_lower = if ctx.content.is_ascii() {
711 ctx.content.to_ascii_lowercase()
712 } else {
713 ctx.content.to_lowercase()
714 };
715 !self.name_variants.iter().any(|name| content_lower.contains(name))
716 }
717
718 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
719 let content = ctx.content;
720 if content.is_empty() || self.config.names.is_empty() || self.combined_pattern.is_none() {
721 return Ok(Vec::new());
722 }
723
724 let content_lower = if content.is_ascii() {
726 content.to_ascii_lowercase()
727 } else {
728 content.to_lowercase()
729 };
730
731 let has_potential_matches = self.name_variants.iter().any(|name| content_lower.contains(name));
733
734 if !has_potential_matches {
735 return Ok(Vec::new());
736 }
737
738 let line_index = &ctx.line_index;
739 let violations = self.find_name_violations(content, ctx, &content_lower);
740
741 let warnings = violations
742 .into_iter()
743 .filter_map(|(line, column, found_name)| {
744 self.get_proper_name_for(&found_name).map(|proper_name| LintWarning {
745 rule_name: Some(self.name().to_string()),
746 line,
747 column,
748 end_line: line,
749 end_column: column + found_name.len(),
750 message: format!("Proper name '{found_name}' should be '{proper_name}'"),
751 severity: Severity::Warning,
752 fix: Some(Fix {
753 range: line_index.line_col_to_byte_range_with_length(line, column, found_name.len()),
754 replacement: proper_name,
755 }),
756 })
757 })
758 .collect();
759
760 Ok(warnings)
761 }
762
763 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
764 let content = ctx.content;
765 if content.is_empty() || self.config.names.is_empty() {
766 return Ok(content.to_string());
767 }
768
769 let content_lower = if content.is_ascii() {
770 content.to_ascii_lowercase()
771 } else {
772 content.to_lowercase()
773 };
774 let violations = self.find_name_violations(content, ctx, &content_lower);
775 if violations.is_empty() {
776 return Ok(content.to_string());
777 }
778
779 let mut fixed_lines = Vec::new();
781
782 let mut violations_by_line: HashMap<usize, Vec<(usize, String)>> = HashMap::new();
784 for (line_num, col_num, found_name) in violations {
785 violations_by_line
786 .entry(line_num)
787 .or_default()
788 .push((col_num, found_name));
789 }
790
791 for violations in violations_by_line.values_mut() {
793 violations.sort_by_key(|b| std::cmp::Reverse(b.0));
794 }
795
796 for (line_idx, line_info) in ctx.lines.iter().enumerate() {
798 let line_num = line_idx + 1;
799
800 if ctx.inline_config().is_rule_disabled(self.name(), line_num) {
802 fixed_lines.push(line_info.content(ctx.content).to_string());
803 continue;
804 }
805
806 if let Some(line_violations) = violations_by_line.get(&line_num) {
807 let mut fixed_line = line_info.content(ctx.content).to_string();
809
810 for (col_num, found_name) in line_violations {
811 if let Some(proper_name) = self.get_proper_name_for(found_name) {
812 let start_col = col_num - 1; let end_col = start_col + found_name.len();
814
815 if end_col <= fixed_line.len()
816 && fixed_line.is_char_boundary(start_col)
817 && fixed_line.is_char_boundary(end_col)
818 {
819 fixed_line.replace_range(start_col..end_col, &proper_name);
820 }
821 }
822 }
823
824 fixed_lines.push(fixed_line);
825 } else {
826 fixed_lines.push(line_info.content(ctx.content).to_string());
828 }
829 }
830
831 let mut result = fixed_lines.join("\n");
833 if content.ends_with('\n') && !result.ends_with('\n') {
834 result.push('\n');
835 }
836 Ok(result)
837 }
838
839 fn as_any(&self) -> &dyn std::any::Any {
840 self
841 }
842
843 fn default_config_section(&self) -> Option<(String, toml::Value)> {
844 let json_value = serde_json::to_value(&self.config).ok()?;
845 Some((
846 self.name().to_string(),
847 crate::rule_config_serde::json_to_toml_value(&json_value)?,
848 ))
849 }
850
851 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
852 where
853 Self: Sized,
854 {
855 let rule_config = crate::rule_config_serde::load_rule_config::<MD044Config>(config);
856 Box::new(Self::from_config_struct(rule_config))
857 }
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863 use crate::lint_context::LintContext;
864
865 fn create_context(content: &str) -> LintContext<'_> {
866 LintContext::new(content, crate::config::MarkdownFlavor::Standard, None)
867 }
868
869 #[test]
870 fn test_correctly_capitalized_names() {
871 let rule = MD044ProperNames::new(
872 vec![
873 "JavaScript".to_string(),
874 "TypeScript".to_string(),
875 "Node.js".to_string(),
876 ],
877 true,
878 );
879
880 let content = "This document uses JavaScript, TypeScript, and Node.js correctly.";
881 let ctx = create_context(content);
882 let result = rule.check(&ctx).unwrap();
883 assert!(result.is_empty(), "Should not flag correctly capitalized names");
884 }
885
886 #[test]
887 fn test_incorrectly_capitalized_names() {
888 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "TypeScript".to_string()], true);
889
890 let content = "This document uses javascript and typescript incorrectly.";
891 let ctx = create_context(content);
892 let result = rule.check(&ctx).unwrap();
893
894 assert_eq!(result.len(), 2, "Should flag two incorrect capitalizations");
895 assert_eq!(result[0].message, "Proper name 'javascript' should be 'JavaScript'");
896 assert_eq!(result[0].line, 1);
897 assert_eq!(result[0].column, 20);
898 assert_eq!(result[1].message, "Proper name 'typescript' should be 'TypeScript'");
899 assert_eq!(result[1].line, 1);
900 assert_eq!(result[1].column, 35);
901 }
902
903 #[test]
904 fn test_names_at_beginning_of_sentences() {
905 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "Python".to_string()], true);
906
907 let content = "javascript is a great language. python is also popular.";
908 let ctx = create_context(content);
909 let result = rule.check(&ctx).unwrap();
910
911 assert_eq!(result.len(), 2, "Should flag names at beginning of sentences");
912 assert_eq!(result[0].line, 1);
913 assert_eq!(result[0].column, 1);
914 assert_eq!(result[1].line, 1);
915 assert_eq!(result[1].column, 33);
916 }
917
918 #[test]
919 fn test_names_in_code_blocks_checked_by_default() {
920 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
921
922 let content = r#"Here is some text with JavaScript.
923
924```javascript
925// This javascript should be checked
926const lang = "javascript";
927```
928
929But this javascript should be flagged."#;
930
931 let ctx = create_context(content);
932 let result = rule.check(&ctx).unwrap();
933
934 assert_eq!(result.len(), 3, "Should flag javascript inside and outside code blocks");
935 assert_eq!(result[0].line, 4);
936 assert_eq!(result[1].line, 5);
937 assert_eq!(result[2].line, 8);
938 }
939
940 #[test]
941 fn test_names_in_code_blocks_ignored_when_disabled() {
942 let rule = MD044ProperNames::new(
943 vec!["JavaScript".to_string()],
944 false, );
946
947 let content = r#"```
948javascript in code block
949```"#;
950
951 let ctx = create_context(content);
952 let result = rule.check(&ctx).unwrap();
953
954 assert_eq!(
955 result.len(),
956 0,
957 "Should not flag javascript in code blocks when code_blocks is false"
958 );
959 }
960
961 #[test]
962 fn test_names_in_inline_code_checked_by_default() {
963 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
964
965 let content = "This is `javascript` in inline code and javascript outside.";
966 let ctx = create_context(content);
967 let result = rule.check(&ctx).unwrap();
968
969 assert_eq!(result.len(), 2, "Should flag javascript inside and outside inline code");
971 assert_eq!(result[0].column, 10); assert_eq!(result[1].column, 41); }
974
975 #[test]
976 fn test_multiple_names_in_same_line() {
977 let rule = MD044ProperNames::new(
978 vec!["JavaScript".to_string(), "TypeScript".to_string(), "React".to_string()],
979 true,
980 );
981
982 let content = "I use javascript, typescript, and react in my projects.";
983 let ctx = create_context(content);
984 let result = rule.check(&ctx).unwrap();
985
986 assert_eq!(result.len(), 3, "Should flag all three incorrect names");
987 assert_eq!(result[0].message, "Proper name 'javascript' should be 'JavaScript'");
988 assert_eq!(result[1].message, "Proper name 'typescript' should be 'TypeScript'");
989 assert_eq!(result[2].message, "Proper name 'react' should be 'React'");
990 }
991
992 #[test]
993 fn test_case_sensitivity() {
994 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
995
996 let content = "JAVASCRIPT, Javascript, javascript, and JavaScript variations.";
997 let ctx = create_context(content);
998 let result = rule.check(&ctx).unwrap();
999
1000 assert_eq!(result.len(), 3, "Should flag all incorrect case variations");
1001 assert!(result.iter().all(|w| w.message.contains("should be 'JavaScript'")));
1003 }
1004
1005 #[test]
1006 fn test_configuration_with_custom_name_list() {
1007 let config = MD044Config {
1008 names: vec!["GitHub".to_string(), "GitLab".to_string(), "DevOps".to_string()],
1009 code_blocks: true,
1010 html_elements: true,
1011 html_comments: true,
1012 };
1013 let rule = MD044ProperNames::from_config_struct(config);
1014
1015 let content = "We use github, gitlab, and devops for our workflow.";
1016 let ctx = create_context(content);
1017 let result = rule.check(&ctx).unwrap();
1018
1019 assert_eq!(result.len(), 3, "Should flag all custom names");
1020 assert_eq!(result[0].message, "Proper name 'github' should be 'GitHub'");
1021 assert_eq!(result[1].message, "Proper name 'gitlab' should be 'GitLab'");
1022 assert_eq!(result[2].message, "Proper name 'devops' should be 'DevOps'");
1023 }
1024
1025 #[test]
1026 fn test_empty_configuration() {
1027 let rule = MD044ProperNames::new(vec![], true);
1028
1029 let content = "This has javascript and typescript but no configured names.";
1030 let ctx = create_context(content);
1031 let result = rule.check(&ctx).unwrap();
1032
1033 assert!(result.is_empty(), "Should not flag anything with empty configuration");
1034 }
1035
1036 #[test]
1037 fn test_names_with_special_characters() {
1038 let rule = MD044ProperNames::new(
1039 vec!["Node.js".to_string(), "ASP.NET".to_string(), "C++".to_string()],
1040 true,
1041 );
1042
1043 let content = "We use nodejs, asp.net, ASP.NET, and c++ in our stack.";
1044 let ctx = create_context(content);
1045 let result = rule.check(&ctx).unwrap();
1046
1047 assert_eq!(result.len(), 3, "Should handle special characters correctly");
1052
1053 let messages: Vec<&str> = result.iter().map(|w| w.message.as_str()).collect();
1054 assert!(messages.contains(&"Proper name 'nodejs' should be 'Node.js'"));
1055 assert!(messages.contains(&"Proper name 'asp.net' should be 'ASP.NET'"));
1056 assert!(messages.contains(&"Proper name 'c++' should be 'C++'"));
1057 }
1058
1059 #[test]
1060 fn test_word_boundaries() {
1061 let rule = MD044ProperNames::new(vec!["Java".to_string(), "Script".to_string()], true);
1062
1063 let content = "JavaScript is not java or script, but Java and Script are separate.";
1064 let ctx = create_context(content);
1065 let result = rule.check(&ctx).unwrap();
1066
1067 assert_eq!(result.len(), 2, "Should respect word boundaries");
1069 assert!(result.iter().any(|w| w.column == 19)); assert!(result.iter().any(|w| w.column == 27)); }
1072
1073 #[test]
1074 fn test_fix_method() {
1075 let rule = MD044ProperNames::new(
1076 vec![
1077 "JavaScript".to_string(),
1078 "TypeScript".to_string(),
1079 "Node.js".to_string(),
1080 ],
1081 true,
1082 );
1083
1084 let content = "I love javascript, typescript, and nodejs!";
1085 let ctx = create_context(content);
1086 let fixed = rule.fix(&ctx).unwrap();
1087
1088 assert_eq!(fixed, "I love JavaScript, TypeScript, and Node.js!");
1089 }
1090
1091 #[test]
1092 fn test_fix_multiple_occurrences() {
1093 let rule = MD044ProperNames::new(vec!["Python".to_string()], true);
1094
1095 let content = "python is great. I use python daily. PYTHON is powerful.";
1096 let ctx = create_context(content);
1097 let fixed = rule.fix(&ctx).unwrap();
1098
1099 assert_eq!(fixed, "Python is great. I use Python daily. Python is powerful.");
1100 }
1101
1102 #[test]
1103 fn test_fix_checks_code_blocks_by_default() {
1104 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1105
1106 let content = r#"I love javascript.
1107
1108```
1109const lang = "javascript";
1110```
1111
1112More javascript here."#;
1113
1114 let ctx = create_context(content);
1115 let fixed = rule.fix(&ctx).unwrap();
1116
1117 let expected = r#"I love JavaScript.
1118
1119```
1120const lang = "JavaScript";
1121```
1122
1123More JavaScript here."#;
1124
1125 assert_eq!(fixed, expected);
1126 }
1127
1128 #[test]
1129 fn test_multiline_content() {
1130 let rule = MD044ProperNames::new(vec!["Rust".to_string(), "Python".to_string()], true);
1131
1132 let content = r#"First line with rust.
1133Second line with python.
1134Third line with RUST and PYTHON."#;
1135
1136 let ctx = create_context(content);
1137 let result = rule.check(&ctx).unwrap();
1138
1139 assert_eq!(result.len(), 4, "Should flag all incorrect occurrences");
1140 assert_eq!(result[0].line, 1);
1141 assert_eq!(result[1].line, 2);
1142 assert_eq!(result[2].line, 3);
1143 assert_eq!(result[3].line, 3);
1144 }
1145
1146 #[test]
1147 fn test_default_config() {
1148 let config = MD044Config::default();
1149 assert!(config.names.is_empty());
1150 assert!(!config.code_blocks);
1151 assert!(config.html_elements);
1152 assert!(config.html_comments);
1153 }
1154
1155 #[test]
1156 fn test_default_config_checks_html_comments() {
1157 let config = MD044Config {
1158 names: vec!["JavaScript".to_string()],
1159 ..MD044Config::default()
1160 };
1161 let rule = MD044ProperNames::from_config_struct(config);
1162
1163 let content = "# Guide\n\n<!-- javascript mentioned here -->\n";
1164 let ctx = create_context(content);
1165 let result = rule.check(&ctx).unwrap();
1166
1167 assert_eq!(result.len(), 1, "Default config should check HTML comments");
1168 assert_eq!(result[0].line, 3);
1169 }
1170
1171 #[test]
1172 fn test_default_config_skips_code_blocks() {
1173 let config = MD044Config {
1174 names: vec!["JavaScript".to_string()],
1175 ..MD044Config::default()
1176 };
1177 let rule = MD044ProperNames::from_config_struct(config);
1178
1179 let content = "# Guide\n\n```\njavascript in code\n```\n";
1180 let ctx = create_context(content);
1181 let result = rule.check(&ctx).unwrap();
1182
1183 assert_eq!(result.len(), 0, "Default config should skip code blocks");
1184 }
1185
1186 #[test]
1187 fn test_standalone_html_comment_checked() {
1188 let config = MD044Config {
1189 names: vec!["Test".to_string()],
1190 ..MD044Config::default()
1191 };
1192 let rule = MD044ProperNames::from_config_struct(config);
1193
1194 let content = "# Heading\n\n<!-- this is a test example -->\n";
1195 let ctx = create_context(content);
1196 let result = rule.check(&ctx).unwrap();
1197
1198 assert_eq!(result.len(), 1, "Should flag proper name in standalone HTML comment");
1199 assert_eq!(result[0].line, 3);
1200 }
1201
1202 #[test]
1203 fn test_inline_config_comments_not_flagged() {
1204 let config = MD044Config {
1205 names: vec!["RUMDL".to_string()],
1206 ..MD044Config::default()
1207 };
1208 let rule = MD044ProperNames::from_config_struct(config);
1209
1210 let content = "<!-- rumdl-disable MD044 -->\nSome rumdl text here.\n<!-- rumdl-enable MD044 -->\n<!-- markdownlint-disable -->\nMore rumdl text.\n<!-- markdownlint-enable -->\n";
1214 let ctx = create_context(content);
1215 let result = rule.check(&ctx).unwrap();
1216
1217 assert_eq!(result.len(), 2, "Should only flag body lines, not config comments");
1218 assert_eq!(result[0].line, 2);
1219 assert_eq!(result[1].line, 5);
1220 }
1221
1222 #[test]
1223 fn test_html_comment_skipped_when_disabled() {
1224 let config = MD044Config {
1225 names: vec!["Test".to_string()],
1226 code_blocks: true,
1227 html_elements: true,
1228 html_comments: false,
1229 };
1230 let rule = MD044ProperNames::from_config_struct(config);
1231
1232 let content = "# Heading\n\n<!-- this is a test example -->\n\nRegular test here.\n";
1233 let ctx = create_context(content);
1234 let result = rule.check(&ctx).unwrap();
1235
1236 assert_eq!(
1237 result.len(),
1238 1,
1239 "Should only flag 'test' outside HTML comment when html_comments=false"
1240 );
1241 assert_eq!(result[0].line, 5);
1242 }
1243
1244 #[test]
1245 fn test_fix_corrects_html_comment_content() {
1246 let config = MD044Config {
1247 names: vec!["JavaScript".to_string()],
1248 ..MD044Config::default()
1249 };
1250 let rule = MD044ProperNames::from_config_struct(config);
1251
1252 let content = "# Guide\n\n<!-- javascript mentioned here -->\n";
1253 let ctx = create_context(content);
1254 let fixed = rule.fix(&ctx).unwrap();
1255
1256 assert_eq!(fixed, "# Guide\n\n<!-- JavaScript mentioned here -->\n");
1257 }
1258
1259 #[test]
1260 fn test_fix_does_not_modify_inline_config_comments() {
1261 let config = MD044Config {
1262 names: vec!["RUMDL".to_string()],
1263 ..MD044Config::default()
1264 };
1265 let rule = MD044ProperNames::from_config_struct(config);
1266
1267 let content = "<!-- rumdl-disable -->\nSome rumdl text.\n<!-- rumdl-enable -->\n";
1268 let ctx = create_context(content);
1269 let fixed = rule.fix(&ctx).unwrap();
1270
1271 assert!(fixed.contains("<!-- rumdl-disable -->"));
1273 assert!(fixed.contains("<!-- rumdl-enable -->"));
1274 assert!(
1276 fixed.contains("Some rumdl text."),
1277 "Line inside rumdl-disable block should not be modified by fix()"
1278 );
1279 }
1280
1281 #[test]
1282 fn test_fix_respects_inline_disable_partial() {
1283 let config = MD044Config {
1284 names: vec!["RUMDL".to_string()],
1285 ..MD044Config::default()
1286 };
1287 let rule = MD044ProperNames::from_config_struct(config);
1288
1289 let content =
1290 "<!-- rumdl-disable MD044 -->\nSome rumdl text.\n<!-- rumdl-enable MD044 -->\n\nSome rumdl text outside.\n";
1291 let ctx = create_context(content);
1292 let fixed = rule.fix(&ctx).unwrap();
1293
1294 assert!(
1296 fixed.contains("Some rumdl text.\n<!-- rumdl-enable"),
1297 "Line inside disable block should not be modified"
1298 );
1299 assert!(
1301 fixed.contains("Some RUMDL text outside."),
1302 "Line outside disable block should be fixed"
1303 );
1304 }
1305
1306 #[test]
1307 fn test_performance_with_many_names() {
1308 let mut names = vec![];
1309 for i in 0..50 {
1310 names.push(format!("ProperName{i}"));
1311 }
1312
1313 let rule = MD044ProperNames::new(names, true);
1314
1315 let content = "This has propername0, propername25, and propername49 incorrectly.";
1316 let ctx = create_context(content);
1317 let result = rule.check(&ctx).unwrap();
1318
1319 assert_eq!(result.len(), 3, "Should handle many configured names efficiently");
1320 }
1321
1322 #[test]
1323 fn test_large_name_count_performance() {
1324 let names = (0..1000).map(|i| format!("ProperName{i}")).collect::<Vec<_>>();
1327
1328 let rule = MD044ProperNames::new(names, true);
1329
1330 assert!(rule.combined_pattern.is_some());
1332
1333 let content = "This has propername0 and propername999 in it.";
1335 let ctx = create_context(content);
1336 let result = rule.check(&ctx).unwrap();
1337
1338 assert_eq!(result.len(), 2, "Should handle 1000 names without issues");
1340 }
1341
1342 #[test]
1343 fn test_cache_behavior() {
1344 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1345
1346 let content = "Using javascript here.";
1347 let ctx = create_context(content);
1348
1349 let result1 = rule.check(&ctx).unwrap();
1351 assert_eq!(result1.len(), 1);
1352
1353 let result2 = rule.check(&ctx).unwrap();
1355 assert_eq!(result2.len(), 1);
1356
1357 assert_eq!(result1[0].line, result2[0].line);
1359 assert_eq!(result1[0].column, result2[0].column);
1360 }
1361
1362 #[test]
1363 fn test_html_comments_not_checked_when_disabled() {
1364 let config = MD044Config {
1365 names: vec!["JavaScript".to_string()],
1366 code_blocks: true, html_elements: true, html_comments: false, };
1370 let rule = MD044ProperNames::from_config_struct(config);
1371
1372 let content = r#"Regular javascript here.
1373<!-- This javascript in HTML comment should be ignored -->
1374More javascript outside."#;
1375
1376 let ctx = create_context(content);
1377 let result = rule.check(&ctx).unwrap();
1378
1379 assert_eq!(result.len(), 2, "Should only flag javascript outside HTML comments");
1380 assert_eq!(result[0].line, 1);
1381 assert_eq!(result[1].line, 3);
1382 }
1383
1384 #[test]
1385 fn test_html_comments_checked_when_enabled() {
1386 let config = MD044Config {
1387 names: vec!["JavaScript".to_string()],
1388 code_blocks: true, html_elements: true, html_comments: true, };
1392 let rule = MD044ProperNames::from_config_struct(config);
1393
1394 let content = r#"Regular javascript here.
1395<!-- This javascript in HTML comment should be checked -->
1396More javascript outside."#;
1397
1398 let ctx = create_context(content);
1399 let result = rule.check(&ctx).unwrap();
1400
1401 assert_eq!(
1402 result.len(),
1403 3,
1404 "Should flag all javascript occurrences including in HTML comments"
1405 );
1406 }
1407
1408 #[test]
1409 fn test_multiline_html_comments() {
1410 let config = MD044Config {
1411 names: vec!["Python".to_string(), "JavaScript".to_string()],
1412 code_blocks: true, html_elements: true, html_comments: false, };
1416 let rule = MD044ProperNames::from_config_struct(config);
1417
1418 let content = r#"Regular python here.
1419<!--
1420This is a multiline comment
1421with javascript and python
1422that should be ignored
1423-->
1424More javascript outside."#;
1425
1426 let ctx = create_context(content);
1427 let result = rule.check(&ctx).unwrap();
1428
1429 assert_eq!(result.len(), 2, "Should only flag names outside HTML comments");
1430 assert_eq!(result[0].line, 1); assert_eq!(result[1].line, 7); }
1433
1434 #[test]
1435 fn test_fix_preserves_html_comments_when_disabled() {
1436 let config = MD044Config {
1437 names: vec!["JavaScript".to_string()],
1438 code_blocks: true, html_elements: true, html_comments: false, };
1442 let rule = MD044ProperNames::from_config_struct(config);
1443
1444 let content = r#"javascript here.
1445<!-- javascript in comment -->
1446More javascript."#;
1447
1448 let ctx = create_context(content);
1449 let fixed = rule.fix(&ctx).unwrap();
1450
1451 let expected = r#"JavaScript here.
1452<!-- javascript in comment -->
1453More JavaScript."#;
1454
1455 assert_eq!(
1456 fixed, expected,
1457 "Should not fix names inside HTML comments when disabled"
1458 );
1459 }
1460
1461 #[test]
1462 fn test_proper_names_in_link_text_are_flagged() {
1463 let rule = MD044ProperNames::new(
1464 vec!["JavaScript".to_string(), "Node.js".to_string(), "Python".to_string()],
1465 true,
1466 );
1467
1468 let content = r#"Check this [javascript documentation](https://javascript.info) for info.
1469
1470Visit [node.js homepage](https://nodejs.org) and [python tutorial](https://python.org).
1471
1472Real javascript should be flagged.
1473
1474Also see the [typescript guide][ts-ref] for more.
1475
1476Real python should be flagged too.
1477
1478[ts-ref]: https://typescript.org/handbook"#;
1479
1480 let ctx = create_context(content);
1481 let result = rule.check(&ctx).unwrap();
1482
1483 assert_eq!(result.len(), 5, "Expected 5 warnings: 3 in link text + 2 standalone");
1490
1491 let line_1_warnings: Vec<_> = result.iter().filter(|w| w.line == 1).collect();
1493 assert_eq!(line_1_warnings.len(), 1);
1494 assert!(
1495 line_1_warnings[0]
1496 .message
1497 .contains("'javascript' should be 'JavaScript'")
1498 );
1499
1500 let line_3_warnings: Vec<_> = result.iter().filter(|w| w.line == 3).collect();
1501 assert_eq!(line_3_warnings.len(), 2); assert!(result.iter().any(|w| w.line == 5 && w.message.contains("'javascript'")));
1505 assert!(result.iter().any(|w| w.line == 9 && w.message.contains("'python'")));
1506 }
1507
1508 #[test]
1509 fn test_link_urls_not_flagged() {
1510 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1511
1512 let content = r#"[Link Text](https://javascript.info/guide)"#;
1514
1515 let ctx = create_context(content);
1516 let result = rule.check(&ctx).unwrap();
1517
1518 assert!(result.is_empty(), "URLs should not be checked for proper names");
1520 }
1521
1522 #[test]
1523 fn test_proper_names_in_image_alt_text_are_flagged() {
1524 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1525
1526 let content = r#"Here is a  image.
1527
1528Real javascript should be flagged."#;
1529
1530 let ctx = create_context(content);
1531 let result = rule.check(&ctx).unwrap();
1532
1533 assert_eq!(result.len(), 2, "Expected 2 warnings: 1 in alt text + 1 standalone");
1537 assert!(result[0].message.contains("'javascript' should be 'JavaScript'"));
1538 assert!(result[0].line == 1); assert!(result[1].message.contains("'javascript' should be 'JavaScript'"));
1540 assert!(result[1].line == 3); }
1542
1543 #[test]
1544 fn test_image_urls_not_flagged() {
1545 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1546
1547 let content = r#""#;
1549
1550 let ctx = create_context(content);
1551 let result = rule.check(&ctx).unwrap();
1552
1553 assert!(result.is_empty(), "Image URLs should not be checked for proper names");
1555 }
1556
1557 #[test]
1558 fn test_reference_link_text_flagged_but_definition_not() {
1559 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "TypeScript".to_string()], true);
1560
1561 let content = r#"Check the [javascript guide][js-ref] for details.
1562
1563Real javascript should be flagged.
1564
1565[js-ref]: https://javascript.info/typescript/guide"#;
1566
1567 let ctx = create_context(content);
1568 let result = rule.check(&ctx).unwrap();
1569
1570 assert_eq!(result.len(), 2, "Expected 2 warnings: 1 in link text + 1 standalone");
1575 assert!(result.iter().any(|w| w.line == 1 && w.message.contains("'javascript'")));
1576 assert!(result.iter().any(|w| w.line == 3 && w.message.contains("'javascript'")));
1577 }
1578
1579 #[test]
1580 fn test_reference_definitions_not_flagged() {
1581 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1582
1583 let content = r#"[js-ref]: https://javascript.info/guide"#;
1585
1586 let ctx = create_context(content);
1587 let result = rule.check(&ctx).unwrap();
1588
1589 assert!(result.is_empty(), "Reference definitions should not be checked");
1591 }
1592
1593 #[test]
1594 fn test_wikilinks_text_is_flagged() {
1595 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1596
1597 let content = r#"[[javascript]]
1599
1600Regular javascript here.
1601
1602[[JavaScript|display text]]"#;
1603
1604 let ctx = create_context(content);
1605 let result = rule.check(&ctx).unwrap();
1606
1607 assert_eq!(result.len(), 2, "Expected 2 warnings: 1 in WikiLink + 1 standalone");
1611 assert!(
1612 result
1613 .iter()
1614 .any(|w| w.line == 1 && w.column == 3 && w.message.contains("'javascript'"))
1615 );
1616 assert!(result.iter().any(|w| w.line == 3 && w.message.contains("'javascript'")));
1617 }
1618
1619 #[test]
1620 fn test_url_link_text_not_flagged() {
1621 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1622
1623 let content = r#"[https://github.com/org/repo](https://github.com/org/repo)
1625
1626[http://github.com/org/repo](http://github.com/org/repo)
1627
1628[www.github.com/org/repo](https://www.github.com/org/repo)"#;
1629
1630 let ctx = create_context(content);
1631 let result = rule.check(&ctx).unwrap();
1632
1633 assert!(
1634 result.is_empty(),
1635 "URL-like link text should not be flagged, got: {result:?}"
1636 );
1637 }
1638
1639 #[test]
1640 fn test_url_link_text_with_leading_space_not_flagged() {
1641 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1642
1643 let content = r#"[ https://github.com/org/repo](https://github.com/org/repo)"#;
1645
1646 let ctx = create_context(content);
1647 let result = rule.check(&ctx).unwrap();
1648
1649 assert!(
1650 result.is_empty(),
1651 "URL-like link text with leading space should not be flagged, got: {result:?}"
1652 );
1653 }
1654
1655 #[test]
1656 fn test_url_link_text_uppercase_scheme_not_flagged() {
1657 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1658
1659 let content = r#"[HTTPS://GITHUB.COM/org/repo](https://github.com/org/repo)"#;
1660
1661 let ctx = create_context(content);
1662 let result = rule.check(&ctx).unwrap();
1663
1664 assert!(
1665 result.is_empty(),
1666 "URL-like link text with uppercase scheme should not be flagged, got: {result:?}"
1667 );
1668 }
1669
1670 #[test]
1671 fn test_non_url_link_text_still_flagged() {
1672 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1673
1674 let content = r#"[github.com/org/repo](https://github.com/org/repo)
1676
1677[Visit github](https://github.com/org/repo)
1678
1679[//github.com/org/repo](//github.com/org/repo)
1680
1681[ftp://github.com/org/repo](ftp://github.com/org/repo)"#;
1682
1683 let ctx = create_context(content);
1684 let result = rule.check(&ctx).unwrap();
1685
1686 assert_eq!(result.len(), 4, "Non-URL link text should be flagged, got: {result:?}");
1687 assert!(result.iter().any(|w| w.line == 1)); assert!(result.iter().any(|w| w.line == 3)); assert!(result.iter().any(|w| w.line == 5)); assert!(result.iter().any(|w| w.line == 7)); }
1692
1693 #[test]
1694 fn test_url_link_text_fix_not_applied() {
1695 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1696
1697 let content = "[https://github.com/org/repo](https://github.com/org/repo)\n";
1698
1699 let ctx = create_context(content);
1700 let result = rule.fix(&ctx).unwrap();
1701
1702 assert_eq!(result, content, "Fix should not modify URL-like link text");
1703 }
1704
1705 #[test]
1706 fn test_mixed_url_and_regular_link_text() {
1707 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1708
1709 let content = r#"[https://github.com/org/repo](https://github.com/org/repo)
1711
1712Visit [github documentation](https://github.com/docs) for details.
1713
1714[www.github.com/pricing](https://www.github.com/pricing)"#;
1715
1716 let ctx = create_context(content);
1717 let result = rule.check(&ctx).unwrap();
1718
1719 assert_eq!(
1721 result.len(),
1722 1,
1723 "Only non-URL link text should be flagged, got: {result:?}"
1724 );
1725 assert_eq!(result[0].line, 3);
1726 }
1727
1728 #[test]
1729 fn test_html_attribute_values_not_flagged() {
1730 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1733 let content = "# Heading\n\ntest\n\n<img src=\"www.example.test/test_image.png\">\n";
1734 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1735 let result = rule.check(&ctx).unwrap();
1736
1737 let line5_violations: Vec<_> = result.iter().filter(|w| w.line == 5).collect();
1739 assert!(
1740 line5_violations.is_empty(),
1741 "Should not flag anything inside HTML tag attributes: {line5_violations:?}"
1742 );
1743
1744 let line3_violations: Vec<_> = result.iter().filter(|w| w.line == 3).collect();
1746 assert_eq!(line3_violations.len(), 1, "Plain 'test' on line 3 should be flagged");
1747 }
1748
1749 #[test]
1750 fn test_html_text_content_still_flagged() {
1751 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1753 let content = "# Heading\n\n<a href=\"https://example.test/page\">test link</a>\n";
1754 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1755 let result = rule.check(&ctx).unwrap();
1756
1757 assert_eq!(
1760 result.len(),
1761 1,
1762 "Should flag only 'test' in anchor text, not in href: {result:?}"
1763 );
1764 assert_eq!(result[0].column, 37, "Should flag col 37 ('test link' in anchor text)");
1765 }
1766
1767 #[test]
1768 fn test_html_attribute_various_not_flagged() {
1769 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1771 let content = concat!(
1772 "# Heading\n\n",
1773 "<img src=\"test.png\" alt=\"test image\">\n",
1774 "<span class=\"test-class\" data-test=\"value\">test content</span>\n",
1775 );
1776 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1777 let result = rule.check(&ctx).unwrap();
1778
1779 assert_eq!(
1781 result.len(),
1782 1,
1783 "Should flag only 'test content' between tags: {result:?}"
1784 );
1785 assert_eq!(result[0].line, 4);
1786 }
1787
1788 #[test]
1789 fn test_plain_text_underscore_boundary_unchanged() {
1790 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1793 let content = "# Heading\n\ntest_image is here and just_test ends here\n";
1794 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1795 let result = rule.check(&ctx).unwrap();
1796
1797 assert_eq!(
1800 result.len(),
1801 2,
1802 "Should flag 'test' in both 'test_image' and 'just_test': {result:?}"
1803 );
1804 let cols: Vec<usize> = result.iter().map(|w| w.column).collect();
1805 assert!(cols.contains(&1), "Should flag col 1 (test_image): {cols:?}");
1806 assert!(cols.contains(&29), "Should flag col 29 (just_test): {cols:?}");
1807 }
1808
1809 #[test]
1810 fn test_frontmatter_yaml_keys_not_flagged() {
1811 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1814
1815 let content = "---\ntitle: Heading\ntest: Some Test value\n---\n\nTest\n";
1816 let ctx = create_context(content);
1817 let result = rule.check(&ctx).unwrap();
1818
1819 assert!(
1823 result.is_empty(),
1824 "Should not flag YAML keys or correctly capitalized values: {result:?}"
1825 );
1826 }
1827
1828 #[test]
1829 fn test_frontmatter_yaml_values_flagged() {
1830 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1832
1833 let content = "---\ntitle: Heading\nkey: a test value\n---\n\nTest\n";
1834 let ctx = create_context(content);
1835 let result = rule.check(&ctx).unwrap();
1836
1837 assert_eq!(result.len(), 1, "Should flag 'test' in YAML value: {result:?}");
1839 assert_eq!(result[0].line, 3);
1840 assert_eq!(result[0].column, 8); }
1842
1843 #[test]
1844 fn test_frontmatter_key_matches_name_not_flagged() {
1845 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1847
1848 let content = "---\ntest: other value\n---\n\nBody text\n";
1849 let ctx = create_context(content);
1850 let result = rule.check(&ctx).unwrap();
1851
1852 assert!(
1853 result.is_empty(),
1854 "Should not flag YAML key that matches configured name: {result:?}"
1855 );
1856 }
1857
1858 #[test]
1859 fn test_frontmatter_empty_value_not_flagged() {
1860 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1862
1863 let content = "---\ntest:\ntest: \n---\n\nBody text\n";
1864 let ctx = create_context(content);
1865 let result = rule.check(&ctx).unwrap();
1866
1867 assert!(
1868 result.is_empty(),
1869 "Should not flag YAML keys with empty values: {result:?}"
1870 );
1871 }
1872
1873 #[test]
1874 fn test_frontmatter_nested_yaml_key_not_flagged() {
1875 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1877
1878 let content = "---\nparent:\n test: nested value\n---\n\nBody text\n";
1879 let ctx = create_context(content);
1880 let result = rule.check(&ctx).unwrap();
1881
1882 assert!(result.is_empty(), "Should not flag nested YAML keys: {result:?}");
1884 }
1885
1886 #[test]
1887 fn test_frontmatter_list_items_checked() {
1888 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1890
1891 let content = "---\ntags:\n - test\n - other\n---\n\nBody text\n";
1892 let ctx = create_context(content);
1893 let result = rule.check(&ctx).unwrap();
1894
1895 assert_eq!(result.len(), 1, "Should flag 'test' in YAML list item: {result:?}");
1897 assert_eq!(result[0].line, 3);
1898 }
1899
1900 #[test]
1901 fn test_frontmatter_value_with_multiple_colons() {
1902 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1904
1905 let content = "---\ntest: description: a test thing\n---\n\nBody text\n";
1906 let ctx = create_context(content);
1907 let result = rule.check(&ctx).unwrap();
1908
1909 assert_eq!(
1912 result.len(),
1913 1,
1914 "Should flag 'test' in value after first colon: {result:?}"
1915 );
1916 assert_eq!(result[0].line, 2);
1917 assert!(result[0].column > 6, "Violation column should be in value portion");
1918 }
1919
1920 #[test]
1921 fn test_frontmatter_does_not_affect_body() {
1922 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1924
1925 let content = "---\ntitle: Heading\n---\n\ntest should be flagged here\n";
1926 let ctx = create_context(content);
1927 let result = rule.check(&ctx).unwrap();
1928
1929 assert_eq!(result.len(), 1, "Should flag 'test' in body text: {result:?}");
1930 assert_eq!(result[0].line, 5);
1931 }
1932
1933 #[test]
1934 fn test_frontmatter_fix_corrects_values_preserves_keys() {
1935 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1937
1938 let content = "---\ntest: a test value\n---\n\ntest here\n";
1939 let ctx = create_context(content);
1940 let fixed = rule.fix(&ctx).unwrap();
1941
1942 assert_eq!(fixed, "---\ntest: a Test value\n---\n\nTest here\n");
1944 }
1945
1946 #[test]
1947 fn test_frontmatter_multiword_value_flagged() {
1948 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "TypeScript".to_string()], true);
1950
1951 let content = "---\ndescription: Learn javascript and typescript\n---\n\nBody\n";
1952 let ctx = create_context(content);
1953 let result = rule.check(&ctx).unwrap();
1954
1955 assert_eq!(result.len(), 2, "Should flag both names in YAML value: {result:?}");
1956 assert!(result.iter().all(|w| w.line == 2));
1957 }
1958
1959 #[test]
1960 fn test_frontmatter_yaml_comments_not_checked() {
1961 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1963
1964 let content = "---\n# test comment\ntitle: Heading\n---\n\nBody text\n";
1965 let ctx = create_context(content);
1966 let result = rule.check(&ctx).unwrap();
1967
1968 assert!(result.is_empty(), "Should not flag names in YAML comments: {result:?}");
1969 }
1970
1971 #[test]
1972 fn test_frontmatter_delimiters_not_checked() {
1973 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1975
1976 let content = "---\ntitle: Heading\n---\n\ntest here\n";
1977 let ctx = create_context(content);
1978 let result = rule.check(&ctx).unwrap();
1979
1980 assert_eq!(result.len(), 1, "Should only flag body text: {result:?}");
1982 assert_eq!(result[0].line, 5);
1983 }
1984
1985 #[test]
1986 fn test_frontmatter_continuation_lines_checked() {
1987 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1989
1990 let content = "---\ndescription: >\n a test value\n continued here\n---\n\nBody\n";
1991 let ctx = create_context(content);
1992 let result = rule.check(&ctx).unwrap();
1993
1994 assert_eq!(result.len(), 1, "Should flag 'test' in continuation line: {result:?}");
1996 assert_eq!(result[0].line, 3);
1997 }
1998
1999 #[test]
2000 fn test_frontmatter_quoted_values_checked() {
2001 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2003
2004 let content = "---\ntitle: \"a test title\"\n---\n\nBody\n";
2005 let ctx = create_context(content);
2006 let result = rule.check(&ctx).unwrap();
2007
2008 assert_eq!(result.len(), 1, "Should flag 'test' in quoted YAML value: {result:?}");
2009 assert_eq!(result[0].line, 2);
2010 }
2011
2012 #[test]
2013 fn test_frontmatter_single_quoted_values_checked() {
2014 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2016
2017 let content = "---\ntitle: 'a test title'\n---\n\nBody\n";
2018 let ctx = create_context(content);
2019 let result = rule.check(&ctx).unwrap();
2020
2021 assert_eq!(
2022 result.len(),
2023 1,
2024 "Should flag 'test' in single-quoted YAML value: {result:?}"
2025 );
2026 assert_eq!(result[0].line, 2);
2027 }
2028
2029 #[test]
2030 fn test_frontmatter_fix_multiword_values() {
2031 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "TypeScript".to_string()], true);
2033
2034 let content = "---\ndescription: Learn javascript and typescript\n---\n\nBody\n";
2035 let ctx = create_context(content);
2036 let fixed = rule.fix(&ctx).unwrap();
2037
2038 assert_eq!(
2039 fixed,
2040 "---\ndescription: Learn JavaScript and TypeScript\n---\n\nBody\n"
2041 );
2042 }
2043
2044 #[test]
2045 fn test_frontmatter_fix_preserves_yaml_structure() {
2046 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2048
2049 let content = "---\ntags:\n - test\n - other\ntitle: a test doc\n---\n\ntest body\n";
2050 let ctx = create_context(content);
2051 let fixed = rule.fix(&ctx).unwrap();
2052
2053 assert_eq!(
2054 fixed,
2055 "---\ntags:\n - Test\n - other\ntitle: a Test doc\n---\n\nTest body\n"
2056 );
2057 }
2058
2059 #[test]
2060 fn test_frontmatter_toml_delimiters_not_checked() {
2061 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2063
2064 let content = "+++\ntitle = \"a test title\"\n+++\n\ntest body\n";
2065 let ctx = create_context(content);
2066 let result = rule.check(&ctx).unwrap();
2067
2068 assert_eq!(result.len(), 2, "Should flag TOML value and body: {result:?}");
2072 let fm_violations: Vec<_> = result.iter().filter(|w| w.line == 2).collect();
2073 assert_eq!(fm_violations.len(), 1, "Should flag 'test' in TOML value: {result:?}");
2074 let body_violations: Vec<_> = result.iter().filter(|w| w.line == 5).collect();
2075 assert_eq!(body_violations.len(), 1, "Should flag body 'test': {result:?}");
2076 }
2077
2078 #[test]
2079 fn test_frontmatter_toml_key_not_flagged() {
2080 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2082
2083 let content = "+++\ntest = \"other value\"\n+++\n\nBody text\n";
2084 let ctx = create_context(content);
2085 let result = rule.check(&ctx).unwrap();
2086
2087 assert!(
2088 result.is_empty(),
2089 "Should not flag TOML key that matches configured name: {result:?}"
2090 );
2091 }
2092
2093 #[test]
2094 fn test_frontmatter_toml_fix_preserves_keys() {
2095 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2097
2098 let content = "+++\ntest = \"a test value\"\n+++\n\ntest here\n";
2099 let ctx = create_context(content);
2100 let fixed = rule.fix(&ctx).unwrap();
2101
2102 assert_eq!(fixed, "+++\ntest = \"a Test value\"\n+++\n\nTest here\n");
2104 }
2105
2106 #[test]
2107 fn test_frontmatter_list_item_mapping_key_not_flagged() {
2108 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2111
2112 let content = "---\nitems:\n - test: nested value\n---\n\nBody text\n";
2113 let ctx = create_context(content);
2114 let result = rule.check(&ctx).unwrap();
2115
2116 assert!(
2117 result.is_empty(),
2118 "Should not flag YAML key in list-item mapping: {result:?}"
2119 );
2120 }
2121
2122 #[test]
2123 fn test_frontmatter_list_item_mapping_value_flagged() {
2124 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2126
2127 let content = "---\nitems:\n - key: a test value\n---\n\nBody text\n";
2128 let ctx = create_context(content);
2129 let result = rule.check(&ctx).unwrap();
2130
2131 assert_eq!(
2132 result.len(),
2133 1,
2134 "Should flag 'test' in list-item mapping value: {result:?}"
2135 );
2136 assert_eq!(result[0].line, 3);
2137 }
2138
2139 #[test]
2140 fn test_frontmatter_bare_list_item_still_flagged() {
2141 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2143
2144 let content = "---\ntags:\n - test\n - other\n---\n\nBody text\n";
2145 let ctx = create_context(content);
2146 let result = rule.check(&ctx).unwrap();
2147
2148 assert_eq!(result.len(), 1, "Should flag 'test' in bare list item: {result:?}");
2149 assert_eq!(result[0].line, 3);
2150 }
2151
2152 #[test]
2153 fn test_frontmatter_flow_mapping_not_flagged() {
2154 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2157
2158 let content = "---\nflow_map: {test: value, other: test}\n---\n\nBody text\n";
2159 let ctx = create_context(content);
2160 let result = rule.check(&ctx).unwrap();
2161
2162 assert!(
2163 result.is_empty(),
2164 "Should not flag names inside flow mappings: {result:?}"
2165 );
2166 }
2167
2168 #[test]
2169 fn test_frontmatter_flow_sequence_not_flagged() {
2170 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2172
2173 let content = "---\nitems: [test, other, test]\n---\n\nBody text\n";
2174 let ctx = create_context(content);
2175 let result = rule.check(&ctx).unwrap();
2176
2177 assert!(
2178 result.is_empty(),
2179 "Should not flag names inside flow sequences: {result:?}"
2180 );
2181 }
2182
2183 #[test]
2184 fn test_frontmatter_list_item_mapping_fix_preserves_key() {
2185 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2187
2188 let content = "---\nitems:\n - test: a test value\n---\n\ntest here\n";
2189 let ctx = create_context(content);
2190 let fixed = rule.fix(&ctx).unwrap();
2191
2192 assert_eq!(fixed, "---\nitems:\n - test: a Test value\n---\n\nTest here\n");
2195 }
2196
2197 #[test]
2200 fn test_angle_bracket_url_in_html_comment_not_flagged() {
2201 let config = MD044Config {
2203 names: vec!["Test".to_string()],
2204 ..MD044Config::default()
2205 };
2206 let rule = MD044ProperNames::from_config_struct(config);
2207
2208 let content = "---\ntitle: Level 1 heading\n---\n\n<https://www.example.test>\n\n<!-- This is a Test https://www.example.test -->\n<!-- This is a Test <https://www.example.test> -->\n";
2209 let ctx = create_context(content);
2210 let result = rule.check(&ctx).unwrap();
2211
2212 let line8_warnings: Vec<_> = result.iter().filter(|w| w.line == 8).collect();
2220 assert!(
2221 line8_warnings.is_empty(),
2222 "Should not flag names inside angle-bracket URLs in HTML comments: {line8_warnings:?}"
2223 );
2224 }
2225
2226 #[test]
2227 fn test_bare_url_in_html_comment_still_flagged() {
2228 let config = MD044Config {
2230 names: vec!["Test".to_string()],
2231 ..MD044Config::default()
2232 };
2233 let rule = MD044ProperNames::from_config_struct(config);
2234
2235 let content = "<!-- This is a test https://www.example.test -->\n";
2236 let ctx = create_context(content);
2237 let result = rule.check(&ctx).unwrap();
2238
2239 assert!(
2242 !result.is_empty(),
2243 "Should flag 'test' in prose text of HTML comment with bare URL"
2244 );
2245 }
2246
2247 #[test]
2248 fn test_angle_bracket_url_in_regular_markdown_not_flagged() {
2249 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2252
2253 let content = "<https://www.example.test>\n";
2254 let ctx = create_context(content);
2255 let result = rule.check(&ctx).unwrap();
2256
2257 assert!(
2258 result.is_empty(),
2259 "Should not flag names inside angle-bracket URLs in regular markdown: {result:?}"
2260 );
2261 }
2262
2263 #[test]
2264 fn test_multiple_angle_bracket_urls_in_one_comment() {
2265 let config = MD044Config {
2266 names: vec!["Test".to_string()],
2267 ..MD044Config::default()
2268 };
2269 let rule = MD044ProperNames::from_config_struct(config);
2270
2271 let content = "<!-- See <https://test.example.com> and <https://www.example.test> for details -->\n";
2272 let ctx = create_context(content);
2273 let result = rule.check(&ctx).unwrap();
2274
2275 assert!(
2277 result.is_empty(),
2278 "Should not flag names inside multiple angle-bracket URLs: {result:?}"
2279 );
2280 }
2281
2282 #[test]
2283 fn test_angle_bracket_non_url_still_flagged() {
2284 assert!(
2287 !MD044ProperNames::is_in_angle_bracket_url("<test> which is not a URL.", 1),
2288 "is_in_angle_bracket_url should return false for non-URL angle brackets"
2289 );
2290 }
2291
2292 #[test]
2293 fn test_angle_bracket_mailto_url_not_flagged() {
2294 let config = MD044Config {
2295 names: vec!["Test".to_string()],
2296 ..MD044Config::default()
2297 };
2298 let rule = MD044ProperNames::from_config_struct(config);
2299
2300 let content = "<!-- Contact <mailto:test@example.com> for help -->\n";
2301 let ctx = create_context(content);
2302 let result = rule.check(&ctx).unwrap();
2303
2304 assert!(
2305 result.is_empty(),
2306 "Should not flag names inside angle-bracket mailto URLs: {result:?}"
2307 );
2308 }
2309
2310 #[test]
2311 fn test_angle_bracket_ftp_url_not_flagged() {
2312 let config = MD044Config {
2313 names: vec!["Test".to_string()],
2314 ..MD044Config::default()
2315 };
2316 let rule = MD044ProperNames::from_config_struct(config);
2317
2318 let content = "<!-- Download from <ftp://test.example.com/file> -->\n";
2319 let ctx = create_context(content);
2320 let result = rule.check(&ctx).unwrap();
2321
2322 assert!(
2323 result.is_empty(),
2324 "Should not flag names inside angle-bracket FTP URLs: {result:?}"
2325 );
2326 }
2327
2328 #[test]
2329 fn test_angle_bracket_url_fix_preserves_url() {
2330 let config = MD044Config {
2332 names: vec!["Test".to_string()],
2333 ..MD044Config::default()
2334 };
2335 let rule = MD044ProperNames::from_config_struct(config);
2336
2337 let content = "<!-- test text <https://www.example.test> -->\n";
2338 let ctx = create_context(content);
2339 let fixed = rule.fix(&ctx).unwrap();
2340
2341 assert!(
2343 fixed.contains("<https://www.example.test>"),
2344 "Fix should preserve angle-bracket URLs: {fixed}"
2345 );
2346 assert!(
2347 fixed.contains("Test text"),
2348 "Fix should correct prose 'test' to 'Test': {fixed}"
2349 );
2350 }
2351
2352 #[test]
2353 fn test_is_in_angle_bracket_url_helper() {
2354 let line = "text <https://example.test> more text";
2356
2357 assert!(MD044ProperNames::is_in_angle_bracket_url(line, 5)); assert!(MD044ProperNames::is_in_angle_bracket_url(line, 6)); assert!(MD044ProperNames::is_in_angle_bracket_url(line, 15)); assert!(MD044ProperNames::is_in_angle_bracket_url(line, 26)); assert!(!MD044ProperNames::is_in_angle_bracket_url(line, 0)); assert!(!MD044ProperNames::is_in_angle_bracket_url(line, 4)); assert!(!MD044ProperNames::is_in_angle_bracket_url(line, 27)); assert!(!MD044ProperNames::is_in_angle_bracket_url("<notaurl>", 1));
2370
2371 assert!(MD044ProperNames::is_in_angle_bracket_url(
2373 "<mailto:test@example.com>",
2374 10
2375 ));
2376
2377 assert!(MD044ProperNames::is_in_angle_bracket_url(
2379 "<ftp://test.example.com>",
2380 10
2381 ));
2382 }
2383
2384 #[test]
2385 fn test_is_in_angle_bracket_url_uppercase_scheme() {
2386 assert!(MD044ProperNames::is_in_angle_bracket_url(
2388 "<HTTPS://test.example.com>",
2389 10
2390 ));
2391 assert!(MD044ProperNames::is_in_angle_bracket_url(
2392 "<Http://test.example.com>",
2393 10
2394 ));
2395 }
2396
2397 #[test]
2398 fn test_is_in_angle_bracket_url_uncommon_schemes() {
2399 assert!(MD044ProperNames::is_in_angle_bracket_url(
2401 "<ssh://test@example.com>",
2402 10
2403 ));
2404 assert!(MD044ProperNames::is_in_angle_bracket_url("<file:///test/path>", 10));
2406 assert!(MD044ProperNames::is_in_angle_bracket_url("<data:text/plain;test>", 10));
2408 }
2409
2410 #[test]
2411 fn test_is_in_angle_bracket_url_unclosed() {
2412 assert!(!MD044ProperNames::is_in_angle_bracket_url(
2414 "<https://test.example.com",
2415 10
2416 ));
2417 }
2418
2419 #[test]
2420 fn test_vale_inline_config_comments_not_flagged() {
2421 let config = MD044Config {
2422 names: vec!["Vale".to_string(), "JavaScript".to_string()],
2423 ..MD044Config::default()
2424 };
2425 let rule = MD044ProperNames::from_config_struct(config);
2426
2427 let content = "\
2428<!-- vale off -->
2429Some javascript text here.
2430<!-- vale on -->
2431<!-- vale Style.Rule = NO -->
2432More javascript text.
2433<!-- vale Style.Rule = YES -->
2434<!-- vale JavaScript.Grammar = NO -->
2435";
2436 let ctx = create_context(content);
2437 let result = rule.check(&ctx).unwrap();
2438
2439 assert_eq!(result.len(), 2, "Should only flag body lines, not Vale config comments");
2441 assert_eq!(result[0].line, 2);
2442 assert_eq!(result[1].line, 5);
2443 }
2444
2445 #[test]
2446 fn test_remark_lint_inline_config_comments_not_flagged() {
2447 let config = MD044Config {
2448 names: vec!["JavaScript".to_string()],
2449 ..MD044Config::default()
2450 };
2451 let rule = MD044ProperNames::from_config_struct(config);
2452
2453 let content = "\
2454<!-- lint disable remark-lint-some-rule -->
2455Some javascript text here.
2456<!-- lint enable remark-lint-some-rule -->
2457<!-- lint ignore remark-lint-some-rule -->
2458More javascript text.
2459";
2460 let ctx = create_context(content);
2461 let result = rule.check(&ctx).unwrap();
2462
2463 assert_eq!(
2464 result.len(),
2465 2,
2466 "Should only flag body lines, not remark-lint config comments"
2467 );
2468 assert_eq!(result[0].line, 2);
2469 assert_eq!(result[1].line, 5);
2470 }
2471
2472 #[test]
2473 fn test_fix_does_not_modify_vale_remark_lint_comments() {
2474 let config = MD044Config {
2475 names: vec!["JavaScript".to_string(), "Vale".to_string()],
2476 ..MD044Config::default()
2477 };
2478 let rule = MD044ProperNames::from_config_struct(config);
2479
2480 let content = "\
2481<!-- vale off -->
2482Some javascript text.
2483<!-- vale on -->
2484<!-- lint disable remark-lint-some-rule -->
2485More javascript text.
2486<!-- lint enable remark-lint-some-rule -->
2487";
2488 let ctx = create_context(content);
2489 let fixed = rule.fix(&ctx).unwrap();
2490
2491 assert!(fixed.contains("<!-- vale off -->"));
2493 assert!(fixed.contains("<!-- vale on -->"));
2494 assert!(fixed.contains("<!-- lint disable remark-lint-some-rule -->"));
2495 assert!(fixed.contains("<!-- lint enable remark-lint-some-rule -->"));
2496 assert!(fixed.contains("Some JavaScript text."));
2498 assert!(fixed.contains("More JavaScript text."));
2499 }
2500
2501 #[test]
2502 fn test_mixed_tool_directives_all_skipped() {
2503 let config = MD044Config {
2504 names: vec!["JavaScript".to_string(), "Vale".to_string()],
2505 ..MD044Config::default()
2506 };
2507 let rule = MD044ProperNames::from_config_struct(config);
2508
2509 let content = "\
2510<!-- rumdl-disable MD044 -->
2511Some javascript text.
2512<!-- markdownlint-disable -->
2513More javascript text.
2514<!-- vale off -->
2515Even more javascript text.
2516<!-- lint disable some-rule -->
2517Final javascript text.
2518<!-- rumdl-enable MD044 -->
2519<!-- markdownlint-enable -->
2520<!-- vale on -->
2521<!-- lint enable some-rule -->
2522";
2523 let ctx = create_context(content);
2524 let result = rule.check(&ctx).unwrap();
2525
2526 assert_eq!(
2528 result.len(),
2529 4,
2530 "Should only flag body lines, not any tool directive comments"
2531 );
2532 assert_eq!(result[0].line, 2);
2533 assert_eq!(result[1].line, 4);
2534 assert_eq!(result[2].line, 6);
2535 assert_eq!(result[3].line, 8);
2536 }
2537
2538 #[test]
2539 fn test_vale_remark_lint_edge_cases_not_matched() {
2540 let config = MD044Config {
2541 names: vec!["JavaScript".to_string(), "Vale".to_string()],
2542 ..MD044Config::default()
2543 };
2544 let rule = MD044ProperNames::from_config_struct(config);
2545
2546 let content = "\
2554<!-- vale -->
2555<!-- vale is a tool for writing -->
2556<!-- valedictorian javascript -->
2557<!-- linting javascript tips -->
2558<!-- vale javascript -->
2559<!-- lint your javascript code -->
2560";
2561 let ctx = create_context(content);
2562 let result = rule.check(&ctx).unwrap();
2563
2564 assert_eq!(
2571 result.len(),
2572 7,
2573 "Should flag proper names in non-directive HTML comments: got {result:?}"
2574 );
2575 assert_eq!(result[0].line, 1); assert_eq!(result[1].line, 2); assert_eq!(result[2].line, 3); assert_eq!(result[3].line, 4); assert_eq!(result[4].line, 5); assert_eq!(result[5].line, 5); assert_eq!(result[6].line, 6); }
2583
2584 #[test]
2585 fn test_vale_style_directives_skipped() {
2586 let config = MD044Config {
2587 names: vec!["JavaScript".to_string(), "Vale".to_string()],
2588 ..MD044Config::default()
2589 };
2590 let rule = MD044ProperNames::from_config_struct(config);
2591
2592 let content = "\
2594<!-- vale style = MyStyle -->
2595<!-- vale styles = Style1, Style2 -->
2596<!-- vale MyRule.Name = YES -->
2597<!-- vale MyRule.Name = NO -->
2598Some javascript text.
2599";
2600 let ctx = create_context(content);
2601 let result = rule.check(&ctx).unwrap();
2602
2603 assert_eq!(
2605 result.len(),
2606 1,
2607 "Should only flag body lines, not Vale style/rule directives: got {result:?}"
2608 );
2609 assert_eq!(result[0].line, 5);
2610 }
2611
2612 #[test]
2615 fn test_backtick_code_single_backticks() {
2616 let line = "hello `world` bye";
2617 assert!(MD044ProperNames::is_in_backtick_code_in_line(line, 7));
2619 assert!(!MD044ProperNames::is_in_backtick_code_in_line(line, 0));
2621 assert!(!MD044ProperNames::is_in_backtick_code_in_line(line, 14));
2623 }
2624
2625 #[test]
2626 fn test_backtick_code_double_backticks() {
2627 let line = "a ``code`` b";
2628 assert!(MD044ProperNames::is_in_backtick_code_in_line(line, 4));
2630 assert!(!MD044ProperNames::is_in_backtick_code_in_line(line, 0));
2632 assert!(!MD044ProperNames::is_in_backtick_code_in_line(line, 11));
2634 }
2635
2636 #[test]
2637 fn test_backtick_code_unclosed() {
2638 let line = "a `code b";
2639 assert!(!MD044ProperNames::is_in_backtick_code_in_line(line, 3));
2641 }
2642
2643 #[test]
2644 fn test_backtick_code_mismatched_count() {
2645 let line = "a `code`` b";
2647 assert!(!MD044ProperNames::is_in_backtick_code_in_line(line, 3));
2650 }
2651
2652 #[test]
2653 fn test_backtick_code_multiple_spans() {
2654 let line = "`first` and `second`";
2655 assert!(MD044ProperNames::is_in_backtick_code_in_line(line, 1));
2657 assert!(!MD044ProperNames::is_in_backtick_code_in_line(line, 8));
2659 assert!(MD044ProperNames::is_in_backtick_code_in_line(line, 13));
2661 }
2662
2663 #[test]
2664 fn test_backtick_code_on_backtick_boundary() {
2665 let line = "`code`";
2666 assert!(!MD044ProperNames::is_in_backtick_code_in_line(line, 0));
2668 assert!(!MD044ProperNames::is_in_backtick_code_in_line(line, 5));
2670 assert!(MD044ProperNames::is_in_backtick_code_in_line(line, 1));
2672 assert!(MD044ProperNames::is_in_backtick_code_in_line(line, 4));
2673 }
2674}