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 && ctx.is_in_code_block_or_span(byte_pos) {
336 continue;
337 }
338
339 if Self::is_in_link(ctx, byte_pos) {
341 continue;
342 }
343
344 if Self::is_in_angle_bracket_url(line, start_pos) {
348 continue;
349 }
350
351 if let Some(proper_name) = self.get_proper_name_for(found_name) {
353 if found_name != proper_name {
355 violations.push((line_num, cap.start() + 1, found_name.to_string()));
356 }
357 }
358 }
359 Err(e) => {
360 eprintln!("Regex execution error on line {line_num}: {e}");
361 }
362 }
363 }
364 }
365
366 if let Ok(mut cache) = self.content_cache.lock() {
368 cache.insert(hash, violations.clone());
369 }
370 violations
371 }
372
373 fn is_in_link(ctx: &crate::lint_context::LintContext, byte_pos: usize) -> bool {
380 use pulldown_cmark::LinkType;
381
382 let link_idx = ctx.links.partition_point(|link| link.byte_offset <= byte_pos);
384 if link_idx > 0 {
385 let link = &ctx.links[link_idx - 1];
386 if byte_pos < link.byte_end {
387 let text_start = if matches!(link.link_type, LinkType::WikiLink { .. }) {
389 link.byte_offset + 2
390 } else {
391 link.byte_offset + 1
392 };
393 let text_end = text_start + link.text.len();
394
395 if byte_pos >= text_start && byte_pos < text_end {
397 return Self::link_text_is_url(&link.text);
398 }
399 return true;
401 }
402 }
403
404 let image_idx = ctx.images.partition_point(|img| img.byte_offset <= byte_pos);
406 if image_idx > 0 {
407 let image = &ctx.images[image_idx - 1];
408 if byte_pos < image.byte_end {
409 let alt_start = image.byte_offset + 2;
411 let alt_end = alt_start + image.alt_text.len();
412
413 if byte_pos >= alt_start && byte_pos < alt_end {
415 return false;
416 }
417 return true;
419 }
420 }
421
422 ctx.is_in_reference_def(byte_pos)
424 }
425
426 fn link_text_is_url(text: &str) -> bool {
429 let lower = text.trim().to_ascii_lowercase();
430 lower.starts_with("http://") || lower.starts_with("https://") || lower.starts_with("www.")
431 }
432
433 fn is_in_angle_bracket_url(line: &str, pos: usize) -> bool {
439 let bytes = line.as_bytes();
440 let len = bytes.len();
441 let mut i = 0;
442 while i < len {
443 if bytes[i] == b'<' {
444 let after_open = i + 1;
445 if after_open < len && bytes[after_open].is_ascii_alphabetic() {
449 let mut s = after_open + 1;
450 let scheme_max = (after_open + 32).min(len);
451 while s < scheme_max
452 && (bytes[s].is_ascii_alphanumeric()
453 || bytes[s] == b'+'
454 || bytes[s] == b'-'
455 || bytes[s] == b'.')
456 {
457 s += 1;
458 }
459 if s < len && bytes[s] == b':' {
460 let mut j = s + 1;
462 let mut found_close = false;
463 while j < len {
464 match bytes[j] {
465 b'>' => {
466 found_close = true;
467 break;
468 }
469 b' ' | b'<' => break,
470 _ => j += 1,
471 }
472 }
473 if found_close && pos >= i && pos <= j {
474 return true;
475 }
476 if found_close {
477 i = j + 1;
478 continue;
479 }
480 }
481 }
482 }
483 i += 1;
484 }
485 false
486 }
487
488 fn is_word_boundary_char(c: char) -> bool {
490 !c.is_alphanumeric()
491 }
492
493 fn is_at_word_boundary(content: &str, pos: usize, is_start: bool) -> bool {
495 if is_start {
496 if pos == 0 {
497 return true;
498 }
499 match content[..pos].chars().next_back() {
500 None => true,
501 Some(c) => Self::is_word_boundary_char(c),
502 }
503 } else {
504 if pos >= content.len() {
505 return true;
506 }
507 match content[pos..].chars().next() {
508 None => true,
509 Some(c) => Self::is_word_boundary_char(c),
510 }
511 }
512 }
513
514 fn frontmatter_value_offset(line: &str) -> usize {
518 let trimmed = line.trim();
519
520 if trimmed == "---" || trimmed == "+++" || trimmed.is_empty() {
522 return usize::MAX;
523 }
524
525 if trimmed.starts_with('#') {
527 return usize::MAX;
528 }
529
530 let stripped = line.trim_start();
532 if let Some(after_dash) = stripped.strip_prefix("- ") {
533 let leading = line.len() - stripped.len();
534 if let Some(result) = Self::kv_value_offset(line, after_dash, leading + 2) {
536 return result;
537 }
538 return leading + 2;
540 }
541 if stripped == "-" {
542 return usize::MAX;
543 }
544
545 if let Some(result) = Self::kv_value_offset(line, stripped, line.len() - stripped.len()) {
547 return result;
548 }
549
550 if let Some(eq_pos) = line.find('=') {
552 let after_eq = eq_pos + 1;
553 if after_eq < line.len() && line.as_bytes()[after_eq] == b' ' {
554 let value_start = after_eq + 1;
555 let value_slice = &line[value_start..];
556 let value_trimmed = value_slice.trim();
557 if value_trimmed.is_empty() {
558 return usize::MAX;
559 }
560 if (value_trimmed.starts_with('"') && value_trimmed.ends_with('"'))
562 || (value_trimmed.starts_with('\'') && value_trimmed.ends_with('\''))
563 {
564 let quote_offset = value_slice.find(['"', '\'']).unwrap_or(0);
565 return value_start + quote_offset + 1;
566 }
567 return value_start;
568 }
569 return usize::MAX;
571 }
572
573 0
575 }
576
577 fn kv_value_offset(line: &str, content: &str, base_offset: usize) -> Option<usize> {
581 let colon_pos = content.find(':')?;
582 let abs_colon = base_offset + colon_pos;
583 let after_colon = abs_colon + 1;
584 if after_colon < line.len() && line.as_bytes()[after_colon] == b' ' {
585 let value_start = after_colon + 1;
586 let value_slice = &line[value_start..];
587 let value_trimmed = value_slice.trim();
588 if value_trimmed.is_empty() {
589 return Some(usize::MAX);
590 }
591 if value_trimmed.starts_with('{') || value_trimmed.starts_with('[') {
593 return Some(usize::MAX);
594 }
595 if (value_trimmed.starts_with('"') && value_trimmed.ends_with('"'))
597 || (value_trimmed.starts_with('\'') && value_trimmed.ends_with('\''))
598 {
599 let quote_offset = value_slice.find(['"', '\'']).unwrap_or(0);
600 return Some(value_start + quote_offset + 1);
601 }
602 return Some(value_start);
603 }
604 Some(usize::MAX)
606 }
607
608 fn get_proper_name_for(&self, found_name: &str) -> Option<String> {
610 let found_lower = found_name.to_lowercase();
611
612 for name in &self.config.names {
614 let lower_name = name.to_lowercase();
615 let lower_name_no_dots = lower_name.replace('.', "");
616
617 if found_lower == lower_name || found_lower == lower_name_no_dots {
619 return Some(name.clone());
620 }
621
622 let ascii_normalized = Self::ascii_normalize(&lower_name);
624
625 let ascii_no_dots = ascii_normalized.replace('.', "");
626
627 if found_lower == ascii_normalized || found_lower == ascii_no_dots {
628 return Some(name.clone());
629 }
630 }
631 None
632 }
633}
634
635impl Rule for MD044ProperNames {
636 fn name(&self) -> &'static str {
637 "MD044"
638 }
639
640 fn description(&self) -> &'static str {
641 "Proper names should have the correct capitalization"
642 }
643
644 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
645 if self.config.names.is_empty() {
646 return true;
647 }
648 let content_lower = if ctx.content.is_ascii() {
650 ctx.content.to_ascii_lowercase()
651 } else {
652 ctx.content.to_lowercase()
653 };
654 !self.name_variants.iter().any(|name| content_lower.contains(name))
655 }
656
657 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
658 let content = ctx.content;
659 if content.is_empty() || self.config.names.is_empty() || self.combined_pattern.is_none() {
660 return Ok(Vec::new());
661 }
662
663 let content_lower = if content.is_ascii() {
665 content.to_ascii_lowercase()
666 } else {
667 content.to_lowercase()
668 };
669
670 let has_potential_matches = self.name_variants.iter().any(|name| content_lower.contains(name));
672
673 if !has_potential_matches {
674 return Ok(Vec::new());
675 }
676
677 let line_index = &ctx.line_index;
678 let violations = self.find_name_violations(content, ctx, &content_lower);
679
680 let warnings = violations
681 .into_iter()
682 .filter_map(|(line, column, found_name)| {
683 self.get_proper_name_for(&found_name).map(|proper_name| LintWarning {
684 rule_name: Some(self.name().to_string()),
685 line,
686 column,
687 end_line: line,
688 end_column: column + found_name.len(),
689 message: format!("Proper name '{found_name}' should be '{proper_name}'"),
690 severity: Severity::Warning,
691 fix: Some(Fix {
692 range: line_index.line_col_to_byte_range(line, column),
693 replacement: proper_name,
694 }),
695 })
696 })
697 .collect();
698
699 Ok(warnings)
700 }
701
702 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
703 let content = ctx.content;
704 if content.is_empty() || self.config.names.is_empty() {
705 return Ok(content.to_string());
706 }
707
708 let content_lower = if content.is_ascii() {
709 content.to_ascii_lowercase()
710 } else {
711 content.to_lowercase()
712 };
713 let violations = self.find_name_violations(content, ctx, &content_lower);
714 if violations.is_empty() {
715 return Ok(content.to_string());
716 }
717
718 let mut fixed_lines = Vec::new();
720
721 let mut violations_by_line: HashMap<usize, Vec<(usize, String)>> = HashMap::new();
723 for (line_num, col_num, found_name) in violations {
724 violations_by_line
725 .entry(line_num)
726 .or_default()
727 .push((col_num, found_name));
728 }
729
730 for violations in violations_by_line.values_mut() {
732 violations.sort_by_key(|b| std::cmp::Reverse(b.0));
733 }
734
735 for (line_idx, line_info) in ctx.lines.iter().enumerate() {
737 let line_num = line_idx + 1;
738
739 if let Some(line_violations) = violations_by_line.get(&line_num) {
740 let mut fixed_line = line_info.content(ctx.content).to_string();
742
743 for (col_num, found_name) in line_violations {
744 if let Some(proper_name) = self.get_proper_name_for(found_name) {
745 let start_col = col_num - 1; let end_col = start_col + found_name.len();
747
748 if end_col <= fixed_line.len()
749 && fixed_line.is_char_boundary(start_col)
750 && fixed_line.is_char_boundary(end_col)
751 {
752 fixed_line.replace_range(start_col..end_col, &proper_name);
753 }
754 }
755 }
756
757 fixed_lines.push(fixed_line);
758 } else {
759 fixed_lines.push(line_info.content(ctx.content).to_string());
761 }
762 }
763
764 let mut result = fixed_lines.join("\n");
766 if content.ends_with('\n') && !result.ends_with('\n') {
767 result.push('\n');
768 }
769 Ok(result)
770 }
771
772 fn as_any(&self) -> &dyn std::any::Any {
773 self
774 }
775
776 fn default_config_section(&self) -> Option<(String, toml::Value)> {
777 let json_value = serde_json::to_value(&self.config).ok()?;
778 Some((
779 self.name().to_string(),
780 crate::rule_config_serde::json_to_toml_value(&json_value)?,
781 ))
782 }
783
784 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
785 where
786 Self: Sized,
787 {
788 let rule_config = crate::rule_config_serde::load_rule_config::<MD044Config>(config);
789 Box::new(Self::from_config_struct(rule_config))
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use crate::lint_context::LintContext;
797
798 fn create_context(content: &str) -> LintContext<'_> {
799 LintContext::new(content, crate::config::MarkdownFlavor::Standard, None)
800 }
801
802 #[test]
803 fn test_correctly_capitalized_names() {
804 let rule = MD044ProperNames::new(
805 vec![
806 "JavaScript".to_string(),
807 "TypeScript".to_string(),
808 "Node.js".to_string(),
809 ],
810 true,
811 );
812
813 let content = "This document uses JavaScript, TypeScript, and Node.js correctly.";
814 let ctx = create_context(content);
815 let result = rule.check(&ctx).unwrap();
816 assert!(result.is_empty(), "Should not flag correctly capitalized names");
817 }
818
819 #[test]
820 fn test_incorrectly_capitalized_names() {
821 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "TypeScript".to_string()], true);
822
823 let content = "This document uses javascript and typescript incorrectly.";
824 let ctx = create_context(content);
825 let result = rule.check(&ctx).unwrap();
826
827 assert_eq!(result.len(), 2, "Should flag two incorrect capitalizations");
828 assert_eq!(result[0].message, "Proper name 'javascript' should be 'JavaScript'");
829 assert_eq!(result[0].line, 1);
830 assert_eq!(result[0].column, 20);
831 assert_eq!(result[1].message, "Proper name 'typescript' should be 'TypeScript'");
832 assert_eq!(result[1].line, 1);
833 assert_eq!(result[1].column, 35);
834 }
835
836 #[test]
837 fn test_names_at_beginning_of_sentences() {
838 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "Python".to_string()], true);
839
840 let content = "javascript is a great language. python is also popular.";
841 let ctx = create_context(content);
842 let result = rule.check(&ctx).unwrap();
843
844 assert_eq!(result.len(), 2, "Should flag names at beginning of sentences");
845 assert_eq!(result[0].line, 1);
846 assert_eq!(result[0].column, 1);
847 assert_eq!(result[1].line, 1);
848 assert_eq!(result[1].column, 33);
849 }
850
851 #[test]
852 fn test_names_in_code_blocks_checked_by_default() {
853 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
854
855 let content = r#"Here is some text with JavaScript.
856
857```javascript
858// This javascript should be checked
859const lang = "javascript";
860```
861
862But this javascript should be flagged."#;
863
864 let ctx = create_context(content);
865 let result = rule.check(&ctx).unwrap();
866
867 assert_eq!(result.len(), 3, "Should flag javascript inside and outside code blocks");
868 assert_eq!(result[0].line, 4);
869 assert_eq!(result[1].line, 5);
870 assert_eq!(result[2].line, 8);
871 }
872
873 #[test]
874 fn test_names_in_code_blocks_ignored_when_disabled() {
875 let rule = MD044ProperNames::new(
876 vec!["JavaScript".to_string()],
877 false, );
879
880 let content = r#"```
881javascript in code block
882```"#;
883
884 let ctx = create_context(content);
885 let result = rule.check(&ctx).unwrap();
886
887 assert_eq!(
888 result.len(),
889 0,
890 "Should not flag javascript in code blocks when code_blocks is false"
891 );
892 }
893
894 #[test]
895 fn test_names_in_inline_code_checked_by_default() {
896 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
897
898 let content = "This is `javascript` in inline code and javascript outside.";
899 let ctx = create_context(content);
900 let result = rule.check(&ctx).unwrap();
901
902 assert_eq!(result.len(), 2, "Should flag javascript inside and outside inline code");
904 assert_eq!(result[0].column, 10); assert_eq!(result[1].column, 41); }
907
908 #[test]
909 fn test_multiple_names_in_same_line() {
910 let rule = MD044ProperNames::new(
911 vec!["JavaScript".to_string(), "TypeScript".to_string(), "React".to_string()],
912 true,
913 );
914
915 let content = "I use javascript, typescript, and react in my projects.";
916 let ctx = create_context(content);
917 let result = rule.check(&ctx).unwrap();
918
919 assert_eq!(result.len(), 3, "Should flag all three incorrect names");
920 assert_eq!(result[0].message, "Proper name 'javascript' should be 'JavaScript'");
921 assert_eq!(result[1].message, "Proper name 'typescript' should be 'TypeScript'");
922 assert_eq!(result[2].message, "Proper name 'react' should be 'React'");
923 }
924
925 #[test]
926 fn test_case_sensitivity() {
927 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
928
929 let content = "JAVASCRIPT, Javascript, javascript, and JavaScript variations.";
930 let ctx = create_context(content);
931 let result = rule.check(&ctx).unwrap();
932
933 assert_eq!(result.len(), 3, "Should flag all incorrect case variations");
934 assert!(result.iter().all(|w| w.message.contains("should be 'JavaScript'")));
936 }
937
938 #[test]
939 fn test_configuration_with_custom_name_list() {
940 let config = MD044Config {
941 names: vec!["GitHub".to_string(), "GitLab".to_string(), "DevOps".to_string()],
942 code_blocks: true,
943 html_elements: true,
944 html_comments: true,
945 };
946 let rule = MD044ProperNames::from_config_struct(config);
947
948 let content = "We use github, gitlab, and devops for our workflow.";
949 let ctx = create_context(content);
950 let result = rule.check(&ctx).unwrap();
951
952 assert_eq!(result.len(), 3, "Should flag all custom names");
953 assert_eq!(result[0].message, "Proper name 'github' should be 'GitHub'");
954 assert_eq!(result[1].message, "Proper name 'gitlab' should be 'GitLab'");
955 assert_eq!(result[2].message, "Proper name 'devops' should be 'DevOps'");
956 }
957
958 #[test]
959 fn test_empty_configuration() {
960 let rule = MD044ProperNames::new(vec![], true);
961
962 let content = "This has javascript and typescript but no configured names.";
963 let ctx = create_context(content);
964 let result = rule.check(&ctx).unwrap();
965
966 assert!(result.is_empty(), "Should not flag anything with empty configuration");
967 }
968
969 #[test]
970 fn test_names_with_special_characters() {
971 let rule = MD044ProperNames::new(
972 vec!["Node.js".to_string(), "ASP.NET".to_string(), "C++".to_string()],
973 true,
974 );
975
976 let content = "We use nodejs, asp.net, ASP.NET, and c++ in our stack.";
977 let ctx = create_context(content);
978 let result = rule.check(&ctx).unwrap();
979
980 assert_eq!(result.len(), 3, "Should handle special characters correctly");
985
986 let messages: Vec<&str> = result.iter().map(|w| w.message.as_str()).collect();
987 assert!(messages.contains(&"Proper name 'nodejs' should be 'Node.js'"));
988 assert!(messages.contains(&"Proper name 'asp.net' should be 'ASP.NET'"));
989 assert!(messages.contains(&"Proper name 'c++' should be 'C++'"));
990 }
991
992 #[test]
993 fn test_word_boundaries() {
994 let rule = MD044ProperNames::new(vec!["Java".to_string(), "Script".to_string()], true);
995
996 let content = "JavaScript is not java or script, but Java and Script are separate.";
997 let ctx = create_context(content);
998 let result = rule.check(&ctx).unwrap();
999
1000 assert_eq!(result.len(), 2, "Should respect word boundaries");
1002 assert!(result.iter().any(|w| w.column == 19)); assert!(result.iter().any(|w| w.column == 27)); }
1005
1006 #[test]
1007 fn test_fix_method() {
1008 let rule = MD044ProperNames::new(
1009 vec![
1010 "JavaScript".to_string(),
1011 "TypeScript".to_string(),
1012 "Node.js".to_string(),
1013 ],
1014 true,
1015 );
1016
1017 let content = "I love javascript, typescript, and nodejs!";
1018 let ctx = create_context(content);
1019 let fixed = rule.fix(&ctx).unwrap();
1020
1021 assert_eq!(fixed, "I love JavaScript, TypeScript, and Node.js!");
1022 }
1023
1024 #[test]
1025 fn test_fix_multiple_occurrences() {
1026 let rule = MD044ProperNames::new(vec!["Python".to_string()], true);
1027
1028 let content = "python is great. I use python daily. PYTHON is powerful.";
1029 let ctx = create_context(content);
1030 let fixed = rule.fix(&ctx).unwrap();
1031
1032 assert_eq!(fixed, "Python is great. I use Python daily. Python is powerful.");
1033 }
1034
1035 #[test]
1036 fn test_fix_checks_code_blocks_by_default() {
1037 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1038
1039 let content = r#"I love javascript.
1040
1041```
1042const lang = "javascript";
1043```
1044
1045More javascript here."#;
1046
1047 let ctx = create_context(content);
1048 let fixed = rule.fix(&ctx).unwrap();
1049
1050 let expected = r#"I love JavaScript.
1051
1052```
1053const lang = "JavaScript";
1054```
1055
1056More JavaScript here."#;
1057
1058 assert_eq!(fixed, expected);
1059 }
1060
1061 #[test]
1062 fn test_multiline_content() {
1063 let rule = MD044ProperNames::new(vec!["Rust".to_string(), "Python".to_string()], true);
1064
1065 let content = r#"First line with rust.
1066Second line with python.
1067Third line with RUST and PYTHON."#;
1068
1069 let ctx = create_context(content);
1070 let result = rule.check(&ctx).unwrap();
1071
1072 assert_eq!(result.len(), 4, "Should flag all incorrect occurrences");
1073 assert_eq!(result[0].line, 1);
1074 assert_eq!(result[1].line, 2);
1075 assert_eq!(result[2].line, 3);
1076 assert_eq!(result[3].line, 3);
1077 }
1078
1079 #[test]
1080 fn test_default_config() {
1081 let config = MD044Config::default();
1082 assert!(config.names.is_empty());
1083 assert!(!config.code_blocks);
1084 assert!(config.html_elements);
1085 assert!(config.html_comments);
1086 }
1087
1088 #[test]
1089 fn test_default_config_checks_html_comments() {
1090 let config = MD044Config {
1091 names: vec!["JavaScript".to_string()],
1092 ..MD044Config::default()
1093 };
1094 let rule = MD044ProperNames::from_config_struct(config);
1095
1096 let content = "# Guide\n\n<!-- javascript mentioned here -->\n";
1097 let ctx = create_context(content);
1098 let result = rule.check(&ctx).unwrap();
1099
1100 assert_eq!(result.len(), 1, "Default config should check HTML comments");
1101 assert_eq!(result[0].line, 3);
1102 }
1103
1104 #[test]
1105 fn test_default_config_skips_code_blocks() {
1106 let config = MD044Config {
1107 names: vec!["JavaScript".to_string()],
1108 ..MD044Config::default()
1109 };
1110 let rule = MD044ProperNames::from_config_struct(config);
1111
1112 let content = "# Guide\n\n```\njavascript in code\n```\n";
1113 let ctx = create_context(content);
1114 let result = rule.check(&ctx).unwrap();
1115
1116 assert_eq!(result.len(), 0, "Default config should skip code blocks");
1117 }
1118
1119 #[test]
1120 fn test_standalone_html_comment_checked() {
1121 let config = MD044Config {
1122 names: vec!["Test".to_string()],
1123 ..MD044Config::default()
1124 };
1125 let rule = MD044ProperNames::from_config_struct(config);
1126
1127 let content = "# Heading\n\n<!-- this is a test example -->\n";
1128 let ctx = create_context(content);
1129 let result = rule.check(&ctx).unwrap();
1130
1131 assert_eq!(result.len(), 1, "Should flag proper name in standalone HTML comment");
1132 assert_eq!(result[0].line, 3);
1133 }
1134
1135 #[test]
1136 fn test_inline_config_comments_not_flagged() {
1137 let config = MD044Config {
1138 names: vec!["RUMDL".to_string()],
1139 ..MD044Config::default()
1140 };
1141 let rule = MD044ProperNames::from_config_struct(config);
1142
1143 let content = "<!-- rumdl-disable MD044 -->\nSome rumdl text here.\n<!-- rumdl-enable MD044 -->\n<!-- markdownlint-disable -->\nMore rumdl text.\n<!-- markdownlint-enable -->\n";
1147 let ctx = create_context(content);
1148 let result = rule.check(&ctx).unwrap();
1149
1150 assert_eq!(result.len(), 2, "Should only flag body lines, not config comments");
1151 assert_eq!(result[0].line, 2);
1152 assert_eq!(result[1].line, 5);
1153 }
1154
1155 #[test]
1156 fn test_html_comment_skipped_when_disabled() {
1157 let config = MD044Config {
1158 names: vec!["Test".to_string()],
1159 code_blocks: true,
1160 html_elements: true,
1161 html_comments: false,
1162 };
1163 let rule = MD044ProperNames::from_config_struct(config);
1164
1165 let content = "# Heading\n\n<!-- this is a test example -->\n\nRegular test here.\n";
1166 let ctx = create_context(content);
1167 let result = rule.check(&ctx).unwrap();
1168
1169 assert_eq!(
1170 result.len(),
1171 1,
1172 "Should only flag 'test' outside HTML comment when html_comments=false"
1173 );
1174 assert_eq!(result[0].line, 5);
1175 }
1176
1177 #[test]
1178 fn test_fix_corrects_html_comment_content() {
1179 let config = MD044Config {
1180 names: vec!["JavaScript".to_string()],
1181 ..MD044Config::default()
1182 };
1183 let rule = MD044ProperNames::from_config_struct(config);
1184
1185 let content = "# Guide\n\n<!-- javascript mentioned here -->\n";
1186 let ctx = create_context(content);
1187 let fixed = rule.fix(&ctx).unwrap();
1188
1189 assert_eq!(fixed, "# Guide\n\n<!-- JavaScript mentioned here -->\n");
1190 }
1191
1192 #[test]
1193 fn test_fix_does_not_modify_inline_config_comments() {
1194 let config = MD044Config {
1195 names: vec!["RUMDL".to_string()],
1196 ..MD044Config::default()
1197 };
1198 let rule = MD044ProperNames::from_config_struct(config);
1199
1200 let content = "<!-- rumdl-disable -->\nSome rumdl text.\n<!-- rumdl-enable -->\n";
1201 let ctx = create_context(content);
1202 let fixed = rule.fix(&ctx).unwrap();
1203
1204 assert!(fixed.contains("<!-- rumdl-disable -->"));
1206 assert!(fixed.contains("<!-- rumdl-enable -->"));
1207 assert!(fixed.contains("Some RUMDL text."));
1208 }
1209
1210 #[test]
1211 fn test_performance_with_many_names() {
1212 let mut names = vec![];
1213 for i in 0..50 {
1214 names.push(format!("ProperName{i}"));
1215 }
1216
1217 let rule = MD044ProperNames::new(names, true);
1218
1219 let content = "This has propername0, propername25, and propername49 incorrectly.";
1220 let ctx = create_context(content);
1221 let result = rule.check(&ctx).unwrap();
1222
1223 assert_eq!(result.len(), 3, "Should handle many configured names efficiently");
1224 }
1225
1226 #[test]
1227 fn test_large_name_count_performance() {
1228 let names = (0..1000).map(|i| format!("ProperName{i}")).collect::<Vec<_>>();
1231
1232 let rule = MD044ProperNames::new(names, true);
1233
1234 assert!(rule.combined_pattern.is_some());
1236
1237 let content = "This has propername0 and propername999 in it.";
1239 let ctx = create_context(content);
1240 let result = rule.check(&ctx).unwrap();
1241
1242 assert_eq!(result.len(), 2, "Should handle 1000 names without issues");
1244 }
1245
1246 #[test]
1247 fn test_cache_behavior() {
1248 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1249
1250 let content = "Using javascript here.";
1251 let ctx = create_context(content);
1252
1253 let result1 = rule.check(&ctx).unwrap();
1255 assert_eq!(result1.len(), 1);
1256
1257 let result2 = rule.check(&ctx).unwrap();
1259 assert_eq!(result2.len(), 1);
1260
1261 assert_eq!(result1[0].line, result2[0].line);
1263 assert_eq!(result1[0].column, result2[0].column);
1264 }
1265
1266 #[test]
1267 fn test_html_comments_not_checked_when_disabled() {
1268 let config = MD044Config {
1269 names: vec!["JavaScript".to_string()],
1270 code_blocks: true, html_elements: true, html_comments: false, };
1274 let rule = MD044ProperNames::from_config_struct(config);
1275
1276 let content = r#"Regular javascript here.
1277<!-- This javascript in HTML comment should be ignored -->
1278More javascript outside."#;
1279
1280 let ctx = create_context(content);
1281 let result = rule.check(&ctx).unwrap();
1282
1283 assert_eq!(result.len(), 2, "Should only flag javascript outside HTML comments");
1284 assert_eq!(result[0].line, 1);
1285 assert_eq!(result[1].line, 3);
1286 }
1287
1288 #[test]
1289 fn test_html_comments_checked_when_enabled() {
1290 let config = MD044Config {
1291 names: vec!["JavaScript".to_string()],
1292 code_blocks: true, html_elements: true, html_comments: true, };
1296 let rule = MD044ProperNames::from_config_struct(config);
1297
1298 let content = r#"Regular javascript here.
1299<!-- This javascript in HTML comment should be checked -->
1300More javascript outside."#;
1301
1302 let ctx = create_context(content);
1303 let result = rule.check(&ctx).unwrap();
1304
1305 assert_eq!(
1306 result.len(),
1307 3,
1308 "Should flag all javascript occurrences including in HTML comments"
1309 );
1310 }
1311
1312 #[test]
1313 fn test_multiline_html_comments() {
1314 let config = MD044Config {
1315 names: vec!["Python".to_string(), "JavaScript".to_string()],
1316 code_blocks: true, html_elements: true, html_comments: false, };
1320 let rule = MD044ProperNames::from_config_struct(config);
1321
1322 let content = r#"Regular python here.
1323<!--
1324This is a multiline comment
1325with javascript and python
1326that should be ignored
1327-->
1328More javascript outside."#;
1329
1330 let ctx = create_context(content);
1331 let result = rule.check(&ctx).unwrap();
1332
1333 assert_eq!(result.len(), 2, "Should only flag names outside HTML comments");
1334 assert_eq!(result[0].line, 1); assert_eq!(result[1].line, 7); }
1337
1338 #[test]
1339 fn test_fix_preserves_html_comments_when_disabled() {
1340 let config = MD044Config {
1341 names: vec!["JavaScript".to_string()],
1342 code_blocks: true, html_elements: true, html_comments: false, };
1346 let rule = MD044ProperNames::from_config_struct(config);
1347
1348 let content = r#"javascript here.
1349<!-- javascript in comment -->
1350More javascript."#;
1351
1352 let ctx = create_context(content);
1353 let fixed = rule.fix(&ctx).unwrap();
1354
1355 let expected = r#"JavaScript here.
1356<!-- javascript in comment -->
1357More JavaScript."#;
1358
1359 assert_eq!(
1360 fixed, expected,
1361 "Should not fix names inside HTML comments when disabled"
1362 );
1363 }
1364
1365 #[test]
1366 fn test_proper_names_in_link_text_are_flagged() {
1367 let rule = MD044ProperNames::new(
1368 vec!["JavaScript".to_string(), "Node.js".to_string(), "Python".to_string()],
1369 true,
1370 );
1371
1372 let content = r#"Check this [javascript documentation](https://javascript.info) for info.
1373
1374Visit [node.js homepage](https://nodejs.org) and [python tutorial](https://python.org).
1375
1376Real javascript should be flagged.
1377
1378Also see the [typescript guide][ts-ref] for more.
1379
1380Real python should be flagged too.
1381
1382[ts-ref]: https://typescript.org/handbook"#;
1383
1384 let ctx = create_context(content);
1385 let result = rule.check(&ctx).unwrap();
1386
1387 assert_eq!(result.len(), 5, "Expected 5 warnings: 3 in link text + 2 standalone");
1394
1395 let line_1_warnings: Vec<_> = result.iter().filter(|w| w.line == 1).collect();
1397 assert_eq!(line_1_warnings.len(), 1);
1398 assert!(
1399 line_1_warnings[0]
1400 .message
1401 .contains("'javascript' should be 'JavaScript'")
1402 );
1403
1404 let line_3_warnings: Vec<_> = result.iter().filter(|w| w.line == 3).collect();
1405 assert_eq!(line_3_warnings.len(), 2); assert!(result.iter().any(|w| w.line == 5 && w.message.contains("'javascript'")));
1409 assert!(result.iter().any(|w| w.line == 9 && w.message.contains("'python'")));
1410 }
1411
1412 #[test]
1413 fn test_link_urls_not_flagged() {
1414 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1415
1416 let content = r#"[Link Text](https://javascript.info/guide)"#;
1418
1419 let ctx = create_context(content);
1420 let result = rule.check(&ctx).unwrap();
1421
1422 assert!(result.is_empty(), "URLs should not be checked for proper names");
1424 }
1425
1426 #[test]
1427 fn test_proper_names_in_image_alt_text_are_flagged() {
1428 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1429
1430 let content = r#"Here is a  image.
1431
1432Real javascript should be flagged."#;
1433
1434 let ctx = create_context(content);
1435 let result = rule.check(&ctx).unwrap();
1436
1437 assert_eq!(result.len(), 2, "Expected 2 warnings: 1 in alt text + 1 standalone");
1441 assert!(result[0].message.contains("'javascript' should be 'JavaScript'"));
1442 assert!(result[0].line == 1); assert!(result[1].message.contains("'javascript' should be 'JavaScript'"));
1444 assert!(result[1].line == 3); }
1446
1447 #[test]
1448 fn test_image_urls_not_flagged() {
1449 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1450
1451 let content = r#""#;
1453
1454 let ctx = create_context(content);
1455 let result = rule.check(&ctx).unwrap();
1456
1457 assert!(result.is_empty(), "Image URLs should not be checked for proper names");
1459 }
1460
1461 #[test]
1462 fn test_reference_link_text_flagged_but_definition_not() {
1463 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "TypeScript".to_string()], true);
1464
1465 let content = r#"Check the [javascript guide][js-ref] for details.
1466
1467Real javascript should be flagged.
1468
1469[js-ref]: https://javascript.info/typescript/guide"#;
1470
1471 let ctx = create_context(content);
1472 let result = rule.check(&ctx).unwrap();
1473
1474 assert_eq!(result.len(), 2, "Expected 2 warnings: 1 in link text + 1 standalone");
1479 assert!(result.iter().any(|w| w.line == 1 && w.message.contains("'javascript'")));
1480 assert!(result.iter().any(|w| w.line == 3 && w.message.contains("'javascript'")));
1481 }
1482
1483 #[test]
1484 fn test_reference_definitions_not_flagged() {
1485 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1486
1487 let content = r#"[js-ref]: https://javascript.info/guide"#;
1489
1490 let ctx = create_context(content);
1491 let result = rule.check(&ctx).unwrap();
1492
1493 assert!(result.is_empty(), "Reference definitions should not be checked");
1495 }
1496
1497 #[test]
1498 fn test_wikilinks_text_is_flagged() {
1499 let rule = MD044ProperNames::new(vec!["JavaScript".to_string()], true);
1500
1501 let content = r#"[[javascript]]
1503
1504Regular javascript here.
1505
1506[[JavaScript|display text]]"#;
1507
1508 let ctx = create_context(content);
1509 let result = rule.check(&ctx).unwrap();
1510
1511 assert_eq!(result.len(), 2, "Expected 2 warnings: 1 in WikiLink + 1 standalone");
1515 assert!(
1516 result
1517 .iter()
1518 .any(|w| w.line == 1 && w.column == 3 && w.message.contains("'javascript'"))
1519 );
1520 assert!(result.iter().any(|w| w.line == 3 && w.message.contains("'javascript'")));
1521 }
1522
1523 #[test]
1524 fn test_url_link_text_not_flagged() {
1525 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1526
1527 let content = r#"[https://github.com/org/repo](https://github.com/org/repo)
1529
1530[http://github.com/org/repo](http://github.com/org/repo)
1531
1532[www.github.com/org/repo](https://www.github.com/org/repo)"#;
1533
1534 let ctx = create_context(content);
1535 let result = rule.check(&ctx).unwrap();
1536
1537 assert!(
1538 result.is_empty(),
1539 "URL-like link text should not be flagged, got: {result:?}"
1540 );
1541 }
1542
1543 #[test]
1544 fn test_url_link_text_with_leading_space_not_flagged() {
1545 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1546
1547 let content = r#"[ https://github.com/org/repo](https://github.com/org/repo)"#;
1549
1550 let ctx = create_context(content);
1551 let result = rule.check(&ctx).unwrap();
1552
1553 assert!(
1554 result.is_empty(),
1555 "URL-like link text with leading space should not be flagged, got: {result:?}"
1556 );
1557 }
1558
1559 #[test]
1560 fn test_url_link_text_uppercase_scheme_not_flagged() {
1561 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1562
1563 let content = r#"[HTTPS://GITHUB.COM/org/repo](https://github.com/org/repo)"#;
1564
1565 let ctx = create_context(content);
1566 let result = rule.check(&ctx).unwrap();
1567
1568 assert!(
1569 result.is_empty(),
1570 "URL-like link text with uppercase scheme should not be flagged, got: {result:?}"
1571 );
1572 }
1573
1574 #[test]
1575 fn test_non_url_link_text_still_flagged() {
1576 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1577
1578 let content = r#"[github.com/org/repo](https://github.com/org/repo)
1580
1581[Visit github](https://github.com/org/repo)
1582
1583[//github.com/org/repo](//github.com/org/repo)
1584
1585[ftp://github.com/org/repo](ftp://github.com/org/repo)"#;
1586
1587 let ctx = create_context(content);
1588 let result = rule.check(&ctx).unwrap();
1589
1590 assert_eq!(result.len(), 4, "Non-URL link text should be flagged, got: {result:?}");
1591 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)); }
1596
1597 #[test]
1598 fn test_url_link_text_fix_not_applied() {
1599 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1600
1601 let content = "[https://github.com/org/repo](https://github.com/org/repo)\n";
1602
1603 let ctx = create_context(content);
1604 let result = rule.fix(&ctx).unwrap();
1605
1606 assert_eq!(result, content, "Fix should not modify URL-like link text");
1607 }
1608
1609 #[test]
1610 fn test_mixed_url_and_regular_link_text() {
1611 let rule = MD044ProperNames::new(vec!["GitHub".to_string()], true);
1612
1613 let content = r#"[https://github.com/org/repo](https://github.com/org/repo)
1615
1616Visit [github documentation](https://github.com/docs) for details.
1617
1618[www.github.com/pricing](https://www.github.com/pricing)"#;
1619
1620 let ctx = create_context(content);
1621 let result = rule.check(&ctx).unwrap();
1622
1623 assert_eq!(
1625 result.len(),
1626 1,
1627 "Only non-URL link text should be flagged, got: {result:?}"
1628 );
1629 assert_eq!(result[0].line, 3);
1630 }
1631
1632 #[test]
1633 fn test_html_attribute_values_not_flagged() {
1634 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1637 let content = "# Heading\n\ntest\n\n<img src=\"www.example.test/test_image.png\">\n";
1638 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1639 let result = rule.check(&ctx).unwrap();
1640
1641 let line5_violations: Vec<_> = result.iter().filter(|w| w.line == 5).collect();
1643 assert!(
1644 line5_violations.is_empty(),
1645 "Should not flag anything inside HTML tag attributes: {line5_violations:?}"
1646 );
1647
1648 let line3_violations: Vec<_> = result.iter().filter(|w| w.line == 3).collect();
1650 assert_eq!(line3_violations.len(), 1, "Plain 'test' on line 3 should be flagged");
1651 }
1652
1653 #[test]
1654 fn test_html_text_content_still_flagged() {
1655 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1657 let content = "# Heading\n\n<a href=\"https://example.test/page\">test link</a>\n";
1658 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1659 let result = rule.check(&ctx).unwrap();
1660
1661 assert_eq!(
1664 result.len(),
1665 1,
1666 "Should flag only 'test' in anchor text, not in href: {result:?}"
1667 );
1668 assert_eq!(result[0].column, 37, "Should flag col 37 ('test link' in anchor text)");
1669 }
1670
1671 #[test]
1672 fn test_html_attribute_various_not_flagged() {
1673 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1675 let content = concat!(
1676 "# Heading\n\n",
1677 "<img src=\"test.png\" alt=\"test image\">\n",
1678 "<span class=\"test-class\" data-test=\"value\">test content</span>\n",
1679 );
1680 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1681 let result = rule.check(&ctx).unwrap();
1682
1683 assert_eq!(
1685 result.len(),
1686 1,
1687 "Should flag only 'test content' between tags: {result:?}"
1688 );
1689 assert_eq!(result[0].line, 4);
1690 }
1691
1692 #[test]
1693 fn test_plain_text_underscore_boundary_unchanged() {
1694 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1697 let content = "# Heading\n\ntest_image is here and just_test ends here\n";
1698 let ctx = crate::lint_context::LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1699 let result = rule.check(&ctx).unwrap();
1700
1701 assert_eq!(
1704 result.len(),
1705 2,
1706 "Should flag 'test' in both 'test_image' and 'just_test': {result:?}"
1707 );
1708 let cols: Vec<usize> = result.iter().map(|w| w.column).collect();
1709 assert!(cols.contains(&1), "Should flag col 1 (test_image): {cols:?}");
1710 assert!(cols.contains(&29), "Should flag col 29 (just_test): {cols:?}");
1711 }
1712
1713 #[test]
1714 fn test_frontmatter_yaml_keys_not_flagged() {
1715 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1718
1719 let content = "---\ntitle: Heading\ntest: Some Test value\n---\n\nTest\n";
1720 let ctx = create_context(content);
1721 let result = rule.check(&ctx).unwrap();
1722
1723 assert!(
1727 result.is_empty(),
1728 "Should not flag YAML keys or correctly capitalized values: {result:?}"
1729 );
1730 }
1731
1732 #[test]
1733 fn test_frontmatter_yaml_values_flagged() {
1734 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1736
1737 let content = "---\ntitle: Heading\nkey: a test value\n---\n\nTest\n";
1738 let ctx = create_context(content);
1739 let result = rule.check(&ctx).unwrap();
1740
1741 assert_eq!(result.len(), 1, "Should flag 'test' in YAML value: {result:?}");
1743 assert_eq!(result[0].line, 3);
1744 assert_eq!(result[0].column, 8); }
1746
1747 #[test]
1748 fn test_frontmatter_key_matches_name_not_flagged() {
1749 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1751
1752 let content = "---\ntest: other value\n---\n\nBody text\n";
1753 let ctx = create_context(content);
1754 let result = rule.check(&ctx).unwrap();
1755
1756 assert!(
1757 result.is_empty(),
1758 "Should not flag YAML key that matches configured name: {result:?}"
1759 );
1760 }
1761
1762 #[test]
1763 fn test_frontmatter_empty_value_not_flagged() {
1764 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1766
1767 let content = "---\ntest:\ntest: \n---\n\nBody text\n";
1768 let ctx = create_context(content);
1769 let result = rule.check(&ctx).unwrap();
1770
1771 assert!(
1772 result.is_empty(),
1773 "Should not flag YAML keys with empty values: {result:?}"
1774 );
1775 }
1776
1777 #[test]
1778 fn test_frontmatter_nested_yaml_key_not_flagged() {
1779 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1781
1782 let content = "---\nparent:\n test: nested value\n---\n\nBody text\n";
1783 let ctx = create_context(content);
1784 let result = rule.check(&ctx).unwrap();
1785
1786 assert!(result.is_empty(), "Should not flag nested YAML keys: {result:?}");
1788 }
1789
1790 #[test]
1791 fn test_frontmatter_list_items_checked() {
1792 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1794
1795 let content = "---\ntags:\n - test\n - other\n---\n\nBody text\n";
1796 let ctx = create_context(content);
1797 let result = rule.check(&ctx).unwrap();
1798
1799 assert_eq!(result.len(), 1, "Should flag 'test' in YAML list item: {result:?}");
1801 assert_eq!(result[0].line, 3);
1802 }
1803
1804 #[test]
1805 fn test_frontmatter_value_with_multiple_colons() {
1806 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1808
1809 let content = "---\ntest: description: a test thing\n---\n\nBody text\n";
1810 let ctx = create_context(content);
1811 let result = rule.check(&ctx).unwrap();
1812
1813 assert_eq!(
1816 result.len(),
1817 1,
1818 "Should flag 'test' in value after first colon: {result:?}"
1819 );
1820 assert_eq!(result[0].line, 2);
1821 assert!(result[0].column > 6, "Violation column should be in value portion");
1822 }
1823
1824 #[test]
1825 fn test_frontmatter_does_not_affect_body() {
1826 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1828
1829 let content = "---\ntitle: Heading\n---\n\ntest should be flagged here\n";
1830 let ctx = create_context(content);
1831 let result = rule.check(&ctx).unwrap();
1832
1833 assert_eq!(result.len(), 1, "Should flag 'test' in body text: {result:?}");
1834 assert_eq!(result[0].line, 5);
1835 }
1836
1837 #[test]
1838 fn test_frontmatter_fix_corrects_values_preserves_keys() {
1839 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1841
1842 let content = "---\ntest: a test value\n---\n\ntest here\n";
1843 let ctx = create_context(content);
1844 let fixed = rule.fix(&ctx).unwrap();
1845
1846 assert_eq!(fixed, "---\ntest: a Test value\n---\n\nTest here\n");
1848 }
1849
1850 #[test]
1851 fn test_frontmatter_multiword_value_flagged() {
1852 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "TypeScript".to_string()], true);
1854
1855 let content = "---\ndescription: Learn javascript and typescript\n---\n\nBody\n";
1856 let ctx = create_context(content);
1857 let result = rule.check(&ctx).unwrap();
1858
1859 assert_eq!(result.len(), 2, "Should flag both names in YAML value: {result:?}");
1860 assert!(result.iter().all(|w| w.line == 2));
1861 }
1862
1863 #[test]
1864 fn test_frontmatter_yaml_comments_not_checked() {
1865 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1867
1868 let content = "---\n# test comment\ntitle: Heading\n---\n\nBody text\n";
1869 let ctx = create_context(content);
1870 let result = rule.check(&ctx).unwrap();
1871
1872 assert!(result.is_empty(), "Should not flag names in YAML comments: {result:?}");
1873 }
1874
1875 #[test]
1876 fn test_frontmatter_delimiters_not_checked() {
1877 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1879
1880 let content = "---\ntitle: Heading\n---\n\ntest here\n";
1881 let ctx = create_context(content);
1882 let result = rule.check(&ctx).unwrap();
1883
1884 assert_eq!(result.len(), 1, "Should only flag body text: {result:?}");
1886 assert_eq!(result[0].line, 5);
1887 }
1888
1889 #[test]
1890 fn test_frontmatter_continuation_lines_checked() {
1891 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1893
1894 let content = "---\ndescription: >\n a test value\n continued here\n---\n\nBody\n";
1895 let ctx = create_context(content);
1896 let result = rule.check(&ctx).unwrap();
1897
1898 assert_eq!(result.len(), 1, "Should flag 'test' in continuation line: {result:?}");
1900 assert_eq!(result[0].line, 3);
1901 }
1902
1903 #[test]
1904 fn test_frontmatter_quoted_values_checked() {
1905 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1907
1908 let content = "---\ntitle: \"a test title\"\n---\n\nBody\n";
1909 let ctx = create_context(content);
1910 let result = rule.check(&ctx).unwrap();
1911
1912 assert_eq!(result.len(), 1, "Should flag 'test' in quoted YAML value: {result:?}");
1913 assert_eq!(result[0].line, 2);
1914 }
1915
1916 #[test]
1917 fn test_frontmatter_single_quoted_values_checked() {
1918 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1920
1921 let content = "---\ntitle: 'a test title'\n---\n\nBody\n";
1922 let ctx = create_context(content);
1923 let result = rule.check(&ctx).unwrap();
1924
1925 assert_eq!(
1926 result.len(),
1927 1,
1928 "Should flag 'test' in single-quoted YAML value: {result:?}"
1929 );
1930 assert_eq!(result[0].line, 2);
1931 }
1932
1933 #[test]
1934 fn test_frontmatter_fix_multiword_values() {
1935 let rule = MD044ProperNames::new(vec!["JavaScript".to_string(), "TypeScript".to_string()], true);
1937
1938 let content = "---\ndescription: Learn javascript and typescript\n---\n\nBody\n";
1939 let ctx = create_context(content);
1940 let fixed = rule.fix(&ctx).unwrap();
1941
1942 assert_eq!(
1943 fixed,
1944 "---\ndescription: Learn JavaScript and TypeScript\n---\n\nBody\n"
1945 );
1946 }
1947
1948 #[test]
1949 fn test_frontmatter_fix_preserves_yaml_structure() {
1950 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1952
1953 let content = "---\ntags:\n - test\n - other\ntitle: a test doc\n---\n\ntest body\n";
1954 let ctx = create_context(content);
1955 let fixed = rule.fix(&ctx).unwrap();
1956
1957 assert_eq!(
1958 fixed,
1959 "---\ntags:\n - Test\n - other\ntitle: a Test doc\n---\n\nTest body\n"
1960 );
1961 }
1962
1963 #[test]
1964 fn test_frontmatter_toml_delimiters_not_checked() {
1965 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1967
1968 let content = "+++\ntitle = \"a test title\"\n+++\n\ntest body\n";
1969 let ctx = create_context(content);
1970 let result = rule.check(&ctx).unwrap();
1971
1972 assert_eq!(result.len(), 2, "Should flag TOML value and body: {result:?}");
1976 let fm_violations: Vec<_> = result.iter().filter(|w| w.line == 2).collect();
1977 assert_eq!(fm_violations.len(), 1, "Should flag 'test' in TOML value: {result:?}");
1978 let body_violations: Vec<_> = result.iter().filter(|w| w.line == 5).collect();
1979 assert_eq!(body_violations.len(), 1, "Should flag body 'test': {result:?}");
1980 }
1981
1982 #[test]
1983 fn test_frontmatter_toml_key_not_flagged() {
1984 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
1986
1987 let content = "+++\ntest = \"other value\"\n+++\n\nBody text\n";
1988 let ctx = create_context(content);
1989 let result = rule.check(&ctx).unwrap();
1990
1991 assert!(
1992 result.is_empty(),
1993 "Should not flag TOML key that matches configured name: {result:?}"
1994 );
1995 }
1996
1997 #[test]
1998 fn test_frontmatter_toml_fix_preserves_keys() {
1999 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2001
2002 let content = "+++\ntest = \"a test value\"\n+++\n\ntest here\n";
2003 let ctx = create_context(content);
2004 let fixed = rule.fix(&ctx).unwrap();
2005
2006 assert_eq!(fixed, "+++\ntest = \"a Test value\"\n+++\n\nTest here\n");
2008 }
2009
2010 #[test]
2011 fn test_frontmatter_list_item_mapping_key_not_flagged() {
2012 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2015
2016 let content = "---\nitems:\n - test: nested value\n---\n\nBody text\n";
2017 let ctx = create_context(content);
2018 let result = rule.check(&ctx).unwrap();
2019
2020 assert!(
2021 result.is_empty(),
2022 "Should not flag YAML key in list-item mapping: {result:?}"
2023 );
2024 }
2025
2026 #[test]
2027 fn test_frontmatter_list_item_mapping_value_flagged() {
2028 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2030
2031 let content = "---\nitems:\n - key: a test value\n---\n\nBody text\n";
2032 let ctx = create_context(content);
2033 let result = rule.check(&ctx).unwrap();
2034
2035 assert_eq!(
2036 result.len(),
2037 1,
2038 "Should flag 'test' in list-item mapping value: {result:?}"
2039 );
2040 assert_eq!(result[0].line, 3);
2041 }
2042
2043 #[test]
2044 fn test_frontmatter_bare_list_item_still_flagged() {
2045 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2047
2048 let content = "---\ntags:\n - test\n - other\n---\n\nBody text\n";
2049 let ctx = create_context(content);
2050 let result = rule.check(&ctx).unwrap();
2051
2052 assert_eq!(result.len(), 1, "Should flag 'test' in bare list item: {result:?}");
2053 assert_eq!(result[0].line, 3);
2054 }
2055
2056 #[test]
2057 fn test_frontmatter_flow_mapping_not_flagged() {
2058 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2061
2062 let content = "---\nflow_map: {test: value, other: test}\n---\n\nBody text\n";
2063 let ctx = create_context(content);
2064 let result = rule.check(&ctx).unwrap();
2065
2066 assert!(
2067 result.is_empty(),
2068 "Should not flag names inside flow mappings: {result:?}"
2069 );
2070 }
2071
2072 #[test]
2073 fn test_frontmatter_flow_sequence_not_flagged() {
2074 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2076
2077 let content = "---\nitems: [test, other, test]\n---\n\nBody text\n";
2078 let ctx = create_context(content);
2079 let result = rule.check(&ctx).unwrap();
2080
2081 assert!(
2082 result.is_empty(),
2083 "Should not flag names inside flow sequences: {result:?}"
2084 );
2085 }
2086
2087 #[test]
2088 fn test_frontmatter_list_item_mapping_fix_preserves_key() {
2089 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2091
2092 let content = "---\nitems:\n - test: a test value\n---\n\ntest here\n";
2093 let ctx = create_context(content);
2094 let fixed = rule.fix(&ctx).unwrap();
2095
2096 assert_eq!(fixed, "---\nitems:\n - test: a Test value\n---\n\nTest here\n");
2099 }
2100
2101 #[test]
2104 fn test_angle_bracket_url_in_html_comment_not_flagged() {
2105 let config = MD044Config {
2107 names: vec!["Test".to_string()],
2108 ..MD044Config::default()
2109 };
2110 let rule = MD044ProperNames::from_config_struct(config);
2111
2112 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";
2113 let ctx = create_context(content);
2114 let result = rule.check(&ctx).unwrap();
2115
2116 let line8_warnings: Vec<_> = result.iter().filter(|w| w.line == 8).collect();
2124 assert!(
2125 line8_warnings.is_empty(),
2126 "Should not flag names inside angle-bracket URLs in HTML comments: {line8_warnings:?}"
2127 );
2128 }
2129
2130 #[test]
2131 fn test_bare_url_in_html_comment_still_flagged() {
2132 let config = MD044Config {
2134 names: vec!["Test".to_string()],
2135 ..MD044Config::default()
2136 };
2137 let rule = MD044ProperNames::from_config_struct(config);
2138
2139 let content = "<!-- This is a test https://www.example.test -->\n";
2140 let ctx = create_context(content);
2141 let result = rule.check(&ctx).unwrap();
2142
2143 assert!(
2146 !result.is_empty(),
2147 "Should flag 'test' in prose text of HTML comment with bare URL"
2148 );
2149 }
2150
2151 #[test]
2152 fn test_angle_bracket_url_in_regular_markdown_not_flagged() {
2153 let rule = MD044ProperNames::new(vec!["Test".to_string()], true);
2156
2157 let content = "<https://www.example.test>\n";
2158 let ctx = create_context(content);
2159 let result = rule.check(&ctx).unwrap();
2160
2161 assert!(
2162 result.is_empty(),
2163 "Should not flag names inside angle-bracket URLs in regular markdown: {result:?}"
2164 );
2165 }
2166
2167 #[test]
2168 fn test_multiple_angle_bracket_urls_in_one_comment() {
2169 let config = MD044Config {
2170 names: vec!["Test".to_string()],
2171 ..MD044Config::default()
2172 };
2173 let rule = MD044ProperNames::from_config_struct(config);
2174
2175 let content = "<!-- See <https://test.example.com> and <https://www.example.test> for details -->\n";
2176 let ctx = create_context(content);
2177 let result = rule.check(&ctx).unwrap();
2178
2179 assert!(
2181 result.is_empty(),
2182 "Should not flag names inside multiple angle-bracket URLs: {result:?}"
2183 );
2184 }
2185
2186 #[test]
2187 fn test_angle_bracket_non_url_still_flagged() {
2188 assert!(
2191 !MD044ProperNames::is_in_angle_bracket_url("<test> which is not a URL.", 1),
2192 "is_in_angle_bracket_url should return false for non-URL angle brackets"
2193 );
2194 }
2195
2196 #[test]
2197 fn test_angle_bracket_mailto_url_not_flagged() {
2198 let config = MD044Config {
2199 names: vec!["Test".to_string()],
2200 ..MD044Config::default()
2201 };
2202 let rule = MD044ProperNames::from_config_struct(config);
2203
2204 let content = "<!-- Contact <mailto:test@example.com> for help -->\n";
2205 let ctx = create_context(content);
2206 let result = rule.check(&ctx).unwrap();
2207
2208 assert!(
2209 result.is_empty(),
2210 "Should not flag names inside angle-bracket mailto URLs: {result:?}"
2211 );
2212 }
2213
2214 #[test]
2215 fn test_angle_bracket_ftp_url_not_flagged() {
2216 let config = MD044Config {
2217 names: vec!["Test".to_string()],
2218 ..MD044Config::default()
2219 };
2220 let rule = MD044ProperNames::from_config_struct(config);
2221
2222 let content = "<!-- Download from <ftp://test.example.com/file> -->\n";
2223 let ctx = create_context(content);
2224 let result = rule.check(&ctx).unwrap();
2225
2226 assert!(
2227 result.is_empty(),
2228 "Should not flag names inside angle-bracket FTP URLs: {result:?}"
2229 );
2230 }
2231
2232 #[test]
2233 fn test_angle_bracket_url_fix_preserves_url() {
2234 let config = MD044Config {
2236 names: vec!["Test".to_string()],
2237 ..MD044Config::default()
2238 };
2239 let rule = MD044ProperNames::from_config_struct(config);
2240
2241 let content = "<!-- test text <https://www.example.test> -->\n";
2242 let ctx = create_context(content);
2243 let fixed = rule.fix(&ctx).unwrap();
2244
2245 assert!(
2247 fixed.contains("<https://www.example.test>"),
2248 "Fix should preserve angle-bracket URLs: {fixed}"
2249 );
2250 assert!(
2251 fixed.contains("Test text"),
2252 "Fix should correct prose 'test' to 'Test': {fixed}"
2253 );
2254 }
2255
2256 #[test]
2257 fn test_is_in_angle_bracket_url_helper() {
2258 let line = "text <https://example.test> more text";
2260
2261 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));
2274
2275 assert!(MD044ProperNames::is_in_angle_bracket_url(
2277 "<mailto:test@example.com>",
2278 10
2279 ));
2280
2281 assert!(MD044ProperNames::is_in_angle_bracket_url(
2283 "<ftp://test.example.com>",
2284 10
2285 ));
2286 }
2287
2288 #[test]
2289 fn test_is_in_angle_bracket_url_uppercase_scheme() {
2290 assert!(MD044ProperNames::is_in_angle_bracket_url(
2292 "<HTTPS://test.example.com>",
2293 10
2294 ));
2295 assert!(MD044ProperNames::is_in_angle_bracket_url(
2296 "<Http://test.example.com>",
2297 10
2298 ));
2299 }
2300
2301 #[test]
2302 fn test_is_in_angle_bracket_url_uncommon_schemes() {
2303 assert!(MD044ProperNames::is_in_angle_bracket_url(
2305 "<ssh://test@example.com>",
2306 10
2307 ));
2308 assert!(MD044ProperNames::is_in_angle_bracket_url("<file:///test/path>", 10));
2310 assert!(MD044ProperNames::is_in_angle_bracket_url("<data:text/plain;test>", 10));
2312 }
2313
2314 #[test]
2315 fn test_is_in_angle_bracket_url_unclosed() {
2316 assert!(!MD044ProperNames::is_in_angle_bracket_url(
2318 "<https://test.example.com",
2319 10
2320 ));
2321 }
2322
2323 #[test]
2324 fn test_vale_inline_config_comments_not_flagged() {
2325 let config = MD044Config {
2326 names: vec!["Vale".to_string(), "JavaScript".to_string()],
2327 ..MD044Config::default()
2328 };
2329 let rule = MD044ProperNames::from_config_struct(config);
2330
2331 let content = "\
2332<!-- vale off -->
2333Some javascript text here.
2334<!-- vale on -->
2335<!-- vale Style.Rule = NO -->
2336More javascript text.
2337<!-- vale Style.Rule = YES -->
2338<!-- vale JavaScript.Grammar = NO -->
2339";
2340 let ctx = create_context(content);
2341 let result = rule.check(&ctx).unwrap();
2342
2343 assert_eq!(result.len(), 2, "Should only flag body lines, not Vale config comments");
2345 assert_eq!(result[0].line, 2);
2346 assert_eq!(result[1].line, 5);
2347 }
2348
2349 #[test]
2350 fn test_remark_lint_inline_config_comments_not_flagged() {
2351 let config = MD044Config {
2352 names: vec!["JavaScript".to_string()],
2353 ..MD044Config::default()
2354 };
2355 let rule = MD044ProperNames::from_config_struct(config);
2356
2357 let content = "\
2358<!-- lint disable remark-lint-some-rule -->
2359Some javascript text here.
2360<!-- lint enable remark-lint-some-rule -->
2361<!-- lint ignore remark-lint-some-rule -->
2362More javascript text.
2363";
2364 let ctx = create_context(content);
2365 let result = rule.check(&ctx).unwrap();
2366
2367 assert_eq!(
2368 result.len(),
2369 2,
2370 "Should only flag body lines, not remark-lint config comments"
2371 );
2372 assert_eq!(result[0].line, 2);
2373 assert_eq!(result[1].line, 5);
2374 }
2375
2376 #[test]
2377 fn test_fix_does_not_modify_vale_remark_lint_comments() {
2378 let config = MD044Config {
2379 names: vec!["JavaScript".to_string(), "Vale".to_string()],
2380 ..MD044Config::default()
2381 };
2382 let rule = MD044ProperNames::from_config_struct(config);
2383
2384 let content = "\
2385<!-- vale off -->
2386Some javascript text.
2387<!-- vale on -->
2388<!-- lint disable remark-lint-some-rule -->
2389More javascript text.
2390<!-- lint enable remark-lint-some-rule -->
2391";
2392 let ctx = create_context(content);
2393 let fixed = rule.fix(&ctx).unwrap();
2394
2395 assert!(fixed.contains("<!-- vale off -->"));
2397 assert!(fixed.contains("<!-- vale on -->"));
2398 assert!(fixed.contains("<!-- lint disable remark-lint-some-rule -->"));
2399 assert!(fixed.contains("<!-- lint enable remark-lint-some-rule -->"));
2400 assert!(fixed.contains("Some JavaScript text."));
2402 assert!(fixed.contains("More JavaScript text."));
2403 }
2404
2405 #[test]
2406 fn test_mixed_tool_directives_all_skipped() {
2407 let config = MD044Config {
2408 names: vec!["JavaScript".to_string(), "Vale".to_string()],
2409 ..MD044Config::default()
2410 };
2411 let rule = MD044ProperNames::from_config_struct(config);
2412
2413 let content = "\
2414<!-- rumdl-disable MD044 -->
2415Some javascript text.
2416<!-- markdownlint-disable -->
2417More javascript text.
2418<!-- vale off -->
2419Even more javascript text.
2420<!-- lint disable some-rule -->
2421Final javascript text.
2422<!-- rumdl-enable MD044 -->
2423<!-- markdownlint-enable -->
2424<!-- vale on -->
2425<!-- lint enable some-rule -->
2426";
2427 let ctx = create_context(content);
2428 let result = rule.check(&ctx).unwrap();
2429
2430 assert_eq!(
2432 result.len(),
2433 4,
2434 "Should only flag body lines, not any tool directive comments"
2435 );
2436 assert_eq!(result[0].line, 2);
2437 assert_eq!(result[1].line, 4);
2438 assert_eq!(result[2].line, 6);
2439 assert_eq!(result[3].line, 8);
2440 }
2441
2442 #[test]
2443 fn test_vale_remark_lint_edge_cases_not_matched() {
2444 let config = MD044Config {
2445 names: vec!["JavaScript".to_string(), "Vale".to_string()],
2446 ..MD044Config::default()
2447 };
2448 let rule = MD044ProperNames::from_config_struct(config);
2449
2450 let content = "\
2458<!-- vale -->
2459<!-- vale is a tool for writing -->
2460<!-- valedictorian javascript -->
2461<!-- linting javascript tips -->
2462<!-- vale javascript -->
2463<!-- lint your javascript code -->
2464";
2465 let ctx = create_context(content);
2466 let result = rule.check(&ctx).unwrap();
2467
2468 assert_eq!(
2475 result.len(),
2476 7,
2477 "Should flag proper names in non-directive HTML comments: got {result:?}"
2478 );
2479 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); }
2487
2488 #[test]
2489 fn test_vale_style_directives_skipped() {
2490 let config = MD044Config {
2491 names: vec!["JavaScript".to_string(), "Vale".to_string()],
2492 ..MD044Config::default()
2493 };
2494 let rule = MD044ProperNames::from_config_struct(config);
2495
2496 let content = "\
2498<!-- vale style = MyStyle -->
2499<!-- vale styles = Style1, Style2 -->
2500<!-- vale MyRule.Name = YES -->
2501<!-- vale MyRule.Name = NO -->
2502Some javascript text.
2503";
2504 let ctx = create_context(content);
2505 let result = rule.check(&ctx).unwrap();
2506
2507 assert_eq!(
2509 result.len(),
2510 1,
2511 "Should only flag body lines, not Vale style/rule directives: got {result:?}"
2512 );
2513 assert_eq!(result[0].line, 5);
2514 }
2515}