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(|c| c.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 == crate::config::MarkdownFlavor::Quarto && 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 undefined.push((link.line - 1, link.start_col, match_len, ref_id.to_string()));
428 reported_refs.insert(reference_lower, true);
429 }
430 }
431 }
432
433 for image in &ctx.images {
435 if !image.is_reference {
436 continue; }
438
439 if ctx.is_in_jinja_range(image.byte_offset) {
441 continue;
442 }
443
444 if Self::is_in_code_span(image.byte_offset, &code_spans) {
446 continue;
447 }
448
449 if ctx.is_in_html_comment(image.byte_offset) || ctx.is_in_mdx_comment(image.byte_offset) {
451 continue;
452 }
453
454 if Self::is_in_html_tag(&html_tags, image.byte_offset) {
456 continue;
457 }
458
459 if is_in_math_context(ctx, image.byte_offset) {
461 continue;
462 }
463
464 if is_in_table_cell(ctx, image.line, image.start_col) {
466 continue;
467 }
468
469 if ctx.line_info(image.line).is_some_and(|info| info.in_front_matter) {
471 continue;
472 }
473
474 if let Some(ref_id) = &image.reference_id {
475 let reference_lower = ref_id.to_lowercase();
476
477 if self.is_known_non_reference_pattern(ref_id) {
479 continue;
480 }
481
482 let stripped_ref = Self::strip_backticks(ref_id);
486 let stripped_alt = Self::strip_backticks(&image.alt_text);
487 if mkdocs_mode
488 && (is_mkdocs_auto_reference(stripped_ref)
489 || is_mkdocs_auto_reference(stripped_alt)
490 || (ref_id != stripped_ref && Self::is_valid_python_identifier(stripped_ref))
491 || (image.alt_text.as_ref() != stripped_alt && Self::is_valid_python_identifier(stripped_alt)))
492 {
493 continue;
494 }
495
496 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
498 if let Some(line_info) = ctx.line_info(image.line) {
500 if OUTPUT_EXAMPLE_START.is_match(line_info.content(ctx.content)) {
501 in_example_section = true;
502 continue;
503 }
504
505 if in_example_section {
506 continue;
507 }
508
509 if LIST_ITEM_REGEX.is_match(line_info.content(ctx.content)) {
511 continue;
512 }
513
514 let trimmed = line_info.content(ctx.content).trim_start();
516 if trimmed.starts_with('<') {
517 continue;
518 }
519 }
520
521 let match_len = image.byte_end - image.byte_offset;
522 undefined.push((image.line - 1, image.start_col, match_len, ref_id.to_string()));
523 reported_refs.insert(reference_lower, true);
524 }
525 }
526 }
527
528 let mut covered_ranges: Vec<(usize, usize)> = Vec::new();
530
531 for link in &ctx.links {
533 covered_ranges.push((link.byte_offset, link.byte_end));
534 }
535
536 for image in &ctx.images {
538 covered_ranges.push((image.byte_offset, image.byte_end));
539 }
540
541 covered_ranges.sort_by_key(|&(start, _)| start);
543
544 if !self.config.shortcut_syntax {
549 return undefined;
550 }
551
552 let lines = ctx.raw_lines();
554 in_example_section = false; for (line_num, line) in lines.iter().enumerate() {
557 if let Some(line_info) = ctx.line_info(line_num + 1)
559 && (line_info.in_front_matter || line_info.in_code_block)
560 {
561 continue;
562 }
563
564 if OUTPUT_EXAMPLE_START.is_match(line) {
566 in_example_section = true;
567 continue;
568 }
569
570 if in_example_section {
571 if line.starts_with('#') && !OUTPUT_EXAMPLE_START.is_match(line) {
573 in_example_section = false;
574 } else {
575 continue;
576 }
577 }
578
579 if LIST_ITEM_REGEX.is_match(line) {
581 continue;
582 }
583
584 let trimmed_line = line.trim_start();
586 if trimmed_line.starts_with('<') {
587 continue;
588 }
589
590 if GITHUB_ALERT_REGEX.is_match(line) {
592 continue;
593 }
594
595 if trimmed_line.starts_with("*[") {
598 continue;
599 }
600
601 let mut url_bracket_ranges: Vec<(usize, usize)> = Vec::new();
604 for mat in URL_WITH_BRACKETS.find_iter(line) {
605 let url_str = mat.as_str();
607 let url_start = mat.start();
608
609 let mut idx = 0;
611 while idx < url_str.len() {
612 if let Some(bracket_start) = url_str[idx..].find('[') {
613 let bracket_start_abs = url_start + idx + bracket_start;
614 if let Some(bracket_end) = url_str[idx + bracket_start + 1..].find(']') {
615 let bracket_end_abs = url_start + idx + bracket_start + 1 + bracket_end + 1;
616 url_bracket_ranges.push((bracket_start_abs, bracket_end_abs));
617 idx += bracket_start + bracket_end + 2;
618 } else {
619 break;
620 }
621 } else {
622 break;
623 }
624 }
625 }
626
627 if let Ok(captures) = SHORTCUT_REF_REGEX.captures_iter(line).collect::<Result<Vec<_>, _>>() {
629 for cap in captures {
630 if let Some(ref_match) = cap.get(1) {
631 let bracket_start = cap.get(0).unwrap().start();
633 let bracket_end = cap.get(0).unwrap().end();
634
635 let is_in_url = url_bracket_ranges
637 .iter()
638 .any(|&(url_start, url_end)| bracket_start >= url_start && bracket_end <= url_end);
639
640 if is_in_url {
641 continue;
642 }
643
644 if bracket_start > 0 {
647 if let Some(byte) = line.as_bytes().get(bracket_start.saturating_sub(1))
649 && *byte == b'^'
650 {
651 continue; }
653 }
654
655 let reference = ref_match.as_str();
656 let reference_lower = reference.to_lowercase();
657
658 if self.is_known_non_reference_pattern(reference) {
660 continue;
661 }
662
663 if let Some(alert_type) = reference.strip_prefix('!')
665 && matches!(
666 alert_type,
667 "NOTE"
668 | "TIP"
669 | "WARNING"
670 | "IMPORTANT"
671 | "CAUTION"
672 | "INFO"
673 | "SUCCESS"
674 | "FAILURE"
675 | "DANGER"
676 | "BUG"
677 | "EXAMPLE"
678 | "QUOTE"
679 )
680 {
681 continue;
682 }
683
684 if mkdocs_mode
687 && (reference.starts_with("start:") || reference.starts_with("end:"))
688 && (crate::utils::mkdocs_snippets::is_snippet_section_start(line)
689 || crate::utils::mkdocs_snippets::is_snippet_section_end(line))
690 {
691 continue;
692 }
693
694 let stripped_ref = Self::strip_backticks(reference);
697 if mkdocs_mode
698 && (is_mkdocs_auto_reference(stripped_ref)
699 || (reference != stripped_ref && Self::is_valid_python_identifier(stripped_ref)))
700 {
701 continue;
702 }
703
704 if !references.contains(&reference_lower) && !reported_refs.contains_key(&reference_lower) {
705 let full_match = cap.get(0).unwrap();
706 let col = full_match.start();
707 let line_start_byte = ctx.line_offsets[line_num];
708 let byte_pos = line_start_byte + col;
709
710 let code_spans = ctx.code_spans();
712 if Self::is_in_code_span(byte_pos, &code_spans) {
713 continue;
714 }
715
716 if ctx.is_in_jinja_range(byte_pos) {
718 continue;
719 }
720
721 if crate::utils::code_block_utils::CodeBlockUtils::is_in_code_block(
723 &ctx.code_blocks,
724 byte_pos,
725 ) {
726 continue;
727 }
728
729 if ctx.is_in_html_comment(byte_pos) || ctx.is_in_mdx_comment(byte_pos) {
731 continue;
732 }
733
734 if Self::is_in_html_tag(&html_tags, byte_pos) {
736 continue;
737 }
738
739 if is_in_math_context(ctx, byte_pos) {
741 continue;
742 }
743
744 if is_in_table_cell(ctx, line_num + 1, col) {
746 continue;
747 }
748
749 let byte_end = byte_pos + (full_match.end() - full_match.start());
750
751 let mut is_covered = false;
753 for &(range_start, range_end) in &covered_ranges {
754 if range_start <= byte_pos && byte_end <= range_end {
755 is_covered = true;
757 break;
758 }
759 if range_start > byte_end {
760 break;
762 }
763 }
764
765 if is_covered {
766 continue;
767 }
768
769 let line_chars: Vec<char> = line.chars().collect();
774 if col > 0 && col <= line_chars.len() && line_chars.get(col - 1) == Some(&']') {
775 let mut bracket_count = 1; let mut check_pos = col.saturating_sub(2);
778 let mut found_opening = false;
779
780 while check_pos > 0 && check_pos < line_chars.len() {
781 match line_chars.get(check_pos) {
782 Some(&']') => bracket_count += 1,
783 Some(&'[') => {
784 bracket_count -= 1;
785 if bracket_count == 0 {
786 if check_pos == 0 || line_chars.get(check_pos - 1) != Some(&'\\') {
788 found_opening = true;
789 }
790 break;
791 }
792 }
793 _ => {}
794 }
795 if check_pos == 0 {
796 break;
797 }
798 check_pos = check_pos.saturating_sub(1);
799 }
800
801 if found_opening {
802 continue;
804 }
805 }
806
807 let before_text = &line[..col];
810 if before_text.contains("\\]") {
811 if let Some(escaped_close_pos) = before_text.rfind("\\]") {
813 let search_text = &before_text[..escaped_close_pos];
814 if search_text.contains("\\[") {
815 continue;
817 }
818 }
819 }
820
821 let match_len = full_match.end() - full_match.start();
822 undefined.push((line_num, col, match_len, reference.to_string()));
823 reported_refs.insert(reference_lower, true);
824 }
825 }
826 }
827 }
828 }
829
830 undefined
831 }
832}
833
834impl Rule for MD052ReferenceLinkImages {
835 fn name(&self) -> &'static str {
836 "MD052"
837 }
838
839 fn description(&self) -> &'static str {
840 "Reference links and images should use a reference that exists"
841 }
842
843 fn category(&self) -> RuleCategory {
844 RuleCategory::Link
845 }
846
847 fn fix_capability(&self) -> FixCapability {
848 FixCapability::Unfixable
849 }
850
851 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
852 let content = ctx.content;
853 let mut warnings = Vec::new();
854
855 if !content.contains('[') {
857 return Ok(warnings);
858 }
859
860 let mkdocs_mode = ctx.flavor == crate::config::MarkdownFlavor::MkDocs;
862
863 let references = self.extract_references(ctx);
864
865 let lines = ctx.raw_lines();
867 for (line_num, col, match_len, reference) in self.find_undefined_references(&references, ctx, mkdocs_mode) {
868 let line_content = lines.get(line_num).unwrap_or(&"");
869
870 let (start_line, start_col, end_line, end_col) =
872 calculate_match_range(line_num + 1, line_content, col, match_len);
873
874 warnings.push(LintWarning {
875 rule_name: Some(self.name().to_string()),
876 line: start_line,
877 column: start_col,
878 end_line,
879 end_column: end_col,
880 message: format!("Reference '{reference}' not found"),
881 severity: Severity::Warning,
882 fix: None,
883 });
884 }
885
886 Ok(warnings)
887 }
888
889 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
891 ctx.content.is_empty() || !ctx.likely_has_links_or_images()
893 }
894
895 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
896 let content = ctx.content;
897 Ok(content.to_string())
899 }
900
901 fn as_any(&self) -> &dyn std::any::Any {
902 self
903 }
904
905 fn default_config_section(&self) -> Option<(String, toml::Value)> {
906 let json_value = serde_json::to_value(&self.config).ok()?;
907 Some((
908 self.name().to_string(),
909 crate::rule_config_serde::json_to_toml_value(&json_value)?,
910 ))
911 }
912
913 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
914 where
915 Self: Sized,
916 {
917 let rule_config = crate::rule_config_serde::load_rule_config::<MD052Config>(config);
918 Box::new(Self::from_config_struct(rule_config))
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use super::*;
925 use crate::lint_context::LintContext;
926
927 #[test]
928 fn test_valid_reference_link() {
929 let rule = MD052ReferenceLinkImages::new();
930 let content = "[text][ref]\n\n[ref]: https://example.com";
931 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
932 let result = rule.check(&ctx).unwrap();
933
934 assert_eq!(result.len(), 0);
935 }
936
937 #[test]
938 fn test_undefined_reference_link() {
939 let rule = MD052ReferenceLinkImages::new();
940 let content = "[text][undefined]";
941 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
942 let result = rule.check(&ctx).unwrap();
943
944 assert_eq!(result.len(), 1);
945 assert!(result[0].message.contains("Reference 'undefined' not found"));
946 }
947
948 #[test]
949 fn test_valid_reference_image() {
950 let rule = MD052ReferenceLinkImages::new();
951 let content = "![alt][img]\n\n[img]: image.jpg";
952 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
953 let result = rule.check(&ctx).unwrap();
954
955 assert_eq!(result.len(), 0);
956 }
957
958 #[test]
959 fn test_undefined_reference_image() {
960 let rule = MD052ReferenceLinkImages::new();
961 let content = "![alt][missing]";
962 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
963 let result = rule.check(&ctx).unwrap();
964
965 assert_eq!(result.len(), 1);
966 assert!(result[0].message.contains("Reference 'missing' not found"));
967 }
968
969 #[test]
970 fn test_case_insensitive_references() {
971 let rule = MD052ReferenceLinkImages::new();
972 let content = "[Text][REF]\n\n[ref]: https://example.com";
973 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
974 let result = rule.check(&ctx).unwrap();
975
976 assert_eq!(result.len(), 0);
977 }
978
979 #[test]
980 fn test_shortcut_reference_valid() {
981 let rule = MD052ReferenceLinkImages::new();
982 let content = "[ref]\n\n[ref]: https://example.com";
983 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
984 let result = rule.check(&ctx).unwrap();
985
986 assert_eq!(result.len(), 0);
987 }
988
989 #[test]
990 fn test_shortcut_reference_undefined_with_shortcut_syntax_enabled() {
991 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
994 shortcut_syntax: true,
995 ..Default::default()
996 });
997 let content = "[undefined]";
998 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
999 let result = rule.check(&ctx).unwrap();
1000
1001 assert_eq!(result.len(), 1);
1002 assert!(result[0].message.contains("Reference 'undefined' not found"));
1003 }
1004
1005 #[test]
1006 fn test_shortcut_reference_not_checked_by_default() {
1007 let rule = MD052ReferenceLinkImages::new();
1009 let content = "[undefined]";
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);
1015 }
1016
1017 #[test]
1018 fn test_inline_links_ignored() {
1019 let rule = MD052ReferenceLinkImages::new();
1020 let content = "[text](https://example.com)";
1021 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1022 let result = rule.check(&ctx).unwrap();
1023
1024 assert_eq!(result.len(), 0);
1025 }
1026
1027 #[test]
1028 fn test_inline_images_ignored() {
1029 let rule = MD052ReferenceLinkImages::new();
1030 let content = "";
1031 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1032 let result = rule.check(&ctx).unwrap();
1033
1034 assert_eq!(result.len(), 0);
1035 }
1036
1037 #[test]
1038 fn test_references_in_code_blocks_ignored() {
1039 let rule = MD052ReferenceLinkImages::new();
1040 let content = "```\n[undefined]\n```\n\n[ref]: https://example.com";
1041 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1042 let result = rule.check(&ctx).unwrap();
1043
1044 assert_eq!(result.len(), 0);
1045 }
1046
1047 #[test]
1048 fn test_references_in_inline_code_ignored() {
1049 let rule = MD052ReferenceLinkImages::new();
1050 let content = "`[undefined]`";
1051 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1052 let result = rule.check(&ctx).unwrap();
1053
1054 assert_eq!(result.len(), 0);
1056 }
1057
1058 #[test]
1059 fn test_comprehensive_inline_code_detection() {
1060 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1062 shortcut_syntax: true,
1063 ..Default::default()
1064 });
1065 let content = r#"# Test
1066
1067This `[inside]` should be ignored.
1068This [outside] should be flagged.
1069Reference links `[text][ref]` in code are ignored.
1070Regular reference [text][missing] should be flagged.
1071Images `![alt][img]` in code are ignored.
1072Regular image ![alt][badimg] should be flagged.
1073
1074Multiple `[one]` and `[two]` in code ignored, but [three] is not.
1075
1076```
1077[code block content] should be ignored
1078```
1079
1080`Multiple [refs] in [same] code span` ignored."#;
1081
1082 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1083 let result = rule.check(&ctx).unwrap();
1084
1085 assert_eq!(result.len(), 4);
1087
1088 let messages: Vec<&str> = result.iter().map(|w| &*w.message).collect();
1089 assert!(messages.iter().any(|m| m.contains("outside")));
1090 assert!(messages.iter().any(|m| m.contains("missing")));
1091 assert!(messages.iter().any(|m| m.contains("badimg")));
1092 assert!(messages.iter().any(|m| m.contains("three")));
1093
1094 assert!(!messages.iter().any(|m| m.contains("inside")));
1096 assert!(!messages.iter().any(|m| m.contains("one")));
1097 assert!(!messages.iter().any(|m| m.contains("two")));
1098 assert!(!messages.iter().any(|m| m.contains("refs")));
1099 assert!(!messages.iter().any(|m| m.contains("same")));
1100 }
1101
1102 #[test]
1103 fn test_multiple_undefined_references() {
1104 let rule = MD052ReferenceLinkImages::new();
1105 let content = "[link1][ref1] [link2][ref2] [link3][ref3]";
1106 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1107 let result = rule.check(&ctx).unwrap();
1108
1109 assert_eq!(result.len(), 3);
1110 assert!(result[0].message.contains("ref1"));
1111 assert!(result[1].message.contains("ref2"));
1112 assert!(result[2].message.contains("ref3"));
1113 }
1114
1115 #[test]
1116 fn test_mixed_valid_and_undefined() {
1117 let rule = MD052ReferenceLinkImages::new();
1118 let content = "[valid][ref] [invalid][missing]\n\n[ref]: https://example.com";
1119 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1120 let result = rule.check(&ctx).unwrap();
1121
1122 assert_eq!(result.len(), 1);
1123 assert!(result[0].message.contains("missing"));
1124 }
1125
1126 #[test]
1127 fn test_empty_reference() {
1128 let rule = MD052ReferenceLinkImages::new();
1129 let content = "[text][]\n\n[ref]: https://example.com";
1130 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1131 let result = rule.check(&ctx).unwrap();
1132
1133 assert_eq!(result.len(), 1);
1135 }
1136
1137 #[test]
1138 fn test_escaped_brackets_ignored() {
1139 let rule = MD052ReferenceLinkImages::new();
1140 let content = "\\[not a link\\]";
1141 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1142 let result = rule.check(&ctx).unwrap();
1143
1144 assert_eq!(result.len(), 0);
1145 }
1146
1147 #[test]
1148 fn test_list_items_ignored() {
1149 let rule = MD052ReferenceLinkImages::new();
1150 let content = "- [undefined]\n* [another]\n+ [third]";
1151 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1152 let result = rule.check(&ctx).unwrap();
1153
1154 assert_eq!(result.len(), 0);
1156 }
1157
1158 #[test]
1159 fn test_output_example_section_ignored() {
1160 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1162 shortcut_syntax: true,
1163 ..Default::default()
1164 });
1165 let content = "## Output\n\n[undefined]\n\n## Normal Section\n\n[missing]";
1166 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1167 let result = rule.check(&ctx).unwrap();
1168
1169 assert_eq!(result.len(), 1);
1171 assert!(result[0].message.contains("missing"));
1172 }
1173
1174 #[test]
1175 fn test_reference_definitions_in_code_blocks_ignored() {
1176 let rule = MD052ReferenceLinkImages::new();
1177 let content = "[link][ref]\n\n```\n[ref]: https://example.com\n```";
1178 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1179 let result = rule.check(&ctx).unwrap();
1180
1181 assert_eq!(result.len(), 1);
1183 assert!(result[0].message.contains("ref"));
1184 }
1185
1186 #[test]
1187 fn test_multiple_references_to_same_undefined() {
1188 let rule = MD052ReferenceLinkImages::new();
1189 let content = "[first][missing] [second][missing] [third][missing]";
1190 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1191 let result = rule.check(&ctx).unwrap();
1192
1193 assert_eq!(result.len(), 1);
1195 assert!(result[0].message.contains("missing"));
1196 }
1197
1198 #[test]
1199 fn test_reference_with_special_characters() {
1200 let rule = MD052ReferenceLinkImages::new();
1201 let content = "[text][ref-with-hyphens]\n\n[ref-with-hyphens]: https://example.com";
1202 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1203 let result = rule.check(&ctx).unwrap();
1204
1205 assert_eq!(result.len(), 0);
1206 }
1207
1208 #[test]
1209 fn test_issue_51_html_attribute_not_reference() {
1210 let rule = MD052ReferenceLinkImages::new();
1212 let content = r#"# Example
1213
1214## Test
1215
1216Want to fill out this form?
1217
1218<form method="post">
1219 <input type="email" name="fields[email]" id="drip-email" placeholder="email@domain.com">
1220</form>"#;
1221 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1222 let result = rule.check(&ctx).unwrap();
1223
1224 assert_eq!(
1225 result.len(),
1226 0,
1227 "HTML attributes with square brackets should not be flagged as undefined references"
1228 );
1229 }
1230
1231 #[test]
1232 fn test_extract_references() {
1233 let rule = MD052ReferenceLinkImages::new();
1234 let content = "[ref1]: url1\n[Ref2]: url2\n[REF3]: url3";
1235 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1236 let refs = rule.extract_references(&ctx);
1237
1238 assert_eq!(refs.len(), 3);
1239 assert!(refs.contains("ref1"));
1240 assert!(refs.contains("ref2"));
1241 assert!(refs.contains("ref3"));
1242 }
1243
1244 #[test]
1245 fn test_inline_code_not_flagged() {
1246 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1248 shortcut_syntax: true,
1249 ..Default::default()
1250 });
1251
1252 let content = r#"# Test
1254
1255Configure with `["JavaScript", "GitHub", "Node.js"]` in your settings.
1256
1257Also, `[todo]` is not a reference link.
1258
1259But this [reference] should be flagged.
1260
1261And this `[inline code]` should not be flagged.
1262"#;
1263
1264 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1265 let warnings = rule.check(&ctx).unwrap();
1266
1267 assert_eq!(warnings.len(), 1, "Should only flag one undefined reference");
1269 assert!(warnings[0].message.contains("'reference'"));
1270 }
1271
1272 #[test]
1273 fn test_code_block_references_ignored() {
1274 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1276 shortcut_syntax: true,
1277 ..Default::default()
1278 });
1279
1280 let content = r#"# Test
1281
1282```markdown
1283[undefined] reference in code block
1284![undefined] image in code block
1285```
1286
1287[real-undefined] reference outside
1288"#;
1289
1290 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1291 let warnings = rule.check(&ctx).unwrap();
1292
1293 assert_eq!(warnings.len(), 1);
1295 assert!(warnings[0].message.contains("'real-undefined'"));
1296 }
1297
1298 #[test]
1299 fn test_html_comments_ignored() {
1300 let rule = MD052ReferenceLinkImages::new();
1302
1303 let content = r#"<!--- write fake_editor.py 'import sys\nopen(*sys.argv[1:], mode="wt").write("2 3 4 4 2 3 2")' -->
1305<!--- set_env EDITOR 'python3 fake_editor.py' -->
1306
1307```bash
1308$ python3 vote.py
13093 votes for: 2
13102 votes for: 3, 4
1311```"#;
1312 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1313 let result = rule.check(&ctx).unwrap();
1314 assert_eq!(result.len(), 0, "Should not flag [1:] inside HTML comments");
1315
1316 let content = r#"<!-- This is [ref1] and [ref2][ref3] -->
1318Normal [text][undefined]
1319<!-- Another [comment][with] references -->"#;
1320 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1321 let result = rule.check(&ctx).unwrap();
1322 assert_eq!(
1323 result.len(),
1324 1,
1325 "Should only flag the undefined reference outside comments"
1326 );
1327 assert!(result[0].message.contains("undefined"));
1328
1329 let content = r#"<!--
1331[ref1]
1332[ref2][ref3]
1333-->
1334[actual][undefined]"#;
1335 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1336 let result = rule.check(&ctx).unwrap();
1337 assert_eq!(
1338 result.len(),
1339 1,
1340 "Should not flag references in multi-line HTML comments"
1341 );
1342 assert!(result[0].message.contains("undefined"));
1343
1344 let content = r#"<!-- Comment with [1:] pattern -->
1346Valid [link][ref]
1347<!-- More [refs][in][comments] -->
1348![image][missing]
1349
1350[ref]: https://example.com"#;
1351 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1352 let result = rule.check(&ctx).unwrap();
1353 assert_eq!(result.len(), 1, "Should only flag missing image reference");
1354 assert!(result[0].message.contains("missing"));
1355 }
1356
1357 #[test]
1358 fn test_frontmatter_ignored() {
1359 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1362 shortcut_syntax: true,
1363 ..Default::default()
1364 });
1365
1366 let content = r#"---
1368layout: post
1369title: "My Jekyll Post"
1370date: 2023-01-01
1371categories: blog
1372tags: ["test", "example"]
1373author: John Doe
1374---
1375
1376# My Blog Post
1377
1378This is the actual markdown content that should be linted.
1379
1380[undefined] reference should be flagged.
1381
1382## Section 1
1383
1384Some content here."#;
1385 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1386 let result = rule.check(&ctx).unwrap();
1387
1388 assert_eq!(
1390 result.len(),
1391 1,
1392 "Should only flag the undefined reference outside frontmatter"
1393 );
1394 assert!(result[0].message.contains("undefined"));
1395
1396 let content = r#"+++
1398title = "My Post"
1399tags = ["example", "test"]
1400+++
1401
1402# Content
1403
1404[missing] reference should be flagged."#;
1405 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1406 let result = rule.check(&ctx).unwrap();
1407 assert_eq!(
1408 result.len(),
1409 1,
1410 "Should only flag the undefined reference outside TOML frontmatter"
1411 );
1412 assert!(result[0].message.contains("missing"));
1413 }
1414
1415 #[test]
1416 fn test_mkdocs_snippet_markers_not_flagged() {
1417 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1420 shortcut_syntax: true,
1421 ..Default::default()
1422 });
1423
1424 let content = r#"# Document with MkDocs Snippets
1426
1427Some content here.
1428
1429# -8<- [start:remote-content]
1430
1431This is the remote content section.
1432
1433# -8<- [end:remote-content]
1434
1435More content here.
1436
1437<!-- --8<-- [start:another-section] -->
1438Content in another section
1439<!-- --8<-- [end:another-section] -->"#;
1440 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1441 let result = rule.check(&ctx).unwrap();
1442
1443 assert_eq!(
1445 result.len(),
1446 0,
1447 "Should not flag MkDocs snippet markers as undefined references"
1448 );
1449
1450 let content = r#"# Document
1453
1454# -8<- [start:section]
1455Content with [reference] inside snippet section
1456# -8<- [end:section]
1457
1458Regular [undefined] reference outside snippet markers."#;
1459 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::MkDocs, None);
1460 let result = rule.check(&ctx).unwrap();
1461
1462 assert_eq!(
1463 result.len(),
1464 2,
1465 "Should flag undefined references but skip snippet marker lines"
1466 );
1467 assert!(result[0].message.contains("reference"));
1469 assert!(result[1].message.contains("undefined"));
1470
1471 let content = r#"# Document
1473
1474# -8<- [start:section]
1475# -8<- [end:section]"#;
1476 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1477 let result = rule.check(&ctx).unwrap();
1478
1479 assert_eq!(
1480 result.len(),
1481 2,
1482 "In standard mode, snippet markers should be flagged as undefined references"
1483 );
1484 }
1485
1486 #[test]
1487 fn test_pandoc_citations_not_flagged() {
1488 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1491 shortcut_syntax: true,
1492 ..Default::default()
1493 });
1494
1495 let content = r#"# Research Paper
1496
1497We are using the **bookdown** package [@R-bookdown] in this sample book.
1498This was built on top of R Markdown and **knitr** [@xie2015].
1499
1500Multiple citations [@citation1; @citation2; @citation3] are also supported.
1501
1502Regular [undefined] reference should still be flagged.
1503"#;
1504 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1505 let result = rule.check(&ctx).unwrap();
1506
1507 assert_eq!(
1509 result.len(),
1510 1,
1511 "Should only flag the undefined reference, not Pandoc citations"
1512 );
1513 assert!(result[0].message.contains("undefined"));
1514 }
1515
1516 #[test]
1517 fn test_pandoc_inline_footnotes_not_flagged() {
1518 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1521 shortcut_syntax: true,
1522 ..Default::default()
1523 });
1524
1525 let content = r#"# Math Document
1526
1527You can use math in footnotes like this^[where we mention $p = \frac{a}{b}$].
1528
1529Another footnote^[with some text and a [link](https://example.com)].
1530
1531But this [reference] without ^ should be flagged.
1532"#;
1533 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1534 let result = rule.check(&ctx).unwrap();
1535
1536 assert_eq!(
1538 result.len(),
1539 1,
1540 "Should only flag the regular reference, not inline footnotes"
1541 );
1542 assert!(result[0].message.contains("reference"));
1543 }
1544
1545 #[test]
1546 fn test_github_alerts_not_flagged() {
1547 let rule = MD052ReferenceLinkImages::from_config_struct(MD052Config {
1550 shortcut_syntax: true,
1551 ..Default::default()
1552 });
1553
1554 let content = r#"# Document with GitHub Alerts
1556
1557> [!NOTE]
1558> This is a note alert.
1559
1560> [!TIP]
1561> This is a tip alert.
1562
1563> [!IMPORTANT]
1564> This is an important alert.
1565
1566> [!WARNING]
1567> This is a warning alert.
1568
1569> [!CAUTION]
1570> This is a caution alert.
1571
1572Regular content with [undefined] reference."#;
1573 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1574 let result = rule.check(&ctx).unwrap();
1575
1576 assert_eq!(
1578 result.len(),
1579 1,
1580 "Should only flag the undefined reference, not GitHub alerts"
1581 );
1582 assert!(result[0].message.contains("undefined"));
1583 assert_eq!(result[0].line, 18); let content = r#"> [!TIP]
1587> Here's a useful tip about [something].
1588> Multiple lines are allowed.
1589
1590[something] is mentioned but not defined."#;
1591 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1592 let result = rule.check(&ctx).unwrap();
1593
1594 assert_eq!(result.len(), 1, "Should flag undefined reference");
1598 assert!(result[0].message.contains("something"));
1599
1600 let content = r#"> [!NOTE]
1602> See [reference] for more details.
1603
1604[reference]: https://example.com"#;
1605 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1606 let result = rule.check(&ctx).unwrap();
1607
1608 assert_eq!(result.len(), 0, "Should not flag GitHub alerts or defined references");
1610 }
1611
1612 #[test]
1613 fn test_ignore_config() {
1614 let config = MD052Config {
1616 shortcut_syntax: true,
1617 ignore: vec!["Vec".to_string(), "HashMap".to_string(), "Option".to_string()],
1618 };
1619 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1620
1621 let content = r#"# Document with Custom Types
1622
1623Use [Vec] for dynamic arrays.
1624Use [HashMap] for key-value storage.
1625Use [Option] for nullable values.
1626Use [Result] for error handling.
1627"#;
1628 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1629 let result = rule.check(&ctx).unwrap();
1630
1631 assert_eq!(result.len(), 1, "Should only flag names not in ignore");
1633 assert!(result[0].message.contains("Result"));
1634 }
1635
1636 #[test]
1637 fn test_ignore_case_insensitive() {
1638 let config = MD052Config {
1640 shortcut_syntax: true,
1641 ignore: vec!["Vec".to_string()],
1642 };
1643 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1644
1645 let content = r#"# Case Insensitivity Test
1646
1647[Vec] should be ignored.
1648[vec] should also be ignored (different case, same match).
1649[VEC] should also be ignored (different case, same match).
1650[undefined] should be flagged (not in ignore list).
1651"#;
1652 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1653 let result = rule.check(&ctx).unwrap();
1654
1655 assert_eq!(result.len(), 1, "Should only flag non-ignored reference");
1657 assert!(result[0].message.contains("undefined"));
1658 }
1659
1660 #[test]
1661 fn test_ignore_empty_by_default() {
1662 let rule = MD052ReferenceLinkImages::new();
1664
1665 let content = "[text][undefined]";
1666 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1667 let result = rule.check(&ctx).unwrap();
1668
1669 assert_eq!(result.len(), 1);
1671 assert!(result[0].message.contains("undefined"));
1672 }
1673
1674 #[test]
1675 fn test_ignore_with_reference_links() {
1676 let config = MD052Config {
1678 shortcut_syntax: false,
1679 ignore: vec!["CustomType".to_string()],
1680 };
1681 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1682
1683 let content = r#"# Test
1684
1685See [documentation][CustomType] for details.
1686See [other docs][MissingRef] for more.
1687"#;
1688 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1689 let result = rule.check(&ctx).unwrap();
1690
1691 for (i, w) in result.iter().enumerate() {
1693 eprintln!("Warning {}: {}", i, w.message);
1694 }
1695
1696 assert_eq!(result.len(), 1, "Expected 1 warning, got {}", result.len());
1699 assert!(
1700 result[0].message.contains("missingref"),
1701 "Expected 'missingref' in message: {}",
1702 result[0].message
1703 );
1704 }
1705
1706 #[test]
1707 fn test_ignore_multiple() {
1708 let config = MD052Config {
1710 shortcut_syntax: true,
1711 ignore: vec![
1712 "i32".to_string(),
1713 "u64".to_string(),
1714 "String".to_string(),
1715 "Arc".to_string(),
1716 "Mutex".to_string(),
1717 ],
1718 };
1719 let rule = MD052ReferenceLinkImages::from_config_struct(config);
1720
1721 let content = r#"# Types
1722
1723[i32] [u64] [String] [Arc] [Mutex] [Box]
1724"#;
1725 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1726 let result = rule.check(&ctx).unwrap();
1727
1728 assert_eq!(result.len(), 1);
1732 assert!(result[0].message.contains("Box"));
1733 }
1734
1735 #[test]
1736 fn test_nested_code_fences_reference_extraction() {
1737 let rule = MD052ReferenceLinkImages::new();
1742
1743 let content = "````\n```\n[ref-inside]: https://example.com\n```\n````\n\n[Use this link][ref-inside]";
1744 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1745 let result = rule.check(&ctx).unwrap();
1746
1747 assert_eq!(
1751 result.len(),
1752 1,
1753 "Reference defined inside nested code fence should not count as a definition"
1754 );
1755 assert!(result[0].message.contains("ref-inside"));
1756 }
1757}