1use crate::rule::{FixCapability, LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::utils::mkdocs_patterns::is_mkdocs_auto_reference;
3use crate::utils::range_utils::calculate_match_range;
4use crate::utils::regex_cache::SHORTCUT_REF_REGEX;
5use crate::utils::skip_context::{is_in_math_context, is_in_table_cell};
6use regex::Regex;
7use std::collections::{HashMap, HashSet};
8use std::sync::LazyLock;
9
10mod md052_config;
11use md052_config::MD052Config;
12
13static REF_REGEX: LazyLock<Regex> =
17 LazyLock::new(|| Regex::new(r"^\s*\[((?:[^\[\]\\]|\\.|\[[^\]]*\])*)\]:\s*.*").unwrap());
18
19static LIST_ITEM_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*[-*+]\s+(?:\[[xX\s]\]\s+)?").unwrap());
21
22static OUTPUT_EXAMPLE_START: LazyLock<Regex> =
24 LazyLock::new(|| Regex::new(r"^#+\s*(?:Output|Example|Output Style|Output Format)\s*$").unwrap());
25
26static GITHUB_ALERT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
29 Regex::new(r"^\s*>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION|INFO|SUCCESS|FAILURE|DANGER|BUG|EXAMPLE|QUOTE)\]")
30 .unwrap()
31});
32
33static URL_WITH_BRACKETS: LazyLock<Regex> =
41 LazyLock::new(|| Regex::new(r"https?://(?:\[[0-9a-fA-F:.%]+\]|[^\s\[\]]+/[^\s]*\[\d+\])").unwrap());
42
43#[derive(Clone, Default)]
56pub struct MD052ReferenceLinkImages {
57 config: MD052Config,
58}
59
60impl MD052ReferenceLinkImages {
61 pub fn new() -> Self {
62 Self {
63 config: MD052Config::default(),
64 }
65 }
66
67 pub fn from_config_struct(config: MD052Config) -> Self {
68 Self { config }
69 }
70
71 fn strip_backticks(s: &str) -> &str {
74 s.trim_start_matches('`').trim_end_matches('`')
75 }
76
77 fn is_valid_python_identifier(s: &str) -> bool {
81 if s.is_empty() {
82 return false;
83 }
84 let first_char = s.chars().next().unwrap();
85 if !first_char.is_ascii_alphabetic() && first_char != '_' {
86 return false;
87 }
88 s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
89 }
90
91 fn is_known_non_reference_pattern(&self, text: &str) -> bool {
100 if self.config.ignore.iter().any(|p| p.eq_ignore_ascii_case(text)) {
104 return true;
105 }
106 if text.chars().all(|c| c.is_ascii_digit()) {
108 return true;
109 }
110
111 if text.contains(':') && text.chars().all(|c| c.is_ascii_digit() || c == ':') {
113 return true;
114 }
115
116 if text.contains('.')
120 && !text.contains(' ')
121 && !text.contains('-')
122 && !text.contains('_')
123 && !text.contains('`')
124 {
125 return true;
127 }
128
129 if text == "*" || text == "..." || text == "**" {
131 return true;
132 }
133
134 if text.contains('/') && !text.contains(' ') && !text.starts_with("http") {
136 return true;
137 }
138
139 if text.contains(',') || text.contains('[') || text.contains(']') {
142 return true;
144 }
145
146 if !text.contains('`')
153 && text.contains('.')
154 && !text.contains(' ')
155 && !text.contains('-')
156 && !text.contains('_')
157 {
158 return true;
159 }
160
161 if text.chars().all(|c| !c.is_alphanumeric() && c != ' ') {
168 return true;
169 }
170
171 if text.len() <= 2 && !text.chars().all(char::is_alphabetic) {
173 return true;
174 }
175
176 if (text.starts_with('"') && text.ends_with('"'))
178 || (text.starts_with('\'') && text.ends_with('\''))
179 || text.contains('"')
180 || text.contains('\'')
181 {
182 return true;
183 }
184
185 if text.contains(':') && text.contains(' ') {
188 return true;
189 }
190
191 if text.starts_with('!') {
193 return true;
194 }
195
196 if text.starts_with('^') {
199 return true;
200 }
201
202 if text.starts_with('@') {
205 return true;
206 }
207
208 if text == "TOC" {
211 return true;
212 }
213
214 if text.len() == 1 && text.chars().all(|c| c.is_ascii_uppercase()) {
216 return true;
217 }
218
219 let common_non_refs = [
222 "object",
224 "Object",
225 "any",
226 "Any",
227 "inv",
228 "void",
229 "bool",
230 "int",
231 "float",
232 "str",
233 "char",
234 "i8",
235 "i16",
236 "i32",
237 "i64",
238 "i128",
239 "isize",
240 "u8",
241 "u16",
242 "u32",
243 "u64",
244 "u128",
245 "usize",
246 "f32",
247 "f64",
248 "null",
250 "true",
251 "false",
252 "NaN",
253 "Infinity",
254 "object Object",
256 ];
257
258 if common_non_refs.contains(&text) {
259 return true;
260 }
261
262 false
263 }
264
265 fn is_in_code_span(byte_pos: usize, code_spans: &[crate::lint_context::CodeSpan]) -> bool {
267 let idx = code_spans.partition_point(|span| span.byte_offset <= byte_pos);
268 idx > 0 && byte_pos < code_spans[idx - 1].byte_end
269 }
270
271 fn is_in_html_tag(html_tags: &[crate::lint_context::HtmlTag], byte_pos: usize) -> bool {
273 let idx = html_tags.partition_point(|tag| tag.byte_offset <= byte_pos);
274 idx > 0 && byte_pos < html_tags[idx - 1].byte_end
275 }
276
277 fn extract_references(&self, ctx: &crate::lint_context::LintContext) -> HashSet<String> {
278 use crate::utils::skip_context::is_mkdocs_snippet_line;
279
280 let mut references = HashSet::new();
281
282 for (line_num, line) in ctx.content.lines().enumerate() {
283 if let Some(line_info) = ctx.line_info(line_num + 1)
285 && line_info.in_code_block
286 {
287 continue;
288 }
289
290 if is_mkdocs_snippet_line(line, ctx.flavor) {
292 continue;
293 }
294
295 if line.trim_start().starts_with("*[") {
298 continue;
299 }
300
301 if let Some(cap) = REF_REGEX.captures(line) {
302 if let Some(reference) = cap.get(1) {
304 references.insert(reference.as_str().to_lowercase());
305 }
306 }
307 }
308
309 references
310 }
311
312 fn find_undefined_references(
313 &self,
314 references: &HashSet<String>,
315 ctx: &crate::lint_context::LintContext,
316 mkdocs_mode: bool,
317 ) -> Vec<(usize, usize, usize, String)> {
318 let mut undefined = Vec::new();
319 let mut reported_refs = HashMap::new();
320 let mut in_example_section = false;
321
322 let code_spans = ctx.code_spans();
324 let html_tags = ctx.html_tags();
325
326 for link in &ctx.links {
328 if !link.is_reference {
329 continue; }
331
332 if ctx.is_in_jinja_range(link.byte_offset) {
334 continue;
335 }
336
337 if Self::is_in_code_span(link.byte_offset, &code_spans) {
339 continue;
340 }
341
342 if ctx.is_in_html_comment(link.byte_offset) || ctx.is_in_mdx_comment(link.byte_offset) {
344 continue;
345 }
346
347 if Self::is_in_html_tag(&html_tags, link.byte_offset) {
349 continue;
350 }
351
352 if is_in_math_context(ctx, link.byte_offset) {
354 continue;
355 }
356
357 if is_in_table_cell(ctx, link.line, link.start_col) {
359 continue;
360 }
361
362 if ctx.line_info(link.line).is_some_and(|info| info.in_front_matter) {
364 continue;
365 }
366
367 if ctx.flavor.is_pandoc_compatible() && ctx.is_in_citation(link.byte_offset) {
370 continue;
371 }
372
373 if ctx.is_in_shortcode(link.byte_offset) {
376 continue;
377 }
378
379 if let Some(ref_id) = &link.reference_id {
380 let reference_lower = ref_id.to_lowercase();
381
382 if self.is_known_non_reference_pattern(ref_id) {
384 continue;
385 }
386
387 let stripped_ref = Self::strip_backticks(ref_id);
391 let stripped_text = Self::strip_backticks(&link.text);
392 if mkdocs_mode
393 && (is_mkdocs_auto_reference(stripped_ref)
394 || is_mkdocs_auto_reference(stripped_text)
395 || (ref_id != stripped_ref && Self::is_valid_python_identifier(stripped_ref))
396 || (link.text.as_ref() != stripped_text && Self::is_valid_python_identifier(stripped_text)))
397 {
398 continue;
399 }
400
401 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
403 if let Some(line_info) = ctx.line_info(link.line) {
405 if OUTPUT_EXAMPLE_START.is_match(line_info.content(ctx.content)) {
406 in_example_section = true;
407 continue;
408 }
409
410 if in_example_section {
411 continue;
412 }
413
414 if LIST_ITEM_REGEX.is_match(line_info.content(ctx.content)) {
416 continue;
417 }
418
419 let trimmed = line_info.content(ctx.content).trim_start();
421 if trimmed.starts_with('<') {
422 continue;
423 }
424 }
425
426 let match_len = link.byte_end - link.byte_offset;
427 let line_start = ctx.line_index.get_line_start_byte(link.line).unwrap_or(0);
430 undefined.push((
431 link.line - 1,
432 link.byte_offset - line_start,
433 match_len,
434 ref_id.to_string(),
435 ));
436 reported_refs.insert(reference_lower, true);
437 }
438 }
439 }
440
441 for image in &ctx.images {
443 if !image.is_reference {
444 continue; }
446
447 if ctx.is_in_jinja_range(image.byte_offset) {
449 continue;
450 }
451
452 if Self::is_in_code_span(image.byte_offset, &code_spans) {
454 continue;
455 }
456
457 if ctx.is_in_html_comment(image.byte_offset) || ctx.is_in_mdx_comment(image.byte_offset) {
459 continue;
460 }
461
462 if Self::is_in_html_tag(&html_tags, image.byte_offset) {
464 continue;
465 }
466
467 if is_in_math_context(ctx, image.byte_offset) {
469 continue;
470 }
471
472 if is_in_table_cell(ctx, image.line, image.start_col) {
474 continue;
475 }
476
477 if ctx.line_info(image.line).is_some_and(|info| info.in_front_matter) {
479 continue;
480 }
481
482 if let Some(ref_id) = &image.reference_id {
483 let reference_lower = ref_id.to_lowercase();
484
485 if self.is_known_non_reference_pattern(ref_id) {
487 continue;
488 }
489
490 let stripped_ref = Self::strip_backticks(ref_id);
494 let stripped_alt = Self::strip_backticks(&image.alt_text);
495 if mkdocs_mode
496 && (is_mkdocs_auto_reference(stripped_ref)
497 || is_mkdocs_auto_reference(stripped_alt)
498 || (ref_id != stripped_ref && Self::is_valid_python_identifier(stripped_ref))
499 || (image.alt_text.as_ref() != stripped_alt && Self::is_valid_python_identifier(stripped_alt)))
500 {
501 continue;
502 }
503
504 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
506 if let Some(line_info) = ctx.line_info(image.line) {
508 if OUTPUT_EXAMPLE_START.is_match(line_info.content(ctx.content)) {
509 in_example_section = true;
510 continue;
511 }
512
513 if in_example_section {
514 continue;
515 }
516
517 if LIST_ITEM_REGEX.is_match(line_info.content(ctx.content)) {
519 continue;
520 }
521
522 let trimmed = line_info.content(ctx.content).trim_start();
524 if trimmed.starts_with('<') {
525 continue;
526 }
527 }
528
529 let match_len = image.byte_end - image.byte_offset;
530 let line_start = ctx.line_index.get_line_start_byte(image.line).unwrap_or(0);
533 undefined.push((
534 image.line - 1,
535 image.byte_offset - line_start,
536 match_len,
537 ref_id.to_string(),
538 ));
539 reported_refs.insert(reference_lower, true);
540 }
541 }
542 }
543
544 let mut covered_ranges: Vec<(usize, usize)> = Vec::new();
546
547 for link in &ctx.links {
549 covered_ranges.push((link.byte_offset, link.byte_end));
550 }
551
552 for image in &ctx.images {
554 covered_ranges.push((image.byte_offset, image.byte_end));
555 }
556
557 covered_ranges.sort_by_key(|&(start, _)| start);
559
560 if !self.config.shortcut_syntax {
565 return undefined;
566 }
567
568 let lines = ctx.raw_lines();
570 in_example_section = false; for (line_num, line) in lines.iter().enumerate() {
573 if let Some(line_info) = ctx.line_info(line_num + 1)
575 && (line_info.in_front_matter || line_info.in_code_block)
576 {
577 continue;
578 }
579
580 if OUTPUT_EXAMPLE_START.is_match(line) {
582 in_example_section = true;
583 continue;
584 }
585
586 if in_example_section {
587 if line.starts_with('#') && !OUTPUT_EXAMPLE_START.is_match(line) {
589 in_example_section = false;
590 } else {
591 continue;
592 }
593 }
594
595 if LIST_ITEM_REGEX.is_match(line) {
597 continue;
598 }
599
600 let trimmed_line = line.trim_start();
602 if trimmed_line.starts_with('<') {
603 continue;
604 }
605
606 if GITHUB_ALERT_REGEX.is_match(line) {
608 continue;
609 }
610
611 if trimmed_line.starts_with("*[") {
614 continue;
615 }
616
617 let mut url_bracket_ranges: Vec<(usize, usize)> = Vec::new();
620 for mat in URL_WITH_BRACKETS.find_iter(line) {
621 let url_str = mat.as_str();
623 let url_start = mat.start();
624
625 let mut idx = 0;
627 while idx < url_str.len() {
628 if let Some(bracket_start) = url_str[idx..].find('[') {
629 let bracket_start_abs = url_start + idx + bracket_start;
630 if let Some(bracket_end) = url_str[idx + bracket_start + 1..].find(']') {
631 let bracket_end_abs = url_start + idx + bracket_start + 1 + bracket_end + 1;
632 url_bracket_ranges.push((bracket_start_abs, bracket_end_abs));
633 idx += bracket_start + bracket_end + 2;
634 } else {
635 break;
636 }
637 } else {
638 break;
639 }
640 }
641 }
642
643 if let Ok(captures) = SHORTCUT_REF_REGEX.captures_iter(line).collect::<Result<Vec<_>, _>>() {
645 for cap in captures {
646 if let Some(ref_match) = cap.get(1) {
647 let bracket_start = cap.get(0).unwrap().start();
649 let bracket_end = cap.get(0).unwrap().end();
650
651 let is_in_url = url_bracket_ranges
653 .iter()
654 .any(|&(url_start, url_end)| bracket_start >= url_start && bracket_end <= url_end);
655
656 if is_in_url {
657 continue;
658 }
659
660 if bracket_start > 0 {
663 if let Some(byte) = line.as_bytes().get(bracket_start.saturating_sub(1))
665 && *byte == b'^'
666 {
667 continue; }
669 }
670
671 let reference = ref_match.as_str();
672 let reference_lower = reference.to_lowercase();
673
674 if self.is_known_non_reference_pattern(reference) {
676 continue;
677 }
678
679 if let Some(alert_type) = reference.strip_prefix('!')
681 && matches!(
682 alert_type,
683 "NOTE"
684 | "TIP"
685 | "WARNING"
686 | "IMPORTANT"
687 | "CAUTION"
688 | "INFO"
689 | "SUCCESS"
690 | "FAILURE"
691 | "DANGER"
692 | "BUG"
693 | "EXAMPLE"
694 | "QUOTE"
695 )
696 {
697 continue;
698 }
699
700 if mkdocs_mode
703 && (reference.starts_with("start:") || reference.starts_with("end:"))
704 && (crate::utils::mkdocs_snippets::is_snippet_section_start(line)
705 || crate::utils::mkdocs_snippets::is_snippet_section_end(line))
706 {
707 continue;
708 }
709
710 let stripped_ref = Self::strip_backticks(reference);
713 if mkdocs_mode
714 && (is_mkdocs_auto_reference(stripped_ref)
715 || (reference != stripped_ref && Self::is_valid_python_identifier(stripped_ref)))
716 {
717 continue;
718 }
719
720 if ctx.flavor.is_pandoc_compatible() && ctx.matches_implicit_header_reference(reference) {
724 continue;
725 }
726
727 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
728 let full_match = cap.get(0).unwrap();
729 let col = full_match.start();
730 let line_start_byte = ctx.line_offsets[line_num];
731 let byte_pos = line_start_byte + col;
732
733 let code_spans = ctx.code_spans();
735 if Self::is_in_code_span(byte_pos, &code_spans) {
736 continue;
737 }
738
739 if ctx.is_in_jinja_range(byte_pos) {
741 continue;
742 }
743
744 if crate::utils::code_block_utils::CodeBlockUtils::is_in_code_block(
746 &ctx.code_blocks,
747 byte_pos,
748 ) {
749 continue;
750 }
751
752 if ctx.is_in_html_comment(byte_pos) || ctx.is_in_mdx_comment(byte_pos) {
754 continue;
755 }
756
757 if Self::is_in_html_tag(&html_tags, byte_pos) {
759 continue;
760 }
761
762 if is_in_math_context(ctx, byte_pos) {
764 continue;
765 }
766
767 if is_in_table_cell(ctx, line_num + 1, col) {
769 continue;
770 }
771
772 let byte_end = byte_pos + (full_match.end() - full_match.start());
773
774 let mut is_covered = false;
776 for &(range_start, range_end) in &covered_ranges {
777 if range_start <= byte_pos && byte_end <= range_end {
778 is_covered = true;
780 break;
781 }
782 if range_start > byte_end {
783 break;
785 }
786 }
787
788 if is_covered {
789 continue;
790 }
791
792 let line_chars: Vec<char> = line.chars().collect();
797 if col > 0 && col <= line_chars.len() && line_chars.get(col - 1) == Some(&']') {
798 let mut bracket_count = 1; let mut check_pos = col.saturating_sub(2);
801 let mut found_opening = false;
802
803 while check_pos > 0 && check_pos < line_chars.len() {
804 match line_chars.get(check_pos) {
805 Some(&']') => bracket_count += 1,
806 Some(&'[') => {
807 bracket_count -= 1;
808 if bracket_count == 0 {
809 if check_pos == 0 || line_chars.get(check_pos - 1) != Some(&'\\') {
811 found_opening = true;
812 }
813 break;
814 }
815 }
816 _ => {}
817 }
818 if check_pos == 0 {
819 break;
820 }
821 check_pos = check_pos.saturating_sub(1);
822 }
823
824 if found_opening {
825 continue;
827 }
828 }
829
830 let before_text = &line[..col];
833 if before_text.contains("\\]") {
834 if let Some(escaped_close_pos) = before_text.rfind("\\]") {
836 let search_text = &before_text[..escaped_close_pos];
837 if search_text.contains("\\[") {
838 continue;
840 }
841 }
842 }
843
844 let match_len = full_match.end() - full_match.start();
845 undefined.push((line_num, col, match_len, reference.to_string()));
846 reported_refs.insert(reference_lower, true);
847 }
848 }
849 }
850 }
851 }
852
853 undefined
854 }
855}
856
857impl Rule for MD052ReferenceLinkImages {
858 fn name(&self) -> &'static str {
859 "MD052"
860 }
861
862 fn description(&self) -> &'static str {
863 "Reference links and images should use a reference that exists"
864 }
865
866 fn category(&self) -> RuleCategory {
867 RuleCategory::Link
868 }
869
870 fn fix_capability(&self) -> FixCapability {
871 FixCapability::Unfixable
872 }
873
874 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
875 let content = ctx.content;
876 let mut warnings = Vec::new();
877
878 if !content.contains('[') {
880 return Ok(warnings);
881 }
882
883 let mkdocs_mode = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
885
886 let references = self.extract_references(ctx);
887
888 let lines = ctx.raw_lines();
890 for (line_num, col, match_len, reference) in self.find_undefined_references(&references, ctx, mkdocs_mode) {
891 let line_content = lines.get(line_num).unwrap_or(&"");
892
893 let (start_line, start_col, end_line, end_col) =
895 calculate_match_range(line_num + 1, line_content, col, match_len);
896
897 warnings.push(LintWarning {
898 rule_name: Some(self.name().to_string()),
899 line: start_line,
900 column: start_col,
901 end_line,
902 end_column: end_col,
903 message: format!("Reference '{reference}' not found"),
904 severity: Severity::Warning,
905 fix: None,
906 });
907 }
908
909 Ok(warnings)
910 }
911
912 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
914 ctx.content.is_empty() || !ctx.likely_has_links_or_images()
916 }
917
918 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
919 let content = ctx.content;
920 Ok(content.to_string())
922 }
923
924 fn as_any(&self) -> &dyn std::any::Any {
925 self
926 }
927
928 crate::impl_rule_config_methods!(MD052Config);
929}
930
931#[cfg(test)]
932mod tests {
933 use super::*;
934 use crate::lint_context::LintContext;
935
936 #[test]
937 fn test_valid_reference_link() {
938 let rule = MD052ReferenceLinkImages::new();
939 let content = "[text][ref]\n\n[ref]: https://example.com";
940 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
941 let result = rule.check(&ctx).unwrap();
942
943 assert_eq!(result.len(), 0);
944 }
945
946 #[test]
947 fn test_undefined_reference_link() {
948 let rule = MD052ReferenceLinkImages::new();
949 let content = "[text][undefined]";
950 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
951 let result = rule.check(&ctx).unwrap();
952
953 assert_eq!(result.len(), 1);
954 assert!(result[0].message.contains("Reference 'undefined' not found"));
955 }
956
957 #[test]
958 fn test_undefined_reference_column_non_ascii_prefix() {
959 let rule = MD052ReferenceLinkImages::new();
963 let content = "你好[text][undefined]";
965 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
966 let result = rule.check(&ctx).unwrap();
967
968 assert_eq!(result.len(), 1);
969 assert_eq!(
970 result[0].column, 3,
971 "Column must be a character offset, not a byte offset"
972 );
973 }
974
975 #[test]
976 fn test_valid_reference_image() {
977 let rule = MD052ReferenceLinkImages::new();
978 let content = "![alt][img]\n\n[img]: image.jpg";
979 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
980 let result = rule.check(&ctx).unwrap();
981
982 assert_eq!(result.len(), 0);
983 }
984
985 #[test]
986 fn test_undefined_reference_image() {
987 let rule = MD052ReferenceLinkImages::new();
988 let content = "![alt][missing]";
989 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
990 let result = rule.check(&ctx).unwrap();
991
992 assert_eq!(result.len(), 1);
993 assert!(result[0].message.contains("Reference 'missing' not found"));
994 }
995
996 #[test]
997 fn test_case_insensitive_references() {
998 let rule = MD052ReferenceLinkImages::new();
999 let content = "[Text][REF]\n\n[ref]: https://example.com";
1000 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1001 let result = rule.check(&ctx).unwrap();
1002
1003 assert_eq!(result.len(), 0);
1004 }
1005
1006 #[test]
1007 fn test_shortcut_reference_valid() {
1008 let rule = MD052ReferenceLinkImages::new();
1009 let content = "[ref]\n\n[ref]: https://example.com";
1010 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1011 let result = rule.check(&ctx).unwrap();
1012
1013 assert_eq!(result.len(), 0);
1014 }
1015
1016 #[test]
1017 fn test_shortcut_reference_undefined_with_shortcut_syntax_enabled() {
1018 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1021 shortcut_syntax: true,
1022 ..Default::default()
1023 });
1024 let content = "[undefined]";
1025 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1026 let result = rule.check(&ctx).unwrap();
1027
1028 assert_eq!(result.len(), 1);
1029 assert!(result[0].message.contains("Reference 'undefined' not found"));
1030 }
1031
1032 #[test]
1033 fn test_shortcut_reference_not_checked_by_default() {
1034 let rule = MD052ReferenceLinkImages::new();
1036 let content = "[undefined]";
1037 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1038 let result = rule.check(&ctx).unwrap();
1039
1040 assert_eq!(result.len(), 0);
1042 }
1043
1044 #[test]
1045 fn test_inline_links_ignored() {
1046 let rule = MD052ReferenceLinkImages::new();
1047 let content = "[text](https://example.com)";
1048 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1049 let result = rule.check(&ctx).unwrap();
1050
1051 assert_eq!(result.len(), 0);
1052 }
1053
1054 #[test]
1055 fn test_inline_images_ignored() {
1056 let rule = MD052ReferenceLinkImages::new();
1057 let content = "";
1058 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1059 let result = rule.check(&ctx).unwrap();
1060
1061 assert_eq!(result.len(), 0);
1062 }
1063
1064 #[test]
1065 fn test_references_in_code_blocks_ignored() {
1066 let rule = MD052ReferenceLinkImages::new();
1067 let content = "```\n[undefined]\n```\n\n[ref]: https://example.com";
1068 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1069 let result = rule.check(&ctx).unwrap();
1070
1071 assert_eq!(result.len(), 0);
1072 }
1073
1074 #[test]
1075 fn test_references_in_inline_code_ignored() {
1076 let rule = MD052ReferenceLinkImages::new();
1077 let content = "`[undefined]`";
1078 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1079 let result = rule.check(&ctx).unwrap();
1080
1081 assert_eq!(result.len(), 0);
1083 }
1084
1085 #[test]
1086 fn test_comprehensive_inline_code_detection() {
1087 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1089 shortcut_syntax: true,
1090 ..Default::default()
1091 });
1092 let content = r#"# Test
1093
1094This `[inside]` should be ignored.
1095This [outside] should be flagged.
1096Reference links `[text][ref]` in code are ignored.
1097Regular reference [text][missing] should be flagged.
1098Images `![alt][img]` in code are ignored.
1099Regular image ![alt][badimg] should be flagged.
1100
1101Multiple `[one]` and `[two]` in code ignored, but [three] is not.
1102
1103```
1104[code block content] should be ignored
1105```
1106
1107`Multiple [refs] in [same] code span` ignored."#;
1108
1109 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1110 let result = rule.check(&ctx).unwrap();
1111
1112 assert_eq!(result.len(), 4);
1114
1115 let messages: Vec<&str> = result.iter().map(|w| &*w.message).collect();
1116 assert!(messages.iter().any(|m| m.contains("outside")));
1117 assert!(messages.iter().any(|m| m.contains("missing")));
1118 assert!(messages.iter().any(|m| m.contains("badimg")));
1119 assert!(messages.iter().any(|m| m.contains("three")));
1120
1121 assert!(!messages.iter().any(|m| m.contains("inside")));
1123 assert!(!messages.iter().any(|m| m.contains("one")));
1124 assert!(!messages.iter().any(|m| m.contains("two")));
1125 assert!(!messages.iter().any(|m| m.contains("refs")));
1126 assert!(!messages.iter().any(|m| m.contains("same")));
1127 }
1128
1129 #[test]
1130 fn test_multiple_undefined_references() {
1131 let rule = MD052ReferenceLinkImages::new();
1132 let content = "[link1][ref1] [link2][ref2] [link3][ref3]";
1133 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1134 let result = rule.check(&ctx).unwrap();
1135
1136 assert_eq!(result.len(), 3);
1137 assert!(result[0].message.contains("ref1"));
1138 assert!(result[1].message.contains("ref2"));
1139 assert!(result[2].message.contains("ref3"));
1140 }
1141
1142 #[test]
1143 fn test_mixed_valid_and_undefined() {
1144 let rule = MD052ReferenceLinkImages::new();
1145 let content = "[valid][ref] [invalid][missing]\n\n[ref]: https://example.com";
1146 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1147 let result = rule.check(&ctx).unwrap();
1148
1149 assert_eq!(result.len(), 1);
1150 assert!(result[0].message.contains("missing"));
1151 }
1152
1153 #[test]
1154 fn test_empty_reference() {
1155 let rule = MD052ReferenceLinkImages::new();
1156 let content = "[text][]\n\n[ref]: https://example.com";
1157 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1158 let result = rule.check(&ctx).unwrap();
1159
1160 assert_eq!(result.len(), 1);
1162 }
1163
1164 #[test]
1165 fn test_escaped_brackets_ignored() {
1166 let rule = MD052ReferenceLinkImages::new();
1167 let content = "\\[not a link\\]";
1168 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1169 let result = rule.check(&ctx).unwrap();
1170
1171 assert_eq!(result.len(), 0);
1172 }
1173
1174 #[test]
1175 fn test_list_items_ignored() {
1176 let rule = MD052ReferenceLinkImages::new();
1177 let content = "- [undefined]\n* [another]\n+ [third]";
1178 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1179 let result = rule.check(&ctx).unwrap();
1180
1181 assert_eq!(result.len(), 0);
1183 }
1184
1185 #[test]
1186 fn test_output_example_section_ignored() {
1187 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1189 shortcut_syntax: true,
1190 ..Default::default()
1191 });
1192 let content = "## Output\n\n[undefined]\n\n## Normal Section\n\n[missing]";
1193 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1194 let result = rule.check(&ctx).unwrap();
1195
1196 assert_eq!(result.len(), 1);
1198 assert!(result[0].message.contains("missing"));
1199 }
1200
1201 #[test]
1202 fn test_reference_definitions_in_code_blocks_ignored() {
1203 let rule = MD052ReferenceLinkImages::new();
1204 let content = "[link][ref]\n\n```\n[ref]: https://example.com\n```";
1205 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1206 let result = rule.check(&ctx).unwrap();
1207
1208 assert_eq!(result.len(), 1);
1210 assert!(result[0].message.contains("ref"));
1211 }
1212
1213 #[test]
1214 fn test_multiple_references_to_same_undefined() {
1215 let rule = MD052ReferenceLinkImages::new();
1216 let content = "[first][missing] [second][missing] [third][missing]";
1217 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1218 let result = rule.check(&ctx).unwrap();
1219
1220 assert_eq!(result.len(), 1);
1222 assert!(result[0].message.contains("missing"));
1223 }
1224
1225 #[test]
1226 fn test_reference_with_special_characters() {
1227 let rule = MD052ReferenceLinkImages::new();
1228 let content = "[text][ref-with-hyphens]\n\n[ref-with-hyphens]: https://example.com";
1229 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1230 let result = rule.check(&ctx).unwrap();
1231
1232 assert_eq!(result.len(), 0);
1233 }
1234
1235 #[test]
1236 fn test_issue_51_html_attribute_not_reference() {
1237 let rule = MD052ReferenceLinkImages::new();
1239 let content = r#"# Example
1240
1241## Test
1242
1243Want to fill out this form?
1244
1245<form method="post">
1246 <input type="email" name="fields[email]" id="drip-email" placeholder="email@domain.com">
1247</form>"#;
1248 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1249 let result = rule.check(&ctx).unwrap();
1250
1251 assert_eq!(
1252 result.len(),
1253 0,
1254 "HTML attributes with square brackets should not be flagged as undefined references"
1255 );
1256 }
1257
1258 #[test]
1259 fn test_extract_references() {
1260 let rule = MD052ReferenceLinkImages::new();
1261 let content = "[ref1]: url1\n[Ref2]: url2\n[REF3]: url3";
1262 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1263 let refs = rule.extract_references(&ctx);
1264
1265 assert_eq!(refs.len(), 3);
1266 assert!(refs.contains("ref1"));
1267 assert!(refs.contains("ref2"));
1268 assert!(refs.contains("ref3"));
1269 }
1270
1271 #[test]
1272 fn test_inline_code_not_flagged() {
1273 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1275 shortcut_syntax: true,
1276 ..Default::default()
1277 });
1278
1279 let content = r#"# Test
1281
1282Configure with `["JavaScript", "GitHub", "Node.js"]` in your settings.
1283
1284Also, `[todo]` is not a reference link.
1285
1286But this [reference] should be flagged.
1287
1288And this `[inline code]` should not be flagged.
1289"#;
1290
1291 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1292 let warnings = rule.check(&ctx).unwrap();
1293
1294 assert_eq!(warnings.len(), 1, "Should only flag one undefined reference");
1296 assert!(warnings[0].message.contains("'reference'"));
1297 }
1298
1299 #[test]
1300 fn test_code_block_references_ignored() {
1301 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1303 shortcut_syntax: true,
1304 ..Default::default()
1305 });
1306
1307 let content = r#"# Test
1308
1309```markdown
1310[undefined] reference in code block
1311![undefined] image in code block
1312```
1313
1314[real-undefined] reference outside
1315"#;
1316
1317 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1318 let warnings = rule.check(&ctx).unwrap();
1319
1320 assert_eq!(warnings.len(), 1);
1322 assert!(warnings[0].message.contains("'real-undefined'"));
1323 }
1324
1325 #[test]
1326 fn test_html_comments_ignored() {
1327 let rule = MD052ReferenceLinkImages::new();
1329
1330 let content = r#"<!--- write fake_editor.py 'import sys\nopen(*sys.argv[1:], mode="wt").write("2 3 4 4 2 3 2")' -->
1332<!--- set_env EDITOR 'python3 fake_editor.py' -->
1333
1334```bash
1335$ python3 vote.py
13363 votes for: 2
13372 votes for: 3, 4
1338```"#;
1339 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1340 let result = rule.check(&ctx).unwrap();
1341 assert_eq!(result.len(), 0, "Should not flag [1:] inside HTML comments");
1342
1343 let content = r#"<!-- This is [ref1] and [ref2][ref3] -->
1345Normal [text][undefined]
1346<!-- Another [comment][with] references -->"#;
1347 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1348 let result = rule.check(&ctx).unwrap();
1349 assert_eq!(
1350 result.len(),
1351 1,
1352 "Should only flag the undefined reference outside comments"
1353 );
1354 assert!(result[0].message.contains("undefined"));
1355
1356 let content = r#"<!--
1358[ref1]
1359[ref2][ref3]
1360-->
1361[actual][undefined]"#;
1362 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1363 let result = rule.check(&ctx).unwrap();
1364 assert_eq!(
1365 result.len(),
1366 1,
1367 "Should not flag references in multi-line HTML comments"
1368 );
1369 assert!(result[0].message.contains("undefined"));
1370
1371 let content = r#"<!-- Comment with [1:] pattern -->
1373Valid [link][ref]
1374<!-- More [refs][in][comments] -->
1375![image][missing]
1376
1377[ref]: https://example.com"#;
1378 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1379 let result = rule.check(&ctx).unwrap();
1380 assert_eq!(result.len(), 1, "Should only flag missing image reference");
1381 assert!(result[0].message.contains("missing"));
1382 }
1383
1384 #[test]
1385 fn test_frontmatter_ignored() {
1386 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1389 shortcut_syntax: true,
1390 ..Default::default()
1391 });
1392
1393 let content = r#"---
1395layout: post
1396title: "My Jekyll Post"
1397date: 2023-01-01
1398categories: blog
1399tags: ["test", "example"]
1400author: John Doe
1401---
1402
1403# My Blog Post
1404
1405This is the actual markdown content that should be linted.
1406
1407[undefined] reference should be flagged.
1408
1409## Section 1
1410
1411Some content here."#;
1412 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1413 let result = rule.check(&ctx).unwrap();
1414
1415 assert_eq!(
1417 result.len(),
1418 1,
1419 "Should only flag the undefined reference outside frontmatter"
1420 );
1421 assert!(result[0].message.contains("undefined"));
1422
1423 let content = r#"+++
1425title = "My Post"
1426tags = ["example", "test"]
1427+++
1428
1429# Content
1430
1431[missing] reference should be flagged."#;
1432 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1433 let result = rule.check(&ctx).unwrap();
1434 assert_eq!(
1435 result.len(),
1436 1,
1437 "Should only flag the undefined reference outside TOML frontmatter"
1438 );
1439 assert!(result[0].message.contains("missing"));
1440 }
1441
1442 #[test]
1443 fn test_mkdocs_snippet_markers_not_flagged() {
1444 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1447 shortcut_syntax: true,
1448 ..Default::default()
1449 });
1450
1451 let content = r#"# Document with MkDocs Snippets
1453
1454Some content here.
1455
1456# -8<- [start:remote-content]
1457
1458This is the remote content section.
1459
1460# -8<- [end:remote-content]
1461
1462More content here.
1463
1464<!-- --8<-- [start:another-section] -->
1465Content in another section
1466<!-- --8<-- [end:another-section] -->"#;
1467 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1468 let result = rule.check(&ctx).unwrap();
1469
1470 assert_eq!(
1472 result.len(),
1473 0,
1474 "Should not flag MkDocs snippet markers as undefined references"
1475 );
1476
1477 let content = r#"# Document
1480
1481# -8<- [start:section]
1482Content with [reference] inside snippet section
1483# -8<- [end:section]
1484
1485Regular [undefined] reference outside snippet markers."#;
1486 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1487 let result = rule.check(&ctx).unwrap();
1488
1489 assert_eq!(
1490 result.len(),
1491 2,
1492 "Should flag undefined references but skip snippet marker lines"
1493 );
1494 assert!(result[0].message.contains("reference"));
1496 assert!(result[1].message.contains("undefined"));
1497
1498 let content = r#"# Document
1500
1501# -8<- [start:section]
1502# -8<- [end:section]"#;
1503 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1504 let result = rule.check(&ctx).unwrap();
1505
1506 assert_eq!(
1507 result.len(),
1508 2,
1509 "In standard mode, snippet markers should be flagged as undefined references"
1510 );
1511 }
1512
1513 #[test]
1514 fn test_pandoc_citations_not_flagged() {
1515 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1518 shortcut_syntax: true,
1519 ..Default::default()
1520 });
1521
1522 let content = r#"# Research Paper
1523
1524We are using the **bookdown** package [@R-bookdown] in this sample book.
1525This was built on top of R Markdown and **knitr** [@xie2015].
1526
1527Multiple citations [@citation1; @citation2; @citation3] are also supported.
1528
1529Regular [undefined] reference should still be flagged.
1530"#;
1531 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1532 let result = rule.check(&ctx).unwrap();
1533
1534 assert_eq!(
1536 result.len(),
1537 1,
1538 "Should only flag the undefined reference, not Pandoc citations"
1539 );
1540 assert!(result[0].message.contains("undefined"));
1541 }
1542
1543 #[test]
1544 fn test_pandoc_inline_footnotes_not_flagged() {
1545 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1548 shortcut_syntax: true,
1549 ..Default::default()
1550 });
1551
1552 let content = r#"# Math Document
1553
1554You can use math in footnotes like this^[where we mention $p = \frac{a}{b}$].
1555
1556Another footnote^[with some text and a [link](https://example.com)].
1557
1558But this [reference] without ^ should be flagged.
1559"#;
1560 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1561 let result = rule.check(&ctx).unwrap();
1562
1563 assert_eq!(
1565 result.len(),
1566 1,
1567 "Should only flag the regular reference, not inline footnotes"
1568 );
1569 assert!(result[0].message.contains("reference"));
1570 }
1571
1572 #[test]
1573 fn test_github_alerts_not_flagged() {
1574 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1577 shortcut_syntax: true,
1578 ..Default::default()
1579 });
1580
1581 let content = r#"# Document with GitHub Alerts
1583
1584> [!NOTE]
1585> This is a note alert.
1586
1587> [!TIP]
1588> This is a tip alert.
1589
1590> [!IMPORTANT]
1591> This is an important alert.
1592
1593> [!WARNING]
1594> This is a warning alert.
1595
1596> [!CAUTION]
1597> This is a caution alert.
1598
1599Regular content with [undefined] reference."#;
1600 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1601 let result = rule.check(&ctx).unwrap();
1602
1603 assert_eq!(
1605 result.len(),
1606 1,
1607 "Should only flag the undefined reference, not GitHub alerts"
1608 );
1609 assert!(result[0].message.contains("undefined"));
1610 assert_eq!(result[0].line, 18); let content = r#"> [!TIP]
1614> Here's a useful tip about [something].
1615> Multiple lines are allowed.
1616
1617[something] is mentioned but not defined."#;
1618 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1619 let result = rule.check(&ctx).unwrap();
1620
1621 assert_eq!(result.len(), 1, "Should flag undefined reference");
1625 assert!(result[0].message.contains("something"));
1626
1627 let content = r#"> [!NOTE]
1629> See [reference] for more details.
1630
1631[reference]: https://example.com"#;
1632 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1633 let result = rule.check(&ctx).unwrap();
1634
1635 assert_eq!(result.len(), 0, "Should not flag GitHub alerts or defined references");
1637 }
1638
1639 #[test]
1640 fn test_ignore_config() {
1641 let config = MD052Config {
1643 shortcut_syntax: true,
1644 ignore: vec!["Vec".to_string(), "HashMap".to_string(), "Option".to_string()],
1645 };
1646 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1647
1648 let content = r#"# Document with Custom Types
1649
1650Use [Vec] for dynamic arrays.
1651Use [HashMap] for key-value storage.
1652Use [Option] for nullable values.
1653Use [Result] for error handling.
1654"#;
1655 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1656 let result = rule.check(&ctx).unwrap();
1657
1658 assert_eq!(result.len(), 1, "Should only flag names not in ignore");
1660 assert!(result[0].message.contains("Result"));
1661 }
1662
1663 #[test]
1664 fn test_ignore_case_insensitive() {
1665 let config = MD052Config {
1667 shortcut_syntax: true,
1668 ignore: vec!["Vec".to_string()],
1669 };
1670 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1671
1672 let content = r#"# Case Insensitivity Test
1673
1674[Vec] should be ignored.
1675[vec] should also be ignored (different case, same match).
1676[VEC] should also be ignored (different case, same match).
1677[undefined] should be flagged (not in ignore list).
1678"#;
1679 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1680 let result = rule.check(&ctx).unwrap();
1681
1682 assert_eq!(result.len(), 1, "Should only flag non-ignored reference");
1684 assert!(result[0].message.contains("undefined"));
1685 }
1686
1687 #[test]
1688 fn test_ignore_empty_by_default() {
1689 let rule = MD052ReferenceLinkImages::new();
1691
1692 let content = "[text][undefined]";
1693 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1694 let result = rule.check(&ctx).unwrap();
1695
1696 assert_eq!(result.len(), 1);
1698 assert!(result[0].message.contains("undefined"));
1699 }
1700
1701 #[test]
1702 fn test_ignore_with_reference_links() {
1703 let config = MD052Config {
1705 shortcut_syntax: false,
1706 ignore: vec!["CustomType".to_string()],
1707 };
1708 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1709
1710 let content = r#"# Test
1711
1712See [documentation][CustomType] for details.
1713See [other docs][MissingRef] for more.
1714"#;
1715 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1716 let result = rule.check(&ctx).unwrap();
1717
1718 for (i, w) in result.iter().enumerate() {
1720 eprintln!("Warning {}: {}", i, w.message);
1721 }
1722
1723 assert_eq!(result.len(), 1, "Expected 1 warning, got {}", result.len());
1726 assert!(
1727 result[0].message.contains("missingref"),
1728 "Expected 'missingref' in message: {}",
1729 result[0].message
1730 );
1731 }
1732
1733 #[test]
1734 fn test_ignore_multiple() {
1735 let config = MD052Config {
1737 shortcut_syntax: true,
1738 ignore: vec![
1739 "i32".to_string(),
1740 "u64".to_string(),
1741 "String".to_string(),
1742 "Arc".to_string(),
1743 "Mutex".to_string(),
1744 ],
1745 };
1746 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1747
1748 let content = r#"# Types
1749
1750[i32] [u64] [String] [Arc] [Mutex] [Box]
1751"#;
1752 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1753 let result = rule.check(&ctx).unwrap();
1754
1755 assert_eq!(result.len(), 1);
1759 assert!(result[0].message.contains("Box"));
1760 }
1761
1762 #[test]
1763 fn test_nested_code_fences_reference_extraction() {
1764 let rule = MD052ReferenceLinkImages::new();
1769
1770 let content = "````\n```\n[ref-inside]: https://example.com\n```\n````\n\n[Use this link][ref-inside]";
1771 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1772 let result = rule.check(&ctx).unwrap();
1773
1774 assert_eq!(
1778 result.len(),
1779 1,
1780 "Reference defined inside nested code fence should not count as a definition"
1781 );
1782 assert!(result[0].message.contains("ref-inside"));
1783 }
1784
1785 #[test]
1786 fn test_pandoc_flavor_skips_citations() {
1787 let rule = MD052ReferenceLinkImages::new();
1790 let content = "See [@smith2020] for details.\n";
1791 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
1792 let result = rule.check(&ctx).unwrap();
1793 assert!(
1794 result.is_empty(),
1795 "MD052 should skip Pandoc citations under Pandoc flavor: {result:?}"
1796 );
1797 }
1798
1799 #[test]
1800 fn md052_pandoc_skips_implicit_header_refs_with_shortcut_syntax() {
1801 use crate::config::MarkdownFlavor;
1808 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1809 shortcut_syntax: true,
1810 ..Default::default()
1811 });
1812 let content = "# My Section\n\nSee [My Section] for details.\n";
1813
1814 let ctx_std = LintContext::new(content, MarkdownFlavor::Standard, None);
1817 let std_result = rule.check(&ctx_std).unwrap();
1818 assert_eq!(
1819 std_result.len(),
1820 1,
1821 "Standard flavor with shortcut_syntax should flag [My Section]: {std_result:?}"
1822 );
1823
1824 let ctx_pandoc = LintContext::new(content, MarkdownFlavor::Pandoc, None);
1826 let pandoc_result = rule.check(&ctx_pandoc).unwrap();
1827 assert!(
1828 pandoc_result.is_empty(),
1829 "Pandoc flavor should accept [My Section] as an implicit header ref: {pandoc_result:?}"
1830 );
1831 }
1832}